fix coverart spotify hook on windows

This commit is contained in:
Boof2015
2026-04-19 19:19:37 -04:00
parent 812014f64b
commit ee26e5b5c9
3 changed files with 127 additions and 1 deletions
+102
View File
@@ -17,6 +17,7 @@
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Media.Control.h>
#include <winrt/Windows.Storage.Streams.h>
#include <algorithm>
#include <atomic>
@@ -252,6 +253,98 @@ Napi::Object createWindowsMediaSupport(
return support;
}
static const char kBase64Chars[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string base64Encode(const std::vector<uint8_t>& data) {
std::string result;
result.reserve(((data.size() + 2) / 3) * 4);
for (size_t i = 0; i < data.size(); i += 3) {
const uint32_t b0 = data[i];
const uint32_t b1 = (i + 1 < data.size()) ? data[i + 1] : 0u;
const uint32_t b2 = (i + 2 < data.size()) ? data[i + 2] : 0u;
result += kBase64Chars[(b0 >> 2) & 0x3F];
result += kBase64Chars[((b0 << 4) | (b1 >> 4)) & 0x3F];
result += (i + 1 < data.size()) ? kBase64Chars[((b1 << 2) | (b2 >> 6)) & 0x3F] : '=';
result += (i + 2 < data.size()) ? kBase64Chars[b2 & 0x3F] : '=';
}
return result;
}
// Thumbnail is fetched on a background thread to avoid blocking the NAPI call
// thread with cross-process WinRT async I/O.
std::mutex s_thumbMutex;
std::string s_thumbTrackKey;
std::string s_thumbDataUrl;
bool s_thumbFetching = false;
void launchThumbnailFetch(
const std::string& trackKey,
winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionMediaProperties props) {
const auto thumbnailRef = props.Thumbnail();
if (!thumbnailRef) {
std::lock_guard<std::mutex> lock(s_thumbMutex);
s_thumbFetching = false;
return;
}
std::thread([trackKey, thumbnailRef]() {
std::string result;
try {
using winrt::Windows::Storage::Streams::DataReader;
const HRESULT hr = RoInitialize(RO_INIT_MULTITHREADED);
if (SUCCEEDED(hr) || hr == RPC_E_CHANGED_MODE) {
const auto stream = thumbnailRef.OpenReadAsync().get();
const uint64_t size = stream.Size();
if (size > 0 && size <= 4u * 1024u * 1024u) {
const auto reader = DataReader(stream);
const uint32_t loaded = reader.LoadAsync(static_cast<uint32_t>(size)).get();
if (loaded > 0) {
std::vector<uint8_t> bytes(loaded);
reader.ReadBytes(bytes);
std::string mimeType = winrt::to_string(stream.ContentType());
if (mimeType.empty()) {
mimeType = "image/jpeg";
}
result = "data:" + mimeType + ";base64," + base64Encode(bytes);
}
}
if (SUCCEEDED(hr)) {
RoUninitialize();
}
}
} catch (...) {}
std::lock_guard<std::mutex> lock(s_thumbMutex);
if (s_thumbTrackKey == trackKey) {
s_thumbDataUrl = std::move(result);
}
s_thumbFetching = false;
}).detach();
}
std::string getOrFetchThumbnail(
const std::string& trackKey,
winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionMediaProperties props) {
bool shouldFetch = false;
std::string result;
{
std::lock_guard<std::mutex> lock(s_thumbMutex);
if (s_thumbTrackKey == trackKey) {
result = s_thumbDataUrl;
} else {
s_thumbTrackKey = trackKey;
s_thumbDataUrl.clear();
if (!s_thumbFetching) {
s_thumbFetching = true;
shouldFetch = true;
}
}
}
if (shouldFetch) {
launchThumbnailFetch(trackKey, std::move(props));
}
return result;
}
std::string getDeviceId(IMMDevice* device) {
if (device == nullptr) {
return {};
@@ -1032,6 +1125,15 @@ Napi::Value WindowsMediaGetSpotifyPlaybackState(const Napi::CallbackInfo& info)
payload.Set(
"sourceAppUserModelId",
Napi::String::New(env, winrt::to_string(session->SourceAppUserModelId())));
const std::string trackKey =
winrt::to_string(mediaProperties.Title()) + "\n" +
winrt::to_string(mediaProperties.Artist());
const std::string artworkDataUrl = getOrFetchThumbnail(trackKey, mediaProperties);
payload.Set(
"artworkDataUrl",
artworkDataUrl.empty() ? env.Null() : Napi::String::New(env, artworkDataUrl));
return payload;
} catch (const winrt::hresult_error& error) {
Napi::Error::New(
+24 -1
View File
@@ -363,6 +363,10 @@ function parseWindowsSpotifyStatusPayload(
: 0,
)
const artworkUrl = typeof payload.artworkDataUrl === 'string' && payload.artworkDataUrl
? payload.artworkDataUrl
: null
const currentTrack = title && artist
? {
id: sourceAppUserModelId
@@ -372,7 +376,7 @@ function parseWindowsSpotifyStatusPayload(
artist,
album,
isFavorite: false,
artworkUrl: null,
artworkUrl,
} satisfies LocalSpotifyTrackSnapshot
: null
@@ -1030,6 +1034,25 @@ export class SpotifyProvider implements NowPlayingProviderService<'spotify'> {
return
}
if (artworkUrl.startsWith('data:')) {
const currentSnapshot = this.currentSnapshot
if (!currentSnapshot || getArtworkKey(
currentSnapshot.currentTrack?.id ?? null,
currentSnapshot.currentTrack?.artworkUrl ?? null,
) !== artworkKey) {
return
}
this.currentArtworkKey = artworkKey
this.currentArtworkDataUrl = artworkUrl
this.failedArtworkKey = null
this.state = {
...this.state,
snapshot: toPublicSnapshot(currentSnapshot, artworkUrl),
}
this.emitState()
return
}
const response = await this.fetchImpl(artworkUrl).catch(() => null)
if (!response || !response.ok) {
this.failedArtworkKey = artworkKey
+1
View File
@@ -5,6 +5,7 @@ export type NativeWindowsMediaSupport = NativeCaptureSupport
export interface NativeWindowsSpotifyPlaybackState {
album: string
artworkDataUrl: string | null
artist: string
durationMs: number
playbackStatus: string