
Apple Music
- 20 installs
- 638 repo stars
- Updated March 7, 2026
- sundial-org/awesome-openclaw-skills
Helps with ai & agent building tasks during AI-assisted development.
About
apple-music is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- apple-music
- AI & Agent Building
- AI-coding skill
Apple Music by the numbers
- 20 all-time installs (skills.sh)
- Ranked #10,459 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/sundial-org/awesome-openclaw-skills --skill apple-musicAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 20 |
|---|---|
| repo stars | ★ 638 |
| Last updated | March 7, 2026 |
| Repository | sundial-org/awesome-openclaw-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Apple Music
Control Apple Music via MusicKit API and AppleScript. Path: ~/.clawdbot/skills/apple-music/
Local (No Setup)
Playback: ./apple-music.sh player [now|play|pause|toggle|next|prev|shuffle|repeat|volume N|song "name"] AirPlay: ./apple-music.sh airplay [list|select N|add N|remove N]
API (Setup Required)
Requires Apple Developer account ($99/yr) + MusicKit key.
Setup
Portal steps first: 1. developer.apple.com → Keys → Create MusicKit key → Download .p8 2. Note your Key ID and Team ID
Then run setup:
./launch-setup.sh # Opens Terminal for interactive setupThe launcher opens Terminal.app and runs the setup script there. Enter your .p8 path, Key ID, Team ID, then authorize in browser and paste the token.
⚠️ Agents: Always use ./launch-setup.sh to open Terminal. Don't run setup.sh through chat (requires interactive input).
Commands
search "query" [--type songs|albums|artists] [--limit N]library add <song-id>playlists [list|create "Name"|add <playlist-id> <song-id>]
Config
config.json stores tokens (valid ~6 months). Re-run ./setup.sh if auth fails.
Errors
- 401: Token expired, re-run setup
- 403: Check Apple Music subscription
- 404: Invalid ID or region-locked
Setup Issues
- 404 on auth page: Setup script auto-fixes with HTTP server verification
- No token in browser: Restart setup.sh
- Browser won't open: Manually open printed URL (Chrome recommended)
#!/bin/bash
#
# Apple Music CLI - Search, library, and playlist management
#
# Usage: apple-music <command> [options]
#
set -e
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SKILL_DIR/config.json"
# Colors (only if terminal supports it)
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
DIM='\033[2m'
BOLD='\033[1m'
NC='\033[0m'
else
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' DIM='' BOLD='' NC=''
fi
# API Base URL
API_BASE="https://api.music.apple.com/v1"
#─────────────────────────────────────────────────────────────────────────────
# Utility Functions
#─────────────────────────────────────────────────────────────────────────────
error() {
echo -e "${RED}Error:${NC} $1" >&2
exit 1
}
warn() {
echo -e "${YELLOW}Warning:${NC} $1" >&2
}
info() {
echo -e "${CYAN}ℹ${NC} $1"
}
# Load configuration
load_config() {
if [[ ! -f "$CONFIG_FILE" ]]; then
error "Not configured. Run: $SKILL_DIR/setup.sh"
fi
DEVELOPER_TOKEN=$(jq -r '.developer_token // empty' "$CONFIG_FILE" 2>/dev/null)
MUSIC_USER_TOKEN=$(jq -r '.music_user_token // empty' "$CONFIG_FILE" 2>/dev/null)
STOREFRONT=$(jq -r '.storefront // "us"' "$CONFIG_FILE" 2>/dev/null)
if [[ -z "$DEVELOPER_TOKEN" ]] || [[ -z "$MUSIC_USER_TOKEN" ]]; then
error "Invalid configuration. Run: $SKILL_DIR/setup.sh"
fi
}
# Make API request
api_request() {
local method="$1"
local endpoint="$2"
local data="$3"
local url="$API_BASE$endpoint"
local args=(-s --compressed -w "\n%{http_code}")
args+=(-H "Authorization: Bearer $DEVELOPER_TOKEN")
args+=(-H "Music-User-Token: $MUSIC_USER_TOKEN")
args+=(-H "Content-Type: application/json")
if [[ "$method" == "POST" ]]; then
args+=(-X POST)
if [[ -n "$data" ]]; then
args+=(-d "$data")
else
# POST without body requires Content-Length: 0
args+=(-H "Content-Length: 0")
fi
fi
local response
response=$(curl "${args[@]}" "$url" 2>&1)
local http_code
http_code=$(echo "$response" | tail -n1)
local body
body=$(echo "$response" | sed '$d')
# Handle HTTP errors
case "$http_code" in
200|201|202|204)
echo "$body"
;;
401)
error "Authentication failed. Your tokens may have expired.\nRun: $SKILL_DIR/setup.sh"
;;
403)
error "Forbidden. Check your Apple Music subscription status."
;;
404)
error "Resource not found. The ID may be incorrect or region-locked."
;;
429)
error "Rate limited. Please wait a moment and try again."
;;
*)
local error_msg
error_msg=$(echo "$body" | jq -r '.errors[0].detail // .errors[0].title // "Unknown error"' 2>/dev/null || echo "HTTP $http_code")
error "$error_msg"
;;
esac
}
#─────────────────────────────────────────────────────────────────────────────
# Search Command
#─────────────────────────────────────────────────────────────────────────────
cmd_search() {
local query=""
local types="songs"
local limit=10
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--type|-t)
types="$2"
shift 2
;;
--limit|-l)
limit="$2"
shift 2
;;
--help|-h)
echo "Usage: apple-music search [options] <query>"
echo ""
echo "Search the Apple Music catalog."
echo ""
echo "Options:"
echo " --type, -t <type> Type to search: songs, albums, artists, playlists"
echo " (default: songs)"
echo " --limit, -l <n> Number of results (default: 10, max: 25)"
echo ""
echo "Examples:"
echo " apple-music search \"Bohemian Rhapsody\""
echo " apple-music search --type albums \"Abbey Road\""
echo " apple-music search --type artists --limit 5 \"Beatles\""
return 0
;;
-*)
error "Unknown option: $1"
;;
*)
query="$1"
shift
;;
esac
done
if [[ -z "$query" ]]; then
error "Search query required. Usage: apple-music search <query>"
fi
# URL encode the query
local encoded_query
encoded_query=$(printf '%s' "$query" | jq -sRr @uri)
info "Searching for: $query"
echo ""
local response
response=$(api_request "GET" "/catalog/$STOREFRONT/search?term=$encoded_query&types=$types&limit=$limit")
# Parse and display results based on type
case "$types" in
songs)
echo "$response" | jq -r '
.results.songs.data // [] |
to_entries |
.[] |
"\(.key + 1). \(.value.attributes.name)\n Artist: \(.value.attributes.artistName)\n Album: \(.value.attributes.albumName)\n ID: \(.value.id)\n"
'
local count
count=$(echo "$response" | jq '.results.songs.data // [] | length')
echo -e "${DIM}Found $count song(s)${NC}"
;;
albums)
echo "$response" | jq -r '
.results.albums.data // [] |
to_entries |
.[] |
"\(.key + 1). \(.value.attributes.name)\n Artist: \(.value.attributes.artistName)\n Year: \(.value.attributes.releaseDate[:4] // "N/A")\n ID: \(.value.id)\n"
'
local count
count=$(echo "$response" | jq '.results.albums.data // [] | length')
echo -e "${DIM}Found $count album(s)${NC}"
;;
artists)
echo "$response" | jq -r '
.results.artists.data // [] |
to_entries |
.[] |
"\(.key + 1). \(.value.attributes.name)\n Genres: \(.value.attributes.genreNames | join(", "))\n ID: \(.value.id)\n"
'
local count
count=$(echo "$response" | jq '.results.artists.data // [] | length')
echo -e "${DIM}Found $count artist(s)${NC}"
;;
playlists)
echo "$response" | jq -r '
.results.playlists.data // [] |
to_entries |
.[] |
"\(.key + 1). \(.value.attributes.name)\n Curator: \(.value.attributes.curatorName // "Apple Music")\n ID: \(.value.id)\n"
'
local count
count=$(echo "$response" | jq '.results.playlists.data // [] | length')
echo -e "${DIM}Found $count playlist(s)${NC}"
;;
*)
echo "$response" | jq '.'
;;
esac
}
#─────────────────────────────────────────────────────────────────────────────
# Library Command
#─────────────────────────────────────────────────────────────────────────────
cmd_library() {
local action="$1"
shift || true
case "$action" in
add)
cmd_library_add "$@"
;;
""|--help|-h)
echo "Usage: apple-music library <action>"
echo ""
echo "Actions:"
echo " add <song-id> Add a song to your library"
echo ""
echo "Examples:"
echo " apple-music library add 1440833098"
return 0
;;
*)
error "Unknown library action: $action"
;;
esac
}
cmd_library_add() {
local song_id=""
local resource_type="songs"
while [[ $# -gt 0 ]]; do
case "$1" in
--type|-t)
resource_type="$2"
shift 2
;;
--help|-h)
echo "Usage: apple-music library add [options] <id>"
echo ""
echo "Add a song/album to your library."
echo ""
echo "Options:"
echo " --type, -t <type> Resource type: songs, albums (default: songs)"
echo ""
echo "Examples:"
echo " apple-music library add 1440833098"
echo " apple-music library add --type albums 1441164495"
return 0
;;
-*)
error "Unknown option: $1"
;;
*)
song_id="$1"
shift
;;
esac
done
if [[ -z "$song_id" ]]; then
error "Song/Album ID required. Usage: apple-music library add <id>"
fi
info "Adding $resource_type ID $song_id to library..."
local data
data=$(jq -n --arg type "$resource_type" --arg id "$song_id" '{
data: [{
id: $id,
type: $type
}]
}')
# URL-encode the brackets in ids[type]=id
api_request "POST" "/me/library?ids%5B${resource_type}%5D=$song_id" "" > /dev/null
echo -e "${GREEN}✓${NC} Added to library successfully!"
}
#─────────────────────────────────────────────────────────────────────────────
# Playlists Command
#─────────────────────────────────────────────────────────────────────────────
cmd_playlists() {
local action="$1"
shift || true
case "$action" in
list|ls)
cmd_playlists_list "$@"
;;
create|new)
cmd_playlists_create "$@"
;;
add)
cmd_playlists_add "$@"
;;
""|--help|-h)
echo "Usage: apple-music playlists <action>"
echo ""
echo "Actions:"
echo " list List your playlists"
echo " create <name> Create a new playlist"
echo " add <pid> <sid> Add a song to a playlist"
echo ""
echo "Examples:"
echo " apple-music playlists list"
echo " apple-music playlists create \"Road Trip\""
echo " apple-music playlists add p.ABC123 1440833098"
return 0
;;
*)
error "Unknown playlists action: $action"
;;
esac
}
cmd_playlists_list() {
local limit=25
while [[ $# -gt 0 ]]; do
case "$1" in
--limit|-l)
limit="$2"
shift 2
;;
--help|-h)
echo "Usage: apple-music playlists list [options]"
echo ""
echo "List your Apple Music library playlists."
echo ""
echo "Options:"
echo " --limit, -l <n> Number of results (default: 25)"
return 0
;;
*)
shift
;;
esac
done
info "Fetching your playlists..."
echo ""
local response
response=$(api_request "GET" "/me/library/playlists?limit=$limit")
echo "$response" | jq -r '
.data // [] |
to_entries |
.[] |
"\(.key + 1). \(.value.attributes.name)\n Tracks: \(.value.attributes.trackCount // "?")\n ID: \(.value.id)\n"
'
local count
count=$(echo "$response" | jq '.data // [] | length')
echo -e "${DIM}Showing $count playlist(s)${NC}"
}
cmd_playlists_create() {
local name=""
local description=""
while [[ $# -gt 0 ]]; do
case "$1" in
--description|-d)
description="$2"
shift 2
;;
--help|-h)
echo "Usage: apple-music playlists create [options] <name>"
echo ""
echo "Create a new playlist in your library."
echo ""
echo "Options:"
echo " --description, -d <text> Playlist description"
echo ""
echo "Examples:"
echo " apple-music playlists create \"Road Trip\""
echo " apple-music playlists create \"Workout\" -d \"High energy songs\""
return 0
;;
-*)
error "Unknown option: $1"
;;
*)
name="$1"
shift
;;
esac
done
if [[ -z "$name" ]]; then
error "Playlist name required. Usage: apple-music playlists create <name>"
fi
info "Creating playlist: $name"
local data
if [[ -n "$description" ]]; then
data=$(jq -n --arg name "$name" --arg desc "$description" '{
attributes: {
name: $name,
description: $desc
}
}')
else
data=$(jq -n --arg name "$name" '{
attributes: {
name: $name
}
}')
fi
local response
response=$(api_request "POST" "/me/library/playlists" "$data")
local playlist_id
playlist_id=$(echo "$response" | jq -r '.data[0].id // empty')
if [[ -n "$playlist_id" ]]; then
echo -e "${GREEN}✓${NC} Playlist created!"
echo " Name: $name"
echo " ID: $playlist_id"
else
echo -e "${GREEN}✓${NC} Playlist created: $name"
fi
}
cmd_playlists_add() {
local playlist_id=""
local song_id=""
while [[ $# -gt 0 ]]; do
case "$1" in
--help|-h)
echo "Usage: apple-music playlists add <playlist-id> <song-id>"
echo ""
echo "Add a song to a playlist."
echo ""
echo "Arguments:"
echo " playlist-id Your library playlist ID (e.g., p.ABC123)"
echo " song-id Catalog song ID (e.g., 1440833098)"
echo ""
echo "Examples:"
echo " apple-music playlists add p.ABC123 1440833098"
return 0
;;
-*)
error "Unknown option: $1"
;;
*)
if [[ -z "$playlist_id" ]]; then
playlist_id="$1"
elif [[ -z "$song_id" ]]; then
song_id="$1"
fi
shift
;;
esac
done
if [[ -z "$playlist_id" ]] || [[ -z "$song_id" ]]; then
error "Both playlist ID and song ID required.\nUsage: apple-music playlists add <playlist-id> <song-id>"
fi
info "Adding song $song_id to playlist $playlist_id..."
local data
data=$(jq -n --arg id "$song_id" '{
data: [{
id: $id,
type: "songs"
}]
}')
api_request "POST" "/me/library/playlists/$playlist_id/tracks" "$data" > /dev/null
echo -e "${GREEN}✓${NC} Song added to playlist!"
}
#─────────────────────────────────────────────────────────────────────────────
# Playback Control (AppleScript - no API setup required)
#─────────────────────────────────────────────────────────────────────────────
cmd_player() {
local subcommand="${1:-}"
shift || true
case "$subcommand" in
play)
if [[ $# -gt 0 ]]; then
# Play specific playlist or search term
local target="$*"
osascript -e "tell application \"Music\" to play playlist \"$target\"" 2>/dev/null || \
osascript -e "tell application \"Music\" to play" 2>/dev/null || \
error "Could not play: $target"
else
osascript -e 'tell application "Music" to play'
fi
echo -e "${GREEN}▶${NC} Playing"
;;
song)
# Search library and play a specific song
if [[ $# -eq 0 ]]; then
error "Specify a song name. Example: apple-music player song Blinding Lights"
fi
local query="$*"
local result
result=$(osascript -e "tell application \"Music\"
set searchResults to (search library playlist 1 for \"$query\" only songs)
if (count of searchResults) > 0 then
play item 1 of searchResults
set t to item 1 of searchResults
return name of t & \" - \" & artist of t
else
return \"NOT_FOUND\"
end if
end tell" 2>/dev/null)
if [[ "$result" == "NOT_FOUND" ]]; then
error "Song not found in library: $query"
else
echo -e "${GREEN}▶${NC} Now playing: $result"
fi
;;
id|catalog)
# Play a song by Apple Music catalog ID (adds to library first, then plays)
if [[ $# -eq 0 ]]; then
error "Specify a song ID. Example: apple-music player id 1581102728"
fi
local song_id="$1"
# Check if config exists for API call
if [[ ! -f "$CONFIG_FILE" ]]; then
error "API not configured. Run setup.sh first to play by catalog ID."
fi
# Load config for API
load_config
# First, get song info from catalog
info "Fetching song info..."
local song_info
song_info=$(api_request "GET" "/catalog/$STOREFRONT/songs/$song_id" "" 2>/dev/null) || {
error "Could not find song with ID: $song_id"
}
local song_name artist_name
song_name=$(echo "$song_info" | jq -r '.data[0].attributes.name // empty' 2>/dev/null)
artist_name=$(echo "$song_info" | jq -r '.data[0].attributes.artistName // empty' 2>/dev/null)
if [[ -z "$song_name" ]]; then
error "Could not get song details for ID: $song_id"
fi
info "Adding \"$song_name\" by $artist_name to library..."
# Add to library using the API directly
local encoded_ids
encoded_ids=$(printf '%s' "ids[songs]=${song_id}" | sed 's/\[/%5B/g; s/\]/%5D/g')
api_request "POST" "/me/library?${encoded_ids}" "" > /dev/null 2>&1 || true
# Wait for library sync (Apple Music needs time to index)
info "Waiting for library sync..."
sleep 3
# Search library by song name and play
local result
result=$(osascript -e "tell application \"Music\"
set searchResults to (search library playlist 1 for \"$song_name\" only songs)
if (count of searchResults) > 0 then
play item 1 of searchResults
set t to item 1 of searchResults
return name of t & \" - \" & artist of t
else
return \"NOT_FOUND\"
end if
end tell" 2>/dev/null)
if [[ "$result" == "NOT_FOUND" ]]; then
warn "Song added but couldn't auto-play. Try: apple-music player song \"$song_name\""
else
echo -e "${GREEN}▶${NC} Now playing: $result"
fi
;;
pause)
osascript -e 'tell application "Music" to pause'
echo -e "${YELLOW}⏸${NC} Paused"
;;
toggle)
osascript -e 'tell application "Music" to playpause'
echo -e "${CYAN}⏯${NC} Toggled play/pause"
;;
next|skip)
osascript -e 'tell application "Music" to next track'
sleep 0.5
cmd_player now
;;
prev|previous|back)
osascript -e 'tell application "Music" to previous track'
sleep 0.5
cmd_player now
;;
shuffle)
local current
current=$(osascript -e 'tell application "Music" to get shuffle enabled')
if [[ "$current" == "true" ]]; then
osascript -e 'tell application "Music" to set shuffle enabled to false'
echo -e "${CYAN}🔀${NC} Shuffle: OFF"
else
osascript -e 'tell application "Music" to set shuffle enabled to true'
echo -e "${CYAN}🔀${NC} Shuffle: ON"
fi
;;
repeat)
local current
current=$(osascript -e 'tell application "Music" to get song repeat')
case "$current" in
off)
osascript -e 'tell application "Music" to set song repeat to all'
echo -e "${CYAN}🔁${NC} Repeat: ALL"
;;
all)
osascript -e 'tell application "Music" to set song repeat to one'
echo -e "${CYAN}🔂${NC} Repeat: ONE"
;;
one)
osascript -e 'tell application "Music" to set song repeat to off'
echo -e "${CYAN}➡️${NC} Repeat: OFF"
;;
esac
;;
now|status|current)
local info
info=$(osascript -e 'tell application "Music"
if player state is playing then
set trackName to name of current track
set artistName to artist of current track
set albumName to album of current track
set shuffleState to shuffle enabled
set repeatState to song repeat as string
set statusLine to "▶ " & trackName & " - " & artistName & " (" & albumName & ")"
if shuffleState then
set statusLine to statusLine & " 🔀"
end if
if repeatState is not "off" then
if repeatState is "one" then
set statusLine to statusLine & " 🔂"
else
set statusLine to statusLine & " 🔁"
end if
end if
return statusLine
else if player state is paused then
set trackName to name of current track
set artistName to artist of current track
set albumName to album of current track
return "⏸ " & trackName & " - " & artistName & " (" & albumName & ")"
else
return "⏹ Not playing"
end if
end tell' 2>/dev/null) || info="⏹ Music app not running"
echo "$info"
;;
vol|volume)
if [[ $# -gt 0 ]]; then
local vol="$1"
# Validate and clamp volume to 0-100
if ! [[ "$vol" =~ ^[0-9]+$ ]]; then
error "Volume must be a number between 0 and 100"
fi
if [[ $vol -lt 0 ]]; then
vol=0
elif [[ $vol -gt 100 ]]; then
vol=100
fi
# Set Apple Music app volume (existing behavior)
osascript -e "tell application \"Music\" to set sound volume to $vol"
# Set macOS system output volume and unmute
osascript -e "set volume output volume $vol"
osascript -e "set volume without output muted"
echo -e "${CYAN}🔊${NC} Volume set to $vol (app + system)"
else
local vol
vol=$(osascript -e 'tell application "Music" to get sound volume')
echo -e "${CYAN}🔊${NC} Volume: $vol"
fi
;;
--help|-h|"")
echo "Player Control"
echo ""
echo "Usage: apple-music player <command>"
echo ""
echo "Commands (no setup required):"
echo " play [playlist] Start playback (optionally specify playlist)"
echo " song <name> Search library and play a specific song"
echo " pause Pause playback"
echo " toggle Toggle play/pause"
echo " next Skip to next track"
echo " prev Go to previous track"
echo " shuffle Toggle shuffle on/off"
echo " repeat Cycle repeat: off → all → one → off"
echo " now Show current track"
echo " volume [0-100] Get or set volume"
echo ""
echo "Commands (requires API setup):"
echo " id <song-id> Play a song by Apple Music catalog ID (adds to library first)"
;;
*)
error "Unknown player command: $subcommand"
;;
esac
}
#─────────────────────────────────────────────────────────────────────────────
# AirPlay Control (AppleScript - no API setup required)
#─────────────────────────────────────────────────────────────────────────────
cmd_airplay() {
local subcommand="${1:-}"
shift || true
case "$subcommand" in
list|devices)
echo -e "${CYAN}ℹ${NC} AirPlay Devices:"
echo ""
osascript -e '
tell application "Music"
set deviceList to ""
set deviceNum to 1
repeat with d in AirPlay devices
set deviceName to name of d
set isSelected to selected of d
if isSelected then
set deviceList to deviceList & deviceNum & ". " & deviceName & " ✓" & "\n"
else
set deviceList to deviceList & deviceNum & ". " & deviceName & "\n"
end if
set deviceNum to deviceNum + 1
end repeat
return deviceList
end tell
' 2>/dev/null | sed '/^$/d'
;;
select|set)
if [[ $# -eq 0 ]]; then
error "Specify device number(s) or name(s). Example: apple-music airplay select 1 2"
fi
# First deselect all
osascript -e '
tell application "Music"
repeat with d in AirPlay devices
set selected of d to false
end repeat
end tell
' 2>/dev/null
# Select specified devices
for arg in "$@"; do
if [[ "$arg" =~ ^[0-9]+$ ]]; then
# Select by number
osascript -e "tell application \"Music\" to set selected of AirPlay device $arg to true" 2>/dev/null || \
warn "Could not select device $arg"
else
# Select by name
osascript -e "tell application \"Music\"
repeat with d in AirPlay devices
if name of d is \"$arg\" then
set selected of d to true
end if
end repeat
end tell" 2>/dev/null || warn "Could not select device: $arg"
fi
done
echo -e "${GREEN}✓${NC} AirPlay devices updated"
cmd_airplay list
;;
add)
if [[ $# -eq 0 ]]; then
error "Specify device number(s) or name(s) to add"
fi
for arg in "$@"; do
if [[ "$arg" =~ ^[0-9]+$ ]]; then
osascript -e "tell application \"Music\" to set selected of AirPlay device $arg to true" 2>/dev/null || \
warn "Could not add device $arg"
else
osascript -e "tell application \"Music\"
repeat with d in AirPlay devices
if name of d is \"$arg\" then
set selected of d to true
end if
end repeat
end tell" 2>/dev/null || warn "Could not add device: $arg"
fi
done
echo -e "${GREEN}✓${NC} Added to AirPlay"
cmd_airplay list
;;
remove)
if [[ $# -eq 0 ]]; then
error "Specify device number(s) or name(s) to remove"
fi
for arg in "$@"; do
if [[ "$arg" =~ ^[0-9]+$ ]]; then
osascript -e "tell application \"Music\" to set selected of AirPlay device $arg to false" 2>/dev/null || \
warn "Could not remove device $arg"
else
osascript -e "tell application \"Music\"
repeat with d in AirPlay devices
if name of d is \"$arg\" then
set selected of d to false
end if
end repeat
end tell" 2>/dev/null || warn "Could not remove device: $arg"
fi
done
echo -e "${GREEN}✓${NC} Removed from AirPlay"
cmd_airplay list
;;
--help|-h|"")
echo "AirPlay Control (no API setup required)"
echo ""
echo "Usage: apple-music airplay <command>"
echo ""
echo "Commands:"
echo " list List AirPlay devices (✓ = active)"
echo " select <1> <2> Select devices (deselects others)"
echo " add <1> <2> Add devices to current selection"
echo " remove <1> <2> Remove devices from selection"
echo ""
echo "Devices can be specified by number or name."
;;
*)
error "Unknown airplay command: $subcommand"
;;
esac
}
#─────────────────────────────────────────────────────────────────────────────
# Main
#─────────────────────────────────────────────────────────────────────────────
show_help() {
echo "Apple Music CLI"
echo ""
echo "Usage: apple-music <command> [options]"
echo ""
echo "Commands (API - requires setup):"
echo " search <query> Search the Apple Music catalog"
echo " library add <id> Add a song to your library"
echo " playlists list List your playlists"
echo " playlists create <name> Create a new playlist"
echo " playlists add <p> <s> Add a song to a playlist"
echo ""
echo "Commands (Local - no setup required):"
echo " player play|pause|next Control playback"
echo " player now Show current track"
echo " airplay list List AirPlay devices"
echo " airplay select <1> <2> Select AirPlay outputs"
echo ""
echo "Options:"
echo " --help, -h Show help for a command"
echo ""
echo "Examples:"
echo " apple-music search \"Bohemian Rhapsody\""
echo " apple-music player now"
echo " apple-music airplay select 1 2"
echo ""
echo "Setup:"
echo " Run $SKILL_DIR/setup.sh to configure your Apple Developer credentials."
}
main() {
local command="${1:-}"
shift || true
# Handle help and version first (no config needed)
case "$command" in
--help|-h|help|"")
show_help
exit 0
;;
--version|-v)
echo "apple-music 1.0.0"
exit 0
;;
esac
# Local commands (no API config needed)
case "$command" in
player|play)
if [[ "$command" == "play" ]]; then
# Shortcut: "apple-music play" = "apple-music player play"
cmd_player play "$@"
else
cmd_player "$@"
fi
exit 0
;;
airplay)
cmd_airplay "$@"
exit 0
;;
esac
# Check if help is requested for subcommands (no config needed)
local needs_config=true
for arg in "$@"; do
if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
needs_config=false
break
fi
done
# Also check for empty subcommands that show help
case "$command" in
library|playlists|playlist)
if [[ $# -eq 0 ]]; then
needs_config=false
fi
;;
esac
# Load config only if needed
if [[ "$needs_config" == "true" ]]; then
load_config
fi
case "$command" in
search)
cmd_search "$@"
;;
library)
cmd_library "$@"
;;
playlists|playlist)
cmd_playlists "$@"
;;
*)
error "Unknown command: $command\nRun 'apple-music --help' for usage."
;;
esac
}
main "$@"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Apple Music Authorization - Clawdbot</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.container {
max-width: 500px;
padding: 40px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
text-align: center;
}
.logo {
font-size: 60px;
margin-bottom: 20px;
}
h1 {
font-size: 24px;
margin-bottom: 10px;
font-weight: 600;
}
.subtitle {
color: rgba(255, 255, 255, 0.7);
margin-bottom: 30px;
}
.status {
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
font-size: 14px;
}
.status.loading {
background: rgba(255, 193, 7, 0.2);
color: #ffc107;
}
.status.success {
background: rgba(76, 175, 80, 0.2);
color: #4caf50;
}
.status.error {
background: rgba(244, 67, 54, 0.2);
color: #f44336;
}
.btn {
background: linear-gradient(135deg, #fc3c44 0%, #fa233b 100%);
color: white;
border: none;
padding: 15px 40px;
font-size: 16px;
font-weight: 600;
border-radius: 30px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
margin: 10px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(252, 60, 68, 0.4);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn.secondary {
background: rgba(255, 255, 255, 0.2);
}
.btn.secondary:hover {
box-shadow: 0 5px 20px rgba(255, 255, 255, 0.2);
}
.token-box {
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
padding: 15px;
margin: 20px 0;
word-break: break-all;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 11px;
max-height: 150px;
overflow-y: auto;
text-align: left;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.copy-hint {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
margin-top: 10px;
}
.instructions {
text-align: left;
background: rgba(0, 0, 0, 0.2);
border-radius: 10px;
padding: 20px;
margin-top: 20px;
font-size: 14px;
}
.instructions ol {
margin-left: 20px;
}
.instructions li {
margin: 10px 0;
}
.hidden {
display: none;
}
#tokenDisplay {
user-select: all;
}
</style>
</head>
<body>
<div class="container">
<div class="logo">🎵</div>
<h1>Apple Music Authorization</h1>
<p class="subtitle">Connect your Apple Music account to Clawdbot</p>
<div id="statusBox" class="status loading">
Initializing MusicKit...
</div>
<div id="authSection" class="hidden">
<button id="authorizeBtn" class="btn" onclick="authorize()">
Sign in with Apple Music
</button>
<p class="copy-hint">You'll be asked to sign in with your Apple ID</p>
</div>
<div id="successSection" class="hidden">
<p style="margin-bottom: 15px;">✅ Authorization successful!</p>
<p style="font-size: 14px; color: rgba(255,255,255,0.7); margin-bottom: 15px;">
Your Music User Token:
</p>
<div class="token-box">
<span id="tokenDisplay"></span>
</div>
<button class="btn" onclick="copyToken()">Copy Token</button>
<button class="btn secondary" onclick="window.close()">Close Window</button>
<div class="instructions">
<strong>Next steps:</strong>
<ol>
<li>Click "Copy Token" above</li>
<li>Go back to your terminal</li>
<li>Paste the token when prompted</li>
</ol>
</div>
</div>
<div id="errorSection" class="hidden">
<p id="errorMessage"></p>
<button class="btn" onclick="location.reload()">Try Again</button>
</div>
</div>
<script src="https://js-cdn.music.apple.com/musickit/v3/musickit.js" async></script>
<script>
// Get developer token from URL parameter
const urlParams = new URLSearchParams(window.location.search);
const developerToken = urlParams.get('token');
let musicUserToken = null;
function showStatus(message, type) {
const statusBox = document.getElementById('statusBox');
statusBox.textContent = message;
statusBox.className = 'status ' + type;
}
function showSection(sectionId) {
['authSection', 'successSection', 'errorSection'].forEach(id => {
document.getElementById(id).classList.add('hidden');
});
document.getElementById(sectionId).classList.remove('hidden');
}
function showError(message) {
showStatus(message, 'error');
document.getElementById('errorMessage').textContent = message;
showSection('errorSection');
}
// Initialize MusicKit when loaded
document.addEventListener('musickitloaded', async function() {
if (!developerToken) {
showError('No developer token provided. Please run setup.sh again.');
return;
}
try {
await MusicKit.configure({
developerToken: developerToken,
app: {
name: 'Clawdbot Apple Music',
build: '1.0.0'
}
});
showStatus('Ready to authorize', 'success');
showSection('authSection');
} catch (err) {
console.error('MusicKit config error:', err);
showError('Failed to initialize MusicKit: ' + err.message);
}
});
async function authorize() {
const btn = document.getElementById('authorizeBtn');
btn.disabled = true;
btn.textContent = 'Authorizing...';
showStatus('Waiting for Apple Music authorization...', 'loading');
try {
const music = MusicKit.getInstance();
musicUserToken = await music.authorize();
if (musicUserToken) {
document.getElementById('tokenDisplay').textContent = musicUserToken;
showStatus('Authorization successful!', 'success');
showSection('successSection');
} else {
throw new Error('No token received');
}
} catch (err) {
console.error('Authorization error:', err);
btn.disabled = false;
btn.textContent = 'Sign in with Apple Music';
if (err.message.includes('cancelled') || err.name === 'USER_CANCELLED') {
showStatus('Authorization cancelled. Try again when ready.', 'loading');
showSection('authSection');
} else {
showError('Authorization failed: ' + err.message);
}
}
}
function copyToken() {
const token = document.getElementById('tokenDisplay').textContent;
navigator.clipboard.writeText(token).then(() => {
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
}).catch(err => {
// Fallback for older browsers
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(document.getElementById('tokenDisplay'));
selection.removeAllRanges();
selection.addRange(range);
document.execCommand('copy');
alert('Token copied to clipboard!');
});
}
// Timeout if MusicKit doesn't load
setTimeout(() => {
if (!window.MusicKit) {
showError('MusicKit failed to load. Please check your internet connection and try again.');
}
}, 10000);
</script>
</body>
</html>
#!/bin/bash
# Launch setup.sh in Terminal.app for interactive setup
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
osascript <<APPLESCRIPT
tell application "Terminal"
activate
do script "cd '$SKILL_DIR' && ./setup.sh"
end tell
APPLESCRIPT
#!/usr/bin/env node
/**
* Generate Apple Music Developer Token (JWT)
*
* Usage: node generate-token.js <key-path> <key-id> <team-id> [expiry-days]
*
* The token is signed with ES256 and valid for up to 6 months (180 days).
*/
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
function base64UrlEncode(data) {
return Buffer.from(data)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
function generateToken(keyPath, keyId, teamId, expiryDays = 180) {
// Read the private key
let privateKey;
try {
privateKey = fs.readFileSync(keyPath, 'utf8');
} catch (err) {
console.error(`Error: Could not read private key file: ${keyPath}`);
console.error(err.message);
process.exit(1);
}
// Validate expiry (max 6 months = ~180 days)
if (expiryDays > 180) {
console.error('Warning: Apple limits tokens to 6 months. Setting to 180 days.');
expiryDays = 180;
}
const now = Math.floor(Date.now() / 1000);
const expiry = now + (expiryDays * 24 * 60 * 60);
// JWT Header
const header = {
alg: 'ES256',
kid: keyId
};
// JWT Payload
const payload = {
iss: teamId,
iat: now,
exp: expiry
};
// Encode header and payload
const headerEncoded = base64UrlEncode(JSON.stringify(header));
const payloadEncoded = base64UrlEncode(JSON.stringify(payload));
const signingInput = `${headerEncoded}.${payloadEncoded}`;
// Sign with ES256
let signature;
try {
const sign = crypto.createSign('SHA256');
sign.update(signingInput);
sign.end();
// Get the DER signature
const derSignature = sign.sign(privateKey);
// Convert DER to raw r||s format for JWT
// DER format: 0x30 [total-length] 0x02 [r-length] [r] 0x02 [s-length] [s]
const rStart = 4;
const rLength = derSignature[3];
let r = derSignature.slice(rStart, rStart + rLength);
const sStart = rStart + rLength + 2;
const sLength = derSignature[rStart + rLength + 1];
let s = derSignature.slice(sStart, sStart + sLength);
// Remove leading zeros and pad to 32 bytes
if (r.length > 32) r = r.slice(r.length - 32);
if (s.length > 32) s = s.slice(s.length - 32);
const rPadded = Buffer.concat([Buffer.alloc(32 - r.length), r]);
const sPadded = Buffer.concat([Buffer.alloc(32 - s.length), s]);
signature = base64UrlEncode(Buffer.concat([rPadded, sPadded]));
} catch (err) {
console.error('Error: Failed to sign token.');
console.error('Make sure the .p8 file is a valid Apple private key.');
console.error(err.message);
process.exit(1);
}
const token = `${signingInput}.${signature}`;
// Output just the token (for capture by shell script)
console.log(token);
}
// Parse arguments
const args = process.argv.slice(2);
if (args.length < 3) {
console.error('Usage: node generate-token.js <key-path> <key-id> <team-id> [expiry-days]');
console.error('');
console.error('Arguments:');
console.error(' key-path Path to your .p8 private key file');
console.error(' key-id The Key ID from Apple Developer portal');
console.error(' team-id Your Apple Developer Team ID');
console.error(' expiry-days Days until expiration (default: 180, max: 180)');
process.exit(1);
}
const [keyPath, keyId, teamId, expiryDays] = args;
generateToken(keyPath, keyId, teamId, parseInt(expiryDays) || 180);
#!/bin/bash
set -e
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SKILL_DIR/config.json"
TOKEN_SCRIPT="$SKILL_DIR/lib/generate-token.js"
# Minimal output helpers
err() { echo "✗ $1" >&2; }
ok() { echo "✓ $1"; }
info() { echo "→ $1"; }
check_requirements() {
command -v node &>/dev/null || { err "Node.js required (brew install node)"; exit 1; }
command -v curl &>/dev/null || { err "curl required"; exit 1; }
ok "Requirements met"
}
check_existing_config() {
if [[ -f "$CONFIG_FILE" ]]; then
read -p "Config exists. Reconfigure? [y/N] " -n 1 -r
echo
[[ $REPLY =~ ^[Yy]$ ]] || exit 0
fi
}
show_requirements() {
echo ""
echo "Apple Music Setup (requires Developer account)"
echo "Need: .p8 key, Key ID, Team ID"
echo "Get at: developer.apple.com → Keys → MusicKit"
echo ""
read -p "Press Enter when ready (Ctrl+C to cancel)... "
}
get_credentials() {
echo ""
# Get .p8 key path
while true; do
read -e -p "Path to .p8 key: " KEY_PATH
KEY_PATH="${KEY_PATH/#\~/$HOME}"
[[ -f "$KEY_PATH" ]] && break
err "File not found"
done
ok "Key found"
# Auto-detect Key ID from filename
FILENAME=$(basename "$KEY_PATH")
if [[ "$FILENAME" =~ ^AuthKey_([A-Z0-9]{8,12})\.p8$ ]]; then
KEY_ID="${BASH_REMATCH[1]}"
info "Key ID: $KEY_ID (auto-detected)"
else
read -p "Key ID: " KEY_ID
fi
read -p "Team ID: " TEAM_ID
read -p "Storefront [us]: " STOREFRONT
STOREFRONT="${STOREFRONT:-us}"
}
generate_developer_token() {
info "Generating developer token..."
DEVELOPER_TOKEN=$(node "$TOKEN_SCRIPT" "$KEY_PATH" "$KEY_ID" "$TEAM_ID" 180 2>&1)
[[ $? -eq 0 && $(echo "$DEVELOPER_TOKEN" | tr '.' '\n' | wc -l) -eq 3 ]] || { err "Token generation failed"; exit 1; }
ok "Developer token created (6 month validity)"
}
get_user_authorization() {
echo ""
info "Starting auth server..."
# Find available port
AUTH_PORT=8765
while lsof -i:$AUTH_PORT &>/dev/null; do AUTH_PORT=$((AUTH_PORT + 1)); done
# Start HTTP server
python3 -c "
import http.server, socketserver, os
os.chdir('$SKILL_DIR')
with socketserver.TCPServer(('127.0.0.1', $AUTH_PORT), http.server.SimpleHTTPRequestHandler) as httpd:
httpd.serve_forever()
" &>/dev/null &
HTTP_SERVER_PID=$!
sleep 2
# Verify server started
curl -s "http://localhost:$AUTH_PORT/auth.html" >/dev/null || { err "Server failed"; kill $HTTP_SERVER_PID 2>/dev/null; exit 1; }
AUTH_URL="http://localhost:$AUTH_PORT/auth.html?token=$DEVELOPER_TOKEN"
# Open browser (Chrome preferred on macOS)
if command -v open &>/dev/null; then
[[ -d "/Applications/Google Chrome.app" ]] && open -a "Google Chrome" "$AUTH_URL" || open "$AUTH_URL"
elif command -v xdg-open &>/dev/null; then
xdg-open "$AUTH_URL"
else
echo "Open manually: $AUTH_URL"
fi
info "Sign in with Apple Music, then paste the token below"
echo ""
read -p "Music User Token: " MUSIC_USER_TOKEN
[[ -n "$MUSIC_USER_TOKEN" ]] || { err "Token required"; exit 1; }
kill $HTTP_SERVER_PID 2>/dev/null || true
ok "Token received"
}
test_tokens() {
info "Testing API..."
HTTP_CODE=$(curl -s -w "%{http_code}" -o /dev/null \
-H "Authorization: Bearer $DEVELOPER_TOKEN" \
-H "Music-User-Token: $MUSIC_USER_TOKEN" \
"https://api.music.apple.com/v1/me/library/playlists?limit=1")
case "$HTTP_CODE" in
200) ok "API test passed" ;;
401) err "Auth failed - check Key ID/Team ID" ;;
403) err "Invalid token or no Apple Music subscription" ;;
*) err "Unexpected: HTTP $HTTP_CODE" ;;
esac
}
save_config() {
cat > "$CONFIG_FILE" << EOF
{
"developer_token": "$DEVELOPER_TOKEN",
"music_user_token": "$MUSIC_USER_TOKEN",
"key_id": "$KEY_ID",
"team_id": "$TEAM_ID",
"storefront": "$STOREFRONT",
"created_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
}
EOF
chmod 600 "$CONFIG_FILE"
ok "Config saved"
}
show_completion() {
echo ""
ok "Setup complete!"
echo ""
echo "Usage: ./apple-music.sh [command]"
echo "Tokens valid for ~6 months. Re-run setup.sh if auth fails."
echo ""
}
main() {
echo "🎵 Apple Music Setup"
check_requirements
check_existing_config
show_requirements
get_credentials
generate_developer_token
get_user_authorization
test_tokens
save_config
show_completion
}
main