I got tired of having to tell people manually what song I was listening to, so I built something that automatically shares my current Apple Music track on my website. It’s 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 thought it would be great to show what’s playing on my website in real time. I had seen similar features on other developers’ 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 extract the current track information from the app and send it to my website without any official integration.

Here’s the overall architecture I ended up with:

System Architecture

Getting Data Out of Apple Music

The first hurdle was actually obtaining the track information. After some research, I discovered that AppleScript could interact with the Music app. Setting up the authentication was frustrating, but once I had the correct permissions, it was straightforward.

Here’s the AppleScript that retrieves 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, as dealing with AppleScript data structures in Python was too much hassle. This means I can simply parse it as JSON in Python.

$ 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, calling the AppleScript every few seconds. When it detects a new song using the persistent ID, it fetches additional metadata from the iTunes API and posts all the information 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 the timing. To avoid spamming the API, the script calculates how long it will take for the current song to end and then sleeps for that duration plus a few seconds’ buffer time.

The API Backend

The Flask backend is extremely simple: it has 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 authentication because I didn’t want random people to be able to update my ‘Currently Playing’ status. The cache simply stores the latest track information returned to anyone who accesses the GET endpoint.

Running It as a Service

Getting this to run automatically when booting macOS was another minor issue. In the end, I used 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 on Apple Music, it appears on my website within a few seconds, complete with artwork, artist information and a dominant colour extracted from the album art for theming.

Music Player Card

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 front-end JavaScript uses the timestamp to calculate real-time progress, meaning 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:

  1. Set up the Flask backend.
  2. Configure the Python script with your credentials.
  3. Manage macOS permissions for AppleScript.
  4. Set up the launchd service.

This solution is not the most elegant, but it works for my needs. The main limitation is that it only works when Apple Music is running on my Mac. Therefore, if I’m listening on my phone or elsewhere, the website shows nothing.