All widgets

Now playing: Apple Music
Comes with Chopstick
Apple Music while it plays: cover, track, timeline you can scrub, play/pause, skip and shuffle.
Already installed? It's in your widget library.


Type it in the notch's search
- Play Apple Music
- Pause Apple Music
- Next track in Apple Music
- Previous track in Apple Music
- Show Apple Music
What it runs
A widget is a folder of plain files. Here is all of its code.
widget.json
{
"id": "now-playing-music",
"name": "Now playing: Apple Music",
"icon": "app:com.apple.Music",
"description": "Apple Music while it plays: cover, track, timeline you can scrub, play/pause, skip and shuffle.",
"ambient": true,
"watch": [
{
"notification": "com.apple.Music.playerInfo",
"as": "music",
"refresh": "music_live"
},
{
"script": "state.sh",
"every": 2,
"as": "music_live"
}
],
"show_when": {
"music.Player State": "Playing"
},
"initial": "playing",
"anytime": true,
"preview": {
"data": {
"music": {
"Player State": "Playing",
"Name": "Blinding Lights",
"Artist": "The Weeknd",
"Album": "After Hours"
},
"music_live": {
"state": "playing",
"name": "Blinding Lights",
"artist": "The Weeknd",
"album": "After Hours",
"artwork": "https://is1-ssl.mzstatic.com/image/thumb/Music125/v4/6f/bc/e6/6fbce6c4-c38c-72d8-4fd0-66cfff32f679/20UMGIM12176.rgb.jpg/300x300bb.jpg",
"position": 74,
"duration_s": 200,
"shuffling": true,
"shuffle_tint": "pink",
"toggle": "pause.fill"
}
}
},
"commands": {
"play": {
"phrase": "Play Apple Music",
"description": "Start or resume playback in Apple Music.",
"do": [
{
"set": {
"command": "play"
}
},
{
"run": "control.sh"
},
{
"refresh": "music_live"
}
]
},
"pause": {
"phrase": "Pause Apple Music",
"description": "Pause Apple Music.",
"do": [
{
"set": {
"command": "pause"
}
},
{
"run": "control.sh"
},
{
"refresh": "music_live"
}
]
},
"next": {
"phrase": "Next track in Apple Music",
"description": "Skip to the next track in Apple Music.",
"do": [
{
"set": {
"command": "next track"
}
},
{
"run": "control.sh"
},
{
"refresh": "music_live"
}
]
},
"previous": {
"phrase": "Previous track in Apple Music",
"description": "Go back to the previous track in Apple Music.",
"do": [
{
"set": {
"command": "previous track"
}
},
{
"run": "control.sh"
},
{
"refresh": "music_live"
}
]
},
"show": {
"phrase": "Show Apple Music",
"description": "Bring the Music app to the front.",
"do": [
{
"set": {
"command": "activate"
}
},
{
"run": "control.sh"
},
{
"refresh": "music_live"
}
]
}
},
"states": {
"playing": {
"compact": {
"leading": {
"image": "{{music_live.artwork}}"
},
"trailing": {
"equalizer": {
"active_when": {
"music_live.state": "playing"
},
"tint_from_image": "{{music_live.artwork}}"
}
}
},
"expanded": {
"size": "compact",
"blocks": [
{
"row": [
{
"image": "{{music_live.artwork}}",
"size": "medium"
},
{
"stack": [
{
"text": "{{music_live.name | Nothing playing}}",
"style": "title",
"lines": 1
},
{
"text": "{{music_live.artist | Music}}",
"tint": "gray",
"lines": 1
}
],
"spacing": "tight"
},
"spacer",
{
"equalizer": {
"active_when": {
"music_live.state": "playing"
},
"tint_from_image": "{{music_live.artwork}}"
}
}
],
"align": "center"
},
{
"slider": {
"value": "{{music_live.position | 0}}",
"max": "{{music_live.duration_s | 0}}",
"format": "time",
"ticking_when": {
"music_live.state": "playing"
},
"tint_from_image": "{{music_live.artwork}}",
"on_change": [
{
"set": {
"command": "set player position to {{item.value}}"
}
},
{
"run": "control.sh"
},
{
"refresh": "music_live"
}
]
}
},
{
"row": [
"spacer",
{
"button": {
"style": "plain",
"do": [
{
"set": {
"command": "set shuffle enabled to not shuffle enabled"
}
},
{
"run": "control.sh"
},
{
"refresh": "music_live"
}
],
"symbol": "shuffle",
"tint": "{{music_live.shuffle_tint}}"
}
},
"spacer",
{
"button": {
"style": "plain",
"do": [
{
"set": {
"command": "previous track"
}
},
{
"run": "control.sh"
},
{
"refresh": "music_live"
}
],
"symbol": "backward.fill"
}
},
"spacer",
{
"button": {
"style": "plain",
"do": [
{
"set": {
"command": "playpause"
}
},
{
"run": "control.sh"
},
{
"refresh": "music_live"
}
],
"symbol": "{{music_live.toggle | play.fill}}",
"size": "large"
}
},
"spacer",
{
"button": {
"style": "plain",
"do": [
{
"set": {
"command": "next track"
}
},
{
"run": "control.sh"
},
{
"refresh": "music_live"
}
],
"symbol": "forward.fill"
}
},
"spacer",
{
"button": {
"style": "plain",
"do": [
{
"set": {
"command": "activate"
}
},
{
"run": "control.sh"
},
{
"refresh": "music_live"
}
],
"image": "app:com.apple.Music"
}
},
"spacer"
],
"align": "center"
}
]
},
"keys": [
{
"key": "esc",
"do": [
"dismiss"
]
}
]
}
}
}
control.sh
#!/bin/sh # Sends $CHOPSTICK_COMMAND to Music: "playpause", "play", "pause", "next track", # "previous track", "set player position to <seconds>", "activate" or # "set shuffle enabled to not shuffle enabled". Asks once for permission to control Music. osascript -e "tell application id \"com.apple.Music\" to $CHOPSTICK_COMMAND"
music.js
// Prints what Music is playing as JSON. Music has no artwork URL, so the current
// track's cover is written once per track to <storage>/artwork-<track id>.<ext>
// and its path printed; covers of earlier tracks are removed.
ObjC.import('Foundation')
function run(argv) {
const m = Application('com.apple.Music'), state = m.playerState()
const out = { state, toggle: state === 'playing' ? 'pause.fill' : 'play.fill' }
try {
const shuffling = m.shuffleEnabled()
Object.assign(out, { shuffling, shuffle_tint: shuffling ? 'pink' : '' })
} catch (e) {}
let t
try { t = m.currentTrack(); t.name() } catch (e) { return JSON.stringify(out) }
Object.assign(out, {
name: t.name(), artist: t.artist(), album: t.album(),
position: Math.round(m.playerPosition() || 0), duration_s: Math.round(t.duration() || 0),
artwork: cover(t, argv[0]) || 'app:com.apple.Music'
})
return JSON.stringify(out)
}
// The cover's file path, written now if this track's isn't there yet; '' if it has none.
function cover(track, storage) {
if (!storage) return ''
const fm = $.NSFileManager.defaultManager
const id = track.persistentID()
for (const ext of ['jpg', 'png']) {
const path = `${storage}/artwork-${id}.${ext}`
if (fm.fileExistsAtPath(path)) return path
}
let ext
try {
const art = track.artworks[0]
ext = String(art.format()).toLowerCase().includes('png') ? 'png' : 'jpg'
} catch (e) { return '' }
const path = `${storage}/artwork-${id}.${ext}`, tmp = `${path}.part`
fm.createDirectoryAtPathWithIntermediateDirectoriesAttributesError(storage, true, $(), null)
// JXA can't write raw artwork data; AppleScript can.
const quoted = tmp.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
$.NSAppleScript.alloc.initWithSource(`
tell application id "com.apple.Music" to set d to raw data of artwork 1 of current track
set f to open for access (POSIX file "${quoted}") with write permission
try
set eof f to 0
write d to f
end try
close access f`).executeAndReturnError(null)
const size = fm.fileExistsAtPath(tmp) ? Number(fm.attributesOfItemAtPathError(tmp, null).fileSize) : 0
const names = ObjC.deepUnwrap(fm.contentsOfDirectoryAtPathError(storage, null)) || []
for (const n of names) if (n.startsWith('artwork-') && `${storage}/${n}` !== tmp) fm.removeItemAtPathError(`${storage}/${n}`, null)
if (!size) { fm.removeItemAtPathError(tmp, null); return '' }
fm.moveItemAtPathToPathError(tmp, path, null)
return path
}
state.sh
#!/bin/sh
# Everything the widget shows, read from Music as JSON: state, track, cover,
# position, length, shuffle. Prints {} without launching Music if it's closed.
pgrep -xq Music || { echo '{}'; exit 0; }
osascript -l JavaScript music.js "$CHOPSTICK_STORAGE" 2>/dev/null || echo '{}'