Warning
This project is still in development and the API may break across versions.
Control and observe media playback from the command line.
Available for all macOS versions, including the latest macOS Tahoe.
You can install media-control
via
Homebrew:
$ brew install media-control
$ media-control get # Get now playing information once
$ media-control get -h # More readable alternative, not for scripting
$ media-control stream # Stream now playing updates in real-time
$ media-control toggle-play-pause # Toggle playback
$ media-control # Print help
- Display the current song in your menu bar
- Dump the cover image once
- Dump all cover images continuously during listening
- Watch for song changes
- Live view of the current timeline position
- Pause media playback once the current track ends
- Skip Spotify ads by reopening the app whenever an ad is detected
You can display your current song using xbar:
Copy the script to your plugins directory:
cp ./examples/xbar-now-playing.1s.py ~/Library/Application\ Support/xbar/plugins
chmod +x ~/Library/Application\ Support/xbar/plugins/xbar-now-playing.1s.py
Open the xbar app and you should see it appear in your menu bar!
This saves the cover artwork of the current song and opens it in the Preview app.
media-control get | \
jq -r .artworkData | \
base64 -d > cover && \
open -a Preview cover
# To determine the file extension:
mv cover "cover.$(file -b cover | sed 's/ .*$//' | tr A-Z a-z)"
Watches all changes and saves each new cover to a separate image file.
index=0; media-control stream | \
while IFS= read -r line; do \
if jq -e .payload.artworkData <<< "$line" >/dev/null; then \
jq -r .payload.artworkData <<< "$line" | base64 -d > cover; \
mv cover "cover-$index.$(file -b cover | sed 's/ .*$//' | tr A-Z a-z)"; \
((index++)); \
fi \
done
This only prints whenever the song itself is changing, i.e. either the application, song title, artist or album name.
media-control stream | \
while IFS= read -r line; do \
jq -r 'if .diff == false then
"\(now | strftime("%H:%M:%S")) (\(.payload.bundleIdentifier
)) \(.payload.title) - \(.payload.artist)"
else
empty
end' <<< "$line"; \
done
18:52:07 (com.spotify.client) Central Park - VELVETEARS
18:54:41 (com.spotify.client) A House Divided - Zander Hawley
18:55:24 (com.spotify.client) redesign - awfultune
18:57:58 (com.spotify.client) look around - Øneheart
Displays the current song and updates it in real-time, including timeline changes:
/usr/bin/python3 ./examples/now-playing-live.py
(com.spotify.client) ▶ 00:46/04:58 Happier Than Ever - Billie Eilish
index=0; media-control stream | \
while IFS= read -r line; do \
if jq -e ".diff == false" <<< "$line" >/dev/null; then \
if [ "$index" -gt 0 ]; then \
media-control pause; \
exit 0; \
fi; \
((index++)); \
fi \
done
No more ads with a vanilla Spotify installation. This makes use of the fact that with advertisments the album name is always an empty string. This script watches for ads, closes the app when an ad is detected, reopens it and starts playback again.
It's so fast, you don't even notice that it was reopened!
The app opens below other windows, so you don't get disturbed.
media-control stream --no-diff | \
while IFS= read -r line; do \
bundle_id="com.spotify.client"; \
if jq -e ".payload.bundleIdentifier != \"$bundle_id\"" <<< "$line" >/dev/null; then \
continue; \
fi; \
if jq -e '(.payload.album != "")' <<< "$line" >/dev/null; then \
continue; \
fi; \
echo "Detected advertisement for $bundle_id, closing app"; \
osascript -e "tell application id \"$bundle_id\" to quit"; \
while [ "$(osascript -e "application id \"$bundle_id\" is running")" = "true" ]; do \
sleep 0.1; \
done; \
echo "Reopening $bundle_id"; \
osascript -e "tell application id \"$bundle_id\" to launch"; \
echo "Waiting for $bundle_id to be the now playing application"; \
while true; do \
osascript -e "tell application id \"$bundle_id\" to play"; \
sleep 0.1; \
if media-control get | jq -e '.bundleIdentifier == "com.spotify.client"' >/dev/null; then \
echo "Spotify is now playing"; \
break; \
fi; \
done; \
done
This project is licensed under the BSD 3-Clause License. See LICENSE for details.
Copyright (c) 2025 Jonas van den Berg