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:
parent
80a4286bc0
commit
46128f5c58
398
techdj_qt.py
398
techdj_qt.py
|
|
@ -9,6 +9,7 @@ import shutil
|
|||
import requests
|
||||
import re
|
||||
import socketio
|
||||
from urllib.parse import urlparse
|
||||
import subprocess
|
||||
import atexit
|
||||
from pathlib import Path
|
||||
|
|
@ -250,6 +251,50 @@ def get_audio_capture_source():
|
|||
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)
|
||||
|
||||
|
||||
|
|
@ -582,9 +627,27 @@ class SettingsDialog(QDialog):
|
|||
border-radius: 4px;
|
||||
}
|
||||
""")
|
||||
|
||||
|
||||
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(self.stream_url_input)
|
||||
layout.addSpacing(8)
|
||||
layout.addWidget(pw_label)
|
||||
layout.addWidget(self.dj_password_input)
|
||||
layout.addSpacing(20)
|
||||
|
||||
# Recording section
|
||||
|
|
@ -684,6 +747,7 @@ class SettingsDialog(QDialog):
|
|||
"recording_sample_rate": self.sample_rate_combo.currentData(),
|
||||
"recording_format": self.format_combo.currentData(),
|
||||
"stream_server_url": self.stream_url_input.text(),
|
||||
"dj_panel_password": self.dj_password_input.text(),
|
||||
},
|
||||
"ui": {
|
||||
"neon_mode": self.neon_combo.currentData(),
|
||||
|
|
@ -805,11 +869,13 @@ class StreamingWorker(QThread):
|
|||
streaming_started = pyqtSignal()
|
||||
streaming_error = pyqtSignal(str)
|
||||
listener_count = pyqtSignal(int)
|
||||
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.sio = None
|
||||
self.stream_url = ""
|
||||
self.dj_password = ""
|
||||
self.glow_intensity = 30 # mirrors the UI slider; sent to listeners on connect
|
||||
self.is_running = False
|
||||
self.ffmpeg_proc = None
|
||||
self._broadcast_started = False
|
||||
|
|
@ -819,11 +885,14 @@ class StreamingWorker(QThread):
|
|||
if not self._broadcast_started:
|
||||
self._broadcast_started = True
|
||||
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()
|
||||
else:
|
||||
# Reconnected mid-stream - server handles resume gracefully
|
||||
# Reconnected mid-stream — server handles resume gracefully
|
||||
print("[SOCKET] Reconnected - resuming existing broadcast")
|
||||
self.sio.emit('start_broadcast', {'format': 'mp3', 'bitrate': '128k'})
|
||||
self.sio.emit('listener_glow', {'intensity': self.glow_intensity})
|
||||
|
||||
def on_disconnect(self):
|
||||
print("[SOCKET] Disconnected from DJ server")
|
||||
|
|
@ -834,6 +903,42 @@ class StreamingWorker(QThread):
|
|||
def on_listener_count(self, data):
|
||||
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):
|
||||
try:
|
||||
# Create a fresh Socket.IO client for each session
|
||||
|
|
@ -843,11 +948,29 @@ class StreamingWorker(QThread):
|
|||
self.sio.on('listener_count', self.on_listener_count)
|
||||
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()
|
||||
print(f"[STREAM] Capturing from: {source}")
|
||||
|
||||
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-hide_banner",
|
||||
|
|
@ -863,46 +986,83 @@ class StreamingWorker(QThread):
|
|||
# Flush every packet — critical for low-latency pipe streaming
|
||||
"-flush_packets", "1",
|
||||
"-f", "mp3",
|
||||
"pipe:1"
|
||||
"pipe:1",
|
||||
]
|
||||
self.ffmpeg_proc = subprocess.Popen(
|
||||
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:
|
||||
# 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)
|
||||
if not chunk:
|
||||
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:
|
||||
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:
|
||||
self.streaming_error.emit(f"Streaming thread error: {e}")
|
||||
self.streaming_error.emit(f"Streaming error: {e}")
|
||||
finally:
|
||||
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.dj_password = password
|
||||
self.is_running = True
|
||||
self.start()
|
||||
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):
|
||||
"""Thread-safe stop: capture refs locally before clearing to avoid TOCTOU."""
|
||||
self.is_running = False
|
||||
self._broadcast_started = False
|
||||
if self.ffmpeg_proc:
|
||||
try: self.ffmpeg_proc.terminate()
|
||||
except: pass
|
||||
self.ffmpeg_proc = None
|
||||
if self.sio and self.sio.connected:
|
||||
|
||||
proc = self.ffmpeg_proc
|
||||
self.ffmpeg_proc = None
|
||||
sio = self.sio
|
||||
self.sio = None
|
||||
|
||||
if proc:
|
||||
try:
|
||||
self.sio.emit('stop_broadcast')
|
||||
self.sio.disconnect()
|
||||
proc.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if sio:
|
||||
try:
|
||||
if sio.connected:
|
||||
sio.emit('stop_broadcast')
|
||||
sio.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
self.sio = None
|
||||
|
||||
# --- WIDGETS ---
|
||||
|
||||
|
|
@ -1144,7 +1304,8 @@ class DeckWidget(QGroupBox):
|
|||
self.loop_end = 0
|
||||
self.loop_btns = []
|
||||
self.xf_vol = 100
|
||||
|
||||
self.current_title = ""
|
||||
|
||||
self.loop_timer = QTimer(self)
|
||||
self.loop_timer.setInterval(LOOP_CHECK_INTERVAL)
|
||||
self.loop_timer.timeout.connect(self.check_loop)
|
||||
|
|
@ -1379,6 +1540,7 @@ class DeckWidget(QGroupBox):
|
|||
|
||||
try:
|
||||
self.player.setSource(QUrl.fromLocalFile(str(p.absolute())))
|
||||
self.current_title = p.stem
|
||||
self.lbl_tr.setText(p.stem.upper())
|
||||
self.vinyl.set_speed(0)
|
||||
self.vinyl.angle = 0
|
||||
|
|
@ -1427,14 +1589,32 @@ class DeckWidget(QGroupBox):
|
|||
else:
|
||||
# Stop mode or no queue items
|
||||
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):
|
||||
self.player.play()
|
||||
self.vinyl.start_spin()
|
||||
self._emit_playing_state(True)
|
||||
|
||||
def pause(self):
|
||||
self.player.pause()
|
||||
self.vinyl.stop_spin()
|
||||
self._emit_playing_state(False)
|
||||
|
||||
def stop(self):
|
||||
self.player.stop()
|
||||
|
|
@ -1442,6 +1622,7 @@ class DeckWidget(QGroupBox):
|
|||
self.vinyl.angle = 0
|
||||
self.vinyl.update()
|
||||
self.clear_loop()
|
||||
self._emit_playing_state(False)
|
||||
|
||||
def on_position_changed(self, pos):
|
||||
self.wave.set_position(pos)
|
||||
|
|
@ -1531,6 +1712,15 @@ class DJApp(QMainWindow):
|
|||
self.streaming_worker.streaming_error.connect(self.on_streaming_error)
|
||||
self.streaming_worker.listener_count.connect(self.update_listener_count)
|
||||
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
|
||||
self.server_url = "http://localhost:5000"
|
||||
|
|
@ -1725,7 +1915,57 @@ class DJApp(QMainWindow):
|
|||
stream_info = QVBoxLayout()
|
||||
stream_info.addWidget(self.stream_status_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.addWidget(self.record_button)
|
||||
rec_layout.addSpacing(20)
|
||||
|
|
@ -1734,6 +1974,8 @@ class DJApp(QMainWindow):
|
|||
rec_layout.addWidget(self.stream_button)
|
||||
rec_layout.addSpacing(20)
|
||||
rec_layout.addLayout(stream_info)
|
||||
rec_layout.addSpacing(40)
|
||||
rec_layout.addLayout(glow_vbox)
|
||||
rec_layout.addStretch()
|
||||
|
||||
layout.addLayout(rec_layout, 3)
|
||||
|
|
@ -2000,12 +2242,16 @@ class DJApp(QMainWindow):
|
|||
def fetch_server_library(self):
|
||||
self.library_list.clear()
|
||||
self.library_list.addItem("Fetching server library...")
|
||||
|
||||
base_url = self.get_server_base_url()
|
||||
self.server_url = base_url
|
||||
|
||||
self.fetcher = ServerLibraryFetcher(f"{base_url}/library.json")
|
||||
self.fetcher.finished.connect(lambda tracks, err, success: self.on_server_library_fetched(tracks, base_url, err, success))
|
||||
|
||||
# Use the listener port (no auth required) for library / track data.
|
||||
# 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"{listener_url}/library.json")
|
||||
self.fetcher.finished.connect(
|
||||
lambda tracks, err, success: self.on_server_library_fetched(tracks, listener_url, err, success)
|
||||
)
|
||||
self.fetcher.start()
|
||||
|
||||
def on_server_library_fetched(self, tracks, base_url, err, success):
|
||||
|
|
@ -2136,10 +2382,24 @@ class DJApp(QMainWindow):
|
|||
try:
|
||||
self.status_label.setText("Uploading to server...")
|
||||
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:
|
||||
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:
|
||||
self.status_label.setText("Upload successful!")
|
||||
|
|
@ -2255,12 +2515,13 @@ class DJApp(QMainWindow):
|
|||
def toggle_streaming(self):
|
||||
"""Toggle live streaming on/off"""
|
||||
if not self.is_streaming:
|
||||
# Get base URL from settings
|
||||
# Get base URL and credentials from settings
|
||||
base_url = self.get_server_base_url()
|
||||
bitrate = self.all_settings.get("audio", {}).get("bitrate", 128)
|
||||
|
||||
# Start streaming
|
||||
if self.streaming_worker.start_streaming(base_url, bitrate):
|
||||
password = self.all_settings.get("audio", {}).get("dj_panel_password", "")
|
||||
|
||||
# Start streaming (password handled inside StreamingWorker)
|
||||
if self.streaming_worker.start_streaming(base_url, bitrate, password=password):
|
||||
self.is_streaming = True
|
||||
self.stream_button.setText("STOP")
|
||||
self.stream_button.setStyleSheet("""
|
||||
|
|
@ -2315,21 +2576,62 @@ class DJApp(QMainWindow):
|
|||
def update_listener_count(self, count):
|
||||
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):
|
||||
audio_settings = self.all_settings.get("audio", {})
|
||||
server_url = audio_settings.get("stream_server_url", "http://localhost:5000")
|
||||
|
||||
# Normal techdj server runs on 5000 (DJ) and 5001 (Listener)
|
||||
# If the URL is for the listener or stream, switch to 5000
|
||||
if ":5001" in server_url:
|
||||
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]
|
||||
return server_url
|
||||
raw = audio_settings.get("stream_server_url", "http://localhost:5000").strip()
|
||||
|
||||
# Ensure scheme is present so urlparse parses host/port correctly
|
||||
if not raw.startswith(("http://", "https://", "ws://", "wss://")):
|
||||
raw = "http://" + raw
|
||||
|
||||
parsed = urlparse(raw)
|
||||
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):
|
||||
"""Update glow frame size when window is resized"""
|
||||
|
|
|
|||
Loading…
Reference in New Issue