summaryrefslogtreecommitdiffstats
path: root/src/detection/media
diff options
context:
space:
mode:
Diffstat (limited to 'src/detection/media')
-rw-r--r--src/detection/media/media.c45
-rw-r--r--src/detection/media/media.h21
-rw-r--r--src/detection/media/media_apple.m209
-rw-r--r--src/detection/media/media_linux.c288
-rw-r--r--src/detection/media/media_nosupport.c5
-rw-r--r--src/detection/media/media_windows.cpp421
6 files changed, 989 insertions, 0 deletions
diff --git a/src/detection/media/media.c b/src/detection/media/media.c
new file mode 100644
index 0000000..7adfb99
--- /dev/null
+++ b/src/detection/media/media.c
@@ -0,0 +1,45 @@
+#include "media.h"
+#include "common/io.h"
+
+void ffDetectMediaImpl(FFMediaResult* media, bool saveCover);
+
+static FFMediaResult result;
+
+static void removeMediaCoverFile(void) {
+ if (result.cover.length > 0) {
+ ffRemoveFile(result.cover.chars);
+ ffStrbufDestroy(&result.cover);
+ }
+}
+
+const FFMediaResult* ffDetectMedia(bool saveCover) {
+ if (result.error.chars == NULL) {
+ ffStrbufInit(&result.error);
+ ffStrbufInit(&result.playerId);
+ ffStrbufInit(&result.player);
+ ffStrbufInit(&result.song);
+ ffStrbufInit(&result.artist);
+ ffStrbufInit(&result.album);
+ ffStrbufInit(&result.url);
+ ffStrbufInit(&result.status);
+ ffStrbufInit(&result.cover);
+ result.length = 0;
+ result.position = 0;
+ result.removeCoverAfterUse = false;
+ ffDetectMediaImpl(&result, saveCover);
+
+ if (result.song.length == 0 && result.error.length == 0) {
+ ffStrbufAppendS(&result.error, "No media found");
+ }
+ ffStrbufTrimRightSpace(&result.song);
+ ffStrbufTrimRightSpace(&result.artist);
+ ffStrbufTrimRightSpace(&result.album);
+ ffStrbufTrimRightSpace(&result.player);
+
+ if (saveCover && result.removeCoverAfterUse) {
+ atexit(removeMediaCoverFile);
+ }
+ }
+
+ return &result;
+}
diff --git a/src/detection/media/media.h b/src/detection/media/media.h
new file mode 100644
index 0000000..ea241e6
--- /dev/null
+++ b/src/detection/media/media.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "fastfetch.h"
+#include "modules/media/option.h"
+
+typedef struct FFMediaResult {
+ FFstrbuf error;
+ FFstrbuf playerId; // Bus name on Linux, app bundle name on macOS. e.g. plasma-browser-integration
+ FFstrbuf player; // e.g. Google Chrome
+ FFstrbuf song;
+ FFstrbuf artist;
+ FFstrbuf album;
+ FFstrbuf url;
+ FFstrbuf status;
+ FFstrbuf cover;
+ uint32_t length; // In milliseconds
+ uint32_t position; // In milliseconds
+ bool removeCoverAfterUse;
+} FFMediaResult;
+
+const FFMediaResult* ffDetectMedia(bool saveCover);
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;
+ }
+ }
+}
diff --git a/src/detection/media/media_linux.c b/src/detection/media/media_linux.c
new file mode 100644
index 0000000..724e834
--- /dev/null
+++ b/src/detection/media/media_linux.c
@@ -0,0 +1,288 @@
+#include "fastfetch.h"
+#include "common/io.h"
+#include "common/strutil.h"
+#include "common/memrchr.h"
+#include "detection/media/media.h"
+
+#include <string.h>
+
+#define FF_DBUS_MPRIS_PREFIX "org.mpris.MediaPlayer2."
+
+#ifdef FF_HAVE_DBUS
+ #include "common/dbus.h"
+
+ #define FF_DBUS_ITER_CONTINUE(dbus, iterator) \
+ { \
+ if (!(dbus)->lib->ffdbus_message_iter_next(iterator)) \
+ break; \
+ continue; \
+ }
+
+static bool parseMprisMetadata(FFDBusData* data, DBusMessageIter* rootIterator, FFMediaResult* result) {
+ DBusMessageIter arrayIterator;
+
+ if (data->lib->ffdbus_message_iter_get_arg_type(rootIterator) == DBUS_TYPE_VARIANT) {
+ DBusMessageIter variantIterator;
+ data->lib->ffdbus_message_iter_recurse(rootIterator, &variantIterator);
+ if (data->lib->ffdbus_message_iter_get_arg_type(&variantIterator) != DBUS_TYPE_ARRAY) {
+ return false;
+ }
+ data->lib->ffdbus_message_iter_recurse(&variantIterator, &arrayIterator);
+ } else {
+ data->lib->ffdbus_message_iter_recurse(rootIterator, &arrayIterator);
+ }
+
+ while (true) {
+ if (data->lib->ffdbus_message_iter_get_arg_type(&arrayIterator) != DBUS_TYPE_DICT_ENTRY) {
+ FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
+ }
+
+ DBusMessageIter dictIterator;
+ data->lib->ffdbus_message_iter_recurse(&arrayIterator, &dictIterator);
+
+ if (data->lib->ffdbus_message_iter_get_arg_type(&dictIterator) != DBUS_TYPE_STRING) {
+ FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
+ }
+
+ if (!data->lib->ffdbus_message_iter_has_next(&dictIterator)) {
+ FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
+ }
+
+ const char* key;
+ data->lib->ffdbus_message_iter_get_basic(&dictIterator, &key);
+
+ data->lib->ffdbus_message_iter_next(&dictIterator);
+
+ if (ffStrStartsWith(key, "xesam:")) {
+ const char* xesam = key + strlen("xesam:");
+ if (ffStrEquals(xesam, "title")) {
+ ffDBusGetString(data, &dictIterator, &result->song);
+ } else if (ffStrEquals(xesam, "album")) {
+ ffDBusGetString(data, &dictIterator, &result->album);
+ } else if (ffStrEquals(xesam, "artist")) {
+ ffDBusGetString(data, &dictIterator, &result->artist);
+ } else if (ffStrEquals(xesam, "url")) {
+ ffDBusGetString(data, &dictIterator, &result->url);
+ }
+
+ if (result->song.length > 0 && result->artist.length > 0 && result->album.length > 0 && result->url.length > 0) {
+ break;
+ }
+ } else if (ffStrStartsWith(key, "mpris:")) {
+ const char* mpris = key + strlen("mpris:");
+ if (ffStrEquals(mpris, "artUrl")) {
+ FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
+ ffDBusGetString(data, &dictIterator, &path);
+ if (ffStrbufStartsWithS(&path, "file:///")) {
+ ffStrbufEnsureFree(&result->cover, path.length - (uint32_t) strlen("file://"));
+ for (uint32_t i = (uint32_t) strlen("file://"); i < path.length; ++i) {
+ if (path.chars[i] == '%') {
+ if (i + 2 >= path.length) {
+ break;
+ }
+ char str[] = { path.chars[i + 1], path.chars[i + 2], 0 };
+ char* end = NULL;
+ const char decodedChar = (char) strtoul(str, &end, 16);
+ if (end == &str[2]) {
+ i += 2;
+ ffStrbufAppendC(&result->cover, decodedChar);
+ } else {
+ ffStrbufAppendC(&result->cover, '%');
+ }
+ } else {
+ ffStrbufAppendC(&result->cover, path.chars[i]);
+ }
+ }
+ }
+ } else if (ffStrEquals(mpris, "length")) {
+ int64_t length = 0; // microseconds
+ if (ffDBusGetInt(data, &dictIterator, &length) && length > 0) {
+ result->length = (uint32_t) (length / 1000);
+ }
+ }
+ }
+
+ FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
+ }
+
+ return true;
+}
+
+static bool getBusProperties(FFDBusData* data, const char* busName, FFMediaResult* result) {
+ // Get all properties at once to reduce the number of IPCs
+ DBusMessage* reply = ffDBusGetAllProperties(data, busName, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player");
+ if (reply == NULL) {
+ return false;
+ }
+
+ DBusMessageIter rootIterator;
+ if (!data->lib->ffdbus_message_iter_init(reply, &rootIterator) &&
+ data->lib->ffdbus_message_iter_get_arg_type(&rootIterator) != DBUS_TYPE_ARRAY) {
+ data->lib->ffdbus_message_unref(reply);
+ return false;
+ }
+
+ DBusMessageIter arrayIterator;
+ data->lib->ffdbus_message_iter_recurse(&rootIterator, &arrayIterator);
+
+ while (true) {
+ if (data->lib->ffdbus_message_iter_get_arg_type(&arrayIterator) != DBUS_TYPE_DICT_ENTRY) {
+ FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
+ }
+
+ DBusMessageIter dictIterator;
+ data->lib->ffdbus_message_iter_recurse(&arrayIterator, &dictIterator);
+
+ const char* key;
+ data->lib->ffdbus_message_iter_get_basic(&dictIterator, &key);
+
+ data->lib->ffdbus_message_iter_next(&dictIterator);
+
+ if (ffStrEquals(key, "Metadata")) {
+ parseMprisMetadata(data, &dictIterator, result);
+ } else if (ffStrEquals(key, "PlaybackStatus")) {
+ ffDBusGetString(data, &dictIterator, &result->status);
+ } else if (ffStrEquals(key, "Position")) {
+ int64_t position = 0; // microseconds
+ if (ffDBusGetInt(data, &dictIterator, &position) && position > 0) {
+ result->position = (uint32_t) (position / 1000);
+ }
+ }
+
+ FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
+ }
+
+ if (result->song.length == 0) {
+ if (result->url.length) {
+ const char* fileName = memrchr(result->url.chars, '/', result->url.length);
+ assert(fileName);
+ ++fileName;
+ ffStrbufEnsureFixedLengthFree(&result->song, result->url.length - (uint32_t) (fileName - result->url.chars));
+ for (; *fileName && *fileName != '?'; ++fileName) {
+ if (*fileName != '%') {
+ ffStrbufAppendC(&result->song, *fileName);
+ } else {
+ if (fileName[1] == 0 || fileName[2] == 0) {
+ break;
+ }
+ char str[] = { fileName[1], fileName[2], 0 };
+ ffStrbufAppendC(&result->song, (char) strtoul(str, NULL, 16));
+ fileName += 2;
+ }
+ }
+ } else {
+ ffStrbufClear(&result->artist);
+ ffStrbufClear(&result->album);
+ ffStrbufClear(&result->url);
+ ffStrbufClear(&result->status);
+ data->lib->ffdbus_message_unref(reply);
+ return false;
+ }
+ }
+
+ // Set short bus name
+ ffStrbufAppendS(&result->playerId, busName + sizeof(FF_DBUS_MPRIS_PREFIX) - 1);
+
+ // We found a song, get the player name
+ if (ffStrbufStartsWithS(&result->playerId, "musikcube.instance")) {
+ // dbus calls are EXTREMELY slow on musikcube, so we set the player name manually
+ ffStrbufSetStatic(&result->player, "musikcube");
+ } else {
+ ffDBusGetPropertyString(data, busName, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2", "Identity", &result->player);
+ if (result->player.length == 0) {
+ ffDBusGetPropertyString(data, busName, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2", "DesktopEntry", &result->player);
+ }
+ if (result->player.length == 0) {
+ ffStrbufAppend(&result->player, &result->playerId);
+ }
+ }
+
+ data->lib->ffdbus_message_unref(reply);
+
+ return true;
+}
+
+static void getCustomBus(FFDBusData* data, const FFstrbuf* playerName, FFMediaResult* result) {
+ if (ffStrbufStartsWithS(playerName, FF_DBUS_MPRIS_PREFIX)) {
+ getBusProperties(data, playerName->chars, result);
+ return;
+ }
+
+ FF_STRBUF_AUTO_DESTROY busName = ffStrbufCreateS(FF_DBUS_MPRIS_PREFIX);
+ ffStrbufAppend(&busName, playerName);
+ getBusProperties(data, busName.chars, result);
+}
+
+static void getBestBus(FFDBusData* data, FFMediaResult* result) {
+ if (
+ getBusProperties(data, FF_DBUS_MPRIS_PREFIX "spotify", result) ||
+ getBusProperties(data, FF_DBUS_MPRIS_PREFIX "vlc", result) ||
+ getBusProperties(data, FF_DBUS_MPRIS_PREFIX "plasma-browser-integration", result)) {
+ return;
+ }
+
+ DBusMessage* reply = ffDBusGetMethodReply(data, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames", NULL, NULL);
+ if (reply == NULL) {
+ return;
+ }
+
+ DBusMessageIter rootIterator;
+ if (!data->lib->ffdbus_message_iter_init(reply, &rootIterator) || data->lib->ffdbus_message_iter_get_arg_type(&rootIterator) != DBUS_TYPE_ARRAY) {
+ return;
+ }
+
+ DBusMessageIter arrayIterator;
+ data->lib->ffdbus_message_iter_recurse(&rootIterator, &arrayIterator);
+
+ while (true) {
+ if (data->lib->ffdbus_message_iter_get_arg_type(&arrayIterator) != DBUS_TYPE_STRING) {
+ FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
+ }
+
+ const char* busName;
+ data->lib->ffdbus_message_iter_get_basic(&arrayIterator, &busName);
+
+ if (!ffStrStartsWith(busName, FF_DBUS_MPRIS_PREFIX) ||
+ ffStrEquals(busName + strlen(FF_DBUS_MPRIS_PREFIX), "playerctld")) {
+ FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
+ }
+
+ if (getBusProperties(data, busName, result)) {
+ break;
+ }
+
+ FF_DBUS_ITER_CONTINUE(data, &arrayIterator)
+ }
+
+ data->lib->ffdbus_message_unref(reply);
+}
+
+static const char* getMedia(FFMediaResult* result) {
+ FF_DBUS_AUTO_DESTROY_DATA FFDBusData data = {};
+ const char* error = ffDBusLoadData(DBUS_BUS_SESSION, &data);
+ if (error != NULL) {
+ return error;
+ }
+
+ // FIXME: This is shared for both player and media module.
+ // However it uses an option in one specific module
+ if (instance.config.general.playerName.length > 0) {
+ getCustomBus(&data, &instance.config.general.playerName, result);
+ } else {
+ getBestBus(&data, result);
+ }
+
+ return NULL;
+}
+
+#endif
+
+void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
+ FF_UNUSED(saveCover); // We don't save the cover to a file for Mpris implementation
+#ifdef FF_HAVE_DBUS
+ const char* error = getMedia(media);
+ ffStrbufAppendS(&media->error, error);
+#else
+ ffStrbufAppendS(&media->error, "Fastfetch was compiled without DBus support");
+#endif
+}
diff --git a/src/detection/media/media_nosupport.c b/src/detection/media/media_nosupport.c
new file mode 100644
index 0000000..e7d1c0f
--- /dev/null
+++ b/src/detection/media/media_nosupport.c
@@ -0,0 +1,5 @@
+#include "media.h"
+
+void ffDetectMediaImpl(FFMediaResult* media) {
+ ffStrbufAppendS(&media->error, "Not supported on this platform");
+}
diff --git a/src/detection/media/media_windows.cpp b/src/detection/media/media_windows.cpp
new file mode 100644
index 0000000..f0ebd35
--- /dev/null
+++ b/src/detection/media/media_windows.cpp
@@ -0,0 +1,421 @@
+extern "C" {
+#include "media.h"
+#include "common/time.h"
+#include "common/windows/unicode.h"
+#include "common/windows/com.h"
+}
+
+#if FF_HAVE_WINRT
+ #include <roapi.h>
+ #include <robuffer.h>
+ #include <winstring.h>
+ #include <asyncinfo.h>
+
+ #include <winrt/Windows.ApplicationModel.h>
+ #include <winrt/Windows.Foundation.h>
+ #include <winrt/Windows.Media.Control.h>
+ #include <winrt/Windows.Storage.Streams.h>
+
+using winrt::impl::abi_t;
+using winrt::Windows::Foundation::IAsyncOperation;
+using winrt::Windows::Foundation::IAsyncOperationWithProgress;
+
+static inline void deleteHstring(HSTRING* pstr) {
+ if (*pstr) {
+ WindowsDeleteString(*pstr);
+ }
+}
+
+static inline void ffStrbufSetHstring(FFstrbuf* destination, HSTRING value) {
+ uint32_t length;
+ const wchar_t* raw = WindowsGetStringRawBuffer(value, &length);
+ ffStrbufSetNWS(destination, length, raw);
+}
+
+template <typename Interface>
+static inline HRESULT ffGetActivationFactory(const wchar_t* className, REFIID iid, Interface** factory) {
+ HSTRING_HEADER header;
+ HSTRING runtimeClass;
+ HRESULT hr = WindowsCreateStringReference(className, (UINT32)::wcslen(className), &header, &runtimeClass);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ return RoGetActivationFactory(runtimeClass, iid, reinterpret_cast<void**>(factory));
+}
+
+template <typename TargetProjection, typename SourceAbi>
+static inline HRESULT ffQueryInterface(SourceAbi* source, abi_t<TargetProjection>** target) {
+ return source->QueryInterface(winrt::guid_of<TargetProjection>(), reinterpret_cast<void**>(target));
+}
+
+template <typename TOperationAbi, typename TResultAbi>
+static HRESULT ffWaitForAsyncOperation(TOperationAbi* operation, TResultAbi** result) {
+ IAsyncInfo* FF_AUTO_RELEASE_COM_OBJECT asyncInfo = NULL;
+ HRESULT hr = ffQueryInterface<IAsyncInfo>(operation, &asyncInfo);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ AsyncStatus status = AsyncStatus::Started;
+
+ for (;;) {
+ hr = asyncInfo->get_Status(&status);
+ if (FAILED(hr)) {
+ return hr;
+ }
+ if (status == AsyncStatus::Started) {
+ ffTimeSleep(0);
+ } else {
+ break;
+ }
+ }
+
+ if (status != AsyncStatus::Completed) {
+ HRESULT errorCode = E_FAIL;
+ asyncInfo->get_ErrorCode(&errorCode);
+ return FAILED(errorCode) ? errorCode : E_FAIL;
+ }
+
+ return operation->GetResults((void**) result);
+}
+
+template <typename TResultProjection, typename TOperation>
+static HRESULT ffRunAndWait(TOperation&& operation, abi_t<TResultProjection>** result) {
+ abi_t<IAsyncOperation<TResultProjection>>* FF_AUTO_RELEASE_COM_OBJECT opResult = NULL;
+ HRESULT hr = operation(reinterpret_cast<void**>(&opResult));
+ if (FAILED(hr) || !opResult) {
+ return hr;
+ }
+
+ return ffWaitForAsyncOperation(opResult, result);
+}
+
+template <typename TResultProjection, typename TOperation>
+static HRESULT ffRunAndWait2(TOperation&& operation, abi_t<TResultProjection>** result) {
+ *result = NULL;
+
+ abi_t<IAsyncOperationWithProgress<TResultProjection, int32_t>>* FF_AUTO_RELEASE_COM_OBJECT opResult = NULL;
+ HRESULT hr = operation(reinterpret_cast<void**>(&opResult));
+ if (FAILED(hr) || !opResult) {
+ return hr;
+ }
+
+ return ffWaitForAsyncOperation(opResult, result);
+}
+
+static HRESULT ffSaveThumbnailToTempPath(
+ abi_t<winrt::Windows::Storage::Streams::IRandomAccessStreamReference>* thumbnail,
+ FFstrbuf* destination) {
+ abi_t<winrt::Windows::Storage::Streams::IRandomAccessStreamWithContentType>* FF_AUTO_RELEASE_COM_OBJECT contentStream = NULL;
+ HRESULT hr = ffRunAndWait<winrt::Windows::Storage::Streams::IRandomAccessStreamWithContentType>([=](void** result) {
+ return thumbnail->OpenReadAsync(result);
+ },
+ &contentStream);
+ if (FAILED(hr) || !contentStream) {
+ return FAILED(hr) ? hr : E_FAIL;
+ }
+
+ abi_t<winrt::Windows::Storage::Streams::IRandomAccessStream>* FF_AUTO_RELEASE_COM_OBJECT randomAccessStream = NULL;
+ hr = ffQueryInterface<winrt::Windows::Storage::Streams::IRandomAccessStream>(contentStream, &randomAccessStream);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ UINT64 size = 0;
+ hr = randomAccessStream->get_Size(&size);
+ if (FAILED(hr) || size == 0) {
+ return FAILED(hr) ? hr : S_FALSE;
+ }
+
+ if (size > 0xFFFFFFFFu) {
+ return HRESULT_FROM_WIN32(ERROR_FILE_TOO_LARGE);
+ }
+
+ abi_t<winrt::Windows::Storage::Streams::IBufferFactory>* FF_AUTO_RELEASE_COM_OBJECT bufferFactory = NULL;
+ hr = ffGetActivationFactory(L"Windows.Storage.Streams.Buffer", winrt::guid_of<winrt::Windows::Storage::Streams::IBufferFactory>(), &bufferFactory);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ abi_t<winrt::Windows::Storage::Streams::IBuffer>* FF_AUTO_RELEASE_COM_OBJECT buffer = NULL;
+ hr = bufferFactory->Create((UINT32) size, reinterpret_cast<void**>(&buffer));
+ if (FAILED(hr) || !buffer) {
+ return FAILED(hr) ? hr : E_FAIL;
+ }
+
+ abi_t<winrt::Windows::Storage::Streams::IInputStream>* FF_AUTO_RELEASE_COM_OBJECT inputStream = NULL;
+ hr = ffQueryInterface<winrt::Windows::Storage::Streams::IInputStream>(contentStream, &inputStream);
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ abi_t<winrt::Windows::Storage::Streams::IBuffer>* FF_AUTO_RELEASE_COM_OBJECT readBuffer = NULL;
+ hr = ffRunAndWait2<winrt::Windows::Storage::Streams::IBuffer>([=](void** result) {
+ return inputStream->ReadAsync(buffer, (uint32_t) size, (uint32_t) winrt::Windows::Storage::Streams::InputStreamOptions::None, result);
+ },
+ &readBuffer);
+ if (FAILED(hr) || !readBuffer) {
+ return FAILED(hr) ? hr : E_FAIL;
+ }
+
+ UINT32 length = 0;
+ hr = readBuffer->get_Length(&length);
+ if (FAILED(hr) || length == 0) {
+ return FAILED(hr) ? hr : S_FALSE;
+ }
+
+ Windows::Storage::Streams::IBufferByteAccess* FF_AUTO_RELEASE_COM_OBJECT byteAccess = NULL;
+ hr = readBuffer->QueryInterface(IID_PPV_ARGS(&byteAccess));
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ byte* bytes = NULL;
+ hr = byteAccess->Buffer(&bytes);
+ if (FAILED(hr) || !bytes) {
+ return FAILED(hr) ? hr : E_FAIL;
+ }
+
+ wchar_t tempDirectory[MAX_PATH];
+ DWORD tempLength = GetTempPathW(MAX_PATH, tempDirectory);
+ if (tempLength == 0 || tempLength >= MAX_PATH) {
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+
+ wchar_t tempFilePath[MAX_PATH];
+ if (!GetTempFileNameW(tempDirectory, L"fft", 0, tempFilePath)) {
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+
+ HANDLE file = CreateFileW(tempFilePath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
+ if (file == INVALID_HANDLE_VALUE) {
+ DWORD writeError = GetLastError();
+ DeleteFileW(tempFilePath);
+ return HRESULT_FROM_WIN32(writeError);
+ }
+
+ DWORD written = 0;
+ BOOL writtenOk = WriteFile(file, bytes, length, &written, NULL);
+ NtClose(file);
+ file = NULL;
+
+ if (!writtenOk || written != length) {
+ DWORD writeError = GetLastError();
+ DeleteFileW(tempFilePath);
+ return HRESULT_FROM_WIN32(writtenOk ? ERROR_WRITE_FAULT : writeError);
+ }
+
+ ffStrbufSetWS(destination, tempFilePath);
+ return S_OK;
+}
+
+static const char* getMedia(FFMediaResult* result, bool saveCover) {
+ const char* error = ffInitCom();
+ if (error) {
+ return error;
+ }
+
+ do {
+ abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManagerStatics>* FF_AUTO_RELEASE_COM_OBJECT managerStatics = NULL;
+ HRESULT hr = ffGetActivationFactory(L"Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager", winrt::guid_of<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManagerStatics>(), &managerStatics);
+ if (FAILED(hr) || !managerStatics) {
+ error = "winrt: RoGetActivationFactory(GlobalSystemMediaTransportControlsSessionManager) failed";
+ break;
+ }
+
+ abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManager>* FF_AUTO_RELEASE_COM_OBJECT manager = NULL;
+ hr = ffRunAndWait<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManager>([=](void** result) {
+ return managerStatics->RequestAsync(result);
+ },
+ &manager);
+ if (FAILED(hr) || !manager) {
+ error = "winrt: RequestAsync().GetResults() failed";
+ break;
+ }
+
+ FF_A_CLEANUP(deleteHstring) HSTRING playerId = NULL;
+
+ abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSession>* FF_AUTO_RELEASE_COM_OBJECT session = NULL;
+ if (instance.config.general.playerName.length) {
+ abi_t<winrt::Windows::Foundation::Collections::IVectorView<winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSession>>* FF_AUTO_RELEASE_COM_OBJECT sessions = NULL;
+ hr = manager->GetSessions(reinterpret_cast<void**>(&sessions));
+ if (FAILED(hr) || !sessions) {
+ error = "winrt: GetSessions() failed";
+ break;
+ }
+ uint32_t sessionCount = 0;
+ hr = sessions->get_Size(&sessionCount);
+ if (FAILED(hr)) {
+ error = "winrt: GetSessions().get_Size() failed";
+ break;
+ }
+ for (uint32_t i = 0; i < sessionCount; i++) {
+ abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSession>* FF_AUTO_RELEASE_COM_OBJECT currentSession = NULL;
+ hr = sessions->GetAt(i, reinterpret_cast<void**>(&currentSession));
+ if (FAILED(hr) || !currentSession) {
+ continue;
+ }
+
+ hr = currentSession->get_SourceAppUserModelId(reinterpret_cast<void**>(&playerId));
+ if (FAILED(hr) || !playerId) {
+ continue;
+ }
+
+ ffStrbufSetHstring(&result->playerId, playerId);
+
+ if (ffStrbufContainIgnCase(&result->playerId, &instance.config.general.playerName)) {
+ session = currentSession;
+ currentSession = NULL; // Don't release the session object
+ break;
+ }
+ deleteHstring(&playerId);
+ ffStrbufClear(&result->playerId);
+ }
+
+ if (!session) {
+ error = "winrt: No media session found with the specified player name";
+ break;
+ }
+ } else {
+ hr = manager->GetCurrentSession(reinterpret_cast<void**>(&session));
+
+ if (FAILED(hr) || !session) {
+ error = "winrt: GetCurrentSession() failed";
+ break;
+ }
+
+ hr = session->get_SourceAppUserModelId(reinterpret_cast<void**>(&playerId));
+ if (FAILED(hr)) {
+ error = "winrt: get_SourceAppUserModelId() failed";
+ break;
+ }
+
+ ffStrbufSetHstring(&result->playerId, playerId);
+ }
+
+ abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionMediaProperties>* FF_AUTO_RELEASE_COM_OBJECT mediaProps = NULL;
+ hr = ffRunAndWait<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionMediaProperties>([=](void** result) {
+ return session->TryGetMediaPropertiesAsync(result);
+ },
+ &mediaProps);
+ if (FAILED(hr) || !mediaProps) {
+ error = "winrt: TryGetMediaPropertiesAsync().GetResults() failed";
+ break;
+ }
+
+ abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionPlaybackInfo>* FF_AUTO_RELEASE_COM_OBJECT playbackInfo = NULL;
+ hr = session->GetPlaybackInfo(reinterpret_cast<void**>(&playbackInfo));
+ bool isPlaying = false;
+ double playbackRate = 1.0;
+ if (SUCCEEDED(hr) && playbackInfo) {
+ int32_t playbackStatusValue = 0;
+ if (SUCCEEDED(playbackInfo->get_PlaybackStatus(&playbackStatusValue))) {
+ isPlaying = playbackStatusValue == static_cast<int32_t>(winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing);
+ switch (static_cast<winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus>(playbackStatusValue)) {
+ #define FF_MEDIA_SET_STATUS(status_code) \
+ case winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus::status_code: \
+ ffStrbufSetStatic(&result->status, #status_code); \
+ break;
+ FF_MEDIA_SET_STATUS(Closed)
+ FF_MEDIA_SET_STATUS(Opened)
+ FF_MEDIA_SET_STATUS(Changing)
+ FF_MEDIA_SET_STATUS(Stopped)
+ FF_MEDIA_SET_STATUS(Playing)
+ FF_MEDIA_SET_STATUS(Paused)
+ #undef FF_MEDIA_SET_STATUS
+ }
+ }
+
+ abi_t<winrt::Windows::Foundation::IReference<double>>* FF_AUTO_RELEASE_COM_OBJECT playbackRateRef = NULL;
+ if (SUCCEEDED(playbackInfo->get_PlaybackRate(reinterpret_cast<void**>(&playbackRateRef))) && playbackRateRef) {
+ if (SUCCEEDED(playbackRateRef->get_Value(&playbackRate)) && playbackRate < 0.0) {
+ playbackRate = 0.0;
+ }
+ }
+ }
+
+ FF_A_CLEANUP(deleteHstring) HSTRING title = NULL;
+ if (SUCCEEDED(mediaProps->get_Title(reinterpret_cast<void**>(&title)))) {
+ ffStrbufSetHstring(&result->song, title);
+ }
+
+ FF_A_CLEANUP(deleteHstring) HSTRING artist = NULL;
+ if (SUCCEEDED(mediaProps->get_Artist(reinterpret_cast<void**>(&artist)))) {
+ ffStrbufSetHstring(&result->artist, artist);
+ }
+
+ FF_A_CLEANUP(deleteHstring) HSTRING album = NULL;
+ if (SUCCEEDED(mediaProps->get_AlbumTitle(reinterpret_cast<void**>(&album)))) {
+ ffStrbufSetHstring(&result->album, album);
+ }
+
+ abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionTimelineProperties>* FF_AUTO_RELEASE_COM_OBJECT timelineProps = NULL;
+ hr = session->GetTimelineProperties(reinterpret_cast<void**>(&timelineProps));
+ if (SUCCEEDED(hr) && timelineProps) {
+ int64_t duration = 0;
+ if (SUCCEEDED(timelineProps->get_EndTime(&duration)) && duration > 0) {
+ result->length = (uint32_t) (duration / 10000); // Convert from 100-nanosecond units to milliseconds
+
+ int64_t position = 0;
+ if (SUCCEEDED(timelineProps->get_Position(&position))) {
+ result->position = (uint32_t) (position / 10000); // Convert from 100-nanosecond units to milliseconds
+
+ int64_t lastUpdatedTime = 0;
+ if (isPlaying && SUCCEEDED(timelineProps->get_LastUpdatedTime(&lastUpdatedTime)) && lastUpdatedTime > 0) {
+ uint64_t lastUpdatedTimeMs = ffFileTimeToUnixMs((uint64_t) lastUpdatedTime);
+ uint64_t nowMs = ffTimeGetNow();
+ if (nowMs > lastUpdatedTimeMs) {
+ result->position += (uint32_t) (((double) (nowMs - lastUpdatedTimeMs)) * playbackRate);
+ }
+ }
+ }
+ }
+ }
+
+ abi_t<winrt::Windows::ApplicationModel::IAppInfoStatics>* FF_AUTO_RELEASE_COM_OBJECT appInfoStatics = NULL;
+ hr = ffGetActivationFactory(L"Windows.ApplicationModel.AppInfo", winrt::guid_of<winrt::Windows::ApplicationModel::IAppInfoStatics>(), &appInfoStatics);
+ if (SUCCEEDED(hr) && appInfoStatics) {
+ abi_t<winrt::Windows::ApplicationModel::IAppInfo>* FF_AUTO_RELEASE_COM_OBJECT appInfo = NULL;
+ if (SUCCEEDED(appInfoStatics->GetFromAppUserModelId(reinterpret_cast<void*>(playerId), reinterpret_cast<void**>(&appInfo))) && appInfo) {
+ abi_t<winrt::Windows::ApplicationModel::IAppDisplayInfo>* FF_AUTO_RELEASE_COM_OBJECT displayInfo = NULL;
+ if (SUCCEEDED(appInfo->get_DisplayInfo(reinterpret_cast<void**>(&displayInfo))) && displayInfo) {
+ FF_A_CLEANUP(deleteHstring) HSTRING displayName = NULL;
+ if (SUCCEEDED(displayInfo->get_DisplayName(reinterpret_cast<void**>(&displayName)))) {
+ ffStrbufSetHstring(&result->player, displayName);
+ }
+ }
+ }
+ }
+
+ if (result->player.length == 0) {
+ ffStrbufSet(&result->player, &result->playerId);
+ if (ffStrbufEndsWithIgnCaseS(&result->player, ".exe")) {
+ ffStrbufSubstrBefore(&result->player, result->player.length - 4);
+ }
+ }
+
+ if (saveCover) {
+ abi_t<winrt::Windows::Storage::Streams::IRandomAccessStreamReference>* FF_AUTO_RELEASE_COM_OBJECT thumbnail = NULL;
+ hr = mediaProps->get_Thumbnail(reinterpret_cast<void**>(&thumbnail));
+ if (SUCCEEDED(hr) && thumbnail) {
+ if (SUCCEEDED(ffSaveThumbnailToTempPath(thumbnail, &result->cover)) && result->cover.length > 0) {
+ result->removeCoverAfterUse = true;
+ }
+ }
+ }
+ } while (false);
+
+ return error;
+}
+#else
+static const char* getMedia(FFMediaResult* media, bool saveCover) {
+ FF_UNUSED(media, saveCover);
+ return "Fastfetch is not compiled with WinRT support";
+}
+#endif // FF_HAVE_WINRT
+
+extern "C" void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
+ const char* error = getMedia(media, saveCover);
+ ffStrbufAppendS(&media->error, error);
+}