4 Commits
2 changed files with 76 additions and 78 deletions
+2 -2
View File
@@ -8,10 +8,10 @@ Simply run the application, and a local web server will run in the background. B
[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>
**[IF YOU FOUND THIS PAGE EARLY, DON'T SHARE IT WITH ANYONE YET OR I'LL FUCKING CUM](https://widgets.nutty.gg/now-playing/settings/)**
**[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).
1. Download the latest `.exe` from the [Releases page](https://github.com/nuttylmao/smtc-bridge/releases).
2. Run the application — a tray icon will appear.
3. Right-click the icon to view the API.
+71 -73
View File
@@ -1,15 +1,43 @@
# Versioning
APP_VERSION = "0.0.5"
APP_VERSION = "1.0.0"
DEVELOPER = "nutty"
# IMPORTANT SHIT STARTS HERE
###############
### IMPORTS ###
###############
import sys
import traceback
import os
import datetime
import traceback
import asyncio
import json
import base64
import threading
import os
import sys
import psutil
import tempfile
import atexit
import pystray
import webbrowser
import configparser
import time
import platform
import socket
import hashlib
from PIL import Image
from flask import Flask, jsonify
from flask_cors import CORS
from winsdk.windows.media.control import GlobalSystemMediaTransportControlsSessionManager as SMTC
from winsdk.windows.storage.streams import DataReader
from plyer import notification
import winsdk._winrt as winrt
from collections import OrderedDict
# IMPORTANT SHIT STARTS HERE
def log_crash(e):
# 1. Ensure the 'logs' folder exists
@@ -44,36 +72,6 @@ def log_crash(e):
pass # Ignore errors if file is locked or already gone
try:
###############
### IMPORTS ###
###############
import asyncio
import json
import base64
import io
import threading
import os
import sys
import psutil
import tempfile
import atexit
import pystray
import webbrowser
import configparser
import time
import platform
from PIL import Image
from flask import Flask, jsonify
from flask_cors import CORS
from winsdk.windows.media.control import GlobalSystemMediaTransportControlsSessionManager as SMTC
from winsdk.windows.storage.streams import DataReader
from plyer import notification
import winsdk._winrt as winrt
#############################
### SINGLE INSTANCE CHECK ###
#############################
@@ -146,6 +144,18 @@ try:
HOST = settings.get('SERVER', 'Host')
PORT = settings.getint('SERVER', 'Port')
def get_local_ip():
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
return "127.0.0.1"
DISPLAY_HOST = get_local_ip() if HOST == "0.0.0.0" else HOST
app = Flask(__name__)
CORS(app)
@@ -164,13 +174,16 @@ try:
smtc_manager = None
last_execution_time = 0.0
last_payload = None
album_art_cache = {}
# Thumbnail cache to avoid reprocessing the same artwork repeatedly
MAX_CACHE_SIZE = 50 # Maximum number of unique thumbnails to cache
thumb_cache = OrderedDict()
async def get_all_media_info():
try:
# We will cache the last execution time and payload to avoid redundant parsing if requests flood in faster than 0.5 seconds.
current_time = time.time()
global smtc_manager, last_execution_time, last_payload, album_art_cache
global smtc_manager, last_execution_time, last_payload, thumb_cache
# If requests flood in faster than 0.5 seconds, return the cached result
# to completely spare the CPU from redundant parsing.
@@ -304,62 +317,47 @@ try:
"Thumbnail": None
}
# Read and save thumbnail to a local temp file with byte-hashing cache
# Read thumbnail and convert straight to Base64
thumb_url = None
if raw_media and raw_media.thumbnail:
try:
stream_ref = raw_media.thumbnail
stream = await stream_ref.open_read_async()
# Only proceed if there's actual data to read
if stream.size > 0:
reader = DataReader(stream.get_input_stream_at(0))
await reader.load_async(stream.size)
buffer = bytearray(stream.size)
reader.read_bytes(buffer)
# Generate a safe filename based on the app_id
safe_app_id = "".join(c for c in app_id if c.isalnum() or c in ('_', '-'))
thumb_filename = f"{safe_app_id}.jpg"
thumb_path = os.path.join(THUMB_DIR, thumb_filename)
# Hash the raw image bytes to check if the artwork actually changed
import hashlib
# Hash the raw bytes to see if the artwork is unique
img_hash = hashlib.md5(buffer).hexdigest()
# Check if we already processed this exact artwork for this app
if album_art_cache.get(safe_app_id) == img_hash and os.path.exists(thumb_path):
# Artwork hasn't changed, reuse existing file and version timestamp
# print("Using cached artwork.")
pass
# If we've already processed this exact image bytes, grab it from cache instantly
if img_hash in thumb_cache:
thumb_cache.move_to_end(img_hash)
thumb_url = thumb_cache[img_hash]
# print(f"Using cached artwork for: {media_data['Artist']} - {media_data['Title']}")
else:
print(f"Processing new artwork for: {media_data['Artist']} - {media_data['Title']}")
# Process and save with Pillow only when bytes actually change
img = Image.open(io.BytesIO(buffer))
if img.mode in ("RGBA", "P"):
img = img.convert("RGB")
# Save at full resolution, but use faster compression settings
img.save(
thumb_path,
"JPEG",
quality=80, # Sweet spot for small size / high visual fidelity
subsampling=2, # Faster compression algorithm for low-end CPUs
optimize=False # Skips the extra CPU pass
)
# Convert raw Windows bytes straight to Base64
encoded_img = base64.b64encode(bytes(buffer)).decode('utf-8')
thumb_url = f"data:image/jpeg;base64,{encoded_img}"
# Update cache hash
album_art_cache[safe_app_id] = img_hash
# Store it in the cache so we never process it again for this track
thumb_cache[img_hash] = thumb_url
# Use file modification time as the version token so browsers cache it aggressively
thumb_version = int(os.path.getmtime(thumb_path) * 1000)
# If the cache is full, remove the least recently used item
if len(thumb_cache) > MAX_CACHE_SIZE:
thumb_cache.popitem(last=False)
print(f"Removed least recently used artwork from cache. Current cache size: {len(thumb_cache)}")
# Construct the URL with the version query parameter
thumb_url = f"http://{HOST}:{PORT}/artwork/{safe_app_id}?v={thumb_version}"
except Exception:
except Exception as e:
thumb_url = None
# Add the local URL to the payload
# Add the Base64 data string to the payload
media_data["Thumbnail"] = thumb_url
# Finally, assemble all the data into a session object, and add it to the session list
@@ -509,8 +507,8 @@ try:
menu = pystray.Menu(
pystray.MenuItem(f"SMTC Bridge v{APP_VERSION} by {DEVELOPER}", None, enabled=False),
pystray.Menu.SEPARATOR,
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 Data (JSON)", lambda: webbrowser.open(f"http://{DISPLAY_HOST}:{PORT}/now-playing")),
pystray.MenuItem("View Active Sessions", lambda: webbrowser.open(f"http://{DISPLAY_HOST}:{PORT}/sessions")),
pystray.Menu.SEPARATOR,
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")),