Media (macOS): adds length & position detection support

This commit is contained in:
Carter Li
2026-05-12 15:07:08 +08:00
parent 7799dbe481
commit 1400291ae9
4 changed files with 150 additions and 59 deletions
+8 -2
View File
@@ -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 BasedOnStyle: LLVM
Language: Cpp
UseTab: Never UseTab: Never
IndentWidth: 4 IndentWidth: 4
@@ -81,3 +80,10 @@ AttributeMacros:
MaxEmptyLinesToKeep: 1 MaxEmptyLinesToKeep: 1
KeepEmptyLinesAtTheStartOfBlocks: false KeepEmptyLinesAtTheStartOfBlocks: false
---
Language: ObjC
ObjCBlockIndentWidth: 4
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
ObjCBreakBeforeNestedBlockParam: true
+41
View File
@@ -34,6 +34,29 @@ const char* ffCfNumGetInt(CFTypeRef cf, int32_t* result) {
return "TypeID is neither 'CFNumber' nor 'CFData'"; 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) { const char* ffCfStrGetString(CFTypeRef cf, FFstrbuf* result) {
ffStrbufClear(result); ffStrbufClear(result);
if (!cf) { if (!cf) {
@@ -149,6 +172,15 @@ const char* ffCfDictGetInt64(CFDictionaryRef dict, CFStringRef key, int64_t* res
return ffCfNumGetInt64(cf, result); 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) { 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); CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
if (cf == NULL) { if (cf == NULL) {
@@ -182,3 +214,12 @@ const char* ffCfDictGetDict(CFDictionaryRef dict, CFStringRef key, CFDictionaryR
*result = cf; *result = cf;
return NULL; 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);
}
+4
View File
@@ -8,14 +8,18 @@
const char* ffCfStrGetString(CFTypeRef cf, FFstrbuf* result); const char* ffCfStrGetString(CFTypeRef cf, FFstrbuf* result);
const char* ffCfNumGetInt(CFTypeRef cf, int32_t* result); const char* ffCfNumGetInt(CFTypeRef cf, int32_t* result);
const char* ffCfNumGetInt64(CFTypeRef cf, int64_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* ffCfDataGetDataAsString(CFTypeRef cf, FFstrbuf* result);
const char* ffCfDictGetString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result); const char* ffCfDictGetString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result);
const char* ffCfDictGetBool(CFDictionaryRef dict, CFStringRef key, bool* result); const char* ffCfDictGetBool(CFDictionaryRef dict, CFStringRef key, bool* result);
const char* ffCfDictGetInt(CFDictionaryRef dict, CFStringRef key, int* result); const char* ffCfDictGetInt(CFDictionaryRef dict, CFStringRef key, int* result);
const char* ffCfDictGetInt64(CFDictionaryRef dict, CFStringRef key, int64_t* 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* 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* ffCfDictGetDataAsString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result);
const char* ffCfDictGetDict(CFDictionaryRef dict, CFStringRef key, CFDictionaryRef* 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) { static inline CFNumberRef ffCfCreateInt(int value) {
return CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &value); return CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &value);
+91 -51
View File
@@ -1,6 +1,7 @@
#include "fastfetch.h" #include "fastfetch.h"
#include "common/processing.h" #include "common/processing.h"
#include "common/apple/cf_helpers.h" #include "common/apple/cf_helpers.h"
#include "common/time.h"
#include "detection/media/media.h" #include "detection/media/media.h"
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
@@ -13,12 +14,31 @@ extern void MRMediaRemoteGetNowPlayingApplicationIsPlaying(dispatch_queue_t queu
extern void MRMediaRemoteGetNowPlayingApplicationDisplayID(dispatch_queue_t queue, void (^callback)(_Nullable CFStringRef displayID)) 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; 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) static uint32_t getTrueElapsedTime(CFDictionaryRef info) {
{ double elapsedTime;
#define FF_TEST_FN_EXISTANCE(fn) if (!fn) return "MediaRemote function " #fn " is not available" if (ffCfDictGetDouble(info, CFSTR("kMRMediaRemoteNowPlayingInfoElapsedTime"), &elapsedTime) != NULL) {
FF_TEST_FN_EXISTANCE(MRMediaRemoteGetNowPlayingInfo); return 0;
FF_TEST_FN_EXISTANCE(MRMediaRemoteGetNowPlayingApplicationIsPlaying); }
#undef FF_TEST_FN_EXISTANCE
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_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
@@ -26,17 +46,19 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
dispatch_group_enter(group); dispatch_group_enter(group);
__block const char* error = NULL; __block const char* error = NULL;
MRMediaRemoteGetNowPlayingInfo(queue, ^(_Nullable CFDictionaryRef info) { MRMediaRemoteGetNowPlayingInfo(queue, ^(_Nullable CFDictionaryRef info) {
if(info != nil) if (info != nil) {
{
ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoTitle"), &result->song); ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoTitle"), &result->song);
ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtist"), &result->artist); ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtist"), &result->artist);
ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoAlbum"), &result->album); 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")); NSData* artworkData = (__bridge NSData*) CFDictionaryGetValue(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtworkData"));
if (artworkData) if (artworkData) {
{
CFStringRef mime = (CFStringRef) CFDictionaryGetValue(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtworkMIMEType")); CFStringRef mime = (CFStringRef) CFDictionaryGetValue(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtworkMIMEType"));
#pragma clang diagnostic push #pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations" #pragma clang diagnostic ignored "-Wdeprecated-declarations"
@@ -50,8 +72,7 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
ffStrbufSetS(&result->cover, path.UTF8String); ffStrbufSetS(&result->cover, path.UTF8String);
} }
} }
} } else
else
error = "MRMediaRemoteGetNowPlayingInfo() failed"; error = "MRMediaRemoteGetNowPlayingInfo() failed";
dispatch_group_leave(group); dispatch_group_leave(group);
@@ -63,8 +84,7 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
dispatch_group_leave(group); dispatch_group_leave(group);
}); });
if (MRMediaRemoteGetNowPlayingApplicationDisplayID) if (MRMediaRemoteGetNowPlayingApplicationDisplayID) {
{
dispatch_group_enter(group); dispatch_group_enter(group);
MRMediaRemoteGetNowPlayingApplicationDisplayID(queue, ^(_Nullable CFStringRef displayID) { MRMediaRemoteGetNowPlayingApplicationDisplayID(queue, ^(_Nullable CFStringRef displayID) {
ffCfStrGetString(displayID, &result->playerId); ffCfStrGetString(displayID, &result->playerId);
@@ -72,8 +92,7 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
}); });
} }
if (MRMediaRemoteGetNowPlayingApplicationDisplayName) if (MRMediaRemoteGetNowPlayingApplicationDisplayName) {
{
dispatch_group_enter(group); dispatch_group_enter(group);
MRMediaRemoteGetNowPlayingApplicationDisplayName(0, queue, ^(_Nullable CFStringRef name) { MRMediaRemoteGetNowPlayingApplicationDisplayName(0, queue, ^(_Nullable CFStringRef name) {
ffCfStrGetString(name, &result->player); ffCfStrGetString(name, &result->player);
@@ -84,15 +103,14 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER); dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// Don't dispatch_release because we are using ARC // Don't dispatch_release because we are using ARC
if(result->song.length > 0) if (result->song.length > 0) {
return NULL; return NULL;
}
return error; return error;
} }
__attribute__((visibility("default"), used)) __attribute__((visibility("default"), used)) int ffPrintMediaByMediaRemote(bool saveCover) {
int ffPrintMediaByMediaRemote(bool saveCover)
{
FFMediaResult media = { FFMediaResult media = {
.status = ffStrbufCreate(), .status = ffStrbufCreate(),
.song = ffStrbufCreate(), .song = ffStrbufCreate(),
@@ -102,15 +120,28 @@ int ffPrintMediaByMediaRemote(bool saveCover)
.player = ffStrbufCreate(), .player = ffStrbufCreate(),
.cover = ffStrbufCreate(), .cover = ffStrbufCreate(),
}; };
if (getMediaByMediaRemote(&media, saveCover) != NULL) if (getMediaByMediaRemote(&media, saveCover) != NULL) {
return 1; return 1;
ffStrbufPutTo(&media.status, stdout); }
ffStrbufPutTo(&media.song, stdout); ffStrbufAppendC(&media.status, '\n');
ffStrbufPutTo(&media.artist, stdout); ffStrbufAppend(&media.status, &media.song);
ffStrbufPutTo(&media.album, stdout); ffStrbufAppendC(&media.status, '\n');
ffStrbufPutTo(&media.playerId, stdout); ffStrbufAppend(&media.status, &media.artist);
ffStrbufPutTo(&media.player, stdout); ffStrbufAppendC(&media.status, '\n');
ffStrbufWriteTo(&media.cover, stdout); 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.status);
ffStrbufDestroy(&media.song); ffStrbufDestroy(&media.song);
ffStrbufDestroy(&media.artist); ffStrbufDestroy(&media.artist);
@@ -121,46 +152,55 @@ int ffPrintMediaByMediaRemote(bool saveCover)
return 0; return 0;
} }
static const char* getMediaByAuthorizedProcess(FFMediaResult* result, bool saveCover) static const char* getMediaByAuthorizedProcess(FFMediaResult* result, bool saveCover) {
{
// #1737 // #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 script = ffStrbufCreateF("import ctypes;ctypes.CDLL('%s').ffPrintMediaByMediaRemote(%s)", instance.state.platform.exePath.chars, saveCover ? "True" : "False");
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
const char* error = ffProcessAppendStdOut(&buffer, (char* const[]) { const char* error = ffProcessAppendStdOut(
"/usr/bin/python3", // Must be signed by Apple. Homebrew python doesn't work &buffer,
(char* const[]) { "/usr/bin/python3", // Must be signed by Apple. Homebrew python doesn't work
"-c", "-c",
script.chars, script.chars,
nil nil });
}); if (error) {
if (error) return error; return error;
if (buffer.length == 0) return "No media found"; }
if (buffer.length == 0) {
return "No media found";
}
// status\ntitle\nartist\nalbum\nbundleName\nappName // status\ntitle\nartist\nalbum\nbundleName\nappName\ncoverPath\nelapsedTimeMS\ndurationMS
FFstrbuf* const varList[] = { &result->status, &result->song, &result->artist, &result->album, &result->playerId, &result->player, &result->cover }; FFstrbuf* const strList[] = { &result->status, &result->song, &result->artist, &result->album, &result->playerId, &result->player, &result->cover };
char* line = NULL; char* line = NULL;
size_t len = 0; size_t len = 0;
for (uint32_t i = 0; i < ARRAY_SIZE(varList) && ffStrbufGetline(&line, &len, &buffer); ++i) for (uint32_t i = 0; i < ARRAY_SIZE(strList) && ffStrbufGetline(&line, &len, &buffer); ++i) {
ffStrbufSetS(varList[i], line); 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; return NULL;
} }
void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
{
const char* error; const char* error;
if (@available(macOS 15.4, *)) if (@available(macOS 15.4, *)) {
error = getMediaByAuthorizedProcess(media, saveCover); error = getMediaByAuthorizedProcess(media, saveCover);
else } else {
error = getMediaByMediaRemote(media, saveCover); error = getMediaByMediaRemote(media, saveCover);
if (error) }
if (error) {
ffStrbufAppendS(&media->error, 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); ffStrbufSet(&media->player, &media->playerId);
if (ffStrbufStartsWithIgnCaseS(&media->player, "com.")) if (ffStrbufStartsWithIgnCaseS(&media->player, "com.")) {
ffStrbufSubstrAfter(&media->player, strlen("com.") - 1); ffStrbufSubstrAfter(&media->player, strlen("com.") - 1);
}
ffStrbufReplaceAllC(&media->player, '.', ' '); ffStrbufReplaceAllC(&media->player, '.', ' ');
if (media->cover.length > 0) if (media->cover.length > 0) {
media->removeCoverAfterUse = true; media->removeCoverAfterUse = true;
} }
} }
}