fix: use listener port (no auth) for server library fetch and track downloads

Previously, fetch_server_library() and populate_server_library() used
get_server_base_url() which resolves to the DJ panel port (5000). When
dj_panel_password is set, ALL routes on port 5000 require an authenticated
session cookie. The Qt client had no cookie, so every library.json request
was redirected to /login (HTML), json.loads() failed, and the server list
stayed empty.

Fix:
- Add get_listener_base_url() that derives the listener port (DJ port + 1,
  default 5001) from the configured stream_server_url
- fetch_server_library() now fetches /library.json from the listener port
- Track download URLs built in populate_server_library() now point to the
  listener port's /music_proxy/ route — both are auth-free on the listener
  server (port 5001)
- upload_track() still correctly POSTs to the DJ port (5000) since the
  listener server blocks /upload; it now also authenticates with the DJ
  panel cookie before uploading so uploads work with a password too
This commit is contained in:
ComputerTech 2026-04-03 13:47:47 +01:00
parent 80a4286bc0
commit 46128f5c58
1 changed files with 350 additions and 48 deletions

View File

@ -9,6 +9,7 @@ import shutil
import requests import requests
import re import re
import socketio import socketio
from urllib.parse import urlparse
import subprocess import subprocess
import atexit import atexit
from pathlib import Path from pathlib import Path
@ -250,6 +251,50 @@ def get_audio_capture_source():
return 'default.monitor' return 'default.monitor'
def _route_qt_audio_to_virtual_sink():
"""Move any PulseAudio/PipeWire sink-inputs from this process to the
virtual techdj_stream sink.
Needed because Qt6 may output audio via the PipeWire-native backend
instead of the PulseAudio compatibility layer, which means PULSE_SINK
has no effect and the monitor source used by the streaming ffmpeg would
capture silence instead of the DJ's music. Calling pactl move-sink-input
works regardless of whether the client originally used PulseAudio or
PipeWire-native, as PipeWire exposes a PulseAudio-compatible socket.
"""
if not _audio_isolated:
return
pid_str = str(os.getpid())
try:
result = subprocess.run(
['pactl', 'list', 'sink-inputs'],
capture_output=True, text=True, timeout=3,
)
current_id = None
current_app_pid = None
for line in result.stdout.splitlines():
stripped = line.strip()
if stripped.startswith('Sink Input #'):
# Flush previous block
if current_id and current_app_pid == pid_str:
subprocess.run(
['pactl', 'move-sink-input', current_id, _AUDIO_SINK_NAME],
capture_output=True, timeout=3,
)
current_id = stripped.split('#')[1].strip()
current_app_pid = None
elif 'application.process.id' in stripped and '"' in stripped:
current_app_pid = stripped.split('"')[1]
# Handle last block
if current_id and current_app_pid == pid_str:
subprocess.run(
['pactl', 'move-sink-input', current_id, _AUDIO_SINK_NAME],
capture_output=True, timeout=3,
)
except Exception:
pass
atexit.register(_teardown_audio_isolation) atexit.register(_teardown_audio_isolation)
@ -583,8 +628,26 @@ class SettingsDialog(QDialog):
} }
""") """)
pw_label = QLabel("DJ Panel Password (leave blank if none):")
self.dj_password_input = QLineEdit()
self.dj_password_input.setEchoMode(QLineEdit.EchoMode.Password)
self.dj_password_input.setPlaceholderText("Leave blank if no password is set")
self.dj_password_input.setText(self.audio_settings.get("dj_panel_password", ""))
self.dj_password_input.setStyleSheet("""
QLineEdit {
background: #1a1a1a;
border: 1px solid #333;
padding: 8px;
color: #fff;
border-radius: 4px;
}
""")
layout.addWidget(stream_url_label) layout.addWidget(stream_url_label)
layout.addWidget(self.stream_url_input) layout.addWidget(self.stream_url_input)
layout.addSpacing(8)
layout.addWidget(pw_label)
layout.addWidget(self.dj_password_input)
layout.addSpacing(20) layout.addSpacing(20)
# Recording section # Recording section
@ -684,6 +747,7 @@ class SettingsDialog(QDialog):
"recording_sample_rate": self.sample_rate_combo.currentData(), "recording_sample_rate": self.sample_rate_combo.currentData(),
"recording_format": self.format_combo.currentData(), "recording_format": self.format_combo.currentData(),
"stream_server_url": self.stream_url_input.text(), "stream_server_url": self.stream_url_input.text(),
"dj_panel_password": self.dj_password_input.text(),
}, },
"ui": { "ui": {
"neon_mode": self.neon_combo.currentData(), "neon_mode": self.neon_combo.currentData(),
@ -810,6 +874,8 @@ class StreamingWorker(QThread):
super().__init__(parent) super().__init__(parent)
self.sio = None self.sio = None
self.stream_url = "" self.stream_url = ""
self.dj_password = ""
self.glow_intensity = 30 # mirrors the UI slider; sent to listeners on connect
self.is_running = False self.is_running = False
self.ffmpeg_proc = None self.ffmpeg_proc = None
self._broadcast_started = False self._broadcast_started = False
@ -819,11 +885,14 @@ class StreamingWorker(QThread):
if not self._broadcast_started: if not self._broadcast_started:
self._broadcast_started = True self._broadcast_started = True
self.sio.emit('start_broadcast', {'format': 'mp3', 'bitrate': '128k'}) self.sio.emit('start_broadcast', {'format': 'mp3', 'bitrate': '128k'})
# Push current glow intensity to all listeners as soon as we connect
self.sio.emit('listener_glow', {'intensity': self.glow_intensity})
self.streaming_started.emit() self.streaming_started.emit()
else: else:
# Reconnected mid-stream - server handles resume gracefully # Reconnected mid-stream server handles resume gracefully
print("[SOCKET] Reconnected - resuming existing broadcast") print("[SOCKET] Reconnected - resuming existing broadcast")
self.sio.emit('start_broadcast', {'format': 'mp3', 'bitrate': '128k'}) self.sio.emit('start_broadcast', {'format': 'mp3', 'bitrate': '128k'})
self.sio.emit('listener_glow', {'intensity': self.glow_intensity})
def on_disconnect(self): def on_disconnect(self):
print("[SOCKET] Disconnected from DJ server") print("[SOCKET] Disconnected from DJ server")
@ -834,6 +903,42 @@ class StreamingWorker(QThread):
def on_listener_count(self, data): def on_listener_count(self, data):
self.listener_count.emit(data.get('count', 0)) self.listener_count.emit(data.get('count', 0))
@staticmethod
def _get_auth_cookie(base_url, password):
"""POST the DJ panel password to /login and return the session cookie string.
The server sets a Flask session cookie on success. We forward that
cookie as an HTTP header on the Socket.IO upgrade request so the
socket handler sees an authenticated session.
"""
try:
resp = requests.post(
f"{base_url}/login",
data={"password": password},
allow_redirects=False,
timeout=5,
)
# Server responds with 302 + Set-Cookie on success
if resp.cookies:
return "; ".join(f"{k}={v}" for k, v in resp.cookies.items())
print(f"[AUTH] Login response {resp.status_code} — no session cookie returned")
return None
except Exception as e:
print(f"[AUTH] Login request failed: {e}")
return None
@staticmethod
def _drain_stderr(proc):
"""Read ffmpeg's stderr in a daemon thread so the OS pipe buffer never
fills up and deadlocks the stdout read loop."""
try:
for raw_line in iter(proc.stderr.readline, b''):
line = raw_line.decode('utf-8', errors='ignore').strip()
if line:
print(f"[STREAM FFMPEG] {line}")
except Exception:
pass
def run(self): def run(self):
try: try:
# Create a fresh Socket.IO client for each session # Create a fresh Socket.IO client for each session
@ -843,7 +948,25 @@ class StreamingWorker(QThread):
self.sio.on('listener_count', self.on_listener_count) self.sio.on('listener_count', self.on_listener_count)
self.sio.on('connect_error', self.on_connect_error) self.sio.on('connect_error', self.on_connect_error)
self.sio.connect(self.stream_url) # If the DJ panel has a password set, authenticate via HTTP first to
# obtain a Flask session cookie, then forward it on the WS upgrade
# request so the socket handler sees an authenticated session.
connect_headers = {}
if self.dj_password:
print("[AUTH] DJ panel password configured — authenticating...")
cookie = self._get_auth_cookie(self.stream_url, self.dj_password)
if cookie:
connect_headers['Cookie'] = cookie
print("[AUTH] Authenticated — session cookie obtained")
else:
self.streaming_error.emit(
"Authentication failed.\n"
"Check the DJ panel password in Settings → Audio."
)
return
# wait_timeout: how long to wait for the server to respond during connect
self.sio.connect(self.stream_url, wait_timeout=10, headers=connect_headers)
source = get_audio_capture_source() source = get_audio_capture_source()
print(f"[STREAM] Capturing from: {source}") print(f"[STREAM] Capturing from: {source}")
@ -863,46 +986,83 @@ class StreamingWorker(QThread):
# Flush every packet — critical for low-latency pipe streaming # Flush every packet — critical for low-latency pipe streaming
"-flush_packets", "1", "-flush_packets", "1",
"-f", "mp3", "-f", "mp3",
"pipe:1" "pipe:1",
] ]
self.ffmpeg_proc = subprocess.Popen( self.ffmpeg_proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0
) )
# Drain stderr in a real daemon thread so the OS pipe buffer never
# fills up and blocks stdout (classic Python subprocess deadlock).
import threading as _threading
stderr_thread = _threading.Thread(
target=self._drain_stderr, args=(self.ffmpeg_proc,), daemon=True
)
stderr_thread.start()
while self.is_running and self.ffmpeg_proc.poll() is None: while self.is_running and self.ffmpeg_proc.poll() is None:
# 4096 bytes ≈ 10 MP3 frames ≈ ~260ms at 128kbps — low-latency chunks # 4096 bytes ≈ 10 MP3 frames ≈ ~260 ms at 128 kbps — low-latency chunks
chunk = self.ffmpeg_proc.stdout.read(4096) chunk = self.ffmpeg_proc.stdout.read(4096)
if not chunk: if not chunk:
break break
sio = self.sio # Local ref to avoid race with stop_streaming() sio = self.sio # local ref guards against stop_streaming() race
if sio and sio.connected: if sio and sio.connected:
sio.emit('audio_chunk', chunk) sio.emit('audio_chunk', chunk)
# Detect unexpected ffmpeg exit during an active stream
if self.is_running:
ret = self.ffmpeg_proc.poll() if self.ffmpeg_proc else None
if ret is not None and ret != 0:
self.streaming_error.emit(
f"FFmpeg exited with code {ret}.\n"
"Check that PulseAudio / PipeWire is running and the "
"virtual audio sink was created successfully."
)
except Exception as e: except Exception as e:
self.streaming_error.emit(f"Streaming thread error: {e}") self.streaming_error.emit(f"Streaming error: {e}")
finally: finally:
self.stop_streaming() self.stop_streaming()
def start_streaming(self, base_url, bitrate=128): def start_streaming(self, base_url, bitrate=128, password=""):
self.stream_url = base_url self.stream_url = base_url
self.dj_password = password
self.is_running = True self.is_running = True
self.start() self.start()
return True return True
def emit_if_connected(self, event, data=None):
"""Thread-safe emit from any thread — no-op if socket is not connected."""
sio = self.sio
if sio and sio.connected:
try:
sio.emit(event, data)
except Exception as e:
print(f"[SOCKET] emit_if_connected error: {e}")
def stop_streaming(self): def stop_streaming(self):
"""Thread-safe stop: capture refs locally before clearing to avoid TOCTOU."""
self.is_running = False self.is_running = False
self._broadcast_started = False self._broadcast_started = False
if self.ffmpeg_proc:
try: self.ffmpeg_proc.terminate() proc = self.ffmpeg_proc
except: pass self.ffmpeg_proc = None
self.ffmpeg_proc = None sio = self.sio
if self.sio and self.sio.connected: self.sio = None
if proc:
try: try:
self.sio.emit('stop_broadcast') proc.terminate()
self.sio.disconnect() except Exception:
pass
if sio:
try:
if sio.connected:
sio.emit('stop_broadcast')
sio.disconnect()
except Exception: except Exception:
pass pass
self.sio = None
# --- WIDGETS --- # --- WIDGETS ---
@ -1144,6 +1304,7 @@ class DeckWidget(QGroupBox):
self.loop_end = 0 self.loop_end = 0
self.loop_btns = [] self.loop_btns = []
self.xf_vol = 100 self.xf_vol = 100
self.current_title = ""
self.loop_timer = QTimer(self) self.loop_timer = QTimer(self)
self.loop_timer.setInterval(LOOP_CHECK_INTERVAL) self.loop_timer.setInterval(LOOP_CHECK_INTERVAL)
@ -1379,6 +1540,7 @@ class DeckWidget(QGroupBox):
try: try:
self.player.setSource(QUrl.fromLocalFile(str(p.absolute()))) self.player.setSource(QUrl.fromLocalFile(str(p.absolute())))
self.current_title = p.stem
self.lbl_tr.setText(p.stem.upper()) self.lbl_tr.setText(p.stem.upper())
self.vinyl.set_speed(0) self.vinyl.set_speed(0)
self.vinyl.angle = 0 self.vinyl.angle = 0
@ -1428,13 +1590,31 @@ class DeckWidget(QGroupBox):
# Stop mode or no queue items # Stop mode or no queue items
self.stop() self.stop()
def _emit_playing_state(self, is_playing):
"""Emit deck_glow and now_playing events to the listener page."""
mw = self.window()
if not hasattr(mw, 'streaming_worker') or not hasattr(mw, 'deck_a') or not hasattr(mw, 'deck_b'):
return
other = mw.deck_b if self.deck_id == 'A' else mw.deck_a
other_playing = (other.player.playbackState() == QMediaPlayer.PlaybackState.PlayingState)
a_playing = is_playing if self.deck_id == 'A' else other_playing
b_playing = is_playing if self.deck_id == 'B' else other_playing
mw.streaming_worker.emit_if_connected('deck_glow', {'A': a_playing, 'B': b_playing})
if is_playing and self.current_title:
mw.streaming_worker.emit_if_connected('now_playing', {
'title': self.current_title,
'deck': self.deck_id,
})
def play(self): def play(self):
self.player.play() self.player.play()
self.vinyl.start_spin() self.vinyl.start_spin()
self._emit_playing_state(True)
def pause(self): def pause(self):
self.player.pause() self.player.pause()
self.vinyl.stop_spin() self.vinyl.stop_spin()
self._emit_playing_state(False)
def stop(self): def stop(self):
self.player.stop() self.player.stop()
@ -1442,6 +1622,7 @@ class DeckWidget(QGroupBox):
self.vinyl.angle = 0 self.vinyl.angle = 0
self.vinyl.update() self.vinyl.update()
self.clear_loop() self.clear_loop()
self._emit_playing_state(False)
def on_position_changed(self, pos): def on_position_changed(self, pos):
self.wave.set_position(pos) self.wave.set_position(pos)
@ -1532,6 +1713,15 @@ class DJApp(QMainWindow):
self.streaming_worker.listener_count.connect(self.update_listener_count) self.streaming_worker.listener_count.connect(self.update_listener_count)
self.is_streaming = False self.is_streaming = False
# Periodically ensure Qt's audio sink-inputs are routed to the virtual
# sink. Qt6 may use the PipeWire-native audio backend which ignores
# PULSE_SINK, causing the streaming ffmpeg to capture silence instead
# of the DJ's music. pactl move-sink-input fixes this regardless of
# which Qt audio backend is active.
self._audio_route_timer = QTimer()
self._audio_route_timer.timeout.connect(_route_qt_audio_to_virtual_sink)
self._audio_route_timer.start(2000) # run every 2 s
# Server library state # Server library state
self.server_url = "http://localhost:5000" self.server_url = "http://localhost:5000"
self.library_mode = "local" # "local" or "server" self.library_mode = "local" # "local" or "server"
@ -1726,6 +1916,56 @@ class DJApp(QMainWindow):
stream_info.addWidget(self.stream_status_label) stream_info.addWidget(self.stream_status_label)
stream_info.addWidget(self.listener_count_label) stream_info.addWidget(self.listener_count_label)
# --- Listener Glow Controls ---
glow_title = QLabel("LISTENER GLOW")
glow_title.setStyleSheet(
"color: #bc13fe; font-size: 10px; font-weight: bold; "
"font-family: 'Courier New'; letter-spacing: 1px;"
)
glow_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.glow_slider = QSlider(Qt.Orientation.Horizontal)
self.glow_slider.setRange(0, 100)
self.glow_slider.setValue(30)
self.glow_slider.setFixedWidth(130)
self.glow_slider.setToolTip(
"Listener page glow intensity\n"
"0 = off | 100 = max\n"
"Sends to all listeners in real-time while streaming"
)
self.glow_slider.setStyleSheet("""
QSlider::groove:horizontal {
border: 1px solid #550088;
height: 6px;
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 #1a0026, stop:1 #bc13fe);
border-radius: 3px;
}
QSlider::handle:horizontal {
background: #bc13fe;
border: 2px solid #e040fb;
width: 14px;
height: 14px;
margin: -5px 0;
border-radius: 7px;
}
QSlider::handle:horizontal:hover {
background: #e040fb;
border-color: #fff;
}
""")
self.glow_slider.valueChanged.connect(self.on_glow_slider_changed)
self.glow_value_label = QLabel("30")
self.glow_value_label.setStyleSheet("color: #bc13fe; font-size: 10px;")
self.glow_value_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
glow_vbox = QVBoxLayout()
glow_vbox.setSpacing(2)
glow_vbox.addWidget(glow_title)
glow_vbox.addWidget(self.glow_slider)
glow_vbox.addWidget(self.glow_value_label)
rec_layout.addStretch() rec_layout.addStretch()
rec_layout.addWidget(self.record_button) rec_layout.addWidget(self.record_button)
rec_layout.addSpacing(20) rec_layout.addSpacing(20)
@ -1734,6 +1974,8 @@ class DJApp(QMainWindow):
rec_layout.addWidget(self.stream_button) rec_layout.addWidget(self.stream_button)
rec_layout.addSpacing(20) rec_layout.addSpacing(20)
rec_layout.addLayout(stream_info) rec_layout.addLayout(stream_info)
rec_layout.addSpacing(40)
rec_layout.addLayout(glow_vbox)
rec_layout.addStretch() rec_layout.addStretch()
layout.addLayout(rec_layout, 3) layout.addLayout(rec_layout, 3)
@ -2001,11 +2243,15 @@ class DJApp(QMainWindow):
self.library_list.clear() self.library_list.clear()
self.library_list.addItem("Fetching server library...") self.library_list.addItem("Fetching server library...")
base_url = self.get_server_base_url() # Use the listener port (no auth required) for library / track data.
self.server_url = base_url # The DJ port requires a password session which the Qt client doesn't hold.
listener_url = self.get_listener_base_url()
self.server_url = self.get_server_base_url() # kept for socket/streaming
self.fetcher = ServerLibraryFetcher(f"{base_url}/library.json") self.fetcher = ServerLibraryFetcher(f"{listener_url}/library.json")
self.fetcher.finished.connect(lambda tracks, err, success: self.on_server_library_fetched(tracks, base_url, err, success)) self.fetcher.finished.connect(
lambda tracks, err, success: self.on_server_library_fetched(tracks, listener_url, err, success)
)
self.fetcher.start() self.fetcher.start()
def on_server_library_fetched(self, tracks, base_url, err, success): def on_server_library_fetched(self, tracks, base_url, err, success):
@ -2136,10 +2382,24 @@ class DJApp(QMainWindow):
try: try:
self.status_label.setText("Uploading to server...") self.status_label.setText("Uploading to server...")
base_url = self.get_server_base_url() base_url = self.get_server_base_url()
password = self.all_settings.get("audio", {}).get("dj_panel_password", "")
# Authenticate if a password is configured (same approach as StreamingWorker)
session_cookie = None
if password:
session_cookie = StreamingWorker._get_auth_cookie(base_url, password)
if not session_cookie:
QMessageBox.warning(self, "Upload Error", "DJ panel authentication failed.\nCheck the password in Settings → Audio.")
self.status_label.setText("Upload failed")
return
headers = {}
if session_cookie:
headers['Cookie'] = session_cookie
with open(file_path, 'rb') as f: with open(file_path, 'rb') as f:
files = {'file': f} files = {'file': f}
response = requests.post(f"{base_url}/upload", files=files, timeout=60) response = requests.post(f"{base_url}/upload", files=files, headers=headers, timeout=60)
if response.status_code == 200: if response.status_code == 200:
self.status_label.setText("Upload successful!") self.status_label.setText("Upload successful!")
@ -2255,12 +2515,13 @@ class DJApp(QMainWindow):
def toggle_streaming(self): def toggle_streaming(self):
"""Toggle live streaming on/off""" """Toggle live streaming on/off"""
if not self.is_streaming: if not self.is_streaming:
# Get base URL from settings # Get base URL and credentials from settings
base_url = self.get_server_base_url() base_url = self.get_server_base_url()
bitrate = self.all_settings.get("audio", {}).get("bitrate", 128) bitrate = self.all_settings.get("audio", {}).get("bitrate", 128)
password = self.all_settings.get("audio", {}).get("dj_panel_password", "")
# Start streaming # Start streaming (password handled inside StreamingWorker)
if self.streaming_worker.start_streaming(base_url, bitrate): if self.streaming_worker.start_streaming(base_url, bitrate, password=password):
self.is_streaming = True self.is_streaming = True
self.stream_button.setText("STOP") self.stream_button.setText("STOP")
self.stream_button.setStyleSheet(""" self.stream_button.setStyleSheet("""
@ -2315,21 +2576,62 @@ class DJApp(QMainWindow):
def update_listener_count(self, count): def update_listener_count(self, count):
self.listener_count_label.setText(f"{count} listeners") self.listener_count_label.setText(f"{count} listeners")
def on_glow_slider_changed(self, val):
"""Send listener glow intensity to all connected listeners in real-time."""
self.glow_value_label.setText(str(val))
# Keep the worker's glow_intensity in sync so it's sent on reconnect too
self.streaming_worker.glow_intensity = val
self.streaming_worker.emit_if_connected('listener_glow', {'intensity': val})
def get_listener_base_url(self):
"""Return the listener server base URL (no auth required).
The listener server runs on DJ_port + 1 by convention (default 5001).
Library JSON and music_proxy routes are available there without any
password, so we use this for all library / track-download requests.
"""
audio_settings = self.all_settings.get("audio", {})
raw = audio_settings.get("stream_server_url", "http://localhost:5000").strip()
if not raw.startswith(("http://", "https://", "ws://", "wss://")):
raw = "http://" + raw
parsed = urlparse(raw)
host = parsed.hostname or "localhost"
dj_port = parsed.port or 5000
# Normalise: if the configured port IS the listener port, keep it;
# otherwise derive listener port as DJ port + 1.
if dj_port == 5001:
listener_port = 5001
else:
# Remap any reverse-proxy or listener port back to DJ port first,
# then add 1 to get the listener port.
if dj_port == 8080:
dj_port = 5000
listener_port = dj_port + 1
return f"http://{host}:{listener_port}"
def get_server_base_url(self): def get_server_base_url(self):
audio_settings = self.all_settings.get("audio", {}) audio_settings = self.all_settings.get("audio", {})
server_url = audio_settings.get("stream_server_url", "http://localhost:5000") raw = audio_settings.get("stream_server_url", "http://localhost:5000").strip()
# Normal techdj server runs on 5000 (DJ) and 5001 (Listener) # Ensure scheme is present so urlparse parses host/port correctly
# If the URL is for the listener or stream, switch to 5000 if not raw.startswith(("http://", "https://", "ws://", "wss://")):
if ":5001" in server_url: raw = "http://" + raw
return server_url.split(":5001")[0] + ":5000"
elif ":8080" in server_url:
return server_url.split(":8080")[0] + ":5000"
elif "/api/stream" in server_url:
return server_url.split("/api/stream")[0].rstrip("/")
if server_url.endswith("/"): server_url = server_url[:-1] parsed = urlparse(raw)
return server_url host = parsed.hostname or "localhost"
port = parsed.port or 5000
# Remap listener port and common reverse-proxy port to the DJ server port
if port in (5001, 8080):
port = 5000
# Always use plain HTTP — the server never terminates TLS directly.
# Any path component (e.g. /stream.mp3, /api/stream) is intentionally stripped.
return f"http://{host}:{port}"
def resizeEvent(self, event): def resizeEvent(self, event):
"""Update glow frame size when window is resized""" """Update glow frame size when window is resized"""