master ohmu/fastfetch-openbsd / src / detection / media / media_windows.cpp
ohmu  ·  2026-10-15
  1extern "C" {
  2#include "media.h"
  3#include "common/debug.h"
  4#include "common/time.h"
  5#include "common/windows/unicode.h"
  6#include "common/windows/com.h"
  7}
  8
  9#if FF_HAVE_WINRT
 10    #include "common/windows/winrt.hpp"
 11
 12    #include <robuffer.h>
 13
 14    #include <shobjidl.h>
 15    #include <shlobj.h>
 16    #include <knownfolders.h>
 17    #include <shlwapi.h>
 18
 19    #include <winrt/Windows.ApplicationModel.h>
 20    #include <winrt/Windows.Foundation.h>
 21    #include <winrt/Windows.Media.Control.h>
 22    #include <winrt/Windows.Storage.Streams.h>
 23
 24static HRESULT ffSaveThumbnailToTempPath(
 25    abi_t<winrt::Windows::Storage::Streams::IRandomAccessStreamReference>* thumbnail,
 26    FFstrbuf* destination) {
 27    FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IRandomAccessStreamWithContentType>* contentStream = nullptr;
 28    HRESULT hr = ffRunAndWait<winrt::Windows::Storage::Streams::IRandomAccessStreamWithContentType>([=](void** result) {
 29        return thumbnail->OpenReadAsync(result);
 30    },
 31        &contentStream);
 32    if (FAILED(hr) || !contentStream) {
 33        return FAILED(hr) ? hr : E_FAIL;
 34    }
 35
 36    FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IRandomAccessStream>* randomAccessStream = nullptr;
 37    hr = ffQueryInterface<winrt::Windows::Storage::Streams::IRandomAccessStream>(contentStream, &randomAccessStream);
 38    if (FAILED(hr)) {
 39        return hr;
 40    }
 41
 42    UINT64 size = 0;
 43    hr = randomAccessStream->get_Size(&size);
 44    if (FAILED(hr) || size == 0) {
 45        return FAILED(hr) ? hr : S_FALSE;
 46    }
 47
 48    if (size > 0xFFFFFFFFu) {
 49        return HRESULT_FROM_WIN32(ERROR_FILE_TOO_LARGE);
 50    }
 51
 52    FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IBufferFactory>* bufferFactory = nullptr;
 53    hr = ffGetActivationFactory<winrt::Windows::Storage::Streams::IBufferFactory>(L"Windows.Storage.Streams.Buffer", &bufferFactory);
 54    if (FAILED(hr)) {
 55        return hr;
 56    }
 57
 58    FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IBuffer>* buffer = nullptr;
 59    hr = bufferFactory->Create((UINT32) size, reinterpret_cast<void**>(&buffer));
 60    if (FAILED(hr) || !buffer) {
 61        return FAILED(hr) ? hr : E_FAIL;
 62    }
 63
 64    FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IInputStream>* inputStream = nullptr;
 65    hr = ffQueryInterface<winrt::Windows::Storage::Streams::IInputStream>(contentStream, &inputStream);
 66    if (FAILED(hr)) {
 67        return hr;
 68    }
 69
 70    FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IBuffer>* readBuffer = nullptr;
 71    hr = ffRunAndWait2<winrt::Windows::Storage::Streams::IBuffer>([=](void** result) {
 72        return inputStream->ReadAsync(buffer, (uint32_t) size, (uint32_t) winrt::Windows::Storage::Streams::InputStreamOptions::None, result);
 73    },
 74        &readBuffer);
 75    if (FAILED(hr) || !readBuffer) {
 76        return FAILED(hr) ? hr : E_FAIL;
 77    }
 78
 79    UINT32 length = 0;
 80    hr = readBuffer->get_Length(&length);
 81    if (FAILED(hr) || length == 0) {
 82        return FAILED(hr) ? hr : S_FALSE;
 83    }
 84
 85    FF_AUTO_RELEASE_COM_OBJECT Windows::Storage::Streams::IBufferByteAccess* byteAccess = nullptr;
 86    hr = readBuffer->QueryInterface(IID_PPV_ARGS(&byteAccess));
 87    if (FAILED(hr)) {
 88        return hr;
 89    }
 90
 91    byte* bytes = nullptr;
 92    hr = byteAccess->Buffer(&bytes);
 93    if (FAILED(hr) || !bytes) {
 94        return FAILED(hr) ? hr : E_FAIL;
 95    }
 96
 97    wchar_t tempDirectory[MAX_PATH];
 98    DWORD tempLength = GetTempPathW(MAX_PATH, tempDirectory);
 99    if (tempLength == 0 || tempLength >= MAX_PATH) {
100        return HRESULT_FROM_WIN32(GetLastError());
101    }
102
103    wchar_t tempFilePath[MAX_PATH];
104    if (!GetTempFileNameW(tempDirectory, L"fft", 0, tempFilePath)) {
105        return HRESULT_FROM_WIN32(GetLastError());
106    }
107
108    HANDLE file = CreateFileW(tempFilePath, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
109    if (file == INVALID_HANDLE_VALUE) {
110        DWORD writeError = GetLastError();
111        DeleteFileW(tempFilePath);
112        return HRESULT_FROM_WIN32(writeError);
113    }
114
115    DWORD written = 0;
116    BOOL writtenOk = WriteFile(file, bytes, length, &written, nullptr);
117    NtClose(file);
118    file = nullptr;
119
120    if (!writtenOk || written != length) {
121        DWORD writeError = GetLastError();
122        DeleteFileW(tempFilePath);
123        return HRESULT_FROM_WIN32(writtenOk ? ERROR_WRITE_FAULT : writeError);
124    }
125
126    ffStrbufSetWS(destination, tempFilePath);
127    return S_OK;
128}
129
130// Path 1: `Windows.ApplicationModel.AppInfo`, which answers straight from the package manifest.
131// It only knows packaged (MSIX) apps, but for those it is the cheap source: resolving a
132// `PackageFamilyName!AppId` AppUserModelId costs about half of what the shell needs, because the
133// shell has to look the application up in the package graph. A *failed* lookup still costs a
134// couple of milliseconds of WinRT class activation, so this is only worth trying on names that
135// are actually packaged -- the caller gates it, see `resolveAppUserModelId`.
136static bool resolvePackagedAppUserModelId(const wchar_t* aumid, FFstrbuf* result) {
137    FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::ApplicationModel::IAppInfoStatics>* statics = nullptr;
138    if (FAILED(ffGetActivationFactory<winrt::Windows::ApplicationModel::IAppInfoStatics>(L"Windows.ApplicationModel.AppInfo", &statics)) || !statics) {
139        return false;
140    }
141
142    HSTRING_HEADER header;
143    HSTRING aumidString;
144    if (FAILED(WindowsCreateStringReference(aumid, (UINT32) wcslen(aumid), &header, &aumidString))) {
145        return false;
146    }
147
148    FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::ApplicationModel::IAppInfo>* appInfo = nullptr;
149    if (FAILED(statics->GetFromAppUserModelId(reinterpret_cast<void*>(aumidString), reinterpret_cast<void**>(&appInfo))) || !appInfo) {
150        return false;
151    }
152
153    FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::ApplicationModel::IAppDisplayInfo>* displayInfo = nullptr;
154    if (FAILED(appInfo->get_DisplayInfo(reinterpret_cast<void**>(&displayInfo))) || !displayInfo) {
155        return false;
156    }
157
158    [[gnu::cleanup(ffDeleteHstring)]] HSTRING displayName = nullptr;
159    if (FAILED(displayInfo->get_DisplayName(reinterpret_cast<void**>(&displayName))) || !displayName) {
160        return false;
161    }
162
163    ffStrbufSetHstring(result, displayName);
164    return result->length > 0;
165}
166
167// Path 2: the Start menu's `AppsFolder` namespace, i.e. the (display name, AppUserModelId) table
168// Windows itself displays. It covers packaged and unpackaged apps alike, and inside that namespace
169// an item's parsing name *is* its AppUserModelId, so `ParseDisplayName` finds a child directly --
170// going through `SHCreateItemFromParsingName(L"shell:AppsFolder\\" + aumid)` costs several times
171// more, because the `shell:` protocol has to be activated first.
172static bool resolveAppsFolderAppUserModelId(const wchar_t* aumid, FFstrbuf* result) {
173    const size_t aumidLength = wcslen(aumid);
174    wchar_t name[512];
175    if (aumidLength == 0 || aumidLength >= ARRAY_SIZE(name)) {
176        return false;
177    }
178    wmemcpy(name, aumid, aumidLength + 1); // `ParseDisplayName` wants a mutable string
179
180    FF_AUTO_RELEASE_COM_OBJECT IShellItem* folder = nullptr;
181    if (FAILED(SHGetKnownFolderItem(FOLDERID_AppsFolder, KF_FLAG_DEFAULT, nullptr, IID_PPV_ARGS(&folder))) || !folder) {
182        return false;
183    }
184
185    FF_AUTO_RELEASE_COM_OBJECT IShellFolder* shellFolder = nullptr;
186    if (FAILED(folder->BindToHandler(nullptr, BHID_SFObject, IID_PPV_ARGS(&shellFolder))) || !shellFolder) {
187        return false;
188    }
189
190    LPITEMIDLIST child = nullptr;
191    ULONG attributes = 0;
192    if (FAILED(shellFolder->ParseDisplayName(nullptr, nullptr, name, &attributes, &child, nullptr)) || !child) {
193        return false;
194    }
195
196    bool success = false;
197    STRRET strret = {};
198    if (SUCCEEDED(shellFolder->GetDisplayNameOf(child, SHGDN_INFOLDER, &strret))) {
199        wchar_t displayName[ARRAY_SIZE(name)];
200        if (SUCCEEDED(StrRetToBufW(&strret, child, displayName, ARRAY_SIZE(displayName)))) {
201            ffStrbufSetWS(result, displayName);
202            success = result->length > 0;
203        }
204    }
205
206    CoTaskMemFree(child);
207    return success;
208}
209
210// The two sources above agree on every packaged app, and the shell alone covers everything else,
211// so picking between them is purely a matter of cost. `PackageFamilyName!AppId` is the shape
212// `AppInfo` can serve, and the `!` is what distinguishes it from an unpackaged AppUserModelId
213// (`Chrome`, `PotPlayerMini64.exe`, a derived path). The shell stays the fallback either way, so a
214// packaged-looking name that `AppInfo` rejects is still resolved.
215static bool resolveAppUserModelId(const wchar_t* aumid, FFstrbuf* result) {
216    if (wcschr(aumid, L'!') && resolvePackagedAppUserModelId(aumid, result)) {
217        return true;
218    }
219
220    return resolveAppsFolderAppUserModelId(aumid, result);
221}
222
223static const char* getMedia(FFMediaResult* result, bool saveCover) {
224    const char* error = ffInitCom();
225    if (error) {
226        return error;
227    }
228
229    do {
230        FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManagerStatics>* managerStatics = nullptr;
231        HRESULT hr = ffGetActivationFactory<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManagerStatics>(L"Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager", &managerStatics);
232        if (FAILED(hr) || !managerStatics) {
233            error = "winrt: RoGetActivationFactory(GlobalSystemMediaTransportControlsSessionManager) failed";
234            break;
235        }
236
237        FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManager>* manager = nullptr;
238        hr = ffRunAndWait<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManager>([=](void** result) {
239            return managerStatics->RequestAsync(result);
240        },
241            &manager);
242        if (FAILED(hr) || !manager) {
243            error = "winrt: RequestAsync().GetResults() failed";
244            break;
245        }
246
247        [[gnu::cleanup(ffDeleteHstring)]] HSTRING playerId = nullptr;
248
249        FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSession>* session = nullptr;
250        if (instance.config.general.playerName.length) {
251            FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Foundation::Collections::IVectorView<winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSession>>* sessions = nullptr;
252            hr = manager->GetSessions(reinterpret_cast<void**>(&sessions));
253            if (FAILED(hr) || !sessions) {
254                error = "winrt: GetSessions() failed";
255                break;
256            }
257            uint32_t sessionCount = 0;
258            hr = sessions->get_Size(&sessionCount);
259            if (FAILED(hr)) {
260                error = "winrt: GetSessions().get_Size() failed";
261                break;
262            }
263            for (uint32_t i = 0; i < sessionCount; i++) {
264                FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSession>* currentSession = nullptr;
265                hr = sessions->GetAt(i, reinterpret_cast<void**>(&currentSession));
266                if (FAILED(hr) || !currentSession) {
267                    continue;
268                }
269
270                hr = currentSession->get_SourceAppUserModelId(reinterpret_cast<void**>(&playerId));
271                if (FAILED(hr) || !playerId) {
272                    continue;
273                }
274
275                ffStrbufSetHstring(&result->playerId, playerId);
276
277                if (ffStrbufContainIgnCase(&result->playerId, &instance.config.general.playerName)) {
278                    session = currentSession;
279                    currentSession = nullptr; // Don't release the session object
280                    break;
281                }
282                ffDeleteHstring(&playerId);
283                ffStrbufClear(&result->playerId);
284            }
285
286            if (!session) {
287                error = "winrt: No media session found with the specified player name";
288                break;
289            }
290        } else {
291            hr = manager->GetCurrentSession(reinterpret_cast<void**>(&session));
292
293            if (FAILED(hr) || !session) {
294                error = "winrt: GetCurrentSession() failed";
295                break;
296            }
297
298            hr = session->get_SourceAppUserModelId(reinterpret_cast<void**>(&playerId));
299            if (FAILED(hr)) {
300                error = "winrt: get_SourceAppUserModelId() failed";
301                break;
302            }
303
304            ffStrbufSetHstring(&result->playerId, playerId);
305        }
306
307        FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionMediaProperties>* mediaProps = nullptr;
308        hr = ffRunAndWait<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionMediaProperties>([=](void** result) {
309            return session->TryGetMediaPropertiesAsync(result);
310        },
311            &mediaProps);
312        if (FAILED(hr) || !mediaProps) {
313            error = "winrt: TryGetMediaPropertiesAsync().GetResults() failed";
314            break;
315        }
316
317        FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionPlaybackInfo>* playbackInfo = nullptr;
318        hr = session->GetPlaybackInfo(reinterpret_cast<void**>(&playbackInfo));
319        bool isPlaying = false;
320        double playbackRate = 1.0;
321        if (SUCCEEDED(hr) && playbackInfo) {
322            int32_t playbackStatusValue = 0;
323            if (SUCCEEDED(playbackInfo->get_PlaybackStatus(&playbackStatusValue))) {
324                isPlaying = playbackStatusValue == static_cast<int32_t>(winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing);
325                switch (static_cast<winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus>(playbackStatusValue)) {
326    #define FF_MEDIA_SET_STATUS(status_code)                                                                       \
327        case winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus::status_code: \
328            ffStrbufSetStatic(&result->status, #status_code);                                                      \
329            break;
330                    FF_MEDIA_SET_STATUS(Closed)
331                    FF_MEDIA_SET_STATUS(Opened)
332                    FF_MEDIA_SET_STATUS(Changing)
333                    FF_MEDIA_SET_STATUS(Stopped)
334                    FF_MEDIA_SET_STATUS(Playing)
335                    FF_MEDIA_SET_STATUS(Paused)
336    #undef FF_MEDIA_SET_STATUS
337                }
338            }
339
340            FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Foundation::IReference<double>>* playbackRateRef = nullptr;
341            if (SUCCEEDED(playbackInfo->get_PlaybackRate(reinterpret_cast<void**>(&playbackRateRef))) && playbackRateRef) {
342                if (SUCCEEDED(playbackRateRef->get_Value(&playbackRate)) && playbackRate < 0.0) {
343                    playbackRate = 0.0;
344                }
345            }
346        }
347
348        [[gnu::cleanup(ffDeleteHstring)]] HSTRING title = nullptr;
349        if (SUCCEEDED(mediaProps->get_Title(reinterpret_cast<void**>(&title)))) {
350            ffStrbufSetHstring(&result->song, title);
351        }
352
353        [[gnu::cleanup(ffDeleteHstring)]] HSTRING artist = nullptr;
354        if (SUCCEEDED(mediaProps->get_Artist(reinterpret_cast<void**>(&artist)))) {
355            ffStrbufSetHstring(&result->artist, artist);
356        }
357
358        [[gnu::cleanup(ffDeleteHstring)]] HSTRING album = nullptr;
359        if (SUCCEEDED(mediaProps->get_AlbumTitle(reinterpret_cast<void**>(&album)))) {
360            ffStrbufSetHstring(&result->album, album);
361        }
362
363        FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionTimelineProperties>* timelineProps = nullptr;
364        hr = session->GetTimelineProperties(reinterpret_cast<void**>(&timelineProps));
365        if (SUCCEEDED(hr) && timelineProps) {
366            int64_t duration = 0;
367            if (SUCCEEDED(timelineProps->get_EndTime(&duration)) && duration > 0) {
368                result->length = (uint32_t) (duration / 10000); // Convert from 100-nanosecond units to milliseconds
369
370                int64_t position = 0;
371                if (SUCCEEDED(timelineProps->get_Position(&position))) {
372                    result->position = (uint32_t) (position / 10000); // Convert from 100-nanosecond units to milliseconds
373
374                    int64_t lastUpdatedTime = 0;
375                    if (isPlaying && SUCCEEDED(timelineProps->get_LastUpdatedTime(&lastUpdatedTime)) && lastUpdatedTime > 0) {
376                        uint64_t lastUpdatedTimeMs = ffFileTimeToUnixMs((uint64_t) lastUpdatedTime);
377                        uint64_t nowMs = ffTimeGetNow();
378                        if (nowMs > lastUpdatedTimeMs) {
379                            result->position += (uint32_t) (((double) (nowMs - lastUpdatedTimeMs)) * playbackRate);
380                        }
381                    }
382                }
383            }
384        }
385
386        if (playerId) {
387            uint32_t aumidLength = 0;
388            resolveAppUserModelId(WindowsGetStringRawBuffer(playerId, &aumidLength), &result->player);
389        }
390
391        if (result->player.length == 0) {
392            ffStrbufSet(&result->player, &result->playerId);
393            if (ffStrbufEndsWithIgnCaseS(&result->player, ".exe")) {
394                ffStrbufSubstrBefore(&result->player, result->player.length - 4);
395            }
396        }
397
398        if (saveCover) {
399            FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IRandomAccessStreamReference>* thumbnail = nullptr;
400            hr = mediaProps->get_Thumbnail(reinterpret_cast<void**>(&thumbnail));
401            if (SUCCEEDED(hr) && thumbnail) {
402                if (SUCCEEDED(ffSaveThumbnailToTempPath(thumbnail, &result->cover)) && result->cover.length > 0) {
403                    result->removeCoverAfterUse = true;
404                }
405            }
406        }
407    } while (false);
408
409    return error;
410}
411#else
412static const char* getMedia(FFMediaResult* media, bool saveCover) {
413    FF_UNUSED(media, saveCover);
414    FF_DEBUG("Fastfetch is not compiled with WinRT support");
415    return "Fastfetch is not compiled with WinRT support";
416}
417#endif // FF_HAVE_WINRT
418
419extern "C" void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
420    const char* error = getMedia(media, saveCover);
421    ffStrbufAppendS(&media->error, error);
422}