I got tired of manually telling people what song I was listening to, so I built a thing that automatically shares my current Apple Music track on my website. Nothing fancy, but it solved my problem.
The Problem I Wanted to Solve
I spend a lot of time listening to music while coding, and I thought it would be cool to have my website show what’s currently playing in real-time. I’d seen similar features on other developer sites and wanted something similar for my own.
The challenge was that Apple Music doesn’t have a public API for this kind of thing. I needed to somehow extract the current track info from the app and get it to my website without any official integration.
Here’s the overall architecture I ended up with:
Getting Data Out of Apple Music
The first hurdle was actually getting the track information. After some research, I discovered AppleScript could interact with the Music app. Getting the auth working was annoying, but once I figured out the right permissions, it was straightforward.
Here’s the AppleScript that grabs the current track:
tell application "Music"
if it is running then
if player state is playing then
set pState to player state
set pPosition to player position
set cTrack to current track
set trackInfo to "{\"status\": \"playing\", \"persistent ID\": \"" & persistent ID of cTrack & "\", \"name\": \"" & name of cTrack & "\", \"time\": \"" & time of cTrack & "\", \"duration\": \"" & duration of cTrack & "\", \"artist\": \"" & artist of cTrack & "\", \"album\": \"" & album of cTrack & "\" }"
return trackInfo
else
return "{\"status\": \"not playing\"}"
end if
else
return "{\"status\": \"not running\"}"
end if
end tell
The script returns JSON directly because dealing with AppleScript data structures in Python was more trouble than it was worth. This way I can just parse it as JSON on the Python side.
$ osascript export.applescript
{"status": "not playing"}
$ osascript export.applescript
{"status": "playing", "persistent ID": "1A3BB81D89DE1B39", "name": "SAUDADE", "time": "2:21", "duration": "141,429000854492", "artist": "Dexa. & 6minaprès", "album artist": "Dexa.", "composer": "Gael six minutes, 6astardcanrise canr rise & Dexa Dexa Dexa", "album": "Station d'Atlas - EP", "genre": "Pop", "played count": "5", "pState": "playing", "pPosition": "1,957999944687" }
Building the Bridge
The Python script runs continuously and calls the AppleScript every few seconds. When it detects a new song (using the persistent ID), it fetches additional metadata from iTunes API and posts everything to my Flask backend:
def get_current_song():
try:
output = subprocess.check_output(['osascript', 'export.applescript']).decode('utf-8').strip()
return output
except subprocess.CalledProcessError as e:
printerr(e)
return e
def main():
persistendId = ''
prevstatus = ''
while True:
currentsong = json.loads(get_current_song())
if currentsong['status'] == 'playing':
if currentsong['persistent ID'] != persistendId:
persistendId = currentsong['persistent ID']
currentsong['timestamp'] = time.time()
# Fetch artwork and iTunes URLs from iTunes API
(currentsong['artwork_url'], currentsong['itunes_url'], currentsong['artist_url']) = get_track_extras(currentsong['name'], currentsong['artist'], currentsong['album'])
# Extract dominant color from artwork for UI theming
currentsong['dominantcolor'] = get_dominant_color_from_url(currentsong['artwork_url'])
printout(f"{post(currentsong)}")
The script logs everything so I can see what’s happening:
14:23:45 : {"message": "Content set successfully."}
14:27:12 : {"message": "Content set successfully."}
14:30:38 : {"message": "Content set successfully."}
One interesting challenge was timing. I didn’t want to spam the API, so the script calculates how long until the current song ends and sleeps for that duration (plus a few seconds buffer).
The API Backend
The Flask backend is dead simple - just two endpoints and an in-memory cache:
from flask import Flask, request, jsonify
import json
import hashlib
app = Flask(__name__)
cache = {}
@app.route('/music/set', methods=['POST'])
def set_content():
data = request.get_json()
user = data.get('user')
password = data.get('password')
if user in users and users[user] == hashlib.sha256(password.encode()).hexdigest():
cache.update(data)
cache.pop('user', None) # Don't store credentials
cache.pop('password', None)
return jsonify({'message': f'Content set successfully.'})
else:
return jsonify({'message': 'Invalid user or password.'}), 401
@app.route('/music/get', methods=['GET'])
def display_content():
return jsonify(cache)
I added basic auth because I didn’t want random people updating my currently playing status. The cache just holds the latest track info that gets returned to anyone who hits the GET endpoint.
Running It as a Service
Getting this to run automatically on macOS boot was another small headache. I ended up using launchd with a plist file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.music-exp</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/python3</string>
<string>/Users/noham/Documents/export.py</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/noham/Documents</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
What I Ended Up With
The system works pretty well. When I play a song in Apple Music, within a few seconds it shows up on my website with the artwork, artist info, and even a dominant color extracted from the album art for theming.
Here’s what the API returns:
{
"album": "Station d'Atlas - EP",
"album artist": "Dexa.",
"artist": "Dexa.",
"artist_url": "https://music.apple.com/us/artist/dexa/1534658078?uo=4",
"artwork_url": "https://is1-ssl.mzstatic.com/image/thumb/Music221/v4/9f/ca/fc/9fcafcb7-a3a1-5c4d-31f0-86dbfcc016ec/artwork.jpg/200x200bb.jpg",
"composer": "Dexa Dexa Dexa & Yanis Yvvid Prod",
"dominantcolor": [132, 69, 13],
"duration": "139,285995483398",
"genre": "Pop",
"itunes_url": "https://music.apple.com/us/album/sadida/1820035593?i=1820035597&uo=4",
"name": "SADIDA",
"pPosition": "81,157997131348",
"pState": "playing",
"persistent ID": "E942F8A52AEAEA1E",
"played count": "5",
"status": "playing",
"time": "2:19",
"timestamp": 1751116587.6413
}
The frontend JavaScript uses the timestamp to calculate real-time progress, so the progress bar moves even if the API isn’t updated every second.
If You Want to Try It
The code is on GitHub if you’re interested. Fair warning - it’s pretty specific to my setup and macOS. You’ll need to:
- Set up the Flask backend somewhere
- Configure the Python script with your credentials
- Deal with macOS permissions for AppleScript
- Set up the launchd service
It’s not the most elegant solution, but it works for what I needed. The main limitation is that it only works when Apple Music is running on my Mac, so if I’m listening on my phone or elsewhere, the website shows nothing.