Phase 4: Medien-Widget mit dreistufiger Kaskade
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.
This commit is contained in:
529
Vendor/mediaremote-adapter/README.md
vendored
Normal file
529
Vendor/mediaremote-adapter/README.md
vendored
Normal file
@@ -0,0 +1,529 @@
|
||||
> **@Apple** *Before breaking this,
|
||||
> please consider giving Mac users the option
|
||||
> to share actively playing media with the apps they use
|
||||
> and to control media playback.
|
||||
> Perhaps by introducing a new entitlement
|
||||
> that can be granted to apps by users in the system settings.
|
||||
> There are
|
||||
> [many](https://musicpresence.app)
|
||||
> [use](https://folivora.ai)
|
||||
> [cases](https://lyricfever.com)
|
||||
> [for](https://theboring.name)
|
||||
> [this](https://github.com/kirtan-shah/nowplaying-cli).*
|
||||
|
||||
> **@Developers** *Please **star**
|
||||
> this repository to show Apple that we care.*
|
||||
|
||||
---
|
||||
|
||||
<!-- BADGES BEGIN -->
|
||||

|
||||

|
||||

|
||||
<!-- BADGES END -->
|
||||
|
||||
# MediaRemote Adapter
|
||||
|
||||
Get now playing information using the MediaRemote framework
|
||||
on all macOS versions, including 15.4 and above.
|
||||
|
||||
This works by using a system binary – `/usr/bin/perl` in this case –
|
||||
which is entitled to use the MediaRemote framework
|
||||
and by dynamically loading a custom helper framework
|
||||
that prints real-time updates to stdout.
|
||||
|
||||
## Example
|
||||
|
||||
Install the [media-control](https://github.com/ungive/media-control)
|
||||
CLI tool to see this project in action. Works on all macOS versions:
|
||||
|
||||
```
|
||||
$ brew tap ungive/media-control
|
||||
$ brew install media-control
|
||||
$ media-control stream
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
This project provides a Perl script
|
||||
with a well-defined CLI interface that you can invoke from your app
|
||||
in order to read now playing information and control media players.
|
||||
The [mediaremote-adapter.pl](./bin/mediaremote-adapter.pl) script
|
||||
needs to be bundled with your app,
|
||||
alongside the `MediaRemoteAdapter.framework`
|
||||
and optionally the `MediaRemoteAdapterTestClient`
|
||||
which are exposed as CMake targets in [CMakeLists.txt](./CMakeLists.txt).
|
||||
You can find instructions to build the framework in the next section.
|
||||
|
||||
The script must then be invoked like this:
|
||||
|
||||
```
|
||||
/usr/bin/perl /path/to/mediaremote-adapter.pl /path/to/MediaRemoteAdapter.framework COMMAND
|
||||
```
|
||||
|
||||
`COMMAND` is a placeholder for one of the commands documented below.
|
||||
|
||||
For the `test` command the `NowPlayingTestCMediaRemoteAdapterTestClientlient` must be passed
|
||||
as an additional argument:
|
||||
|
||||
```
|
||||
/usr/bin/perl /path/to/mediaremote-adapter.pl /path/to/MediaRemoteAdapter.framework /path/to/MediaRemoteAdapterTestClient test
|
||||
```
|
||||
|
||||
For ease of use you can also always pass the path to the test client:
|
||||
|
||||
```
|
||||
/usr/bin/perl /path/to/mediaremote-adapter.pl /path/to/MediaRemoteAdapter.framework /path/to/MediaRemoteAdapterTestClient COMMAND
|
||||
```
|
||||
|
||||
For more help on available commands read below or omit the `COMMAND` argument.
|
||||
|
||||
> [!WARNING]
|
||||
> This project is still in development
|
||||
> and the API may experience breaking changes across minor revisions.
|
||||
|
||||
> [!NOTE]
|
||||
> For a maintained Swift package look at this excellent fork:
|
||||
> https://github.com/ejbills/mediaremote-adapter
|
||||
|
||||
## Build from source
|
||||
|
||||
```
|
||||
$ git clone https://github.com/ungive/mediaremote-adapter.git
|
||||
$ cd mediaremote-adapter
|
||||
$ mkdir build && cd build
|
||||
$ cmake ..
|
||||
$ cmake --build .
|
||||
$ cd ..
|
||||
$ FRAMEWORK_PATH=$(realpath ./build/MediaRemoteAdapter.framework)
|
||||
$ /usr/bin/perl ./bin/mediaremote-adapter.pl "$FRAMEWORK_PATH" stream
|
||||
```
|
||||
|
||||
This creates the `MediaRemoteAdapter.framework` in the build directory,
|
||||
which must be *bundled* with your app, but *not linked against*.
|
||||
The framework is only used by the script
|
||||
and must merely be passed as a script argument.
|
||||
|
||||
If you want to be able to test whether the adapter still works,
|
||||
which can be useful to e.g. automatically fall back to AppleScript,
|
||||
you need to also bundle the `MediaRemoteAdapterTestClient` executable with your app
|
||||
and pass it as an additional argument:
|
||||
|
||||
```
|
||||
$ HELPER_PATH=$(realpath ./build/MediaRemoteAdapterTestClient)
|
||||
$ /usr/bin/perl ./bin/mediaremote-adapter.pl "$FRAMEWORK_PATH" "$HELPER_PATH" test
|
||||
```
|
||||
|
||||
An exit code of `0` then means the adapter is functional and safe to use.
|
||||
|
||||
The framework and test executable are built for the following architectures:
|
||||
`x86_64` `arm64`
|
||||
|
||||
## Commands
|
||||
|
||||
- [get](#get)
|
||||
- [stream](#stream)
|
||||
- [send COMMAND](#send-command)
|
||||
- [seek POSITION](#seek-position)
|
||||
- [shuffle MODE](#shuffle-mode)
|
||||
- [repeat MODE](#repeat-mode)
|
||||
- [speed SPEED](#speed-speed)
|
||||
- [test](#test)
|
||||
|
||||
### get
|
||||
|
||||
Prints now playing information once with all available metadata.
|
||||
|
||||
Output is encoded as JSON and characterized by either `null`
|
||||
or a dictionary with any of the following keys:
|
||||
|
||||
> `bundleIdentifier`
|
||||
`parentApplicationBundleIdentifier`
|
||||
`playing`
|
||||
`title`
|
||||
`artist`
|
||||
`album`
|
||||
`duration`
|
||||
`elapsedTime`
|
||||
`timestamp`
|
||||
`artworkMimeType`
|
||||
`artworkData`
|
||||
`chapterNumber`
|
||||
`composer`
|
||||
`genre`
|
||||
`isAdvertisement`
|
||||
`isBanned`
|
||||
`isInWishList`
|
||||
`isLiked`
|
||||
`isMusicApp`
|
||||
`playbackRate`
|
||||
`prohibitsSkip`
|
||||
`queueIndex`
|
||||
`radioStationIdentifier`
|
||||
`repeatMode`
|
||||
`shuffleMode`
|
||||
`startTime`
|
||||
`supportsFastForward15Seconds`
|
||||
`supportsIsBanned`
|
||||
`supportsIsLiked`
|
||||
`supportsRewind15Seconds`
|
||||
`totalChapterCount`
|
||||
`totalDiscCount`
|
||||
`totalQueueCount`
|
||||
`totalTrackCount`
|
||||
`trackNumber`
|
||||
`uniqueIdentifier`
|
||||
`contentItemIdentifier`
|
||||
`radioStationHash`
|
||||
`mediaType`
|
||||
|
||||
The following mandatory keys never have a null value:
|
||||
`bundleIdentifier`
|
||||
`playing`
|
||||
`title`.
|
||||
If any of the mandatory keys cannot be determined,
|
||||
the command prints `null`.
|
||||
Media without a title is considered invalid.
|
||||
|
||||
The `mediaType` may contain one of the following values:
|
||||
- `MRMediaRemoteMediaTypeMusic`
|
||||
- `kMRMediaRemoteNowPlayingInfoTypeAudio`
|
||||
- Possibly others, this key is not very well documented
|
||||
|
||||
**Caveats**
|
||||
|
||||
Metadata such as `artworkData` and `artworkMimeType`
|
||||
often takes a bit of time to load
|
||||
and may not appear in the output in all cases.
|
||||
Do not rely on this key to be present reliably.
|
||||
Either use the `stream` command or poll `get` regularly,
|
||||
to ensure you get the artwork data *eventually*.
|
||||
|
||||
**Options**
|
||||
|
||||
`--now` Adds an `elapsedTimeNow` key with an estimation of the current
|
||||
elapsed playback time. This estimation may be off by up to a second.
|
||||
To determine a more accurate time without polling `get` continuously,
|
||||
calculate it using the `elapsedTime` and `timestamp` keys. `elapsedTime`
|
||||
contains the elapsed time at the time that is stored in `timestamp`.
|
||||
|
||||
`--micros` Replaces the following keys with microsecond equivalents:
|
||||
|
||||
| Original key | Converted key name | Comment |
|
||||
|------------------|------------------------|---------------------------|
|
||||
| `duration` | `durationMicros` | - |
|
||||
| `elapsedTime` | `elapsedTimeMicros` | - |
|
||||
| `elapsedTimeNow` | `elapsedTimeNowMicros` | Only present with `--now` |
|
||||
| `timestamp` | `timestampEpochMicros` | Converted to epoch time |
|
||||
|
||||
`--no-artwork` Omits the `artworkData` and `artworkMimeType` keys
|
||||
from the payload. Useful for consumers that do not render artwork,
|
||||
since this avoids emitting several hundred kilobytes of base64-encoded
|
||||
image data per update.
|
||||
|
||||
---
|
||||
|
||||
### stream
|
||||
|
||||
Streams now playing information updates in real-time
|
||||
until the script receives a SIGTERM signal.
|
||||
|
||||
Output is encoded as JSON and characterized by
|
||||
a dictionary with the following keys:
|
||||
|
||||
> `type`
|
||||
`diff`
|
||||
`payload`
|
||||
|
||||
`type` is always a string with the value `"data"`.
|
||||
|
||||
`payload` contains the now playing information and is a dictionary
|
||||
that is structurally identical to the output of the `get` command,
|
||||
with the same keys. The dictionary itself is never `null`.
|
||||
No keys are set at all,
|
||||
when no media player is reporting now playing information.
|
||||
Some keys may have a `null` value,
|
||||
when the media player reports `null` for them (this happens rarely, if ever).
|
||||
Any key may be `null`, when `diff` is set to true and the key vanishes.
|
||||
|
||||
`diff` is a boolean that indicates whether the `payload`
|
||||
contains only fields whose values have been updated.
|
||||
When set to `false`,
|
||||
the payload is to be considered the current now playing state
|
||||
with all available keys and their values,
|
||||
regardless of any payloads that have been sent in the past.
|
||||
When set to `true` on the other hand,
|
||||
the last sent non-diff payload must be updated with these new values,
|
||||
in order to have a representation of the the current now playing state.
|
||||
When a key is not present anymore, it's set to `null` in the payload
|
||||
and its previous value should be removed.
|
||||
**Diffing is enabled by default, but can be disabled with a command line flag.**
|
||||
|
||||
**Options**
|
||||
|
||||
`--no-diff` Disables diffing. `diff` is always `false`
|
||||
and `payload` always contains all current information.
|
||||
|
||||
`--debounce=N` Adds a debounce delay in milliseconds
|
||||
between the point where changes are detected
|
||||
and when they are printed.
|
||||
If a new update comes in during delaying,
|
||||
the delay is restarted and all updates are merged.
|
||||
This is useful to prevent bursts of smaller updates.
|
||||
The default is 0.
|
||||
|
||||
`--micros` Identical to the `--micros` option of the `get` command.
|
||||
|
||||
`--no-artwork` Identical to the `--no-artwork` option of the `get`
|
||||
command. Particularly useful with `stream` with the `--no-diff` parameter,
|
||||
since it otherwise re-emits the full artwork payload on every update.
|
||||
|
||||
**Experimental options**
|
||||
|
||||
`--experimental-peculiar-debounce:BUNDLE_ID=N` 
|
||||
Adds a debounce delay in milliseconds for the case when the media player
|
||||
with the given bundle identifier (`BUNDLE_ID`) reports metadata that
|
||||
contains parts of the previous track and parts of the next track,
|
||||
but doesn't contain the full metadata of the next track.
|
||||
Whenever the track title changes,
|
||||
the update for it is either delayed for the given debounce delay
|
||||
or the update is printed when all other metadata updated as well,
|
||||
whichever happens earlier.
|
||||
Currently only `com.tidal.desktop` can be passed for `BUNDLE_ID`,
|
||||
since it is the only media player that is known to have this issue.
|
||||
A value of `1000` for `N` is recommended for TIDAL specifically.
|
||||
|
||||
---
|
||||
|
||||
### send COMMAND
|
||||
|
||||
Sends a MediaRemote command to the now playing application.
|
||||
|
||||
The value for `COMMAND` must be a valid ID from the table below.
|
||||
|
||||
| ID | MediaRemote key | Description |
|
||||
|:--:|-------------------------|-------------------------------|
|
||||
| 0 | kMRPlay | Start playback |
|
||||
| 1 | kMRPause | Pause playback |
|
||||
| 2 | kMRTogglePlayPause | Toggle between play and pause |
|
||||
| 3 | kMRStop | Stop playback |
|
||||
| 4 | kMRNextTrack | Skip to the next track |
|
||||
| 5 | kMRPreviousTrack | Return to the previous track |
|
||||
| 6 | kMRToggleShuffle | Toggle shuffle mode |
|
||||
| 7 | kMRToggleRepeat | Toggle repeat mode |
|
||||
| 8 | kMRStartForwardSeek | Start seeking forward |
|
||||
| 9 | kMREndForwardSeek | Stop seeking forward |
|
||||
| 10 | kMRStartBackwardSeek | Start seeking backward |
|
||||
| 11 | kMREndBackwardSeek | Stop seeking backward |
|
||||
| 12 | kMRGoBackFifteenSeconds | Go back 15 seconds |
|
||||
| 13 | kMRSkipFifteenSeconds | Skip ahead 15 seconds |
|
||||
|
||||
---
|
||||
|
||||
### seek POSITION
|
||||
|
||||
Seeks to a specific timeline position with the now playing application.
|
||||
|
||||
The value for `POSITION` must a valid positive integer.
|
||||
The unit is microseconds.
|
||||
|
||||
---
|
||||
|
||||
### shuffle MODE
|
||||
|
||||
Sets the shuffle mode.
|
||||
|
||||
The value for `MODE` must be a valid ID from the table below.
|
||||
|
||||
| ID | Description |
|
||||
|:--:|----------------|
|
||||
| 1 | Disable |
|
||||
| 2 | Shuffle albums |
|
||||
| 3 | Shuffle tracks |
|
||||
|
||||
---
|
||||
|
||||
### repeat MODE
|
||||
|
||||
Sets the repeat mode.
|
||||
|
||||
The value for `MODE` must be a valid ID from the table below.
|
||||
|
||||
| ID | Description |
|
||||
|:--:|-----------------|
|
||||
| 1 | Disable |
|
||||
| 2 | Repeat track |
|
||||
| 3 | Repeat playlist |
|
||||
|
||||
---
|
||||
|
||||
### speed SPEED
|
||||
|
||||
Sets the playback speed.
|
||||
|
||||
The value for `SPEED` must be a valid positive integer.
|
||||
|
||||
---
|
||||
|
||||
### test
|
||||
|
||||
Tests if the adapter is entitled to use the MediaRemote framework
|
||||
and if it is able to execute any of the supported commands without failure.
|
||||
An exit code of 0 means the adapter is functional and safe to use.
|
||||
|
||||
This can be integrated into your app
|
||||
to help confirm that the adapter is still functional and if not,
|
||||
fall back to other methods for media detection (e.g. AppleScript),
|
||||
since future macOS updates may break MediaRemote access again.
|
||||
|
||||
#### Usage
|
||||
|
||||
```
|
||||
/usr/bin/perl /path/to/mediaremote-adapter.pl /path/to/MediaRemoteAdapter.framework /path/to/MediaRemoteAdapterTestClient test
|
||||
```
|
||||
|
||||
Note that the `test` command requires the absolute path
|
||||
to the `MediaRemoteAdapterTestClient` executable after the framework path.
|
||||
For ease of use you can always pass the path to the test client executable,
|
||||
even when using other commands, like `get` or `stream`.
|
||||
|
||||
#### Output
|
||||
|
||||
An exit code of `0` indicates that the adapter is still functional
|
||||
and can safely be used to detect media.
|
||||
Any other exit indicates that the adapter is likely broken.
|
||||
|
||||
If you ever get an exit code other than `0`,
|
||||
please [report this](https://github.com/ungive/mediaremote-adapter/issues). Thank you!
|
||||
|
||||
#### How this works
|
||||
|
||||
1. Now playing information is attempted to be read normally using `get`
|
||||
2. If no media is detected, the `MediaRemoteAdapterTestClient` helper process is launched to simulate media playback
|
||||
3. While the helper process is running, now playing information is attempted to be read again using `get`
|
||||
4. Afterwards the helper process is terminated
|
||||
5. If any of the `get` attempts yielded media information, the command exits with an exit code of `0`
|
||||
6. Otherwise the command exits with an exit code of `1`
|
||||
|
||||
> [!WARNING]
|
||||
> **May interfere with other apps using MediaRemote**
|
||||
> The test can create a fake media entry that will briefly appear
|
||||
as the now playing application.
|
||||
This only happens when no other media is playing.
|
||||
Since the helper process has no bundle identifier,
|
||||
it is mostly ignored by the `stream` and `get` commands —
|
||||
`stream` won't update, and `get` will print `null`.
|
||||
|
||||
---
|
||||
|
||||
## Built-in fixes
|
||||
|
||||
This library has some fixes built-in
|
||||
to accomodate for inconsistencies within the MediaRemote framework:
|
||||
|
||||
- Artwork data sometimes unloads for a brief moment,
|
||||
e.g. when changing the current timeline position of a track.
|
||||
To combat this, artwork data is reused when the track has not changed,
|
||||
the track had artwork data before and the artwork data has disappeared.
|
||||
This fix is applied when using the `stream` command
|
||||
|
||||
If you need a way to disable any or all of these fixes,
|
||||
please open an issue or create a pull request.
|
||||
|
||||
---
|
||||
|
||||
## Implementation notes
|
||||
|
||||
- Consider `NSJSONSerialization` for JSON deserialization.
|
||||
This is what is used for encoding
|
||||
- You can use `NSData`'s `initWithBase64EncodedString`
|
||||
for decoding of base64 data
|
||||
- Every line printed to stderr is an error message.
|
||||
If the script did not exit with a non-zero exit code,
|
||||
then any of these errors are non-fatal and can be safely ignored
|
||||
- Other apps using MediaRemote Adapter may run `test` which should not interfere with the `stream` and `get` commands, but will generate a missing bundle identifier error message, which can be ignored. See the `test` command section for more information.
|
||||
- You should not reinvoke the script when a fatal error occurs
|
||||
(non-zero exit code)
|
||||
- Make sure to pass the absolute path of the bundled framework and helper executable
|
||||
as arguments and not a relative path
|
||||
|
||||
## Why this works
|
||||
|
||||
According to the findings by [@My-Iris](https://github.com/Mx-Iris) in
|
||||
[this comment](https://github.com/aviwad/LyricFever/issues/94#issuecomment-2746155419)
|
||||
processes with a bundle identifier starting with `com.apple.`
|
||||
are granted permission to access the MediaRemote framework.
|
||||
The Perl platform binary `/usr/bin/perl`
|
||||
is reported as having the bundle identifier `com.apple.perl` (or a variation).
|
||||
|
||||
You can confirm this by streaming log messages using the Console.app
|
||||
whilst running the script:
|
||||
|
||||
`default 14:44:55.871495+0200 mediaremoted Adding client <MRDMediaRemoteClient 0x15820b1a0, bundleIdentifier = com.apple.perl5, pid = 86889>`
|
||||
|
||||
## Motivation
|
||||
|
||||
This project was created due to the MediaRemote framework
|
||||
being completely non-functional when being loaded directly from within an app,
|
||||
starting with macOS 15.4 (see the numerous issues linked below).
|
||||
|
||||
The aim of this project is to provide a tool (and perhaps soon a full library)
|
||||
that serves as a fully functional alternative to using MediaRemote directly
|
||||
and perhaps to inspire Apple to give us a public API
|
||||
to read now playing information and control media playback on the device
|
||||
(see the note at the top of this file).
|
||||
|
||||
## Projects that use this library
|
||||
|
||||
- [Now Playing Display](https://www.gxlabs.co/now-playing/): A macOS "now playing" hardware display for the Seeed XIAO ESP32-C6 paired with the Seeed Round Display for XIAO. Shows album art, track info, progress, and playback controls on a 240x240 circular TFT — connected over USB. Source code: https://github.com/gxlabs/now-playing-device
|
||||
- [Music Presence](https://musicpresence.app) is a cross-platform desktop application
|
||||
for showing what you are listening to in your Discord status.
|
||||
It uses this library since version [2.3.1](https://github.com/ungive/discord-music-presence/releases/tag/v2.3.1)
|
||||
to detect media from all media players again. Website: https://musicpresence.app
|
||||
- [media-control](https://github.com/ungive/media-control)
|
||||
is a CLI tool to control and observe media playback on any macOS version.
|
||||
You can install it directly via brew: `$ brew tap ungive/media-control && brew install media-control`. Source code: https://github.com/ungive/media-control
|
||||
|
||||
*If you use this library in your project, please
|
||||
[let me know](https://github.com/ungive/mediaremote-adapter/issues) and I'll add it to the list!*
|
||||
|
||||
## Useful links
|
||||
|
||||
- Issues regarding MediaRemote breaking since macOS 15.4
|
||||
- https://github.com/vincentneo/LosslessSwitcher/issues/161
|
||||
- https://github.com/aviwad/LyricFever/issues/94
|
||||
- https://github.com/TheBoredTeam/boring.notch/issues/417
|
||||
- https://community.folivora.ai/t/now-playing-is-no-longer-working-on-macos-15-4/42802/11
|
||||
- https://github.com/ungive/discord-music-presence/issues/165
|
||||
- https://github.com/ungive/discord-music-presence/issues/245
|
||||
- https://github.com/kirtan-shah/nowplaying-cli/issues/28
|
||||
- https://github.com/FelixKratz/SketchyBar/issues/708
|
||||
- Getting now playing information using `osascript` and `MRNowPlayingRequest`.
|
||||
Note that this is unable to load the song artwork
|
||||
and it is impossible to get real-time updates with this solution.
|
||||
It is much simpler to implement though
|
||||
- https://github.com/EinTim23/PlayerLink/commit/9821b6a294873f975852f06419a0baf2fe404800
|
||||
- https://github.com/fastfetch-cli/fastfetch/commit/1557f0c5564a8288604824e55db47508f65e82c9
|
||||
- https://gist.github.com/SKaplanOfficial/f9f5bdd6455436203d0d318c078358de
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Thank you [@Alexander5015](https://github.com/Alexander5015) for implementing the `test` command,
|
||||
so we're able to detect when the adapter stops working!
|
||||
|
||||
Thank you [@EinTim23](https://github.com/EinTim23) for bringing
|
||||
a [similar workaround](https://github.com/EinTim23/PlayerLink/commit/9821b6a294873f975852f06419a0baf2fe404800) to my attention!
|
||||
Without your hint I most likely would not have dug into this anytime soon
|
||||
and my app [Music Presence](https://musicpresence.app)
|
||||
would still only work with AppleScript automation.
|
||||
|
||||
Thank you [@My-Iris](https://github.com/Mx-Iris)
|
||||
for providing insight into the changes made since macOS 15.4:
|
||||
[aviwad/LyricFever#94](https://github.com/aviwad/LyricFever/issues/94#issuecomment-2746155419)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the BSD 3-Clause License.
|
||||
See [LICENSE](./LICENSE) for details.
|
||||
|
||||
Copyright (c) 2025 Jonas van den Berg and contributors
|
||||
Reference in New Issue
Block a user