diff options
| author | sumuel <samuel@yakubos.org> | 2026-08-17 20:44:55 +0000 |
|---|---|---|
| committer | sumuel <samuel@yakubos.org> | 2026-08-17 20:44:55 +0000 |
| commit | 76424950e373d3b04ac3dd13019151bfba3e8423 (patch) | |
| tree | 4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/detection/media | |
Add the files
Diffstat (limited to 'src/detection/media')
| -rw-r--r-- | src/detection/media/media.c | 45 | ||||
| -rw-r--r-- | src/detection/media/media.h | 21 | ||||
| -rw-r--r-- | src/detection/media/media_apple.m | 209 | ||||
| -rw-r--r-- | src/detection/media/media_linux.c | 288 | ||||
| -rw-r--r-- | src/detection/media/media_nosupport.c | 5 | ||||
| -rw-r--r-- | src/detection/media/media_windows.cpp | 421 |
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"), ×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); + + 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**>(¤tSession)); + 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); +} |