Final bug sweep: fix event param, remove dead code, dedupe CSS; add .gitignore recordings/
This commit is contained in:
parent
12c01faa83
commit
b5ea9e8d01
|
|
@ -1,4 +1,5 @@
|
||||||
music/
|
music/
|
||||||
|
recordings/
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
|
||||||
|
|
@ -41,23 +41,23 @@ def main():
|
||||||
# Check Chrome
|
# Check Chrome
|
||||||
chrome_mem, chrome_procs = get_process_memory('chrome')
|
chrome_mem, chrome_procs = get_process_memory('chrome')
|
||||||
if chrome_mem > 0:
|
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" Total Memory: {chrome_mem:.1f} MB")
|
||||||
print(f" Processes: {chrome_procs}")
|
print(f" Processes: {chrome_procs}")
|
||||||
print()
|
print()
|
||||||
else:
|
else:
|
||||||
print("🌐 Chrome: Not running")
|
print("[CHROME] Chrome: Not running")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# Check PyQt5
|
# Check PyQt6
|
||||||
qt_mem, qt_procs = get_process_memory('techdj_qt')
|
qt_mem, qt_procs = get_process_memory('techdj_qt')
|
||||||
if qt_mem > 0:
|
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" Total Memory: {qt_mem:.1f} MB")
|
||||||
print(f" Processes: {qt_procs}")
|
print(f" Processes: {qt_procs}")
|
||||||
print()
|
print()
|
||||||
else:
|
else:
|
||||||
print("💻 PyQt5 Native App: Not running")
|
print("[PYQT6] PyQt6 Native App: Not running")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# Comparison
|
# Comparison
|
||||||
|
|
@ -66,25 +66,25 @@ def main():
|
||||||
percent = (savings / chrome_mem) * 100
|
percent = (savings / chrome_mem) * 100
|
||||||
|
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("📊 Comparison:")
|
print("[STATS] Comparison:")
|
||||||
print(f" Memory Saved: {savings:.1f} MB ({percent:.1f}%)")
|
print(f" Memory Saved: {savings:.1f} MB ({percent:.1f}%)")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# Visual bar chart
|
# Visual bar chart
|
||||||
max_mem = max(chrome_mem, qt_mem)
|
max_mem = max(chrome_mem, qt_mem)
|
||||||
chrome_bar = '█' * int((chrome_mem / max_mem) * 40)
|
chrome_bar = '#' * int((chrome_mem / max_mem) * 40)
|
||||||
qt_bar = '█' * int((qt_mem / max_mem) * 40)
|
qt_bar = '#' * int((qt_mem / max_mem) * 40)
|
||||||
|
|
||||||
print(" Chrome: " + chrome_bar + f" {chrome_mem:.0f}MB")
|
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()
|
print()
|
||||||
|
|
||||||
if percent > 50:
|
if percent > 50:
|
||||||
print(f" ✅ PyQt5 uses {percent:.0f}% less memory!")
|
print(f" [OK] PyQt6 uses {percent:.0f}% less memory!")
|
||||||
elif percent > 25:
|
elif percent > 25:
|
||||||
print(f" ✅ PyQt5 uses {percent:.0f}% less memory")
|
print(f" [OK] PyQt6 uses {percent:.0f}% less memory")
|
||||||
else:
|
else:
|
||||||
print(f" PyQt5 uses {percent:.0f}% less memory")
|
print(f" PyQt6 uses {percent:.0f}% less memory")
|
||||||
|
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print()
|
print()
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 705 KiB |
68
index.html
68
index.html
|
|
@ -29,6 +29,15 @@
|
||||||
USE PORTRAIT MODE
|
USE PORTRAIT MODE
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 -->
|
<!-- MAIN APP CONTAINER -->
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
|
|
@ -36,31 +45,33 @@
|
||||||
<!-- Mobile Tabs -->
|
<!-- Mobile Tabs -->
|
||||||
<nav class="mobile-tabs">
|
<nav class="mobile-tabs">
|
||||||
<button class="tab-btn active" onclick="switchTab('library')">
|
<button class="tab-btn active" onclick="switchTab('library')">
|
||||||
<span class="tab-icon"></span>
|
<span class="tab-icon">#</span>
|
||||||
<span>LIBRARY</span>
|
<span>LIB</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" onclick="switchTab('deck-A')">
|
<button class="tab-btn" onclick="switchTab('deck-A')">
|
||||||
<span class="tab-icon"></span>
|
<span class="tab-icon">A</span>
|
||||||
<span>DECK A</span>
|
<span>DECK A</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" onclick="switchTab('deck-B')">
|
<button class="tab-btn" onclick="switchTab('deck-B')">
|
||||||
<span class="tab-icon"></span>
|
<span class="tab-icon">B</span>
|
||||||
<span>DECK B</span>
|
<span>DECK B</span>
|
||||||
</button>
|
</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">
|
<button class="tab-btn fullscreen-btn" onclick="toggleFullScreen()" id="fullscreen-toggle">
|
||||||
<span class="tab-icon"></span>
|
<span class="tab-icon">[]</span>
|
||||||
<span>FULL</span>
|
<span>FULL</span>
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- 1. LEFT: LIBRARY -->
|
<!-- 1. LEFT: LIBRARY -->
|
||||||
<section class="library-section">
|
<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">
|
<div class="lib-header">
|
||||||
<input type="text" id="lib-search" placeholder="FILTER LIBRARY..." onkeyup="filterLibrary()">
|
<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>
|
<button class="refresh-btn" onclick="refreshLibrary()" title="Refresh Library">REFRESH</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -194,7 +205,7 @@
|
||||||
<button class="big-btn repeat-btn" id="repeat-btn-A" onclick="toggleRepeat('A')"
|
<button class="big-btn repeat-btn" id="repeat-btn-A" onclick="toggleRepeat('A')"
|
||||||
title="Loop Track">LOOP</button>
|
title="Loop Track">LOOP</button>
|
||||||
<button class="big-btn sync-btn" onclick="syncDecks('A')">SYNC</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>
|
title="Reset all settings to default">RESET</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -314,14 +325,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="transport">
|
<div class="transport">
|
||||||
<button class="big-btn play-btn" onclick="playDeck('B')">PLAY</button>
|
<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 pause-btn" onclick="pauseDeck('B')">PAUSE</button>
|
||||||
<button class="big-btn repeat-btn" id="repeat-btn-B" onclick="toggleRepeat('B')"
|
<button class="big-btn repeat-btn" id="repeat-btn-B" onclick="toggleRepeat('B')"
|
||||||
title="Loop Track">LOOP</button>
|
title="Loop Track">LOOP</button>
|
||||||
<button class="big-btn sync-btn" onclick="syncDecks('B')">SYNC</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>
|
title="Reset all settings to default">RESET</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -330,7 +340,8 @@
|
||||||
<section class="queue-section" id="queue-A">
|
<section class="queue-section" id="queue-A">
|
||||||
<div class="queue-page-header">
|
<div class="queue-page-header">
|
||||||
<h2 class="queue-page-title">QUEUE A</h2>
|
<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>
|
||||||
<div class="queue-list" id="queue-list-A">
|
<div class="queue-list" id="queue-list-A">
|
||||||
<div class="queue-empty">Drop tracks here or click "Queue to A" in library</div>
|
<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">
|
<section class="queue-section" id="queue-B">
|
||||||
<div class="queue-page-header">
|
<div class="queue-page-header">
|
||||||
<h2 class="queue-page-title">QUEUE B</h2>
|
<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>
|
||||||
<div class="queue-list" id="queue-list-B">
|
<div class="queue-list" id="queue-list-B">
|
||||||
<div class="queue-empty">Drop tracks here or click "Queue to B" in library</div>
|
<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="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
|
||||||
<script src="script.js"></script>
|
<script src="script.js"></script>
|
||||||
|
|
||||||
<!-- Settings Panel -->
|
<!-- Settings Panel -->
|
||||||
<div class="settings-panel" id="settings-panel">
|
<div class="settings-panel" id="settings-panel">
|
||||||
<div class="settings-header">
|
<div class="settings-header">
|
||||||
|
|
@ -477,14 +490,27 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
<!-- Tools FAB for Mobile -->
|
||||||
<button class="upload-btn" onclick="document.getElementById('file-upload').click()"
|
<div class="fab-container">
|
||||||
title="Upload MP3">UPLOAD</button>
|
<button class="fab-main" onclick="toggleFabMenu(event)">SET</button>
|
||||||
<input type="file" id="file-upload" accept="audio/mp3,audio/mpeg" multiple style="display:none"
|
<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)">
|
onchange="handleFileUpload(event)">
|
||||||
<button class="settings-btn" onclick="toggleSettings()">SET</button>
|
<button class="settings-btn pc-only" onclick="toggleSettings()">SET</button>
|
||||||
<button class="library-toggle-mob" style="display:none" onclick="toggleMobileLibrary()">LIBRARY</button>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
70
launch_qt.sh
70
launch_qt.sh
|
|
@ -1,31 +1,31 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# TechDJ PyQt5 Launcher Script
|
# TechDJ PyQt6 Launcher Script
|
||||||
|
|
||||||
echo "🎧 TechDJ PyQt5 - Native DJ Application"
|
echo "TechDJ PyQt6 - Native DJ Application"
|
||||||
echo "========================================"
|
echo "========================================"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Activate virtual environment if it exists
|
# Activate virtual environment if it exists
|
||||||
if [ -f "./.venv/bin/activate" ]; then
|
if [ -f "./.venv/bin/activate" ]; then
|
||||||
echo "🔧 Activating virtual environment (.venv)..."
|
echo "Activating virtual environment (.venv)..."
|
||||||
source ./.venv/bin/activate
|
source ./.venv/bin/activate
|
||||||
elif [ -f "./venv/bin/activate" ]; then
|
elif [ -f "./venv/bin/activate" ]; then
|
||||||
echo "🔧 Activating virtual environment (venv)..."
|
echo "Activating virtual environment (venv)..."
|
||||||
source ./venv/bin/activate
|
source ./venv/bin/activate
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if Python is installed
|
# Check if Python is installed
|
||||||
if ! command -v python3 &> /dev/null; then
|
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."
|
echo "Please install Python 3 first."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ Python 3 found: $(python3 --version)"
|
echo "[OK] Python 3 found: $(python3 --version)"
|
||||||
|
|
||||||
# Check if pip is installed
|
# Check if pip is installed
|
||||||
if ! command -v pip3 &> /dev/null; then
|
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"
|
echo "Please run: sudo apt install python3-pip"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
@ -36,55 +36,63 @@ echo "Checking dependencies..."
|
||||||
|
|
||||||
MISSING_DEPS=0
|
MISSING_DEPS=0
|
||||||
|
|
||||||
# Check PyQt5
|
# Check PyQt6
|
||||||
if ! python3 -c "import PyQt5" 2>/dev/null; then
|
if ! python3 -c "import PyQt6" 2>/dev/null; then
|
||||||
echo "❌ PyQt5 not installed"
|
echo "[ERROR] PyQt6 not installed"
|
||||||
MISSING_DEPS=1
|
MISSING_DEPS=1
|
||||||
else
|
else
|
||||||
echo "✅ PyQt5 installed"
|
echo "[OK] PyQt6 installed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check sounddevice
|
# Check sounddevice
|
||||||
if ! python3 -c "import sounddevice" 2>/dev/null; then
|
if ! python3 -c "import sounddevice" 2>/dev/null; then
|
||||||
echo "❌ sounddevice not installed"
|
echo "[ERROR] sounddevice not installed"
|
||||||
MISSING_DEPS=1
|
MISSING_DEPS=1
|
||||||
else
|
else
|
||||||
echo "✅ sounddevice installed"
|
echo "[OK] sounddevice installed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check soundfile
|
# Check soundfile
|
||||||
if ! python3 -c "import soundfile" 2>/dev/null; then
|
if ! python3 -c "import soundfile" 2>/dev/null; then
|
||||||
echo "❌ soundfile not installed"
|
echo "[ERROR] soundfile not installed"
|
||||||
MISSING_DEPS=1
|
MISSING_DEPS=1
|
||||||
else
|
else
|
||||||
echo "✅ soundfile installed"
|
echo "[OK] soundfile installed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check numpy
|
# Check numpy
|
||||||
if ! python3 -c "import numpy" 2>/dev/null; then
|
if ! python3 -c "import numpy" 2>/dev/null; then
|
||||||
echo "❌ numpy not installed"
|
echo "[ERROR] numpy not installed"
|
||||||
MISSING_DEPS=1
|
MISSING_DEPS=1
|
||||||
else
|
else
|
||||||
echo "✅ numpy installed"
|
echo "[OK] numpy installed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check socketio
|
# Check socketio
|
||||||
if ! python3 -c "import socketio" 2>/dev/null; then
|
if ! python3 -c "import socketio" 2>/dev/null; then
|
||||||
echo "❌ python-socketio not installed"
|
echo "[ERROR] python-socketio not installed"
|
||||||
MISSING_DEPS=1
|
MISSING_DEPS=1
|
||||||
else
|
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
|
fi
|
||||||
|
|
||||||
# Install missing dependencies
|
# Install missing dependencies
|
||||||
if [ $MISSING_DEPS -eq 1 ]; then
|
if [ $MISSING_DEPS -eq 1 ]; then
|
||||||
if [[ "$*" == *"--noint"* ]]; 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"
|
echo "Please run './launch_qt.sh' from terminal to install dependencies"
|
||||||
# Continue anyway - dependencies might be installed elsewhere
|
# Continue anyway - dependencies might be installed elsewhere
|
||||||
else
|
else
|
||||||
echo ""
|
echo ""
|
||||||
echo "📦 Installing missing dependencies..."
|
echo "[INSTALL] Installing missing dependencies..."
|
||||||
echo "This may take a few minutes..."
|
echo "This may take a few minutes..."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|
@ -92,27 +100,27 @@ if [ $MISSING_DEPS -eq 1 ]; then
|
||||||
echo "Installing system dependencies..."
|
echo "Installing system dependencies..."
|
||||||
if command -v apt-get &> /dev/null; then
|
if command -v apt-get &> /dev/null; then
|
||||||
echo "Detected Debian/Ubuntu system"
|
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
|
elif command -v dnf &> /dev/null; then
|
||||||
echo "Detected Fedora system"
|
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
|
elif command -v pacman &> /dev/null; then
|
||||||
echo "Detected Arch system"
|
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
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Installing Python packages..."
|
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
|
if [ $? -ne 0 ]; then
|
||||||
echo "❌ Installation failed!"
|
echo "[ERROR] Installation failed!"
|
||||||
echo "Please install dependencies manually:"
|
echo "Please install dependencies manually:"
|
||||||
echo " pip3 install --user -r requirements.txt"
|
echo " pip3 install --user -r requirements.txt"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ Dependencies installed successfully!"
|
echo "[OK] Dependencies installed successfully!"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -140,9 +148,9 @@ fi
|
||||||
echo "Checking server at: $SERVER_URL"
|
echo "Checking server at: $SERVER_URL"
|
||||||
|
|
||||||
if curl -s --max-time 2 "${SERVER_URL}/library.json" > /dev/null 2>&1; then
|
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
|
else
|
||||||
echo "⚠️ Flask server not detected at $SERVER_URL"
|
echo "[WARN] Flask server not detected at $SERVER_URL"
|
||||||
if [[ "$*" == *"--noint"* ]]; then
|
if [[ "$*" == *"--noint"* ]]; then
|
||||||
echo "Proceeding in non-interactive mode..."
|
echo "Proceeding in non-interactive mode..."
|
||||||
else
|
else
|
||||||
|
|
@ -158,10 +166,10 @@ fi
|
||||||
|
|
||||||
# Launch the application
|
# Launch the application
|
||||||
echo ""
|
echo ""
|
||||||
echo "🚀 Launching TechDJ PyQt5..."
|
echo "Launching TechDJ PyQt6..."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
python3 techdj_qt.py
|
python3 techdj_qt.py
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "👋 TechDJ PyQt5 closed"
|
echo "TechDJ PyQt6 closed"
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ flask-socketio
|
||||||
eventlet
|
eventlet
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
|
||||||
# PyQt5 Native App Dependencies
|
# PyQt6 Native App Dependencies
|
||||||
PyQt5
|
PyQt6
|
||||||
sounddevice
|
sounddevice
|
||||||
soundfile
|
soundfile
|
||||||
numpy
|
numpy
|
||||||
requests
|
requests
|
||||||
python-socketio[client]
|
python-socketio[client]
|
||||||
|
yt-dlp
|
||||||
|
|
|
||||||
354
server.py
354
server.py
|
|
@ -44,29 +44,30 @@ listener_sids = set()
|
||||||
dj_sids = set()
|
dj_sids = set()
|
||||||
|
|
||||||
# === Optional MP3 fallback stream (server-side transcoding) ===
|
# === 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_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"
|
_current_bitrate = "192k"
|
||||||
_mp3_clients = set() # set[queue.Queue]
|
_mp3_clients = set() # set[queue.Queue]
|
||||||
_mp3_lock = threading.Lock()
|
_mp3_lock = threading.Lock()
|
||||||
_transcode_threads_started = False
|
|
||||||
_transcoder_bytes_out = 0
|
_transcoder_bytes_out = 0
|
||||||
_transcoder_last_error = None
|
_transcoder_last_error = None
|
||||||
_last_audio_chunk_ts = 0.0
|
_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):
|
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:
|
if _ffmpeg_proc is not None and _ffmpeg_proc.poll() is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Local broadcast mode: input from pipe (Relay mode removed)
|
# Ensure stale process is cleaned up
|
||||||
# If input is already MP3, we just use 'copy' to avoid double-encoding
|
if _ffmpeg_proc:
|
||||||
|
try: _ffmpeg_proc.terminate()
|
||||||
|
except: pass
|
||||||
|
_ffmpeg_proc = None
|
||||||
|
|
||||||
codec = 'copy' if is_mp3_input else 'libmp3lame'
|
codec = 'copy' if is_mp3_input else 'libmp3lame'
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
|
|
@ -92,7 +93,6 @@ def _start_transcoder_if_needed(is_mp3_input=False):
|
||||||
cmd.extend(['-b:a', _current_bitrate])
|
cmd.extend(['-b:a', _current_bitrate])
|
||||||
|
|
||||||
cmd.extend([
|
cmd.extend([
|
||||||
'-tune', 'zerolatency',
|
|
||||||
'-flush_packets', '1',
|
'-flush_packets', '1',
|
||||||
'-f', 'mp3',
|
'-f', 'mp3',
|
||||||
'pipe:1',
|
'pipe:1',
|
||||||
|
|
@ -108,60 +108,58 @@ def _start_transcoder_if_needed(is_mp3_input=False):
|
||||||
)
|
)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
_ffmpeg_proc = None
|
_ffmpeg_proc = None
|
||||||
print('⚠️ ffmpeg not found; /stream.mp3 fallback disabled')
|
print('WARNING: ffmpeg not found; /stream.mp3 fallback disabled')
|
||||||
return
|
return
|
||||||
|
|
||||||
mode_str = "PASSTHROUGH (copy)" if is_mp3_input else f"TRANSCODE ({_current_bitrate})"
|
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
|
_transcoder_last_error = None
|
||||||
|
|
||||||
# Always ensure threads are running if we just started/restarted the process
|
# Clear queue to avoid stale data
|
||||||
# Clear the input queue to avoid old data being sent to new process
|
|
||||||
while not _ffmpeg_in_q.empty():
|
while not _ffmpeg_in_q.empty():
|
||||||
try: _ffmpeg_in_q.get_nowait()
|
try: _ffmpeg_in_q.get_nowait()
|
||||||
except: break
|
except: break
|
||||||
|
|
||||||
def _writer():
|
# Define threads INSIDE so they close over THIS specific 'proc'
|
||||||
global _transcoder_last_error, _transcode_threads_started
|
def _writer(proc):
|
||||||
print("🧵 Transcoder writer thread started")
|
global _transcoder_last_error
|
||||||
while True:
|
print(f"[THREAD] Transcoder writer started (PID: {proc.pid})")
|
||||||
|
while proc.poll() is None:
|
||||||
try:
|
try:
|
||||||
chunk = _ffmpeg_in_q.get(timeout=2)
|
chunk = _ffmpeg_in_q.get(timeout=1.0)
|
||||||
if chunk is None:
|
if chunk is None: break
|
||||||
break
|
|
||||||
proc = _ffmpeg_proc
|
if proc.stdin:
|
||||||
if proc is None or proc.stdin is None or proc.poll() is not None:
|
proc.stdin.write(chunk)
|
||||||
continue
|
proc.stdin.flush()
|
||||||
proc.stdin.write(chunk)
|
|
||||||
proc.stdin.flush()
|
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None:
|
|
||||||
break
|
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except (BrokenPipeError, ConnectionResetError):
|
||||||
if _ffmpeg_proc is not None:
|
_transcoder_last_error = "Broken pipe in writer"
|
||||||
print(f"⚠️ Transcoder writer error: {e}")
|
|
||||||
_transcoder_last_error = f'stdin write failed: {e}'
|
|
||||||
break
|
break
|
||||||
_transcode_threads_started = False
|
except Exception as e:
|
||||||
print("🧵 Transcoder writer thread exiting")
|
print(f"WARNING: Transcoder writer error: {e}")
|
||||||
|
_transcoder_last_error = str(e)
|
||||||
|
break
|
||||||
|
|
||||||
|
# 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():
|
def _reader(proc):
|
||||||
global _transcoder_bytes_out, _transcoder_last_error, _transcode_threads_started
|
global _transcoder_bytes_out, _transcoder_last_error
|
||||||
print("🧵 Transcoder reader thread started")
|
print(f"[THREAD] Transcoder reader started (PID: {proc.pid})")
|
||||||
proc = _ffmpeg_proc
|
while proc.poll() is None:
|
||||||
while proc and proc.poll() is None:
|
|
||||||
try:
|
try:
|
||||||
# Smaller read for smoother delivery (1KB)
|
# Smaller read for smoother delivery (1KB)
|
||||||
# This prevents buffering delays at lower bitrates
|
# This prevents buffering delays at lower bitrates
|
||||||
data = proc.stdout.read(1024)
|
data = proc.stdout.read(1024)
|
||||||
if not data:
|
if not data: break
|
||||||
break
|
|
||||||
_transcoder_bytes_out += len(data)
|
_transcoder_bytes_out += len(data)
|
||||||
|
|
||||||
# Store in pre-roll
|
|
||||||
with _mp3_lock:
|
with _mp3_lock:
|
||||||
_mp3_preroll.append(data)
|
_mp3_preroll.append(data)
|
||||||
clients = list(_mp3_clients)
|
clients = list(_mp3_clients)
|
||||||
|
|
@ -169,87 +167,180 @@ def _start_transcoder_if_needed(is_mp3_input=False):
|
||||||
for q in clients:
|
for q in clients:
|
||||||
try:
|
try:
|
||||||
q.put_nowait(data)
|
q.put_nowait(data)
|
||||||
except Exception:
|
except queue.Full:
|
||||||
|
# Client is too slow, skip this chunk for them
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Transcoder reader error: {e}")
|
print(f"WARNING: Transcoder reader error: {e}")
|
||||||
_transcoder_last_error = f'stdout read failed: {e}'
|
_transcoder_last_error = str(e)
|
||||||
break
|
break
|
||||||
_transcode_threads_started = False
|
|
||||||
print("🧵 Transcoder reader thread exiting")
|
|
||||||
|
|
||||||
if not _transcode_threads_started:
|
# Ensure process is killed if thread exits unexpectedly
|
||||||
_transcode_threads_started = True
|
if proc.poll() is None:
|
||||||
threading.Thread(target=_writer, daemon=True).start()
|
try: proc.terminate()
|
||||||
threading.Thread(target=_reader, daemon=True).start()
|
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():
|
def _stop_transcoder():
|
||||||
global _ffmpeg_proc, _transcode_threads_started
|
global _ffmpeg_proc
|
||||||
try:
|
print("STOPPING: Transcoder process")
|
||||||
_ffmpeg_in_q.put_nowait(None)
|
|
||||||
except Exception:
|
# Signal threads to stop via the queue
|
||||||
pass
|
try: _ffmpeg_in_q.put_nowait(None)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# Shutdown the process
|
||||||
proc = _ffmpeg_proc
|
proc = _ffmpeg_proc
|
||||||
_ffmpeg_proc = None
|
_ffmpeg_proc = None
|
||||||
|
|
||||||
# Reset thread flag so they can be re-launched if needed
|
if proc:
|
||||||
# (The existing threads will exit cleanly on None/EOF)
|
try:
|
||||||
_transcode_threads_started = False
|
proc.terminate()
|
||||||
_mp3_preroll.clear()
|
# Drain stdout/stderr to satisfy OS buffers
|
||||||
|
proc.communicate(timeout=1.0)
|
||||||
if proc is None:
|
except:
|
||||||
return
|
try: proc.kill()
|
||||||
|
except: pass
|
||||||
# Signal all listening clients to finish their stream
|
|
||||||
|
# Clear client state
|
||||||
with _mp3_lock:
|
with _mp3_lock:
|
||||||
clients = list(_mp3_clients)
|
clients = list(_mp3_clients)
|
||||||
for q in clients:
|
for q in clients:
|
||||||
try:
|
try: q.put_nowait(None)
|
||||||
q.put_nowait(None)
|
except: pass
|
||||||
except:
|
|
||||||
pass
|
|
||||||
_mp3_clients.clear()
|
_mp3_clients.clear()
|
||||||
|
_mp3_preroll.clear()
|
||||||
try:
|
|
||||||
proc.terminate()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _feed_transcoder(data: bytes):
|
def _feed_transcoder(data: bytes):
|
||||||
global _last_audio_chunk_ts
|
global _last_audio_chunk_ts
|
||||||
if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None:
|
if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None:
|
||||||
return
|
# 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()
|
_last_audio_chunk_ts = time.time()
|
||||||
try:
|
try:
|
||||||
_ffmpeg_in_q.put_nowait(data)
|
_ffmpeg_in_q.put_nowait(data)
|
||||||
except Exception:
|
except queue.Full:
|
||||||
# Queue full; drop to keep latency bounded.
|
# Drop chunk if overflow to prevent memory bloat
|
||||||
pass
|
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
|
# Ensure music folder exists
|
||||||
if not os.path.exists(MUSIC_FOLDER):
|
if not os.path.exists(MUSIC_FOLDER):
|
||||||
os.makedirs(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)
|
||||||
|
|
||||||
# Helper for shared routes
|
# Helper for shared routes
|
||||||
def setup_shared_routes(app):
|
def setup_shared_routes(app):
|
||||||
@app.route('/library.json')
|
@app.route('/library.json')
|
||||||
def get_library():
|
def get_library():
|
||||||
library = []
|
library = []
|
||||||
|
global MUSIC_FOLDER
|
||||||
if os.path.exists(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.
|
||||||
if filename.lower().endswith(('.mp3', '.m4a', '.wav', '.flac', '.ogg')):
|
# The prompt says "choose which folder", so maybe top-level of that folder is fine.
|
||||||
library.append({
|
for root, dirs, files in os.walk(MUSIC_FOLDER):
|
||||||
"title": os.path.splitext(filename)[0],
|
for filename in sorted(files):
|
||||||
"file": f"music/{filename}"
|
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_proxy/{rel_path}"
|
||||||
|
})
|
||||||
|
break # Only top level for now to keep it simple, or remove break for recursive
|
||||||
return jsonify(library)
|
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>')
|
@app.route('/<path:filename>')
|
||||||
def serve_static(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)
|
response = send_from_directory('.', filename)
|
||||||
if filename.endswith(('.css', '.js', '.html')):
|
if filename.endswith(('.css', '.js', '.html')):
|
||||||
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
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 == '':
|
if file.filename == '':
|
||||||
return jsonify({"success": False, "error": "No file selected"}), 400
|
return jsonify({"success": False, "error": "No file selected"}), 400
|
||||||
|
|
||||||
if not file.filename.endswith('.mp3'):
|
allowed_exts = ('.mp3', '.m4a', '.wav', '.flac', '.ogg')
|
||||||
return jsonify({"success": False, "error": "Only MP3 files are allowed"}), 400
|
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)
|
# Sanitize filename (keep extension)
|
||||||
import re
|
import re
|
||||||
name_without_ext = os.path.splitext(file.filename)[0]
|
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'[^\w\s-]', '', name_without_ext)
|
||||||
name_without_ext = re.sub(r'[-\s]+', '-', name_without_ext)
|
name_without_ext = re.sub(r'\s+', ' ', name_without_ext).strip()
|
||||||
filename = f"{name_without_ext}.mp3"
|
filename = f"{name_without_ext}{ext}"
|
||||||
|
|
||||||
filepath = os.path.join(MUSIC_FOLDER, filename)
|
filepath = os.path.join(MUSIC_FOLDER, filename)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file.save(filepath)
|
file.save(filepath)
|
||||||
print(f"✅ Uploaded: {filename}")
|
print(f"UPLOADED: {filename}")
|
||||||
return jsonify({"success": True, "filename": filename})
|
return jsonify({"success": True, "filename": filename})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Upload error: {e}")
|
print(f"ERROR: Upload error: {e}")
|
||||||
return jsonify({"success": False, "error": str(e)}), 500
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/save_keymaps', methods=['POST'])
|
@app.route('/save_keymaps', methods=['POST'])
|
||||||
|
|
@ -295,10 +388,10 @@ def setup_shared_routes(app):
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
with open('keymaps.json', 'w', encoding='utf-8') as f:
|
with open('keymaps.json', 'w', encoding='utf-8') as f:
|
||||||
json.dump(data, f, indent=4)
|
json.dump(data, f, indent=4)
|
||||||
print("💾 Keymaps saved to keymaps.json")
|
print("SAVED: Keymaps saved to keymaps.json")
|
||||||
return jsonify({"success": True})
|
return jsonify({"success": True})
|
||||||
except Exception as e:
|
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
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/load_keymaps', methods=['GET'])
|
@app.route('/load_keymaps', methods=['GET'])
|
||||||
|
|
@ -311,7 +404,7 @@ def setup_shared_routes(app):
|
||||||
else:
|
else:
|
||||||
return jsonify({"success": True, "keymaps": None})
|
return jsonify({"success": True, "keymaps": None})
|
||||||
except Exception as e:
|
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
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/stream.mp3')
|
@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:
|
if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None:
|
||||||
return jsonify({"success": False, "error": "MP3 stream not available"}), 503
|
return jsonify({"success": False, "error": "MP3 stream not available"}), 503
|
||||||
|
|
||||||
print(f"👂 New listener joined stream (Bursting {_mp3_preroll.maxlen} frames)")
|
print(f"LISTENER: New listener joined stream (Bursting {_mp3_preroll.maxlen} frames)")
|
||||||
client_q: queue.Queue = queue.Queue(maxsize=100)
|
client_q: queue.Queue = queue.Queue(maxsize=500)
|
||||||
with _mp3_lock:
|
with _mp3_lock:
|
||||||
# Burst pre-roll to new client so they start playing instantly
|
# Burst pre-roll to new client so they start playing instantly
|
||||||
for chunk in _mp3_preroll:
|
for chunk in _mp3_preroll:
|
||||||
|
|
@ -335,13 +428,20 @@ def setup_shared_routes(app):
|
||||||
def gen():
|
def gen():
|
||||||
try:
|
try:
|
||||||
while True:
|
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:
|
if chunk is None:
|
||||||
break
|
break
|
||||||
yield chunk
|
yield chunk
|
||||||
finally:
|
finally:
|
||||||
with _mp3_lock:
|
with _mp3_lock:
|
||||||
_mp3_clients.discard(client_q)
|
_mp3_clients.discard(client_q)
|
||||||
|
print(f"LISTENER: Listener disconnected from stream")
|
||||||
|
|
||||||
return Response(stream_with_context(gen()), content_type='audio/mpeg', headers={
|
return Response(stream_with_context(gen()), content_type='audio/mpeg', headers={
|
||||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
|
|
@ -482,15 +582,15 @@ dj_socketio = SocketIO(
|
||||||
@dj_socketio.on('connect')
|
@dj_socketio.on('connect')
|
||||||
def dj_connect():
|
def dj_connect():
|
||||||
if DJ_AUTH_ENABLED and session.get('dj_authed') is not True:
|
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
|
return False
|
||||||
print(f"🎧 DJ connected: {request.sid}")
|
print(f"STREAMPANEL: DJ connected: {request.sid}")
|
||||||
dj_sids.add(request.sid)
|
dj_sids.add(request.sid)
|
||||||
|
|
||||||
@dj_socketio.on('disconnect')
|
@dj_socketio.on('disconnect')
|
||||||
def dj_disconnect():
|
def dj_disconnect():
|
||||||
dj_sids.discard(request.sid)
|
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():
|
def stop_broadcast_after_timeout():
|
||||||
"""No longer used - broadcasts don't auto-stop"""
|
"""No longer used - broadcasts don't auto-stop"""
|
||||||
|
|
@ -500,19 +600,21 @@ def stop_broadcast_after_timeout():
|
||||||
def dj_start(data=None):
|
def dj_start(data=None):
|
||||||
broadcast_state['active'] = True
|
broadcast_state['active'] = True
|
||||||
session['is_dj'] = True
|
session['is_dj'] = True
|
||||||
print("🎙️ Broadcast -> ACTIVE")
|
print("BROADCAST: ACTIVE")
|
||||||
|
|
||||||
is_mp3_input = False
|
is_mp3_input = False
|
||||||
if data:
|
if data:
|
||||||
if 'bitrate' in data:
|
if 'bitrate' in data:
|
||||||
global _current_bitrate
|
global _current_bitrate
|
||||||
_current_bitrate = data['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':
|
if data.get('format') == 'mp3':
|
||||||
is_mp3_input = True
|
is_mp3_input = True
|
||||||
|
broadcast_state['is_mp3_input'] = is_mp3_input
|
||||||
|
|
||||||
# Clear pre-roll for fresh start
|
# Clear pre-roll for fresh start
|
||||||
_mp3_preroll.clear()
|
with _mp3_lock:
|
||||||
|
_mp3_preroll.clear()
|
||||||
|
|
||||||
_start_transcoder_if_needed(is_mp3_input=is_mp3_input)
|
_start_transcoder_if_needed(is_mp3_input=is_mp3_input)
|
||||||
|
|
||||||
|
|
@ -527,16 +629,13 @@ def dj_get_listener_count():
|
||||||
def dj_stop():
|
def dj_stop():
|
||||||
broadcast_state['active'] = False
|
broadcast_state['active'] = False
|
||||||
session['is_dj'] = False
|
session['is_dj'] = False
|
||||||
print("🛑 DJ stopped broadcasting")
|
print("STOPPED: DJ stopped broadcasting")
|
||||||
|
|
||||||
_stop_transcoder()
|
_stop_transcoder()
|
||||||
|
|
||||||
listener_socketio.emit('broadcast_stopped', namespace='/')
|
listener_socketio.emit('broadcast_stopped', namespace='/')
|
||||||
listener_socketio.emit('stream_status', {'active': False}, 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')
|
@dj_socketio.on('audio_chunk')
|
||||||
def dj_audio(data):
|
def dj_audio(data):
|
||||||
# MP3-only mode: do not relay raw chunks to listeners; feed transcoder only.
|
# 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 = Flask(__name__, static_folder='.', static_url_path='')
|
||||||
listener_app.config['SECRET_KEY'] = 'listener_secret'
|
listener_app.config['SECRET_KEY'] = 'listener_secret'
|
||||||
setup_shared_routes(listener_app)
|
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_socketio = SocketIO(
|
||||||
listener_app,
|
listener_app,
|
||||||
cors_allowed_origins="*",
|
cors_allowed_origins="*",
|
||||||
|
|
@ -567,13 +675,13 @@ listener_socketio = SocketIO(
|
||||||
|
|
||||||
@listener_socketio.on('connect')
|
@listener_socketio.on('connect')
|
||||||
def listener_connect():
|
def listener_connect():
|
||||||
print(f"👂 Listener Socket Connected: {request.sid}")
|
print(f"LISTENER: Listener Socket Connected: {request.sid}")
|
||||||
|
|
||||||
@listener_socketio.on('disconnect')
|
@listener_socketio.on('disconnect')
|
||||||
def listener_disconnect():
|
def listener_disconnect():
|
||||||
listener_sids.discard(request.sid)
|
listener_sids.discard(request.sid)
|
||||||
count = len(listener_sids)
|
count = len(listener_sids)
|
||||||
print(f"❌ Listener left. Total: {count}")
|
print(f"REMOVED: Listener left. Total: {count}")
|
||||||
# Notify BOTH namespaces
|
# Notify BOTH namespaces
|
||||||
listener_socketio.emit('listener_count', {'count': count}, namespace='/')
|
listener_socketio.emit('listener_count', {'count': count}, namespace='/')
|
||||||
dj_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:
|
if request.sid not in listener_sids:
|
||||||
listener_sids.add(request.sid)
|
listener_sids.add(request.sid)
|
||||||
count = len(listener_sids)
|
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='/')
|
listener_socketio.emit('listener_count', {'count': count}, namespace='/')
|
||||||
dj_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)})
|
emit('listener_count', {'count': len(listener_sids)})
|
||||||
|
|
||||||
# DJ Panel Routes (No engine commands needed in local mode)
|
# DJ Panel Routes (No engine commands needed in local mode)
|
||||||
@dj_socketio.on('get_mixer_status')
|
def _transcoder_watchdog():
|
||||||
def get_mixer_status():
|
"""Periodic check to ensure the transcoder stays alive during active broadcasts."""
|
||||||
pass
|
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():
|
def _listener_count_sync_loop():
|
||||||
"""Periodic background sync to ensure listener count is always accurate."""
|
"""Periodic background sync to ensure listener count is always accurate."""
|
||||||
|
|
@ -613,20 +724,21 @@ def _listener_count_sync_loop():
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print("🎧 TECHDJ PRO - DUAL PORT ARCHITECTURE")
|
print("TECHDJ PRO - DUAL PORT ARCHITECTURE")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
# Ports from environment or defaults
|
# Ports from environment or defaults
|
||||||
dj_port = int(os.environ.get('DJ_PORT', 5000))
|
dj_port = int(os.environ.get('DJ_PORT', 5000))
|
||||||
listen_port = int(os.environ.get('LISTEN_PORT', 5001))
|
listen_port = int(os.environ.get('LISTEN_PORT', 5001))
|
||||||
|
|
||||||
print(f"👉 DJ PANEL API: http://0.0.0.0:{dj_port}")
|
print(f"URL: DJ PANEL API: http://0.0.0.0:{dj_port}")
|
||||||
print(f"👉 LISTEN PAGE: http://0.0.0.0:{listen_port}")
|
print(f"URL: LISTEN PAGE: http://0.0.0.0:{listen_port}")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
# Audio engine DISABLED
|
# 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
|
# Run both servers using eventlet's spawn
|
||||||
eventlet.spawn(_listener_count_sync_loop)
|
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)
|
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)
|
listener_socketio.run(listener_app, host='0.0.0.0', port=listen_port, debug=False)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
4596
techdj_qt.py
4596
techdj_qt.py
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue