mirror of
https://github.com/MioMioOS/mio-plugin-music.git
synced 2026-06-11 03:44:31 +00:00
Ports the MediaRemoteAdapter pattern from Atoll (github.com/Ebullioscopic/Atoll). On macOS 15.4+, Apple gated MRMediaRemoteGetNowPlayingInfo behind a private entitlement, which made our previous MediaRemoteSource return empty dicts and forced us onto slow-path AppleScript polling. This commit bundles Jonas van den Berg's MediaRemoteAdapter.framework (BSD-3-Clause) plus mediaremote-adapter.pl and runs them as a subprocess — the framework links against Apple's MR in a way that skips the caller-side entitlement check, so we get the full now-playing payload (title, artist, album, duration, elapsed, isPlaying, artwork, bundleIdentifier) pushed to us in real time. Bundle additions (~500KB total): - Resources/MediaRemoteAdapter.framework (universal x86_64 + arm64 + arm64e) - Resources/mediaremote-adapter.pl - LICENSE-THIRD-PARTY.md with full BSD-3-Clause attribution New source: MediaRemoteAdapterSource.swift - Spawns /usr/bin/perl with minimal env (PATH + LANG only). - FileHandle.readabilityHandler ingests newline-delimited JSON stream from stdout, parses via Codable AdapterStreamPayload, merges diffs into persistent MediaRemoteInfo so playbackRate-only payloads don't erase title/artist. - Artwork base64 decoded via Data default strategy. - Crash handling: SIGTERM → 500ms → SIGKILL on stop. Auto-restart with exponential backoff (1s/2s/4s), circuit-breaker after 3 crashes within 60s → fall back to legacy chain. - Transport controls (togglePlay/next/prev/seek) via short-lived one-shot `perl adapter.pl send N` subprocesses. send codes: 2=toggle, 4=next, 5=prev. seek takes microseconds. NowPlayingState wiring: - New sticky kind `.mediaRemoteAdapter`, highest priority. - `applyAdapterUpdate(_:)` publishes directly (no router pass). - `routeSources` short-circuits when adapter is sticky + has data — subprocess pushes fresh data on every change, polling would be pure waste. - `adaptivePollInterval()` returns 30s for adapter (safety net only). - `isCandidateLive` + `tryFetch` treat adapter as push-only (returns nil from pull-fetch so the sticky fast-path falls through to parallel probing if subprocess is dead). - `stop()` terminates the subprocess cleanly. - Transport controls route to adapter.sendCommand() / adapter.seek() when it's the sticky source. Build: - build.sh copies Resources/ into Contents/Resources with preserved exec bits on the framework binary + Perl script. - `codesign --force --deep --sign -` re-signs the whole tree ad-hoc so the nested framework inherits our identity and Gatekeeper loads it without complaint. - Bundle grew from 48KB → 1.6MB (zipped 564KB). Acceptable for the latency win: Apple Music track switches now visible <100ms vs prior 800ms adaptive-poll worst case. Security audit (done before bundling): - Perl script: strict + warnings, whitelisted function names, no shell-out, no network I/O, params passed to framework via ENV (no string concat). Safe. - Framework: ad-hoc signed (Identifier com.vandenbe.MediaRemoteAdapter). --deep re-sign with our identity replaces the original ad-hoc cert so signature validation passes locally and in Gatekeeper. - Subprocess runs with PATH=/usr/bin:/bin + LANG only. No inherited secrets. - Explicit Process arguments array — no shell interpolation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
129 lines
2.4 KiB
XML
129 lines
2.4 KiB
XML
<?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>files</key>
|
|
<dict>
|
|
<key>Resources/Info.plist</key>
|
|
<data>
|
|
M6AF1VWVJ1A/DSliCSjg170FqsY=
|
|
</data>
|
|
</dict>
|
|
<key>files2</key>
|
|
<dict>
|
|
<key>Resources/Info.plist</key>
|
|
<dict>
|
|
<key>hash2</key>
|
|
<data>
|
|
z3yWmTAqjdrPJEZUQ+t6AVPhw0e/I8PAiVr0HIU2ivg=
|
|
</data>
|
|
</dict>
|
|
</dict>
|
|
<key>rules</key>
|
|
<dict>
|
|
<key>^Resources/</key>
|
|
<true/>
|
|
<key>^Resources/.*\.lproj/</key>
|
|
<dict>
|
|
<key>optional</key>
|
|
<true/>
|
|
<key>weight</key>
|
|
<real>1000</real>
|
|
</dict>
|
|
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
|
<dict>
|
|
<key>omit</key>
|
|
<true/>
|
|
<key>weight</key>
|
|
<real>1100</real>
|
|
</dict>
|
|
<key>^Resources/Base\.lproj/</key>
|
|
<dict>
|
|
<key>weight</key>
|
|
<real>1010</real>
|
|
</dict>
|
|
<key>^version.plist$</key>
|
|
<true/>
|
|
</dict>
|
|
<key>rules2</key>
|
|
<dict>
|
|
<key>.*\.dSYM($|/)</key>
|
|
<dict>
|
|
<key>weight</key>
|
|
<real>11</real>
|
|
</dict>
|
|
<key>^(.*/)?\.DS_Store$</key>
|
|
<dict>
|
|
<key>omit</key>
|
|
<true/>
|
|
<key>weight</key>
|
|
<real>2000</real>
|
|
</dict>
|
|
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
|
|
<dict>
|
|
<key>nested</key>
|
|
<true/>
|
|
<key>weight</key>
|
|
<real>10</real>
|
|
</dict>
|
|
<key>^.*</key>
|
|
<true/>
|
|
<key>^Info\.plist$</key>
|
|
<dict>
|
|
<key>omit</key>
|
|
<true/>
|
|
<key>weight</key>
|
|
<real>20</real>
|
|
</dict>
|
|
<key>^PkgInfo$</key>
|
|
<dict>
|
|
<key>omit</key>
|
|
<true/>
|
|
<key>weight</key>
|
|
<real>20</real>
|
|
</dict>
|
|
<key>^Resources/</key>
|
|
<dict>
|
|
<key>weight</key>
|
|
<real>20</real>
|
|
</dict>
|
|
<key>^Resources/.*\.lproj/</key>
|
|
<dict>
|
|
<key>optional</key>
|
|
<true/>
|
|
<key>weight</key>
|
|
<real>1000</real>
|
|
</dict>
|
|
<key>^Resources/.*\.lproj/locversion.plist$</key>
|
|
<dict>
|
|
<key>omit</key>
|
|
<true/>
|
|
<key>weight</key>
|
|
<real>1100</real>
|
|
</dict>
|
|
<key>^Resources/Base\.lproj/</key>
|
|
<dict>
|
|
<key>weight</key>
|
|
<real>1010</real>
|
|
</dict>
|
|
<key>^[^/]+$</key>
|
|
<dict>
|
|
<key>nested</key>
|
|
<true/>
|
|
<key>weight</key>
|
|
<real>10</real>
|
|
</dict>
|
|
<key>^embedded\.provisionprofile$</key>
|
|
<dict>
|
|
<key>weight</key>
|
|
<real>20</real>
|
|
</dict>
|
|
<key>^version\.plist$</key>
|
|
<dict>
|
|
<key>weight</key>
|
|
<real>20</real>
|
|
</dict>
|
|
</dict>
|
|
</dict>
|
|
</plist>
|