summaryrefslogtreecommitdiffstats
path: root/src/detection/media/media_apple.m
diff options
context:
space:
mode:
Diffstat (limited to 'src/detection/media/media_apple.m')
-rw-r--r--src/detection/media/media_apple.m209
1 files changed, 209 insertions, 0 deletions
diff --git a/src/detection/media/media_apple.m b/src/detection/media/media_apple.m
new file mode 100644
index 0000000..d9501d1
--- /dev/null
+++ b/src/detection/media/media_apple.m
@@ -0,0 +1,209 @@
+#include "fastfetch.h"
+#include "common/processing.h"
+#include "common/apple/cf_helpers.h"
+#include "common/time.h"
+#include "detection/media/media.h"
+
+#import <Foundation/Foundation.h>
+#import <CoreFoundation/CoreFoundation.h>
+#import <CoreServices/CoreServices.h>
+
+// https://github.com/andrewwiik/iOS-Blocks/blob/master/Widgets/Music/MediaRemote.h
+extern void MRMediaRemoteGetNowPlayingInfo(dispatch_queue_t dispatcher, void (^callback)(_Nullable CFDictionaryRef info)) FF_A_WEAK_IMPORT;
+extern void MRMediaRemoteGetNowPlayingApplicationIsPlaying(dispatch_queue_t queue, void (^callback)(BOOL playing)) FF_A_WEAK_IMPORT;
+extern void MRMediaRemoteGetNowPlayingApplicationDisplayID(dispatch_queue_t queue, void (^callback)(_Nullable CFStringRef displayID)) FF_A_WEAK_IMPORT;
+extern void MRMediaRemoteGetNowPlayingApplicationDisplayName(int unknown, dispatch_queue_t queue, void (^callback)(_Nullable CFStringRef name)) FF_A_WEAK_IMPORT;
+
+static uint32_t getTrueElapsedTime(CFDictionaryRef info) {
+ double elapsedTime;
+ if (ffCfDictGetDouble(info, CFSTR("kMRMediaRemoteNowPlayingInfoElapsedTime"), &elapsedTime) != NULL) {
+ return 0;
+ }
+
+ elapsedTime *= 1000;
+
+ double playbackRate;
+ uint64_t timestampEpoch;
+ if (ffCfDictGetDouble(info, CFSTR("kMRMediaRemoteNowPlayingInfoPlaybackRate"), &playbackRate) == NULL &&
+ ffCfDictGetDateAsEpoch(info, CFSTR("kMRMediaRemoteNowPlayingInfoTimestamp"), &timestampEpoch) == NULL) {
+ uint64_t timeDiff = ffTimeGetNow() - timestampEpoch;
+ elapsedTime += (double) timeDiff * playbackRate;
+ }
+
+ return (uint32_t) elapsedTime;
+}
+
+static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover) {
+#define FF_TEST_FN_EXISTENCE(fn) \
+ if (!fn) return "MediaRemote function " #fn " is not available"
+ FF_TEST_FN_EXISTENCE(MRMediaRemoteGetNowPlayingInfo);
+ FF_TEST_FN_EXISTENCE(MRMediaRemoteGetNowPlayingApplicationIsPlaying);
+#undef FF_TEST_FN_EXISTENCE
+
+ dispatch_group_t group = dispatch_group_create();
+ dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
+
+ dispatch_group_enter(group);
+ __block const char* error = NULL;
+ MRMediaRemoteGetNowPlayingInfo(queue, ^(_Nullable CFDictionaryRef info) {
+ if (info != nil) {
+ ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoTitle"), &result->song);
+ ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtist"), &result->artist);
+ ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoAlbum"), &result->album);
+ double value;
+ if (ffCfDictGetDouble(info, CFSTR("kMRMediaRemoteNowPlayingInfoDuration"), &value) == NULL) {
+ result->length = (uint32_t) (value * 1000);
+ result->position = getTrueElapsedTime(info);
+ }
+
+ if (saveCover) {
+ NSData* artworkData = (__bridge NSData*) CFDictionaryGetValue(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtworkData"));
+ if (artworkData) {
+ CFStringRef mime = (CFStringRef) CFDictionaryGetValue(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtworkMIMEType"));
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ FF_CFTYPE_AUTO_RELEASE CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mime, NULL);
+ FF_CFTYPE_AUTO_RELEASE CFStringRef ext = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension);
+#pragma clang diagnostic pop
+ NSString* tmpDir = NSTemporaryDirectory();
+ NSString* uuid = NSUUID.UUID.UUIDString;
+ NSString* path = [tmpDir stringByAppendingPathComponent:[NSString stringWithFormat:@"ff_%@.%@", uuid, ext ? (__bridge NSString*) ext : @"img"]];
+ if ([artworkData writeToFile:path atomically:NO])
+ ffStrbufSetS(&result->cover, path.UTF8String);
+ }
+ }
+ } else
+ error = "MRMediaRemoteGetNowPlayingInfo() failed";
+
+ dispatch_group_leave(group);
+ });
+
+ dispatch_group_enter(group);
+ MRMediaRemoteGetNowPlayingApplicationIsPlaying(queue, ^(BOOL playing) {
+ ffStrbufSetStatic(&result->status, playing ? "Playing" : "Paused");
+ dispatch_group_leave(group);
+ });
+
+ if (MRMediaRemoteGetNowPlayingApplicationDisplayID) {
+ dispatch_group_enter(group);
+ MRMediaRemoteGetNowPlayingApplicationDisplayID(queue, ^(_Nullable CFStringRef displayID) {
+ ffCfStrGetString(displayID, &result->playerId);
+ dispatch_group_leave(group);
+ });
+ }
+
+ if (MRMediaRemoteGetNowPlayingApplicationDisplayName) {
+ dispatch_group_enter(group);
+ MRMediaRemoteGetNowPlayingApplicationDisplayName(0, queue, ^(_Nullable CFStringRef name) {
+ ffCfStrGetString(name, &result->player);
+ dispatch_group_leave(group);
+ });
+ }
+
+ dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
+ // Don't dispatch_release because we are using ARC
+
+ if (result->song.length > 0) {
+ return NULL;
+ }
+
+ return error;
+}
+
+#if !FF_MODULE_DISABLE_MEDIA
+__attribute__((visibility("default"), used))
+int ffPrintMediaByMediaRemote(bool saveCover) {
+ FFMediaResult media = {
+ .status = ffStrbufCreate(),
+ .song = ffStrbufCreate(),
+ .artist = ffStrbufCreate(),
+ .album = ffStrbufCreate(),
+ .playerId = ffStrbufCreate(),
+ .player = ffStrbufCreate(),
+ .cover = ffStrbufCreate(),
+ };
+ if (getMediaByMediaRemote(&media, saveCover) != NULL) {
+ return 1;
+ }
+ ffStrbufAppendC(&media.status, '\n');
+ ffStrbufAppend(&media.status, &media.song);
+ ffStrbufAppendC(&media.status, '\n');
+ ffStrbufAppend(&media.status, &media.artist);
+ ffStrbufAppendC(&media.status, '\n');
+ ffStrbufAppend(&media.status, &media.album);
+ ffStrbufAppendC(&media.status, '\n');
+ ffStrbufAppend(&media.status, &media.playerId);
+ ffStrbufAppendC(&media.status, '\n');
+ ffStrbufAppend(&media.status, &media.player);
+ ffStrbufAppendC(&media.status, '\n');
+ if (saveCover) {
+ ffStrbufAppend(&media.status, &media.cover);
+ }
+ ffStrbufAppendC(&media.status, '\n');
+ ffStrbufAppendUInt(&media.status, media.position);
+ ffStrbufAppendC(&media.status, '\n');
+ ffStrbufAppendUInt(&media.status, media.length);
+ write(STDOUT_FILENO, media.status.chars, media.status.length);
+ ffStrbufDestroy(&media.status);
+ ffStrbufDestroy(&media.song);
+ ffStrbufDestroy(&media.artist);
+ ffStrbufDestroy(&media.album);
+ ffStrbufDestroy(&media.playerId);
+ ffStrbufDestroy(&media.player);
+ ffStrbufDestroy(&media.cover);
+ return 0;
+}
+#endif
+
+static const char* getMediaByAuthorizedProcess(FFMediaResult* result, bool saveCover) {
+ // #1737
+ FF_STRBUF_AUTO_DESTROY script = ffStrbufCreateF("import ctypes;ctypes.CDLL('%s').ffPrintMediaByMediaRemote(%s)", instance.state.platform.exePath.chars, saveCover ? "True" : "False");
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+
+ const char* error = ffProcessAppendStdOut(
+ &buffer,
+ (char* const[]) { "/usr/bin/python3", // Must be signed by Apple. Homebrew python doesn't work
+ "-c",
+ script.chars,
+ nil });
+ if (error) {
+ return error;
+ }
+ if (buffer.length == 0) {
+ return "No media found";
+ }
+
+ // status\ntitle\nartist\nalbum\nbundleName\nappName\ncoverPath\nelapsedTimeMS\ndurationMS
+ FFstrbuf* const strList[] = { &result->status, &result->song, &result->artist, &result->album, &result->playerId, &result->player, &result->cover };
+ char* line = NULL;
+ size_t len = 0;
+ for (uint32_t i = 0; i < ARRAY_SIZE(strList) && ffStrbufGetline(&line, &len, &buffer); ++i) {
+ ffStrbufSetS(strList[i], line);
+ }
+ uint32_t* const numList[] = { &result->position, &result->length };
+ for (uint32_t i = 0; i < ARRAY_SIZE(numList) && ffStrbufGetline(&line, &len, &buffer); ++i) {
+ *numList[i] = (uint32_t) strtoul(line, NULL, 10);
+ }
+ return NULL;
+}
+
+void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
+ const char* error;
+ if (@available(macOS 15.4, *)) {
+ error = getMediaByAuthorizedProcess(media, saveCover);
+ } else {
+ error = getMediaByMediaRemote(media, saveCover);
+ }
+ if (error) {
+ ffStrbufAppendS(&media->error, error);
+ } else if (media->player.length == 0 && media->playerId.length > 0) {
+ ffStrbufSet(&media->player, &media->playerId);
+ if (ffStrbufStartsWithIgnCaseS(&media->player, "com.")) {
+ ffStrbufSubstrAfter(&media->player, strlen("com.") - 1);
+ }
+ ffStrbufReplaceAllC(&media->player, '.', ' ');
+ if (media->cover.length > 0) {
+ media->removeCoverAfterUse = true;
+ }
+ }
+}