ohmu
·
2026-10-15
1#include "fastfetch.h"
2#include "common/processing.h"
3#include "common/apple/cf_helpers.h"
4#include "common/time.h"
5#include "detection/media/media.h"
6
7#import <Foundation/Foundation.h>
8#import <CoreFoundation/CoreFoundation.h>
9#import <CoreServices/CoreServices.h>
10
11// https://github.com/andrewwiik/iOS-Blocks/blob/master/Widgets/Music/MediaRemote.h
12[[clang::weak_import]] extern void MRMediaRemoteGetNowPlayingInfo(dispatch_queue_t dispatcher, void (^callback)(_Nullable CFDictionaryRef info));
13[[clang::weak_import]] extern void MRMediaRemoteGetNowPlayingApplicationIsPlaying(dispatch_queue_t queue, void (^callback)(BOOL playing));
14[[clang::weak_import]] extern void MRMediaRemoteGetNowPlayingApplicationDisplayID(dispatch_queue_t queue, void (^callback)(_Nullable CFStringRef displayID));
15[[clang::weak_import]] extern void MRMediaRemoteGetNowPlayingApplicationDisplayName(int unknown, dispatch_queue_t queue, void (^callback)(_Nullable CFStringRef name));
16
17static uint32_t getTrueElapsedTime(CFDictionaryRef info) {
18 double elapsedTime;
19 if (ffCfDictGetDouble(info, CFSTR("kMRMediaRemoteNowPlayingInfoElapsedTime"), &elapsedTime) != nullptr) {
20 return 0;
21 }
22
23 elapsedTime *= 1000;
24
25 double playbackRate;
26 uint64_t timestampEpoch;
27 if (ffCfDictGetDouble(info, CFSTR("kMRMediaRemoteNowPlayingInfoPlaybackRate"), &playbackRate) == nullptr &&
28 ffCfDictGetDateAsEpoch(info, CFSTR("kMRMediaRemoteNowPlayingInfoTimestamp"), ×tampEpoch) == nullptr) {
29 uint64_t timeDiff = ffTimeGetNow() - timestampEpoch;
30 elapsedTime += (double) timeDiff * playbackRate;
31 }
32
33 return (uint32_t) elapsedTime;
34}
35
36static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover) {
37#define FF_TEST_FN_EXISTENCE(fn) \
38 if (!fn) return "MediaRemote function " #fn " is not available"
39 FF_TEST_FN_EXISTENCE(MRMediaRemoteGetNowPlayingInfo);
40 FF_TEST_FN_EXISTENCE(MRMediaRemoteGetNowPlayingApplicationIsPlaying);
41#undef FF_TEST_FN_EXISTENCE
42
43 dispatch_group_t group = dispatch_group_create();
44 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
45
46 dispatch_group_enter(group);
47 __block const char* error = nullptr;
48 MRMediaRemoteGetNowPlayingInfo(queue, ^(_Nullable CFDictionaryRef info) {
49 if (info != nil) {
50 ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoTitle"), &result->song);
51 ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtist"), &result->artist);
52 ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoAlbum"), &result->album);
53 double value;
54 if (ffCfDictGetDouble(info, CFSTR("kMRMediaRemoteNowPlayingInfoDuration"), &value) == nullptr) {
55 result->length = (uint32_t) (value * 1000);
56 result->position = getTrueElapsedTime(info);
57 }
58
59 if (saveCover) {
60 NSData* artworkData = (__bridge NSData*) CFDictionaryGetValue(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtworkData"));
61 if (artworkData) {
62 CFStringRef mime = (CFStringRef) CFDictionaryGetValue(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtworkMIMEType"));
63#pragma clang diagnostic push
64#pragma clang diagnostic ignored "-Wdeprecated-declarations"
65 FF_CFTYPE_AUTO_RELEASE CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mime, nullptr);
66 FF_CFTYPE_AUTO_RELEASE CFStringRef ext = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension);
67#pragma clang diagnostic pop
68 NSString* tmpDir = NSTemporaryDirectory();
69 NSString* uuid = NSUUID.UUID.UUIDString;
70 NSString* path = [tmpDir stringByAppendingPathComponent:[NSString stringWithFormat:@"ff_%@.%@", uuid, ext ? (__bridge NSString*) ext : @"img"]];
71 if ([artworkData writeToFile:path atomically:NO])
72 ffStrbufSetS(&result->cover, path.UTF8String);
73 }
74 }
75 } else
76 error = "MRMediaRemoteGetNowPlayingInfo() failed";
77
78 dispatch_group_leave(group);
79 });
80
81 dispatch_group_enter(group);
82 MRMediaRemoteGetNowPlayingApplicationIsPlaying(queue, ^(BOOL playing) {
83 ffStrbufSetStatic(&result->status, playing ? "Playing" : "Paused");
84 dispatch_group_leave(group);
85 });
86
87 if (MRMediaRemoteGetNowPlayingApplicationDisplayID) {
88 dispatch_group_enter(group);
89 MRMediaRemoteGetNowPlayingApplicationDisplayID(queue, ^(_Nullable CFStringRef displayID) {
90 ffCfStrGetString(displayID, &result->playerId);
91 dispatch_group_leave(group);
92 });
93 }
94
95 if (MRMediaRemoteGetNowPlayingApplicationDisplayName) {
96 dispatch_group_enter(group);
97 MRMediaRemoteGetNowPlayingApplicationDisplayName(0, queue, ^(_Nullable CFStringRef name) {
98 ffCfStrGetString(name, &result->player);
99 dispatch_group_leave(group);
100 });
101 }
102
103 dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
104 // Don't dispatch_release because we are using ARC
105
106 if (result->song.length > 0) {
107 return nullptr;
108 }
109
110 return error;
111}
112
113void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
114 const char* error = getMediaByMediaRemote(media, saveCover);
115
116 if (error) {
117 ffStrbufAppendS(&media->error, error);
118 } else {
119 if (media->player.length == 0 && media->playerId.length > 0) {
120 ffStrbufSet(&media->player, &media->playerId);
121 if (ffStrbufStartsWithIgnCaseS(&media->player, "com.")) {
122 ffStrbufSubstrAfter(&media->player, strlen("com.") - 1);
123 }
124 ffStrbufReplaceAllC(&media->player, '.', ' ');
125 }
126 // The cover is a temporary file that this process created, so it is ours to delete. This used
127 // to be flagged only in the branch above, which meant the file was left behind on every run
128 // whose player name was already known — that is, almost all of them.
129 if (media->cover.length > 0) {
130 media->removeCoverAfterUse = true;
131 }
132 }
133}