Adapter für alles inklusive Browser, AppleScript für Spotify und Musik, Medientasten als letzte Reserve. Die Reihenfolge ist keine Vorliebe, sondern Absicherung: Apple hat die Now-Playing-Schnittstelle in macOS 15.4 schon einmal zugemacht. Fällt der Adapter aus, verliert das Widget seine Anzeige — aber nicht seine Funktion. Deshalb bleiben die Transporttasten auch dann bedienbar, wenn gar keine Metadaten da sind. Der Fortschritt zählt zwischen den Ereignissen selbst hoch. Spike B hatte gezeigt, dass elapsedTime nicht laufend nachgeliefert wird: ein Scrubber, der stur den letzten Wert anzeigt, steht still, während der Titel läuft. Gerechnet wird aus asOf und playbackRate, begrenzt auf die Titellänge — sonst zeigt die Anzeige nach dem Ende weiter steigende Zahlen. Teilmeldungen (diff = true) ergänzen den Stand, statt ihn zu ersetzen. Eine Meldung mit nur der Position würde sonst Titel, Interpret und Cover löschen. In AnyDecodable wird Bool vor Double geprüft. Andernfalls wird "playing": true zu 1 und die Wiedergabe gilt für immer als angehalten. Play/Pause schaltet sofort um, statt auf das nächste Ereignis zu warten. Ein Knopf, der eine halbe Sekunde später reagiert, fühlt sich kaputt an; das echte Ereignis korrigiert den Stand ohnehin. Der Adapter liegt als Quellcode unter Vendor/ (BSD-3, Stand in ONYX-VERSION.txt) und wird beim Bauen kompiliert, statt als fertige Binary eingecheckt zu werden. Er wird bewusst nicht gelinkt: geladen wird er von /usr/bin/perl, im eigenen Prozess greift Apples Entitlement-Prüfung. 124 Tests grün.
63 lines
1.8 KiB
Python
Executable File
63 lines
1.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import subprocess
|
|
import urllib.parse
|
|
import os
|
|
import re
|
|
|
|
|
|
STATIC_BADGES = """
|
|

|
|
"""
|
|
|
|
|
|
def get_output(command):
|
|
result = subprocess.run(
|
|
command,
|
|
shell=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL,
|
|
text=True,
|
|
)
|
|
return result.stdout.strip()
|
|
|
|
|
|
def normalize_whitespace(text):
|
|
return re.sub(r'\s+', ' ', text.strip())
|
|
|
|
|
|
def generate_badges():
|
|
system_version = get_output(
|
|
"system_profiler SPSoftwareDataType | sed -En 's/.*System Version: *//p'"
|
|
)
|
|
date_output = normalize_whitespace(get_output("date"))
|
|
encoded_system_version = urllib.parse.quote(system_version)
|
|
encoded_date = urllib.parse.quote(date_output)
|
|
badge1 = f""
|
|
badge2 = f""
|
|
return f"{STATIC_BADGES.strip()}\n{badge1}\n{badge2}"
|
|
|
|
|
|
def update_readme(script_dir, new_badges):
|
|
readme_path = os.path.join(script_dir, "..", "README.md")
|
|
with open(readme_path, "r") as file:
|
|
content = file.read()
|
|
new_content = re.sub(
|
|
r"<!-- BADGES BEGIN -->.*?<!-- BADGES END -->",
|
|
f"<!-- BADGES BEGIN -->\n{new_badges}\n<!-- BADGES END -->",
|
|
content,
|
|
flags=re.DOTALL,
|
|
)
|
|
with open(readme_path, "w") as file:
|
|
file.write(new_content)
|
|
|
|
|
|
def main():
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
badges = generate_badges()
|
|
update_readme(script_dir, badges)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|