Final bug sweep: fix event param, remove dead code, dedupe CSS; add .gitignore recordings/

This commit is contained in:
ComputerTech 2026-03-05 14:28:17 +00:00
parent 12c01faa83
commit b5ea9e8d01
12 changed files with 6215 additions and 3168 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
music/
recordings/
# Python
__pycache__/

View File

@ -41,23 +41,23 @@ def main():
# Check Chrome
chrome_mem, chrome_procs = get_process_memory('chrome')
if chrome_mem > 0:
print(f"🌐 Chrome (Web Panel):")
print(f"[CHROME] Chrome (Web Panel):")
print(f" Total Memory: {chrome_mem:.1f} MB")
print(f" Processes: {chrome_procs}")
print()
else:
print("🌐 Chrome: Not running")
print("[CHROME] Chrome: Not running")
print()
# Check PyQt5
# Check PyQt6
qt_mem, qt_procs = get_process_memory('techdj_qt')
if qt_mem > 0:
print(f"💻 PyQt5 Native App:")
print(f"[PYQT6] PyQt6 Native App:")
print(f" Total Memory: {qt_mem:.1f} MB")
print(f" Processes: {qt_procs}")
print()
else:
print("💻 PyQt5 Native App: Not running")
print("[PYQT6] PyQt6 Native App: Not running")
print()
# Comparison
@ -66,25 +66,25 @@ def main():
percent = (savings / chrome_mem) * 100
print("=" * 60)
print("📊 Comparison:")
print("[STATS] Comparison:")
print(f" Memory Saved: {savings:.1f} MB ({percent:.1f}%)")
print()
# Visual bar chart
max_mem = max(chrome_mem, qt_mem)
chrome_bar = '' * int((chrome_mem / max_mem) * 40)
qt_bar = '' * int((qt_mem / max_mem) * 40)
chrome_bar = '#' * int((chrome_mem / max_mem) * 40)
qt_bar = '#' * int((qt_mem / max_mem) * 40)
print(" Chrome: " + chrome_bar + f" {chrome_mem:.0f}MB")
print(" PyQt5: " + qt_bar + f" {qt_mem:.0f}MB")
print(" PyQt6: " + qt_bar + f" {qt_mem:.0f}MB")
print()
if percent > 50:
print(f" ✅ PyQt5 uses {percent:.0f}% less memory!")
print(f" [OK] PyQt6 uses {percent:.0f}% less memory!")
elif percent > 25:
print(f" ✅ PyQt5 uses {percent:.0f}% less memory")
print(f" [OK] PyQt6 uses {percent:.0f}% less memory")
else:
print(f" PyQt5 uses {percent:.0f}% less memory")
print(f" PyQt6 uses {percent:.0f}% less memory")
print("=" * 60)
print()

BIN
dj_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

View File

@ -29,6 +29,15 @@
USE PORTRAIT MODE
</button>
</div>
<!-- Mobile Top Bar -->
<div class="mobile-top-bar">
<div class="status-indicator">
<span class="status-dot"></span>
<span id="system-status">NEON_v2</span>
</div>
<div class="app-logo">TECHDJ PRO</div>
<div class="clock-display" id="clock-display">00:00</div>
</div>
<!-- MAIN APP CONTAINER -->
<div class="app-container">
@ -36,31 +45,33 @@
<!-- Mobile Tabs -->
<nav class="mobile-tabs">
<button class="tab-btn active" onclick="switchTab('library')">
<span class="tab-icon"></span>
<span>LIBRARY</span>
<span class="tab-icon">#</span>
<span>LIB</span>
</button>
<button class="tab-btn" onclick="switchTab('deck-A')">
<span class="tab-icon"></span>
<span class="tab-icon">A</span>
<span>DECK A</span>
</button>
<button class="tab-btn" onclick="switchTab('deck-B')">
<span class="tab-icon"></span>
<span class="tab-icon">B</span>
<span>DECK B</span>
</button>
<button class="tab-btn" onclick="switchQueueTab()">
<span class="tab-icon">Q</span>
<span id="queue-tab-label">QUEUE A</span>
</button>
<button class="tab-btn fullscreen-btn" onclick="toggleFullScreen()" id="fullscreen-toggle">
<span class="tab-icon"></span>
<span class="tab-icon">[]</span>
<span>FULL</span>
</button>
</nav>
<!-- 1. LEFT: LIBRARY -->
<section class="library-section">
<div class="lib-mode-toggle mobile-only">
<button class="mode-btn active" id="btn-server-lib" onclick="setLibraryMode('server')">SERVER</button>
<button class="mode-btn" id="btn-local-lib" onclick="setLibraryMode('local')">LOCAL</button>
</div>
<div class="lib-header">
<input type="text" id="lib-search" placeholder="FILTER LIBRARY..." onkeyup="filterLibrary()">
<button class="folder-btn" onclick="openFolderPicker()" title="Choose Folder">OPEN</button>
<button class="refresh-btn" onclick="refreshLibrary()" title="Refresh Library">REFRESH</button>
</div>
@ -194,7 +205,7 @@
<button class="big-btn repeat-btn" id="repeat-btn-A" onclick="toggleRepeat('A')"
title="Loop Track">LOOP</button>
<button class="big-btn sync-btn" onclick="syncDecks('A')">SYNC</button>
<button class="big-btn reset-btn" onclick="resetDeck('A')"
<button class="big-btn reset-btn mob-hide" onclick="resetDeck('A')"
title="Reset all settings to default">RESET</button>
</div>
</div>
@ -314,14 +325,13 @@
</div>
</div>
<div class="transport">
<button class="big-btn play-btn" onclick="playDeck('B')">PLAY</button>
<button class="big-btn pause-btn" onclick="pauseDeck('B')">PAUSE</button>
<button class="big-btn repeat-btn" id="repeat-btn-B" onclick="toggleRepeat('B')"
title="Loop Track">LOOP</button>
<button class="big-btn sync-btn" onclick="syncDecks('B')">SYNC</button>
<button class="big-btn reset-btn" onclick="resetDeck('B')"
<button class="big-btn reset-btn mob-hide" onclick="resetDeck('B')"
title="Reset all settings to default">RESET</button>
</div>
</div>
@ -330,7 +340,8 @@
<section class="queue-section" id="queue-A">
<div class="queue-page-header">
<h2 class="queue-page-title">QUEUE A</h2>
<button class="queue-clear-btn" onclick="clearQueue('A')" title="Clear queue">Clear All</button>
<button class="queue-clear-btn" onclick="clearQueue('A')" title="Clear queue">Clear
All</button>
</div>
<div class="queue-list" id="queue-list-A">
<div class="queue-empty">Drop tracks here or click "Queue to A" in library</div>
@ -340,7 +351,8 @@
<section class="queue-section" id="queue-B">
<div class="queue-page-header">
<h2 class="queue-page-title">QUEUE B</h2>
<button class="queue-clear-btn" onclick="clearQueue('B')" title="Clear queue">Clear All</button>
<button class="queue-clear-btn" onclick="clearQueue('B')" title="Clear queue">Clear
All</button>
</div>
<div class="queue-list" id="queue-list-B">
<div class="queue-empty">Drop tracks here or click "Queue to B" in library</div>
@ -441,6 +453,7 @@
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script src="script.js"></script>
<!-- Settings Panel -->
<div class="settings-panel" id="settings-panel">
<div class="settings-header">
@ -477,14 +490,27 @@
</div>
</div>
</div>
<button class="keyboard-btn" onclick="openKeyboardSettings()" title="Keyboard Shortcuts (H)">KB</button>
<button class="streaming-btn" onclick="toggleStreamingPanel()" title="Live Streaming">STREAM</button>
<button class="upload-btn" onclick="document.getElementById('file-upload').click()"
title="Upload MP3">UPLOAD</button>
<input type="file" id="file-upload" accept="audio/mp3,audio/mpeg" multiple style="display:none"
<!-- Tools FAB for Mobile -->
<div class="fab-container">
<button class="fab-main" onclick="toggleFabMenu(event)">SET</button>
<div class="fab-menu" id="fab-menu">
<button class="fab-item" onclick="toggleSettings(); toggleFabMenu()">SET</button>
<button class="fab-item"
onclick="document.getElementById('file-upload').click(); toggleFabMenu()">UP</button>
<button class="fab-item" onclick="toggleStreamingPanel(); toggleFabMenu()">LIVE</button>
<button class="fab-item" onclick="openKeyboardSettings(); toggleFabMenu()">KB</button>
</div>
</div>
<button class="keyboard-btn pc-only" onclick="openKeyboardSettings()">KB</button>
<button class="streaming-btn pc-only" onclick="toggleStreamingPanel()">STREAM</button>
<button class="upload-btn pc-only"
onclick="document.getElementById('file-upload').click()">UPLOAD</button>
<input type="file" id="file-upload" accept=".mp3,.m4a,.wav,.flac,.ogg" multiple style="display:none"
onchange="handleFileUpload(event)">
<button class="settings-btn" onclick="toggleSettings()">SET</button>
<button class="library-toggle-mob" style="display:none" onclick="toggleMobileLibrary()">LIBRARY</button>
<button class="settings-btn pc-only" onclick="toggleSettings()">SET</button>
</body>
</html>

View File

@ -1,31 +1,31 @@
#!/bin/bash
# TechDJ PyQt5 Launcher Script
# TechDJ PyQt6 Launcher Script
echo "🎧 TechDJ PyQt5 - Native DJ Application"
echo "TechDJ PyQt6 - Native DJ Application"
echo "========================================"
echo ""
# Activate virtual environment if it exists
if [ -f "./.venv/bin/activate" ]; then
echo "🔧 Activating virtual environment (.venv)..."
echo "Activating virtual environment (.venv)..."
source ./.venv/bin/activate
elif [ -f "./venv/bin/activate" ]; then
echo "🔧 Activating virtual environment (venv)..."
echo "Activating virtual environment (venv)..."
source ./venv/bin/activate
fi
# Check if Python is installed
if ! command -v python3 &> /dev/null; then
echo " Python 3 is not installed!"
echo "[ERROR] Python 3 is not installed!"
echo "Please install Python 3 first."
exit 1
fi
echo " Python 3 found: $(python3 --version)"
echo "[OK] Python 3 found: $(python3 --version)"
# Check if pip is installed
if ! command -v pip3 &> /dev/null; then
echo "⚠️ pip3 not found. Installing..."
echo "[WARN] pip3 not found. Installing..."
echo "Please run: sudo apt install python3-pip"
exit 1
fi
@ -36,55 +36,63 @@ echo "Checking dependencies..."
MISSING_DEPS=0
# Check PyQt5
if ! python3 -c "import PyQt5" 2>/dev/null; then
echo "❌ PyQt5 not installed"
# Check PyQt6
if ! python3 -c "import PyQt6" 2>/dev/null; then
echo "[ERROR] PyQt6 not installed"
MISSING_DEPS=1
else
echo "✅ PyQt5 installed"
echo "[OK] PyQt6 installed"
fi
# Check sounddevice
if ! python3 -c "import sounddevice" 2>/dev/null; then
echo " sounddevice not installed"
echo "[ERROR] sounddevice not installed"
MISSING_DEPS=1
else
echo " sounddevice installed"
echo "[OK] sounddevice installed"
fi
# Check soundfile
if ! python3 -c "import soundfile" 2>/dev/null; then
echo " soundfile not installed"
echo "[ERROR] soundfile not installed"
MISSING_DEPS=1
else
echo " soundfile installed"
echo "[OK] soundfile installed"
fi
# Check numpy
if ! python3 -c "import numpy" 2>/dev/null; then
echo " numpy not installed"
echo "[ERROR] numpy not installed"
MISSING_DEPS=1
else
echo " numpy installed"
echo "[OK] numpy installed"
fi
# Check socketio
if ! python3 -c "import socketio" 2>/dev/null; then
echo " python-socketio not installed"
echo "[ERROR] python-socketio not installed"
MISSING_DEPS=1
else
echo "✅ python-socketio installed"
echo "[OK] python-socketio installed"
fi
# Check yt-dlp
if ! python3 -c "import yt_dlp" 2>/dev/null; then
echo "[ERROR] yt-dlp not installed"
MISSING_DEPS=1
else
echo "[OK] yt-dlp installed"
fi
# Install missing dependencies
if [ $MISSING_DEPS -eq 1 ]; then
if [[ "$*" == *"--noint"* ]]; then
echo "⚠️ Missing dependencies detected in non-interactive mode"
echo "[WARN] Missing dependencies detected in non-interactive mode"
echo "Please run './launch_qt.sh' from terminal to install dependencies"
# Continue anyway - dependencies might be installed elsewhere
else
echo ""
echo "📦 Installing missing dependencies..."
echo "[INSTALL] Installing missing dependencies..."
echo "This may take a few minutes..."
echo ""
@ -92,27 +100,27 @@ if [ $MISSING_DEPS -eq 1 ]; then
echo "Installing system dependencies..."
if command -v apt-get &> /dev/null; then
echo "Detected Debian/Ubuntu system"
echo "You may need to run: sudo apt-get install portaudio19-dev python3-pyqt5"
echo "You may need to run: sudo apt-get install portaudio19-dev python3-pyqt6 python3-pyqt6.qtmultimedia libqt6multimedia6-plugins"
elif command -v dnf &> /dev/null; then
echo "Detected Fedora system"
echo "You may need to run: sudo dnf install portaudio-devel python3-qt5"
echo "You may need to run: sudo dnf install portaudio-devel python3-qt6"
elif command -v pacman &> /dev/null; then
echo "Detected Arch system"
echo "You may need to run: sudo pacman -S portaudio python-pyqt5"
echo "You may need to run: sudo pacman -S portaudio python-pyqt6"
fi
echo ""
echo "Installing Python packages..."
pip3 install --user PyQt5 sounddevice soundfile numpy python-socketio[client] requests
pip3 install --user PyQt6 sounddevice soundfile numpy python-socketio[client] requests yt-dlp
if [ $? -ne 0 ]; then
echo " Installation failed!"
echo "[ERROR] Installation failed!"
echo "Please install dependencies manually:"
echo " pip3 install --user -r requirements.txt"
exit 1
fi
echo " Dependencies installed successfully!"
echo "[OK] Dependencies installed successfully!"
fi
fi
@ -140,9 +148,9 @@ fi
echo "Checking server at: $SERVER_URL"
if curl -s --max-time 2 "${SERVER_URL}/library.json" > /dev/null 2>&1; then
echo " Flask server is running at $SERVER_URL"
echo "[OK] Flask server is running at $SERVER_URL"
else
echo "⚠️ Flask server not detected at $SERVER_URL"
echo "[WARN] Flask server not detected at $SERVER_URL"
if [[ "$*" == *"--noint"* ]]; then
echo "Proceeding in non-interactive mode..."
else
@ -158,10 +166,10 @@ fi
# Launch the application
echo ""
echo "🚀 Launching TechDJ PyQt5..."
echo "Launching TechDJ PyQt6..."
echo ""
python3 techdj_qt.py
echo ""
echo "👋 TechDJ PyQt5 closed"
echo "TechDJ PyQt6 closed"

View File

@ -4,10 +4,11 @@ flask-socketio
eventlet
python-dotenv
# PyQt5 Native App Dependencies
PyQt5
# PyQt6 Native App Dependencies
PyQt6
sounddevice
soundfile
numpy
requests
python-socketio[client]
yt-dlp

631
script.js

File diff suppressed because it is too large Load Diff

334
server.py
View File

@ -44,29 +44,30 @@ listener_sids = set()
dj_sids = set()
# === Optional MP3 fallback stream (server-side transcoding) ===
# This allows listeners on browsers that don't support WebM/Opus via MediaSource
# (notably some Safari / locked-down environments) to still hear the stream.
_ffmpeg_proc = None
_ffmpeg_in_q = queue.Queue(maxsize=40)
_ffmpeg_in_q = queue.Queue(maxsize=20) # Optimized for low-latency live streaming
_current_bitrate = "192k"
_mp3_clients = set() # set[queue.Queue]
_mp3_lock = threading.Lock()
_transcode_threads_started = False
_transcoder_bytes_out = 0
_transcoder_last_error = None
_last_audio_chunk_ts = 0.0
_mp3_preroll = collections.deque(maxlen=256) # Pre-roll (~10s at 192k with 1KB chunks)
_mp3_preroll = collections.deque(maxlen=512) # Larger pre-roll (~512KB)
def _start_transcoder_if_needed(is_mp3_input=False):
global _ffmpeg_proc, _transcode_threads_started, _transcoder_last_error
global _ffmpeg_proc, _transcoder_last_error
# If already running, check if we need to restart for mode change
# If already running, check if we need to restart
if _ffmpeg_proc is not None and _ffmpeg_proc.poll() is None:
return
# Local broadcast mode: input from pipe (Relay mode removed)
# If input is already MP3, we just use 'copy' to avoid double-encoding
# Ensure stale process is cleaned up
if _ffmpeg_proc:
try: _ffmpeg_proc.terminate()
except: pass
_ffmpeg_proc = None
codec = 'copy' if is_mp3_input else 'libmp3lame'
cmd = [
@ -92,7 +93,6 @@ def _start_transcoder_if_needed(is_mp3_input=False):
cmd.extend(['-b:a', _current_bitrate])
cmd.extend([
'-tune', 'zerolatency',
'-flush_packets', '1',
'-f', 'mp3',
'pipe:1',
@ -108,60 +108,58 @@ def _start_transcoder_if_needed(is_mp3_input=False):
)
except FileNotFoundError:
_ffmpeg_proc = None
print('⚠️ ffmpeg not found; /stream.mp3 fallback disabled')
print('WARNING: ffmpeg not found; /stream.mp3 fallback disabled')
return
mode_str = "PASSTHROUGH (copy)" if is_mp3_input else f"TRANSCODE ({_current_bitrate})"
print(f'🎛️ ffmpeg transcoder started for /stream.mp3 ({mode_str})')
print(f'INFO: ffmpeg transcoder started ({mode_str})')
# Reset error state
_transcoder_last_error = None
# Always ensure threads are running if we just started/restarted the process
# Clear the input queue to avoid old data being sent to new process
# Clear queue to avoid stale data
while not _ffmpeg_in_q.empty():
try: _ffmpeg_in_q.get_nowait()
except: break
def _writer():
global _transcoder_last_error, _transcode_threads_started
print("🧵 Transcoder writer thread started")
while True:
# Define threads INSIDE so they close over THIS specific 'proc'
def _writer(proc):
global _transcoder_last_error
print(f"[THREAD] Transcoder writer started (PID: {proc.pid})")
while proc.poll() is None:
try:
chunk = _ffmpeg_in_q.get(timeout=2)
if chunk is None:
break
proc = _ffmpeg_proc
if proc is None or proc.stdin is None or proc.poll() is not None:
continue
chunk = _ffmpeg_in_q.get(timeout=1.0)
if chunk is None: break
if proc.stdin:
proc.stdin.write(chunk)
proc.stdin.flush()
except queue.Empty:
if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None:
break
continue
except Exception as e:
if _ffmpeg_proc is not None:
print(f"⚠️ Transcoder writer error: {e}")
_transcoder_last_error = f'stdin write failed: {e}'
except (BrokenPipeError, ConnectionResetError):
_transcoder_last_error = "Broken pipe in writer"
break
except Exception as e:
print(f"WARNING: Transcoder writer error: {e}")
_transcoder_last_error = str(e)
break
_transcode_threads_started = False
print("🧵 Transcoder writer thread exiting")
def _reader():
global _transcoder_bytes_out, _transcoder_last_error, _transcode_threads_started
print("🧵 Transcoder reader thread started")
proc = _ffmpeg_proc
while proc and proc.poll() is None:
# Ensure process is killed if thread exits unexpectedly
if proc.poll() is None:
try: proc.terminate()
except: pass
print(f"[THREAD] Transcoder writer finished (PID: {proc.pid})")
def _reader(proc):
global _transcoder_bytes_out, _transcoder_last_error
print(f"[THREAD] Transcoder reader started (PID: {proc.pid})")
while proc.poll() is None:
try:
# Smaller read for smoother delivery (1KB)
# This prevents buffering delays at lower bitrates
data = proc.stdout.read(1024)
if not data:
break
if not data: break
_transcoder_bytes_out += len(data)
# Store in pre-roll
with _mp3_lock:
_mp3_preroll.append(data)
clients = list(_mp3_clients)
@ -169,67 +167,91 @@ def _start_transcoder_if_needed(is_mp3_input=False):
for q in clients:
try:
q.put_nowait(data)
except Exception:
except queue.Full:
# Client is too slow, skip this chunk for them
pass
except Exception as e:
print(f"⚠️ Transcoder reader error: {e}")
_transcoder_last_error = f'stdout read failed: {e}'
print(f"WARNING: Transcoder reader error: {e}")
_transcoder_last_error = str(e)
break
_transcode_threads_started = False
print("🧵 Transcoder reader thread exiting")
if not _transcode_threads_started:
_transcode_threads_started = True
threading.Thread(target=_writer, daemon=True).start()
threading.Thread(target=_reader, daemon=True).start()
# Ensure process is killed if thread exits unexpectedly
if proc.poll() is None:
try: proc.terminate()
except: pass
print(f"[THREAD] Transcoder reader finished (PID: {proc.pid})")
# Start greenlets/threads for THIS process specifically
eventlet.spawn(_writer, _ffmpeg_proc)
eventlet.spawn(_reader, _ffmpeg_proc)
def _stop_transcoder():
global _ffmpeg_proc, _transcode_threads_started
try:
_ffmpeg_in_q.put_nowait(None)
except Exception:
pass
global _ffmpeg_proc
print("STOPPING: Transcoder process")
# Signal threads to stop via the queue
try: _ffmpeg_in_q.put_nowait(None)
except: pass
# Shutdown the process
proc = _ffmpeg_proc
_ffmpeg_proc = None
# Reset thread flag so they can be re-launched if needed
# (The existing threads will exit cleanly on None/EOF)
_transcode_threads_started = False
_mp3_preroll.clear()
if proc:
try:
proc.terminate()
# Drain stdout/stderr to satisfy OS buffers
proc.communicate(timeout=1.0)
except:
try: proc.kill()
except: pass
if proc is None:
return
# Signal all listening clients to finish their stream
# Clear client state
with _mp3_lock:
clients = list(_mp3_clients)
for q in clients:
try:
q.put_nowait(None)
except:
pass
try: q.put_nowait(None)
except: pass
_mp3_clients.clear()
try:
proc.terminate()
except Exception:
pass
_mp3_preroll.clear()
def _feed_transcoder(data: bytes):
global _last_audio_chunk_ts
if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None:
# If active but dead, restart it automatically
if broadcast_state.get('active'):
_start_transcoder_if_needed(is_mp3_input=broadcast_state.get('is_mp3_input', False))
else:
return
_last_audio_chunk_ts = time.time()
try:
_ffmpeg_in_q.put_nowait(data)
except Exception:
# Queue full; drop to keep latency bounded.
except queue.Full:
# Drop chunk if overflow to prevent memory bloat
pass
MUSIC_FOLDER = "music"
# Load settings to get MUSIC_FOLDER
def _load_settings():
try:
if os.path.exists('settings.json'):
with open('settings.json', 'r', encoding='utf-8') as f:
return json.load(f)
except:
pass
return {}
SETTINGS = _load_settings()
MUSIC_FOLDER = SETTINGS.get('library', {}).get('music_folder', 'music')
# Ensure music folder exists
if not os.path.exists(MUSIC_FOLDER):
try:
os.makedirs(MUSIC_FOLDER)
except:
# Fallback to default if custom path fails
MUSIC_FOLDER = "music"
if not os.path.exists(MUSIC_FOLDER):
os.makedirs(MUSIC_FOLDER)
@ -238,18 +260,87 @@ def setup_shared_routes(app):
@app.route('/library.json')
def get_library():
library = []
global MUSIC_FOLDER
if os.path.exists(MUSIC_FOLDER):
for filename in sorted(os.listdir(MUSIC_FOLDER)):
# Recursively find music files if desired, or stay top-level.
# The prompt says "choose which folder", so maybe top-level of that folder is fine.
for root, dirs, files in os.walk(MUSIC_FOLDER):
for filename in sorted(files):
if filename.lower().endswith(('.mp3', '.m4a', '.wav', '.flac', '.ogg')):
rel_path = os.path.relpath(os.path.join(root, filename), MUSIC_FOLDER)
library.append({
"title": os.path.splitext(filename)[0],
"file": f"music/{filename}"
"file": f"music_proxy/{rel_path}"
})
break # Only top level for now to keep it simple, or remove break for recursive
return jsonify(library)
@app.route('/music_proxy/<path:filename>')
def music_proxy(filename):
return send_from_directory(MUSIC_FOLDER, filename)
@app.route('/browse_directories', methods=['GET'])
def browse_directories():
path = request.args.get('path', os.path.expanduser('~'))
try:
entries = []
if os.path.exists(path) and os.path.isdir(path):
# Add parent
parent = os.path.dirname(os.path.abspath(path))
entries.append({"name": "..", "path": parent, "isDir": True})
for item in sorted(os.listdir(path)):
full_path = os.path.join(path, item)
if os.path.isdir(full_path) and not item.startswith('.'):
entries.append({
"name": item,
"path": full_path,
"isDir": True
})
return jsonify({"success": True, "path": os.path.abspath(path), "entries": entries})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/update_settings', methods=['POST'])
def update_settings():
try:
data = request.get_json()
# Load existing
settings = _load_settings()
# Update selectively
if 'library' not in settings: settings['library'] = {}
if 'music_folder' in data.get('library', {}):
new_folder = data['library']['music_folder']
if os.path.exists(new_folder) and os.path.isdir(new_folder):
settings['library']['music_folder'] = new_folder
global MUSIC_FOLDER
MUSIC_FOLDER = new_folder
else:
return jsonify({"success": False, "error": "Invalid folder path"}), 400
with open('settings.json', 'w', encoding='utf-8') as f:
json.dump(settings, f, indent=4)
return jsonify({"success": True})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/<path:filename>')
def serve_static(filename):
# Block access to sensitive files
blocked = ('.py', '.pyc', '.env', '.json', '.sh', '.bak', '.log', '.pem', '.key')
# Allow specific safe JSON/JS/CSS files
allowed_extensions = ('.css', '.js', '.html', '.htm', '.png', '.jpg', '.jpeg',
'.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.map')
if filename.endswith(blocked) and not filename.endswith(('.css', '.js')):
from flask import abort
abort(403)
# Prevent path traversal
if '..' in filename or filename.startswith('/'):
from flask import abort
abort(403)
response = send_from_directory('.', filename)
if filename.endswith(('.css', '.js', '.html')):
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
@ -269,24 +360,26 @@ def setup_shared_routes(app):
if file.filename == '':
return jsonify({"success": False, "error": "No file selected"}), 400
if not file.filename.endswith('.mp3'):
return jsonify({"success": False, "error": "Only MP3 files are allowed"}), 400
allowed_exts = ('.mp3', '.m4a', '.wav', '.flac', '.ogg')
ext = os.path.splitext(file.filename)[1].lower()
if ext not in allowed_exts:
return jsonify({"success": False, "error": f"Supported formats: {', '.join(allowed_exts)}"}), 400
# Sanitize filename (keep extension)
import re
name_without_ext = os.path.splitext(file.filename)[0]
name_without_ext = re.sub(r'[^\w\s-]', '', name_without_ext)
name_without_ext = re.sub(r'[-\s]+', '-', name_without_ext)
filename = f"{name_without_ext}.mp3"
name_without_ext = re.sub(r'\s+', ' ', name_without_ext).strip()
filename = f"{name_without_ext}{ext}"
filepath = os.path.join(MUSIC_FOLDER, filename)
try:
file.save(filepath)
print(f"✅ Uploaded: {filename}")
print(f"UPLOADED: {filename}")
return jsonify({"success": True, "filename": filename})
except Exception as e:
print(f" Upload error: {e}")
print(f"ERROR: Upload error: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/save_keymaps', methods=['POST'])
@ -295,10 +388,10 @@ def setup_shared_routes(app):
data = request.get_json()
with open('keymaps.json', 'w', encoding='utf-8') as f:
json.dump(data, f, indent=4)
print("💾 Keymaps saved to keymaps.json")
print("SAVED: Keymaps saved to keymaps.json")
return jsonify({"success": True})
except Exception as e:
print(f" Save keymaps error: {e}")
print(f"ERROR: Save keymaps error: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/load_keymaps', methods=['GET'])
@ -311,7 +404,7 @@ def setup_shared_routes(app):
else:
return jsonify({"success": True, "keymaps": None})
except Exception as e:
print(f" Load keymaps error: {e}")
print(f"ERROR: Load keymaps error: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/stream.mp3')
@ -321,8 +414,8 @@ def setup_shared_routes(app):
if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None:
return jsonify({"success": False, "error": "MP3 stream not available"}), 503
print(f"👂 New listener joined stream (Bursting {_mp3_preroll.maxlen} frames)")
client_q: queue.Queue = queue.Queue(maxsize=100)
print(f"LISTENER: New listener joined stream (Bursting {_mp3_preroll.maxlen} frames)")
client_q: queue.Queue = queue.Queue(maxsize=500)
with _mp3_lock:
# Burst pre-roll to new client so they start playing instantly
for chunk in _mp3_preroll:
@ -335,13 +428,20 @@ def setup_shared_routes(app):
def gen():
try:
while True:
chunk = client_q.get()
try:
chunk = client_q.get(timeout=30)
except queue.Empty:
# No data for 30s - check if broadcast is still active
if not broadcast_state.get('active'):
break
continue
if chunk is None:
break
yield chunk
finally:
with _mp3_lock:
_mp3_clients.discard(client_q)
print(f"LISTENER: Listener disconnected from stream")
return Response(stream_with_context(gen()), content_type='audio/mpeg', headers={
'Cache-Control': 'no-cache, no-store, must-revalidate',
@ -482,15 +582,15 @@ dj_socketio = SocketIO(
@dj_socketio.on('connect')
def dj_connect():
if DJ_AUTH_ENABLED and session.get('dj_authed') is not True:
print(f" DJ socket rejected (unauthorized): {request.sid}")
print(f"REJECTED: DJ socket rejected (unauthorized): {request.sid}")
return False
print(f"🎧 DJ connected: {request.sid}")
print(f"STREAMPANEL: DJ connected: {request.sid}")
dj_sids.add(request.sid)
@dj_socketio.on('disconnect')
def dj_disconnect():
dj_sids.discard(request.sid)
print("⚠️ DJ disconnected - broadcast will continue until manually stopped")
print("WARNING: DJ disconnected - broadcast will continue until manually stopped")
def stop_broadcast_after_timeout():
"""No longer used - broadcasts don't auto-stop"""
@ -500,18 +600,20 @@ def stop_broadcast_after_timeout():
def dj_start(data=None):
broadcast_state['active'] = True
session['is_dj'] = True
print("🎙️ Broadcast -> ACTIVE")
print("BROADCAST: ACTIVE")
is_mp3_input = False
if data:
if 'bitrate' in data:
global _current_bitrate
_current_bitrate = data['bitrate']
print(f"📡 Setting stream bitrate to: {_current_bitrate}")
print(f"BITRATE: Setting stream bitrate to: {_current_bitrate}")
if data.get('format') == 'mp3':
is_mp3_input = True
broadcast_state['is_mp3_input'] = is_mp3_input
# Clear pre-roll for fresh start
with _mp3_lock:
_mp3_preroll.clear()
_start_transcoder_if_needed(is_mp3_input=is_mp3_input)
@ -527,16 +629,13 @@ def dj_get_listener_count():
def dj_stop():
broadcast_state['active'] = False
session['is_dj'] = False
print("🛑 DJ stopped broadcasting")
print("STOPPED: DJ stopped broadcasting")
_stop_transcoder()
listener_socketio.emit('broadcast_stopped', namespace='/')
listener_socketio.emit('stream_status', {'active': False}, namespace='/')
listener_socketio.emit('broadcast_stopped', namespace='/')
listener_socketio.emit('stream_status', {'active': False}, namespace='/')
@dj_socketio.on('audio_chunk')
def dj_audio(data):
# MP3-only mode: do not relay raw chunks to listeners; feed transcoder only.
@ -554,6 +653,15 @@ def dj_audio(data):
listener_app = Flask(__name__, static_folder='.', static_url_path='')
listener_app.config['SECRET_KEY'] = 'listener_secret'
setup_shared_routes(listener_app)
# Block write/admin endpoints on the listener server
@listener_app.before_request
def _restrict_listener_routes():
"""Prevent listeners from accessing DJ-only write endpoints."""
blocked_paths = ('/update_settings', '/upload', '/save_keymaps', '/browse_directories')
if request.path in blocked_paths:
from flask import abort
abort(403)
listener_socketio = SocketIO(
listener_app,
cors_allowed_origins="*",
@ -567,13 +675,13 @@ listener_socketio = SocketIO(
@listener_socketio.on('connect')
def listener_connect():
print(f"👂 Listener Socket Connected: {request.sid}")
print(f"LISTENER: Listener Socket Connected: {request.sid}")
@listener_socketio.on('disconnect')
def listener_disconnect():
listener_sids.discard(request.sid)
count = len(listener_sids)
print(f" Listener left. Total: {count}")
print(f"REMOVED: Listener left. Total: {count}")
# Notify BOTH namespaces
listener_socketio.emit('listener_count', {'count': count}, namespace='/')
dj_socketio.emit('listener_count', {'count': count}, namespace='/')
@ -583,7 +691,7 @@ def listener_join():
if request.sid not in listener_sids:
listener_sids.add(request.sid)
count = len(listener_sids)
print(f"👂 New listener joined. Total: {count}")
print(f"LISTENER: New listener joined. Total: {count}")
listener_socketio.emit('listener_count', {'count': count}, namespace='/')
dj_socketio.emit('listener_count', {'count': count}, namespace='/')
@ -594,13 +702,16 @@ def listener_get_count():
emit('listener_count', {'count': len(listener_sids)})
# DJ Panel Routes (No engine commands needed in local mode)
@dj_socketio.on('get_mixer_status')
def get_mixer_status():
pass
def _transcoder_watchdog():
"""Periodic check to ensure the transcoder stays alive during active broadcasts."""
while True:
if broadcast_state.get('active'):
if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None:
# Only log if it's actually dead and supposed to be alive
print("WARNING: Watchdog: Transcoder dead during active broadcast, reviving...")
_start_transcoder_if_needed(is_mp3_input=broadcast_state.get('is_mp3_input', False))
eventlet.sleep(5)
@dj_socketio.on('audio_sync_queue')
def audio_sync_queue(data):
pass
def _listener_count_sync_loop():
"""Periodic background sync to ensure listener count is always accurate."""
@ -613,20 +724,21 @@ def _listener_count_sync_loop():
if __name__ == '__main__':
print("=" * 50)
print("🎧 TECHDJ PRO - DUAL PORT ARCHITECTURE")
print("TECHDJ PRO - DUAL PORT ARCHITECTURE")
print("=" * 50)
# Ports from environment or defaults
dj_port = int(os.environ.get('DJ_PORT', 5000))
listen_port = int(os.environ.get('LISTEN_PORT', 5001))
print(f"👉 DJ PANEL API: http://0.0.0.0:{dj_port}")
print(f"👉 LISTEN PAGE: http://0.0.0.0:{listen_port}")
print(f"URL: DJ PANEL API: http://0.0.0.0:{dj_port}")
print(f"URL: LISTEN PAGE: http://0.0.0.0:{listen_port}")
print("=" * 50)
# Audio engine DISABLED
print(f" Local Radio server ready on ports {dj_port} & {listen_port}")
print(f"READY: Local Radio server ready on ports {dj_port} & {listen_port}")
# Run both servers using eventlet's spawn
eventlet.spawn(_listener_count_sync_loop)
eventlet.spawn(_transcoder_watchdog)
eventlet.spawn(dj_socketio.run, dj_app, host='0.0.0.0', port=dj_port, debug=False)
listener_socketio.run(listener_app, host='0.0.0.0', port=listen_port, debug=False)

22
settings.json Normal file
View File

@ -0,0 +1,22 @@
{
"shortcuts": {
"Deck A: Load": "Ctrl+L",
"Deck A: Queue": "Ctrl+Shift+L",
"Deck A: Play/Pause": "Space",
"Deck B: Load": "Ctrl+R",
"Deck B: Queue": "Ctrl+Shift+R",
"Deck B: Play/Pause": "Ctrl+Space"
},
"audio": {
"recording_sample_rate": 48000,
"recording_format": "wav",
"stream_server_url": "http://54.37.246.24:5000"
},
"ui": {
"neon_mode": 2
},
"library": {
"auto_scan": true,
"yt_default_format": "mp3"
}
}

809
style.css

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

2801
techdj_qt_v1.py.bak Normal file

File diff suppressed because one or more lines are too long