Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
031315c591 | ||
|
|
f32d5739e1 | ||
|
|
e6558ebbe0 | ||
|
|
2ca3e220bd | ||
|
|
1ea584f6cc | ||
|
|
82ff0d5519 | ||
|
|
a2beea8d75 | ||
|
|
4aa2f2be1e | ||
|
|
47f42c9f39 | ||
|
|
afe00cbc95 | ||
|
|
0fc7d26001 | ||
|
|
6524888c8e | ||
|
|
53808bee74 | ||
|
|
cd17f85f81 | ||
|
|
758a63a6a2 |
@@ -0,0 +1,2 @@
|
|||||||
|
build/
|
||||||
|
dist/
|
||||||
@@ -1,10 +1,19 @@
|
|||||||
# SMTC Bridge
|
# SMTC Bridge
|
||||||
|
|
||||||
SMTC Bridge was designed to expose Windows *System Media Transport Controls (SMTC)* as a clean REST API.
|
SMTC Bridge is a lightweight system tray application that exposes Windows ***System Media Transport Controls (SMTC)*** as a clean REST API.
|
||||||
|
|
||||||
This was designed for developers to easily create their own "Now Playing" widgets that work with anything supported by SMTC, bypassing the complicated API limitations imposed by streaming platforms (fuck you Spotify).
|
This tool allows developers to easily create their own "Now Playing" widgets that work with anything supported by SMTC, bypassing the complex API limitations imposed by streaming platforms (fuck you Spotify).
|
||||||
|
|
||||||
Simply open run the system tray application, and a local web server will run in the background, accessible at `http://127.0.0.1:5000/now-playing/`
|
Simply run the application, and a local web server will run in the background. By default, it can be accessed here after running the application:<br>
|
||||||
|
[http://127.0.0.1:5000/now-playing/](http://127.0.0.1:5000/now-playing/)
|
||||||
|
|
||||||
|
A ready-to-use "Now Playing" widget utilizing SMTC Bridge is available to try here:<br>
|
||||||
|
**[https://widgets.nutty.gg/now-playing/settings/](https://widgets.nutty.gg/now-playing/settings/)**
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
1. Download the latest `.exe` from the [Releases page](releases).
|
||||||
|
2. Run the application — a tray icon will appear.
|
||||||
|
3. Right-click the icon to view the API.
|
||||||
|
|
||||||
### Endpoints
|
### Endpoints
|
||||||
|
|
||||||
@@ -13,10 +22,73 @@ Simply open run the system tray application, and a local web server will run in
|
|||||||
| `GET` | `/now-playing` | Returns the current media state as JSON. |
|
| `GET` | `/now-playing` | Returns the current media state as JSON. |
|
||||||
| `GET` | `/sessions` | Returns a list of all active media sessions. |
|
| `GET` | `/sessions` | Returns a list of all active media sessions. |
|
||||||
|
|
||||||
## Quick Start
|
### Schema
|
||||||
1. Download the latest `.exe` from the [Releases page](releases).
|
The `/now-playing` endpoint provides a real-time snapshot of your active media sessions. Developers can integrate this into their own web widgets using the following JSON structure:
|
||||||
2. Run the application — a tray icon will appear.
|
|
||||||
3. Right-click the icon to view the API.
|
```json
|
||||||
|
{
|
||||||
|
"app_version": "string",
|
||||||
|
"current_session_id": "string",
|
||||||
|
"sessions": [
|
||||||
|
{
|
||||||
|
"source_app_id": "string",
|
||||||
|
"media_properties": {
|
||||||
|
"Title": "string",
|
||||||
|
"Artist": "string",
|
||||||
|
"AlbumTitle": "string",
|
||||||
|
"AlbumArtist": "string",
|
||||||
|
"Thumbnail": "string (base64)",
|
||||||
|
"AlbumTrackCount": "integer",
|
||||||
|
"TrackNumber": "integer",
|
||||||
|
"Genres": "array of strings",
|
||||||
|
"Subtitle": "string"
|
||||||
|
},
|
||||||
|
"playback_info": {
|
||||||
|
"PlaybackStatus": "integer",
|
||||||
|
"PlaybackType": "integer",
|
||||||
|
"PlaybackRate": "number or null",
|
||||||
|
"IsShuffleActive": "boolean",
|
||||||
|
"AutoRepeatMode": "integer"
|
||||||
|
},
|
||||||
|
"timeline_properties": {
|
||||||
|
"Position": "integer (ms)",
|
||||||
|
"StartTime": "integer (ms)",
|
||||||
|
"EndTime": "integer (ms)",
|
||||||
|
"MinSeekTime": "integer (ms)",
|
||||||
|
"MaxSeekTime": "integer (ms)",
|
||||||
|
"LastUpdatedTime": "string (ISO 8601)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enums
|
||||||
|
|
||||||
|
#### Playback Status
|
||||||
|
| Value | Status | Description |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `0` | `CLOSED` | Engine uninitialized or empty |
|
||||||
|
| `1` | `OPENED` | Pipeline loaded but idling |
|
||||||
|
| `2` | `CHANGING` | Buffering, track skipping, or seeking |
|
||||||
|
| `3` | `STOPPED` | Track queued but fully stopped |
|
||||||
|
| `4` | `PLAYING` | Audio actively streaming |
|
||||||
|
| `5` | `PAUSED` | Audio frozen |
|
||||||
|
|
||||||
|
#### Playback Type
|
||||||
|
| Value | Type | Description |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `0` | `UNKNOWN` | Generic audio wrapper |
|
||||||
|
| `1` | `MUSIC` | Pure audio pipeline (Spotify, iTunes, etc.) |
|
||||||
|
| `2` | `VIDEO` | Visual media feed (YouTube, Twitch, etc.) |
|
||||||
|
| `3` | `IMAGE` | Static slideshow presentation hook |
|
||||||
|
|
||||||
|
#### Auto Repeat Mode
|
||||||
|
| Value | Mode | Description |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `0` | `NONE` | Plays queue through and terminates |
|
||||||
|
| `1` | `TRACK` | Single active song looping |
|
||||||
|
| `2` | `LIST` | Parent playlist/album looping |
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
Built and maintained by **nutty**.
|
Built and maintained by **nutty**.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 120 KiB |
+47
-26
@@ -1,5 +1,5 @@
|
|||||||
# Versioning
|
# Versioning
|
||||||
APP_VERSION = "0.0.1"
|
APP_VERSION = "0.0.2"
|
||||||
DEVELOPER = "nutty"
|
DEVELOPER = "nutty"
|
||||||
|
|
||||||
|
|
||||||
@@ -262,29 +262,52 @@ try:
|
|||||||
"Thumbnail": None
|
"Thumbnail": None
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add the album art (i.e. "Thumbnail") as a Base64 string.
|
# CACHE THE THUMBNAIL, DON'T ENCODE IT EVERY TIME, IT'S SLOW AS FUCK
|
||||||
# We will cache this so we don't have to read it every single call.
|
# # Add the album art (i.e. "Thumbnail") as a Base64 string.
|
||||||
# Use the track title and artist as the key -> {title} - {artist}
|
# # 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
|
# # Build the key
|
||||||
title = raw_media.title if raw_media else "Unknown"
|
# title = raw_media.title if raw_media else "Unknown"
|
||||||
artist = raw_media.artist if raw_media else "Unknown"
|
# artist = raw_media.artist if raw_media else "Unknown"
|
||||||
track_key = f"{title} - {artist}"
|
# track_key = f"{title} - {artist}"
|
||||||
|
|
||||||
# Check if the album art is cached.
|
# # Check if the album art is cached.
|
||||||
# If yes:
|
# # If yes:
|
||||||
# - Retrieve it from the cache.
|
# # - Retrieve it from the cache.
|
||||||
# - Add it to the payload
|
# # - Add it to the payload
|
||||||
if app_id in ARTWORK_CACHE and ARTWORK_CACHE[app_id]["track_key"] == track_key:
|
# if app_id in ARTWORK_CACHE and ARTWORK_CACHE[app_id]["track_key"] == track_key:
|
||||||
media_data["Thumbnail"] = ARTWORK_CACHE[app_id]["base64"]
|
# 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))
|
||||||
|
# await reader.load_async(stream.size)
|
||||||
|
# buffer = bytearray(stream.size)
|
||||||
|
# reader.read_bytes(buffer)
|
||||||
|
|
||||||
|
# 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}
|
||||||
|
|
||||||
|
# # Add it to the payload
|
||||||
|
# media_data["Thumbnail"] = base64_art
|
||||||
|
# except Exception:
|
||||||
|
# pass
|
||||||
|
|
||||||
# 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
|
# Read the Base64 string
|
||||||
|
base64_art = None
|
||||||
|
if raw_media and raw_media.thumbnail:
|
||||||
|
try:
|
||||||
stream_ref = raw_media.thumbnail
|
stream_ref = raw_media.thumbnail
|
||||||
stream = await stream_ref.open_read_async()
|
stream = await stream_ref.open_read_async()
|
||||||
reader = DataReader(stream.get_input_stream_at(0))
|
reader = DataReader(stream.get_input_stream_at(0))
|
||||||
@@ -294,14 +317,12 @@ try:
|
|||||||
|
|
||||||
img = Image.open(io.BytesIO(buffer))
|
img = Image.open(io.BytesIO(buffer))
|
||||||
base64_art = f"data:image/png;base64,{base64.b64encode(buffer).decode('utf-8')}"
|
base64_art = f"data:image/png;base64,{base64.b64encode(buffer).decode('utf-8')}"
|
||||||
|
except Exception:
|
||||||
# Cache the Base64 string
|
# Fallback if the stream fails to open or read
|
||||||
ARTWORK_CACHE[app_id] = {"track_key": track_key, "base64": base64_art}
|
base64_art = None
|
||||||
|
|
||||||
# Add it to the payload
|
# Add it to the payload
|
||||||
media_data["Thumbnail"] = base64_art
|
media_data["Thumbnail"] = base64_art
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Finally, assemble all the data into a session object, and add it to the session list
|
# Finally, assemble all the data into a session object, and add it to the session list
|
||||||
sessions_list.append({
|
sessions_list.append({
|
||||||
@@ -392,7 +413,7 @@ try:
|
|||||||
pystray.MenuItem("View Data (JSON)", lambda: webbrowser.open(f"http://{HOST}:{PORT}/now-playing")),
|
pystray.MenuItem("View Data (JSON)", lambda: webbrowser.open(f"http://{HOST}:{PORT}/now-playing")),
|
||||||
pystray.MenuItem("View Active Sessions", lambda: webbrowser.open(f"http://{HOST}:{PORT}/sessions")),
|
pystray.MenuItem("View Active Sessions", lambda: webbrowser.open(f"http://{HOST}:{PORT}/sessions")),
|
||||||
pystray.Menu.SEPARATOR,
|
pystray.Menu.SEPARATOR,
|
||||||
pystray.MenuItem("★ Customize Overlay", lambda: webbrowser.open(f"https://beta.nutty.gg/now-playing/settings/")),
|
pystray.MenuItem("★ Customize Overlay", lambda: webbrowser.open(f"https://widgets.nutty.gg/now-playing/settings/")),
|
||||||
pystray.MenuItem("★ Try my stream widgets!", lambda: webbrowser.open(f"https://nutty.gg/collections/member-exclusive-widgets")),
|
pystray.MenuItem("★ Try my stream widgets!", lambda: webbrowser.open(f"https://nutty.gg/collections/member-exclusive-widgets")),
|
||||||
pystray.Menu.SEPARATOR,
|
pystray.Menu.SEPARATOR,
|
||||||
pystray.MenuItem("Quit", on_quit)
|
pystray.MenuItem("Quit", on_quit)
|
||||||
|
|||||||
Reference in New Issue
Block a user