master ohmu/fastfetch-openbsd / src / detection / media / media_apple.m
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"), &timestampEpoch) == 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}