Initial Release
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
echo --- Checking Dependencies ---
|
||||||
|
:: Ensure pip is installed
|
||||||
|
python -m ensurepip --default-pip >nul 2>&1
|
||||||
|
|
||||||
|
:: Install or upgrade requirements
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
:: Ensure PyInstaller is installed
|
||||||
|
pip show pyinstaller >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo Installing PyInstaller...
|
||||||
|
pip install pyinstaller
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo --- Cleaning Old Builds ---
|
||||||
|
if exist "build" rmdir /s /q "build"
|
||||||
|
if exist "dist" rmdir /s /q "dist"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo --- Building Executable ---
|
||||||
|
pyinstaller --noconfirm smtc-bridge.spec
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo --- Build Complete! ---
|
||||||
|
echo Check the 'dist' folder for your smtc-bridge.exe.
|
||||||
|
pause
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
python .\smtc-bridge.pyw
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
flask
|
||||||
|
flask-cors
|
||||||
|
psutil
|
||||||
|
pystray
|
||||||
|
Pillow
|
||||||
|
plyer
|
||||||
|
winsdk
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[SERVER]
|
||||||
|
host = 127.0.0.1
|
||||||
|
port = 5000
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
+446
@@ -0,0 +1,446 @@
|
|||||||
|
# Versioning
|
||||||
|
APP_VERSION = "0.0.1"
|
||||||
|
DEVELOPER = "nutty"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# IMPORTANT SHIT STARTS HERE
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
def log_crash(e):
|
||||||
|
# 1. Ensure the 'logs' folder exists
|
||||||
|
log_dir = "logs"
|
||||||
|
if not os.path.exists(log_dir):
|
||||||
|
os.makedirs(log_dir)
|
||||||
|
|
||||||
|
# 2. Create timestamped filename: yyyy-MM-dd hh-mm-ss
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
|
||||||
|
filename = os.path.join(log_dir, f"crash_{timestamp}.txt")
|
||||||
|
|
||||||
|
# 3. Write the error log
|
||||||
|
with open(filename, "w", encoding="utf-8") as f:
|
||||||
|
f.write("--- CRASH DETECTED ---\n")
|
||||||
|
f.write(f"Time: {timestamp}\n")
|
||||||
|
f.write(str(e) + "\n\n")
|
||||||
|
f.write(traceback.format_exc())
|
||||||
|
|
||||||
|
# 4. Keep only the 10 most recent logs
|
||||||
|
# Get all .txt files in the log directory
|
||||||
|
files = [os.path.join(log_dir, f) for f in os.listdir(log_dir) if f.endswith(".txt")]
|
||||||
|
|
||||||
|
# Sort by modification time (oldest first)
|
||||||
|
files.sort(key=os.path.getmtime)
|
||||||
|
|
||||||
|
# If we have more than 10, delete the oldest
|
||||||
|
while len(files) > 10:
|
||||||
|
oldest_file = files.pop(0)
|
||||||
|
try:
|
||||||
|
os.remove(oldest_file)
|
||||||
|
except OSError:
|
||||||
|
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
|
||||||
|
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 ###
|
||||||
|
#############################
|
||||||
|
|
||||||
|
def is_already_running():
|
||||||
|
# Use the system temp directory
|
||||||
|
lockfile = os.path.join(tempfile.gettempdir(), 'smtc_bridge.lock')
|
||||||
|
|
||||||
|
# Check if the lockfile already exists
|
||||||
|
if os.path.exists(lockfile):
|
||||||
|
try:
|
||||||
|
with open(lockfile, 'r') as f:
|
||||||
|
pid = int(f.read())
|
||||||
|
# Check if that PID is actually still running
|
||||||
|
if psutil.pid_exists(pid):
|
||||||
|
return True # A real instance is running
|
||||||
|
else:
|
||||||
|
os.remove(lockfile) # Stale lock from a crash, clean it up
|
||||||
|
except (ValueError, PermissionError):
|
||||||
|
# If the file is garbled or we can't read it, treat it as a stale lock
|
||||||
|
os.remove(lockfile)
|
||||||
|
|
||||||
|
# No instance found, write our PID to the lockfile
|
||||||
|
try:
|
||||||
|
with open(lockfile, 'w') as f:
|
||||||
|
f.write(str(os.getpid()))
|
||||||
|
except IOError:
|
||||||
|
pass # Could not write lockfile, but let's proceed anyway
|
||||||
|
|
||||||
|
# Register a cleanup function to delete the file when the app closes
|
||||||
|
atexit.register(lambda: os.remove(lockfile) if os.path.exists(lockfile) else None)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
if is_already_running():
|
||||||
|
print("Another instance is already running. Exiting...")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
######################
|
||||||
|
### INITIALIZATION ###
|
||||||
|
######################
|
||||||
|
|
||||||
|
def get_resource_path(relative_path):
|
||||||
|
""" Get absolute path to resource, works for dev and for PyInstaller """
|
||||||
|
try:
|
||||||
|
# PyInstaller creates a temp folder and stores path in _MEIPASS
|
||||||
|
base_path = sys._MEIPASS
|
||||||
|
except Exception:
|
||||||
|
base_path = os.path.abspath(".")
|
||||||
|
return os.path.join(base_path, relative_path)
|
||||||
|
|
||||||
|
def load_settings():
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config['SERVER'] = {'Host': '127.0.0.1', 'Port': '5000'}
|
||||||
|
|
||||||
|
# Ensure settings.ini is looked for in the executable's directory
|
||||||
|
exe_dir = os.path.dirname(sys.executable if getattr(sys, 'frozen', False) else __file__)
|
||||||
|
ini_path = os.path.join(exe_dir, 'settings.ini')
|
||||||
|
|
||||||
|
if os.path.exists(ini_path):
|
||||||
|
config.read(ini_path)
|
||||||
|
else:
|
||||||
|
with open(ini_path, 'w') as f:
|
||||||
|
config.write(f)
|
||||||
|
return config
|
||||||
|
|
||||||
|
settings = load_settings()
|
||||||
|
HOST = settings.get('SERVER', 'Host')
|
||||||
|
PORT = settings.getint('SERVER', 'Port')
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
ARTWORK_CACHE = {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
######################
|
||||||
|
### CORE FUNCTIONS ###
|
||||||
|
######################
|
||||||
|
|
||||||
|
async def get_all_media_info():
|
||||||
|
global ARTWORK_CACHE
|
||||||
|
|
||||||
|
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: 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 = {
|
||||||
|
# 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: 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}"
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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 {
|
||||||
|
"app_version": APP_VERSION,
|
||||||
|
"current_session_id": current_session_id,
|
||||||
|
"sessions": sessions_list
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {"current_session_id": None, "sessions": [], "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#################
|
||||||
|
### ENDPOINTS ###
|
||||||
|
#################
|
||||||
|
|
||||||
|
@app.route('/now-playing')
|
||||||
|
def now_playing():
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
try:
|
||||||
|
return jsonify(loop.run_until_complete(get_all_media_info()))
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
@app.route('/sessions', methods=['GET'])
|
||||||
|
def get_sessions():
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
async def fetch():
|
||||||
|
manager = await SMTC.request_async()
|
||||||
|
if not manager: return []
|
||||||
|
return list(set([s.source_app_user_model_id for s in manager.get_sessions()]))
|
||||||
|
|
||||||
|
try:
|
||||||
|
sessions = loop.run_until_complete(fetch())
|
||||||
|
|
||||||
|
# Wrapped in a <body> tag with a dark background and some padding
|
||||||
|
html_list = """
|
||||||
|
<body style='background-color: #121212; color: white; font-family: sans-serif; padding: 20px;'>
|
||||||
|
<h3 style='margin-top: 0;'>Active Audio Sources:</h3>
|
||||||
|
<ul>
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not sessions:
|
||||||
|
html_list += "<li style='color: #888;'>No active audio sources found.</li>"
|
||||||
|
else:
|
||||||
|
for s in sessions:
|
||||||
|
html_list += f"<li style='margin-bottom: 8px; font-size: 1.1em;'>{s}</li>"
|
||||||
|
|
||||||
|
html_list += "</ul></body>"
|
||||||
|
return html_list
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return f"<body style='background-color: #121212; color: #ff5555;'>Error: {str(e)}</body>"
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###########################
|
||||||
|
### SYSTEM TRAY CONTROL ###
|
||||||
|
###########################
|
||||||
|
|
||||||
|
# "Quit" menu item
|
||||||
|
def on_quit(icon, item):
|
||||||
|
icon.stop()
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
# Setup the tray icon
|
||||||
|
def setup_tray():
|
||||||
|
# Set the icon
|
||||||
|
image = Image.open(get_resource_path("smtc-bridge.ico"))
|
||||||
|
|
||||||
|
# Add the menu items
|
||||||
|
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.Menu.SEPARATOR,
|
||||||
|
pystray.MenuItem("★ Customize Overlay", lambda: webbrowser.open(f"https://beta.nutty.gg/now-playing/settings/")),
|
||||||
|
pystray.MenuItem("★ Try my stream widgets!", lambda: webbrowser.open(f"https://nutty.gg/collections/member-exclusive-widgets")),
|
||||||
|
pystray.Menu.SEPARATOR,
|
||||||
|
pystray.MenuItem("Quit", on_quit)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Put it all together
|
||||||
|
icon = pystray.Icon("MediaBridge", image, "SMTC Bridge", menu=menu)
|
||||||
|
icon.run()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#################
|
||||||
|
### KICK OFF! ###
|
||||||
|
#################
|
||||||
|
|
||||||
|
def run_flask():
|
||||||
|
# Disable flask startup messages
|
||||||
|
import logging
|
||||||
|
log = logging.getLogger('werkzeug')
|
||||||
|
log.setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
# Define a helper to trigger the notification
|
||||||
|
def send_notification():
|
||||||
|
notification.notify(
|
||||||
|
title=f'SMTC Bridge v{APP_VERSION}',
|
||||||
|
# Use the PORT variable here
|
||||||
|
message=f'Server successfully started on port {PORT}',
|
||||||
|
app_icon=get_resource_path('smtc-bridge.ico'),
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run the notification
|
||||||
|
send_notification()
|
||||||
|
|
||||||
|
# Start the server
|
||||||
|
try:
|
||||||
|
app.run(host=HOST, port=PORT, threaded=True, use_reloader=False)
|
||||||
|
except Exception as e:
|
||||||
|
with open("error.txt", "w") as f:
|
||||||
|
f.write(str(e))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
flask_thread = threading.Thread(target=run_flask, daemon=True)
|
||||||
|
flask_thread.start()
|
||||||
|
|
||||||
|
# Setup the Tray
|
||||||
|
setup_tray()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log_crash(e)
|
||||||
|
sys.exit(1)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
from PyInstaller.utils.hooks import collect_all
|
||||||
|
|
||||||
|
# Initialize lists
|
||||||
|
datas = [('smtc-bridge.ico', '.')]
|
||||||
|
binaries = []
|
||||||
|
hiddenimports = ['winsdk', 'winsdk._winrt']
|
||||||
|
|
||||||
|
# Collect all necessary files for complex packages
|
||||||
|
for pkg in ['winsdk', 'plyer']:
|
||||||
|
tmp_ret = collect_all(pkg)
|
||||||
|
datas += tmp_ret[0]
|
||||||
|
binaries += tmp_ret[1]
|
||||||
|
hiddenimports += tmp_ret[2]
|
||||||
|
|
||||||
|
# Add specific platform backend for plyer to ensure notification support
|
||||||
|
hiddenimports.append('plyer.platforms.win.notification')
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['smtc-bridge.pyw'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=binaries,
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='SMTC-Bridge',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon=['smtc-bridge.ico'],
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user