diff --git a/.clang-format b/.clang-format index 957f565d3..8584e3781 100644 --- a/.clang-format +++ b/.clang-format @@ -1,7 +1,6 @@ -# find . -type f \( -name "*.c" -o -name "*.h" -o -name "*.cpp" -o -name "*.hpp" \) -print0 | xargs -0 clang-format -i +# find . -type f \( -name "*.c" -o -name "*.h" -o -name "*.cpp" -o -name "*.hpp" -o -name "*.m" -o -name "*.mm" \) -print0 | xargs -0 clang-format -i BasedOnStyle: LLVM -Language: Cpp UseTab: Never IndentWidth: 4 @@ -81,3 +80,10 @@ AttributeMacros: MaxEmptyLinesToKeep: 1 KeepEmptyLinesAtTheStartOfBlocks: false + +--- +Language: ObjC +ObjCBlockIndentWidth: 4 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true +ObjCBreakBeforeNestedBlockParam: true diff --git a/src/common/apple/cf_helpers.c b/src/common/apple/cf_helpers.c index e7a9c4fa1..60e1a1c59 100644 --- a/src/common/apple/cf_helpers.c +++ b/src/common/apple/cf_helpers.c @@ -34,6 +34,29 @@ const char* ffCfNumGetInt(CFTypeRef cf, int32_t* result) { return "TypeID is neither 'CFNumber' nor 'CFData'"; } +const char* ffCfNumGetDouble(CFTypeRef cf, double* result) { + if (CFGetTypeID(cf) == CFNumberGetTypeID()) { + if (!CFNumberGetValue((CFNumberRef) cf, kCFNumberDoubleType, result) && + !CFNumberGetValue((CFNumberRef) cf, kCFNumberFloatType, result)) { + return "Number type is not Double or Float"; + } + return NULL; + } + + return "TypeID is neither 'CFNumber'"; +} + +const char* ffCfDateGetEpoch(CFTypeRef cf, uint64_t* result) { + if (CFGetTypeID(cf) != CFDateGetTypeID()) { + return "TypeID is not 'CFDate'"; + } + + CFAbsoluteTime absTime = CFDateGetAbsoluteTime((CFDateRef) cf); + // Convert from seconds to milliseconds and add the difference between 1970 and 2001 in milliseconds + *result = (uint64_t) ((absTime + 978307200 /*kCFAbsoluteTimeIntervalSince1970*/) * 1000); + return NULL; +} + const char* ffCfStrGetString(CFTypeRef cf, FFstrbuf* result) { ffStrbufClear(result); if (!cf) { @@ -149,6 +172,15 @@ const char* ffCfDictGetInt64(CFDictionaryRef dict, CFStringRef key, int64_t* res return ffCfNumGetInt64(cf, result); } +const char* ffCfDictGetDouble(CFDictionaryRef dict, CFStringRef key, double* result) { + CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key); + if (cf == NULL) { + return "CFDictionaryGetValue() failed"; + } + + return ffCfNumGetDouble(cf, result); +} + const char* ffCfDictGetData(CFDictionaryRef dict, CFStringRef key, uint32_t offset, uint32_t size, uint8_t* result, uint32_t* length) { CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key); if (cf == NULL) { @@ -182,3 +214,12 @@ const char* ffCfDictGetDict(CFDictionaryRef dict, CFStringRef key, CFDictionaryR *result = cf; return NULL; } + +const char* ffCfDictGetDateAsEpoch(CFDictionaryRef dict, CFStringRef key, uint64_t* result) { + CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key); + if (cf == NULL) { + return "CFDictionaryGetValue() failed"; + } + + return ffCfDateGetEpoch(cf, result); +} diff --git a/src/common/apple/cf_helpers.h b/src/common/apple/cf_helpers.h index 8ce58ccc3..a929f030e 100644 --- a/src/common/apple/cf_helpers.h +++ b/src/common/apple/cf_helpers.h @@ -8,14 +8,18 @@ const char* ffCfStrGetString(CFTypeRef cf, FFstrbuf* result); const char* ffCfNumGetInt(CFTypeRef cf, int32_t* result); const char* ffCfNumGetInt64(CFTypeRef cf, int64_t* result); +const char* ffCfNumGetDouble(CFTypeRef cf, double* result); +const char* ffCfDateGetEpoch(CFTypeRef cf, uint64_t* result); const char* ffCfDataGetDataAsString(CFTypeRef cf, FFstrbuf* result); const char* ffCfDictGetString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result); const char* ffCfDictGetBool(CFDictionaryRef dict, CFStringRef key, bool* result); const char* ffCfDictGetInt(CFDictionaryRef dict, CFStringRef key, int* result); const char* ffCfDictGetInt64(CFDictionaryRef dict, CFStringRef key, int64_t* result); +const char* ffCfDictGetDouble(CFDictionaryRef dict, CFStringRef key, double* result); const char* ffCfDictGetData(CFDictionaryRef dict, CFStringRef key, uint32_t offset, uint32_t size, uint8_t* result, uint32_t* length); const char* ffCfDictGetDataAsString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result); const char* ffCfDictGetDict(CFDictionaryRef dict, CFStringRef key, CFDictionaryRef* result); +const char* ffCfDictGetDateAsEpoch(CFDictionaryRef dict, CFStringRef key, uint64_t* result); static inline CFNumberRef ffCfCreateInt(int value) { return CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &value); diff --git a/src/detection/media/media_apple.m b/src/detection/media/media_apple.m index b646ebcc6..e99196c86 100644 --- a/src/detection/media/media_apple.m +++ b/src/detection/media/media_apple.m @@ -1,6 +1,7 @@ #include "fastfetch.h" #include "common/processing.h" #include "common/apple/cf_helpers.h" +#include "common/time.h" #include "detection/media/media.h" #import @@ -8,17 +9,36 @@ #import // 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 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 const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover) -{ - #define FF_TEST_FN_EXISTANCE(fn) if (!fn) return "MediaRemote function " #fn " is not available" - FF_TEST_FN_EXISTANCE(MRMediaRemoteGetNowPlayingInfo); - FF_TEST_FN_EXISTANCE(MRMediaRemoteGetNowPlayingApplicationIsPlaying); - #undef FF_TEST_FN_EXISTANCE +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"), ×tampEpoch) == 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); @@ -26,32 +46,33 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover) dispatch_group_enter(group); __block const char* error = NULL; MRMediaRemoteGetNowPlayingInfo(queue, ^(_Nullable CFDictionaryRef info) { - if(info != nil) - { + 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) - { + if (saveCover) { NSData* artworkData = (__bridge NSData*) CFDictionaryGetValue(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtworkData")); - if (artworkData) - { + 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"]]; + 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 + } else error = "MRMediaRemoteGetNowPlayingInfo() failed"; dispatch_group_leave(group); @@ -63,8 +84,7 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover) dispatch_group_leave(group); }); - if (MRMediaRemoteGetNowPlayingApplicationDisplayID) - { + if (MRMediaRemoteGetNowPlayingApplicationDisplayID) { dispatch_group_enter(group); MRMediaRemoteGetNowPlayingApplicationDisplayID(queue, ^(_Nullable CFStringRef displayID) { ffCfStrGetString(displayID, &result->playerId); @@ -72,8 +92,7 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover) }); } - if (MRMediaRemoteGetNowPlayingApplicationDisplayName) - { + if (MRMediaRemoteGetNowPlayingApplicationDisplayName) { dispatch_group_enter(group); MRMediaRemoteGetNowPlayingApplicationDisplayName(0, queue, ^(_Nullable CFStringRef name) { ffCfStrGetString(name, &result->player); @@ -84,15 +103,14 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover) dispatch_group_wait(group, DISPATCH_TIME_FOREVER); // Don't dispatch_release because we are using ARC - if(result->song.length > 0) + if (result->song.length > 0) { return NULL; + } return error; } -__attribute__((visibility("default"), used)) -int ffPrintMediaByMediaRemote(bool saveCover) -{ +__attribute__((visibility("default"), used)) int ffPrintMediaByMediaRemote(bool saveCover) { FFMediaResult media = { .status = ffStrbufCreate(), .song = ffStrbufCreate(), @@ -102,15 +120,28 @@ int ffPrintMediaByMediaRemote(bool saveCover) .player = ffStrbufCreate(), .cover = ffStrbufCreate(), }; - if (getMediaByMediaRemote(&media, saveCover) != NULL) + if (getMediaByMediaRemote(&media, saveCover) != NULL) { return 1; - ffStrbufPutTo(&media.status, stdout); - ffStrbufPutTo(&media.song, stdout); - ffStrbufPutTo(&media.artist, stdout); - ffStrbufPutTo(&media.album, stdout); - ffStrbufPutTo(&media.playerId, stdout); - ffStrbufPutTo(&media.player, stdout); - ffStrbufWriteTo(&media.cover, stdout); + } + 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); @@ -121,46 +152,55 @@ int ffPrintMediaByMediaRemote(bool saveCover) return 0; } -static const char* getMediaByAuthorizedProcess(FFMediaResult* result, bool saveCover) -{ +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"; + 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 - FFstrbuf* const varList[] = { &result->status, &result->song, &result->artist, &result->album, &result->playerId, &result->player, &result->cover }; + // 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(varList) && ffStrbufGetline(&line, &len, &buffer); ++i) - ffStrbufSetS(varList[i], line); + 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) -{ +void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) { const char* error; - if (@available(macOS 15.4, *)) + if (@available(macOS 15.4, *)) { error = getMediaByAuthorizedProcess(media, saveCover); - else + } else { error = getMediaByMediaRemote(media, saveCover); - if (error) + } + if (error) { ffStrbufAppendS(&media->error, error); - else if (media->player.length == 0 && media->playerId.length > 0) - { + } else if (media->player.length == 0 && media->playerId.length > 0) { ffStrbufSet(&media->player, &media->playerId); - if (ffStrbufStartsWithIgnCaseS(&media->player, "com.")) + if (ffStrbufStartsWithIgnCaseS(&media->player, "com.")) { ffStrbufSubstrAfter(&media->player, strlen("com.") - 1); + } ffStrbufReplaceAllC(&media->player, '.', ' '); - if (media->cover.length > 0) + if (media->cover.length > 0) { media->removeCoverAfterUse = true; + } } }