From 4575b0d2c611a28f96cb92c82c1e9464c755bb8d Mon Sep 17 00:00:00 2001 From: nutty Date: Mon, 29 Jun 2026 00:56:23 +1000 Subject: [PATCH] =?UTF-8?q?=E2=80=A2=20Renamed=20"Base64Image"=20to=20"Thu?= =?UTF-8?q?mbnail:=20=E2=80=A2=20Minor=20CSS=20bug=20fixes=20=E2=80=A2=20F?= =?UTF-8?q?ully=20documented=20python=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- now-playing/settings/settings.json | 2 + now-playing/smtc-bridge.py | 114 +++++++++++++++++++++++--- now-playing/themes/card/script.js | 4 +- now-playing/themes/card/style.css | 6 +- now-playing/themes/compact/script.js | 2 +- now-playing/themes/simple/script.js | 2 +- now-playing/themes/simple/style.css | 4 +- now-playing/themes/standard/script.js | 4 +- now-playing/themes/standard/style.css | 4 +- 9 files changed, 119 insertions(+), 23 deletions(-) diff --git a/now-playing/settings/settings.json b/now-playing/settings/settings.json index 3bffb9b..4f0bcd2 100644 --- a/now-playing/settings/settings.json +++ b/now-playing/settings/settings.json @@ -39,6 +39,7 @@ "label": "Font Size", "description": "", "type": "number", + "min": 0, "defaultValue": 20, "group": "Appearance" }, @@ -47,6 +48,7 @@ "label": "Max Width", "description": "Set to 0 to utilize full browser width", "type": "number", + "min": 0, "defaultValue": 500, "group": "Appearance" }, diff --git a/now-playing/smtc-bridge.py b/now-playing/smtc-bridge.py index ddc245b..3cf7992 100644 --- a/now-playing/smtc-bridge.py +++ b/now-playing/smtc-bridge.py @@ -42,44 +42,133 @@ async def get_all_media_info(): pass try: + # Instantiate the SMTC manager -> This allows use to "talk" to the Windows Media API manager = await SMTC.request_async() + + # If it returns null, then no media is playing, or something fucked up and I have no + # idea what to do, so just return an empty session list if not manager: return {"current_session_id": None, "sessions": []} + # This is the session for the current media player -> Whatever Windows deems is "in focus" + # will be the current session. + # We will store the SourceAppUserModelId, which we all add to the final payload. + # For all available properties/methods/events, see the official docs: + # https://learn.microsoft.com/en-us/uwp/api/windows.media.control.globalsystemmediatransportcontrolssession?view=winrt-28000 current_focused = manager.get_current_session() current_session_id = current_focused.source_app_user_model_id if current_focused else None + # We will also get all sessions, not just the current session. + # This will provide the client with all the necessary info if they want to target just + # one application. all_sessions = manager.get_sessions() sessions_list = [] + # We will not iterate over all the sessions, and grab all the available info for session in all_sessions: + # Get all the available info for each session object: + # For all available properties/methods/events, see the official docs: + # https://learn.microsoft.com/en-us/uwp/api/windows.media.control.globalsystemmediatransportcontrolssession?view=winrt-28000 app_id = session.source_app_user_model_id raw_playback = session.get_playback_info() raw_timeline = session.get_timeline_properties() raw_media = await session.try_get_media_properties_async() + # Playback info + # https://learn.microsoft.com/en-us/uwp/api/windows.media.control.globalsystemmediatransportcontrolssessionplaybackinfo?view=winrt-28000 playback_data = { + # AutoRepeatMode: Specifies the repeat mode of the session. + "AutoRepeatMode": raw_playback.auto_repeat_mode.value if (raw_playback and raw_playback.auto_repeat_mode) else 0, + + # IsShuffleActive: Specifies whether the session is currently playing content in a shuffled order. + "IsShuffleActive": raw_playback.is_shuffle_active if raw_playback else False, + + # PlaybackRate: The rate at which playback is happening (e.g., 1.0 is normal speed). + "PlaybackRate": raw_playback.playback_rate if raw_playback else 1.0, + + # PlaybackStatus: The current playback state of the session (e.g., Playing, Paused). "PlaybackStatus": raw_playback.playback_status.value if (raw_playback and raw_playback.playback_status) else 0, - "PlaybackType": raw_playback.playback_type.value if (raw_playback and raw_playback.playback_type) else 0, + + # PlaybackType: Specifies what type of content the session has (e.g., Music, Video). + "PlaybackType": raw_playback.playback_type.value if (raw_playback and raw_playback.playback_type) else 0 } + # Timeline properties + # https://learn.microsoft.com/en-us/uwp/api/windows.media.control.globalsystemmediatransportcontrolssessiontimelineproperties?view=winrt-28000 timeline_data = { - "Position": int(raw_timeline.position.total_seconds() * 1000) if raw_timeline.position else 0, + # EndTime: The end timestamp of the current media item. "EndTime": int(raw_timeline.end_time.total_seconds() * 1000) if raw_timeline.end_time else 0, - "LastUpdatedTime": str(raw_timeline.last_updated_time) if raw_timeline.last_updated_time else None + + # LastUpdatedTime: The UTC time at which the timeline properties were last updated. + "LastUpdatedTime": str(raw_timeline.last_updated_time) if raw_timeline.last_updated_time else None, + + # MaxSeekTime: The furthest timestamp at which the content can currently seek to. + "MaxSeekTime": int(raw_timeline.max_seek_time.total_seconds() * 1000) if raw_timeline.max_seek_time else 0, + + # MinSeekTime: The earliest timestamp at which the current media item can currently seek to. + "MinSeekTime": int(raw_timeline.min_seek_time.total_seconds() * 1000) if raw_timeline.min_seek_time else 0, + + # Position: The playback position, current as of LastUpdatedTime. + "Position": int(raw_timeline.position.total_seconds() * 1000) if raw_timeline.position else 0, + + # StartTime: The starting timestamp of the current media item. + "StartTime": int(raw_timeline.start_time.total_seconds() * 1000) if raw_timeline.start_time else 0, } + # Media properties + # https://learn.microsoft.com/en-us/uwp/api/windows.media.control.globalsystemmediatransportcontrolssessionmediaproperties?view=winrt-28000 + media_data = { + # Title: The title of the track. + "Title": raw_media.title if raw_media else "Unknown", + + # Artist: The name of the artist. + "Artist": raw_media.artist if raw_media else "Unknown", + + # AlbumTitle: The title of the album. + "AlbumTitle": raw_media.album_title if raw_media else "Unknown", + + # AlbumArtist: The artist associated with the album. + "AlbumArtist": raw_media.album_artist if raw_media else "Unknown", + + # TrackNumber: The track number within the album. + "TrackNumber": raw_media.track_number if raw_media else 0, + + # AlbumTrackCount: The total number of tracks on the album. + "AlbumTrackCount": raw_media.album_track_count if raw_media else 0, + + # Genres: The list of genres associated with the track. + "Genres": list(raw_media.genres) if raw_media else [], + + # Subtitle: Any subtitle information (common in podcasts/videos). + "Subtitle": raw_media.subtitle if raw_media else "", + + # Thumbnail: Our cached thumbnail representation. + "Thumbnail": None + } + + # Add the album art (i.e. "Thumbnail") as a Base64 string. + # We will cache this so we don't have to read it every single call. + # Use the track title and artist as the key -> {title} - {artist} + + # Build the key title = raw_media.title if raw_media else "Unknown" artist = raw_media.artist if raw_media else "Unknown" track_key = f"{title} - {artist}" - - media_data = {"Title": title, "Artist": artist, "Base64Image": None} - - if app_id in ARTWORK_CACHE and ARTWORK_CACHE[app_id]["track_key"] == track_key: - media_data["Base64Image"] = ARTWORK_CACHE[app_id]["base64"] + # Check if the album art is cached. + # If yes: + # - Retrieve it from the cache. + # - Add it to the payload + if app_id in ARTWORK_CACHE and ARTWORK_CACHE[app_id]["track_key"] == track_key: + media_data["Thumbnail"] = ARTWORK_CACHE[app_id]["base64"] + + # If not + # - Read the Base64 string + # - Cache the Base64 string + # - Add it to the payload elif raw_media and raw_media.thumbnail: try: + # Read the Base64 string stream_ref = raw_media.thumbnail stream = await stream_ref.open_read_async() reader = DataReader(stream.get_input_stream_at(0)) @@ -90,18 +179,23 @@ async def get_all_media_info(): img = Image.open(io.BytesIO(buffer)) base64_art = f"data:image/png;base64,{base64.b64encode(buffer).decode('utf-8')}" + # Cache the Base64 string ARTWORK_CACHE[app_id] = {"track_key": track_key, "base64": base64_art} - media_data["Base64Image"] = base64_art + + # Add it to the payload + media_data["Thumbnail"] = base64_art except Exception: pass + # Finally, assemble all the data into a session object, and add it to the session list sessions_list.append({ "source_app_id": app_id, "playback_info": playback_data, "timeline_properties": timeline_data, "media_properties": media_data }) - + + # Build the final payload return {"current_session_id": current_session_id, "sessions": sessions_list} except Exception as e: return {"current_session_id": None, "sessions": [], "error": str(e)} diff --git a/now-playing/themes/card/script.js b/now-playing/themes/card/script.js index 88baa7f..4334a27 100644 --- a/now-playing/themes/card/script.js +++ b/now-playing/themes/card/script.js @@ -74,7 +74,7 @@ async function UpdatePlayerState(data) { const timelineProps = targetSession.timeline_properties; // Calcualte an accent color - const accentColorPalette = await GetAccentPalette(mediaProps.Base64Image); + const accentColorPalette = await GetAccentPalette(mediaProps.Thumbnail); // 1. Check if playback status has changed and update visibility accordingly if (playbackInfo.PlaybackStatus !== CurrentPlaybackStatus) { @@ -168,7 +168,7 @@ async function ChangeTrack(mediaProps, tintColor) { artistLabel.innerText = swapArtistTrack ? mediaProps.Title : mediaProps.Artist; // Extract the image source string (use fallback if Windows has no art) - const newArtUrl = mediaProps.Base64Image; + const newArtUrl = mediaProps.Thumbnail; // Set the image backgroundLayer.style.backgroundImage = `url('${newArtUrl}')`; diff --git a/now-playing/themes/card/style.css b/now-playing/themes/card/style.css index 506c4f5..04922a8 100644 --- a/now-playing/themes/card/style.css +++ b/now-playing/themes/card/style.css @@ -32,12 +32,12 @@ transition: opacity 0.5s ease; } -#art-layer { +#album-art-layer { z-index: 1; transition: all 0.5s ease; } -#art-transition-layer { +#album-art-transition-layer { z-index: 0; } @@ -82,7 +82,7 @@ position: relative; /* Keeps it in the document flow */ z-index: 2; /* Sits above the background-layer */ - padding: 1.5em 1.25em; + padding: 2em 1.75em; display: flex; flex-direction: column; gap: 0.3em; diff --git a/now-playing/themes/compact/script.js b/now-playing/themes/compact/script.js index 6efd239..b91acaf 100644 --- a/now-playing/themes/compact/script.js +++ b/now-playing/themes/compact/script.js @@ -69,7 +69,7 @@ async function UpdatePlayerState(data) { const timelineProps = targetSession.timeline_properties; // Calcualte an accent color - const accentColorPalette = await GetAccentPalette(mediaProps.Base64Image); + const accentColorPalette = await GetAccentPalette(mediaProps.Thumbnail); // 1. Check if playback status has changed and update visibility accordingly if (playbackInfo.PlaybackStatus !== CurrentPlaybackStatus) { diff --git a/now-playing/themes/simple/script.js b/now-playing/themes/simple/script.js index 100a4c0..f3074c4 100644 --- a/now-playing/themes/simple/script.js +++ b/now-playing/themes/simple/script.js @@ -165,7 +165,7 @@ async function ChangeTrack(mediaProps) { artistLabel.innerText = swapArtistTrack ? mediaProps.Title : mediaProps.Artist; // Extract the image source string (use fallback if Windows has no art) - const newArtUrl = mediaProps.Base64Image; + const newArtUrl = mediaProps.Thumbnail; // const accent = mediaProps.AccentColor || "#ffffff"; // Set the image diff --git a/now-playing/themes/simple/style.css b/now-playing/themes/simple/style.css index 9f5dfbf..5a879ef 100644 --- a/now-playing/themes/simple/style.css +++ b/now-playing/themes/simple/style.css @@ -32,12 +32,12 @@ transition: opacity 0.5s ease; } -#art-layer { +#album-art-layer { z-index: 1; transition: all 0.5s ease; } -#art-transition-layer { +#album-art-transition-layer { z-index: 0; } diff --git a/now-playing/themes/standard/script.js b/now-playing/themes/standard/script.js index b80f03c..4adec98 100644 --- a/now-playing/themes/standard/script.js +++ b/now-playing/themes/standard/script.js @@ -74,7 +74,7 @@ async function UpdatePlayerState(data) { const timelineProps = targetSession.timeline_properties; // Calcualte an accent color - const accentColorPalette = await GetAccentPalette(mediaProps.Base64Image); + const accentColorPalette = await GetAccentPalette(mediaProps.Thumbnail); // 1. Check if playback status has changed and update visibility accordingly if (playbackInfo.PlaybackStatus !== CurrentPlaybackStatus) { @@ -168,7 +168,7 @@ async function ChangeTrack(mediaProps, tintColor) { artistLabel.innerText = swapArtistTrack ? mediaProps.Title : mediaProps.Artist; // Extract the image source string (use fallback if Windows has no art) - const newArtUrl = mediaProps.Base64Image; + const newArtUrl = mediaProps.Thumbnail; // Set the image backgroundLayer.style.backgroundImage = `url('${newArtUrl}')`; diff --git a/now-playing/themes/standard/style.css b/now-playing/themes/standard/style.css index 5cce20e..fc8054f 100644 --- a/now-playing/themes/standard/style.css +++ b/now-playing/themes/standard/style.css @@ -31,12 +31,12 @@ transition: opacity 0.5s ease; } -#art-layer { +#album-art-layer { z-index: 1; transition: all 0.5s ease; } -#art-transition-layer { +#album-art-transition-layer { z-index: 0; }