J’ai construit un truc qui partage automatiquement ma musique Apple Music actuelle sur mon site web. Rien de fancy, mais ça a résolu mon problème.

Le problème que je voulais résoudre

Je passe beaucoup de temps à écouter de la musique en codant, et je trouvais que ce serait cool d’avoir mon site web qui affiche ce qui joue actuellement en temps réel. J’avais vu des fonctionnalités similaires sur d’autres sites de développeurs et je voulais quelque chose de similaire pour le mien.

Le défi était qu’Apple Music n’a pas d’API publique pour ce genre de chose. Je devais trouver un moyen d’extraire les infos de la musique actuelle depuis l’app et les faire arriver sur mon site web sans aucune intégration officielle.

Voici l’architecture globale avec laquelle j’ai fini :

Architecture du système

Récupérer les données d’Apple Music

Le premier obstacle était de récupérer les informations de la musique. Après quelques recherches, j’ai découvert qu’AppleScript pouvait interagir avec l’app Music. Faire fonctionner l’auth était chiant, mais une fois que j’ai compris les bonnes permissions, c’était simple.

Voici l’AppleScript qui récupère la musique actuelle :

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

Le script retourne du JSON directement parce que gérer les structures de données AppleScript en Python était plus compliqué que ça en valait la peine. De cette façon je peux juste le parser comme du JSON côté 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" }

Construire le pont

Le script Python tourne en continu et appelle l’AppleScript toutes les quelques secondes. Quand il détecte une nouvelle chanson (en utilisant l’ID persistant), il récupère des métadonnées supplémentaires depuis l’API iTunes et envoie tout à mon backend Flask :

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()
                # Récupère l'artwork et les URLs iTunes depuis l'API iTunes
                (currentsong['artwork_url'], currentsong['itunes_url'], currentsong['artist_url']) = get_track_extras(currentsong['name'], currentsong['artist'], currentsong['album'])
                # Extrait la couleur dominante de l'artwork pour le thème UI
                currentsong['dominantcolor'] = get_dominant_color_from_url(currentsong['artwork_url'])
                printout(f"{post(currentsong)}")

Le script log tout pour que je puisse voir ce qui se passe :

14:23:45 : {"message": "Content set successfully."}
14:27:12 : {"message": "Content set successfully."}
14:30:38 : {"message": "Content set successfully."}

Un défi intéressant était le timing. Je ne voulais pas spammer l’API, alors le script calcule combien de temps il reste jusqu’à la fin de la chanson actuelle et dort pendant cette durée (plus quelques secondes de buffer).

Le backend API

Le backend Flask est super simple - juste deux endpoints et un cache en mémoire :

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)  # Ne stocke pas les 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)

J’ai ajouté une auth basique parce que je ne voulais pas que des gens random mettent à jour mon statut de lecture. Le cache contient juste les dernières infos de la musique qui sont retournées à quiconque hit l’endpoint GET.

Le faire tourner comme un service

Faire en sorte que ça se lance automatiquement au boot de macOS était une autre petite galère. J’ai fini par utiliser launchd avec un fichier plist :

<?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>

Ce avec quoi j’ai fini

Le système marche plutôt bien. Quand je joue une chanson dans Apple Music, en quelques secondes elle apparaît sur mon site web avec l’artwork, les infos de l’artiste, et même une couleur dominante extraite de l’art de l’album pour le thème.

Carte du lecteur musical

Voici ce que l’API retourne :

{
  "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
}

Le JavaScript frontend utilise le timestamp pour calculer le progrès en temps réel, donc la barre de progression bouge même si l’API n’est pas mise à jour chaque seconde.

Si tu veux l’essayer

Le code est sur GitHub si ça t’intéresse. Attention - c’est assez spécifique à mon setup et macOS. Tu devras :

  1. Mettre en place le backend Flask quelque part
  2. Configurer le script Python avec tes credentials
  3. Gérer les permissions macOS pour AppleScript
  4. Configurer le service launchd

C’est pas la solution la plus élégante, mais ça marche pour ce dont j’avais besoin. La limitation principale est que ça ne marche que quand Apple Music tourne sur mon Mac, donc si j’écoute sur mon téléphone ou ailleurs, le site web n’affiche rien.