#!/usr/bin/env python3 """ TechDJ - PyQt5 Native DJ Application Pixel-perfect replica of the web DJ panel with neon aesthetic """ import sys import os import json import requests import numpy as np import sounddevice as sd import soundfile as sf from pathlib import Path from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QSlider, QListWidget, QListWidgetItem, QLineEdit, QFrame, QSplitter, QProgressBar, QMessageBox, QDialog, QGridLayout, QCheckBox, QComboBox, QFileDialog) from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, QRectF, QPropertyAnimation, QEasingCurve from PyQt5.QtGui import (QPainter, QColor, QPen, QFont, QLinearGradient, QRadialGradient, QBrush, QPainterPath, QFontDatabase) import socketio import queue import subprocess import time import threading # Color constants matching web panel BG_DARK = QColor(10, 10, 18) PANEL_BG = QColor(20, 20, 30, 204) # 0.8 alpha PRIMARY_CYAN = QColor(0, 243, 255) SECONDARY_MAGENTA = QColor(188, 19, 254) TEXT_MAIN = QColor(224, 224, 224) TEXT_DIM = QColor(136, 136, 136) class AudioEngine: """Efficient local audio processing engine""" def __init__(self): self.decks = { 'A': { 'audio_data': None, 'sample_rate': 44100, 'position': 0, 'playing': False, 'volume': 0.8, 'speed': 1.0, 'eq': {'low': 0, 'mid': 0, 'high': 0}, 'filters': {'lowpass': 100, 'highpass': 0}, 'duration': 0, 'filename': None, 'cues': {}, 'loop_start': None, 'loop_end': None, 'loop_active': False }, 'B': { 'audio_data': None, 'sample_rate': 44100, 'position': 0, 'playing': False, 'volume': 0.8, 'speed': 1.0, 'eq': {'low': 0, 'mid': 0, 'high': 0}, 'filters': {'lowpass': 100, 'highpass': 0}, 'duration': 0, 'filename': None, 'cues': {}, 'loop_start': None, 'loop_end': None, 'loop_active': False } } self.crossfader = 0.5 self.master_volume = 0.8 self.stream = None self.running = False self.broadcast_queue = queue.Queue(maxsize=100) self.is_broadcasting = False self.lock = threading.Lock() # Pre-allocate reuse buffers for the audio thread self._target_indices = np.arange(2048, dtype=np.float32) # Matches blocksize def start_stream(self): if self.stream is not None: return self.running = True self.stream = sd.OutputStream( channels=2, samplerate=44100, blocksize=2048, callback=self._audio_callback ) self.stream.start() print("🎵 Audio stream started") def stop_stream(self): self.running = False if self.stream: self.stream.stop() self.stream.close() self.stream = None def _audio_callback(self, outdata, frames, time_info, status): output = np.zeros((frames, 2), dtype=np.float32) output_samplerate = 44100 with self.lock: for deck_id in ['A', 'B']: deck = self.decks[deck_id] if not deck['playing'] or deck['audio_data'] is None: continue # Calculate source indices via linear interpolation rate_ratio = deck['sample_rate'] / output_samplerate step = rate_ratio * deck['speed'] # Start and end in source domain src_start = deck['position'] num_src_samples_needed = frames * step src_end = src_start + num_src_samples_needed # Bounds check if src_start >= len(deck['audio_data']) - 1: deck['playing'] = False continue # Prepare source data # Ensure we don't read past the end read_end = int(np.ceil(src_end)) + 1 if read_end > len(deck['audio_data']): read_end = len(deck['audio_data']) src_chunk = deck['audio_data'][int(src_start):read_end] if len(src_chunk) < 2: deck['playing'] = False continue if src_chunk.ndim == 1: src_chunk = np.column_stack((src_chunk, src_chunk)) # Time indices for interpolation if len(self._target_indices) != frames: self._target_indices = np.arange(frames, dtype=np.float32) x_target = self._target_indices * step x_source = np.arange(len(src_chunk)) # Interp each channel try: resampled_l = np.interp(x_target, x_source, src_chunk[:, 0]) resampled_r = np.interp(x_target, x_source, src_chunk[:, 1]) chunk = np.column_stack((resampled_l, resampled_r)) # Apply processing chunk = chunk * deck['volume'] if deck_id == 'A': chunk = chunk * (1.0 - self.crossfader) else: chunk = chunk * self.crossfader output += chunk # Update position deck['position'] += num_src_samples_needed except Exception as e: print(f"Audio thread error in interp: {e}") deck['playing'] = False continue # Handle looping if deck['loop_active'] and deck['loop_start'] is not None and deck['loop_end'] is not None: loop_start_frame = deck['loop_start'] * deck['sample_rate'] loop_end_frame = deck['loop_end'] * deck['sample_rate'] if deck['position'] >= loop_end_frame: deck['position'] = loop_start_frame + (deck['position'] - loop_end_frame) # Auto-stop at end if deck['position'] >= len(deck['audio_data']): deck['playing'] = False output = output * self.master_volume outdata[:] = output # Capture for broadcast if self.is_broadcasting: try: self.broadcast_queue.put_nowait(output.tobytes()) except queue.Full: pass def load_track(self, deck_id, filepath): try: audio_data, sample_rate = sf.read(filepath, dtype='float32') with self.lock: self.decks[deck_id]['audio_data'] = audio_data self.decks[deck_id]['sample_rate'] = sample_rate self.decks[deck_id]['position'] = 0 self.decks[deck_id]['duration'] = len(audio_data) / sample_rate self.decks[deck_id]['filename'] = os.path.basename(filepath) print(f"✅ Loaded {os.path.basename(filepath)} to Deck {deck_id}") return True except Exception as e: print(f"❌ Error loading {filepath}: {e}") return False def play(self, deck_id): with self.lock: if self.decks[deck_id]['audio_data'] is not None: self.decks[deck_id]['playing'] = True def pause(self, deck_id): with self.lock: self.decks[deck_id]['playing'] = False def seek(self, deck_id, position_seconds): with self.lock: deck = self.decks[deck_id] if deck['audio_data'] is not None: deck['position'] = int(position_seconds * deck['sample_rate']) def set_volume(self, deck_id, volume): with self.lock: self.decks[deck_id]['volume'] = max(0.0, min(1.0, volume)) def set_speed(self, deck_id, speed): with self.lock: self.decks[deck_id]['speed'] = max(0.5, min(1.5, speed)) def set_crossfader(self, value): with self.lock: self.crossfader = max(0.0, min(1.0, value)) def get_position(self, deck_id): with self.lock: deck = self.decks[deck_id] if deck['audio_data'] is not None: return deck['position'] / deck['sample_rate'] return 0.0 def set_cue(self, deck_id, cue_num): position = self.get_position(deck_id) with self.lock: self.decks[deck_id]['cues'][cue_num] = position def jump_to_cue(self, deck_id, cue_num): with self.lock: if cue_num in self.decks[deck_id]['cues']: position = self.decks[deck_id]['cues'][cue_num] self.seek(deck_id, position) def set_eq(self, deck_id, band, value): with self.lock: self.decks[deck_id]['eq'][band] = value def set_filter(self, deck_id, filter_type, value): with self.lock: self.decks[deck_id]['filters'][filter_type] = value class DownloadThread(QThread): progress = pyqtSignal(int) finished = pyqtSignal(str, bool) def __init__(self, url, filepath): super().__init__() self.url = url self.filepath = filepath def run(self): try: response = requests.get(self.url, stream=True) total_size = int(response.headers.get('content-length', 0)) os.makedirs(os.path.dirname(self.filepath), exist_ok=True) downloaded = 0 with open(self.filepath, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded += len(chunk) if total_size > 0: progress = int((downloaded / total_size) * 100) self.progress.emit(progress) self.finished.emit(self.filepath, True) except Exception as e: print(f"Download error: {e}") self.finished.emit(self.filepath, False) class BroadcastThread(QThread): """Thread to handle FFmpeg encoding and streaming""" chunk_ready = pyqtSignal(bytes) error = pyqtSignal(str) def __init__(self, audio_queue, bitrate="192k"): super().__init__() self.audio_queue = audio_queue self.bitrate = bitrate self.running = False self.process = None def run(self): self.running = True # FFmpeg command to read raw f32le PCM and output MP3 chunks to stdout # Using CBR and zerolatency tune for stability cmd = [ 'ffmpeg', '-y', '-fflags', 'nobuffer', '-flags', 'low_delay', '-probesize', '32', '-analyzeduration', '0', '-f', 'f32le', '-ar', '44100', '-ac', '2', '-i', 'pipe:0', '-codec:a', 'libmp3lame', '-b:a', self.bitrate, '-maxrate', self.bitrate, '-minrate', self.bitrate, '-bufsize', '64k', '-tune', 'zerolatency', '-flush_packets', '1', '-f', 'mp3', 'pipe:1' ] try: self.process = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0 ) # Thread to read encoded chunks from stdout def read_output(): # Smaller buffer for more frequent updates (4KB = ~0.15s @ 192k) buffer_size = 4096 while self.running: try: data = self.process.stdout.read(buffer_size) if data: self.chunk_ready.emit(data) else: break except Exception as e: print(f"Broadcast output error: {e}") break output_thread = threading.Thread(target=read_output, daemon=True) output_thread.start() # Worker to feed stdin from the broadcast queue while self.running: try: # Clear queue if it's way too full, but be less aggressive # 100 chunks is ~4.6 seconds. If we hit 200, we're definitely lagging. if self.audio_queue.qsize() > 200: while self.audio_queue.qsize() > 50: self.audio_queue.get_nowait() chunk = self.audio_queue.get(timeout=0.1) if chunk and self.process and self.process.stdin: self.process.stdin.write(chunk) self.process.stdin.flush() except queue.Empty: continue except Exception as e: print(f"Broadcast input error: {e}") break print(f"📡 FFmpeg broadcast process started ({self.bitrate})") except Exception as e: self.error.emit(str(e)) self.running = False return def stop(self): self.running = False if self.process: self.process.terminate() try: self.process.wait(timeout=2) except: self.process.kill() self.process = None print("🛑 Broadcast process stopped") class WaveformWidget(QWidget): """Waveform display matching web panel style""" def __init__(self, deck_id, parent=None): super().__init__(parent) self.deck_id = deck_id self.waveform_data = [] self.position = 0.0 self.duration = 1.0 self.cues = {} self.setMinimumHeight(80) self.setStyleSheet("background: #000; border: 1px solid #333; border-radius: 4px;") def set_waveform(self, audio_data, sample_rate): if audio_data is None: self.waveform_data = [] return samples = 1000 if audio_data.ndim > 1: audio_data = np.mean(audio_data, axis=1) block_size = max(1, len(audio_data) // samples) self.waveform_data = [] for i in range(samples): start = i * block_size end = min(start + block_size, len(audio_data)) if start < len(audio_data): chunk = audio_data[start:end] self.waveform_data.append(np.max(np.abs(chunk))) self.update() def set_position(self, position, duration): self.position = position self.duration = max(duration, 0.01) self.update() def set_cues(self, cues): self.cues = cues self.update() def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) # Background painter.fillRect(self.rect(), QColor(0, 0, 0)) if not self.waveform_data: return # Draw waveform width = self.width() height = self.height() bar_width = width / len(self.waveform_data) wave_color = PRIMARY_CYAN if self.deck_id == 'A' else SECONDARY_MAGENTA painter.setPen(Qt.NoPen) painter.setBrush(wave_color) for i, amplitude in enumerate(self.waveform_data): x = i * bar_width bar_height = amplitude * height * 5 y = (height - bar_height) / 2 painter.drawRect(int(x), int(y), max(1, int(bar_width)), int(bar_height)) # Draw cue markers if self.duration > 0: painter.setPen(QPen(QColor(255, 255, 255), 1)) for cue_time in self.cues.values(): x = (cue_time / self.duration) * width painter.drawLine(int(x), 0, int(x), height) # Draw playhead if self.duration > 0: playhead_x = (self.position / self.duration) * width painter.setPen(QPen(QColor(255, 255, 0), 2)) painter.drawLine(int(playhead_x), 0, int(playhead_x), height) def mousePressEvent(self, event): """Allow seeking by clicking on waveform""" if self.duration > 0: percent = event.x() / self.width() seek_time = percent * self.duration self.parent().parent().seek_deck(seek_time) class VinylDiskWidget(QWidget): """Animated vinyl disk matching web panel""" clicked = pyqtSignal() def __init__(self, deck_id, parent=None): super().__init__(parent) self.deck_id = deck_id self.rotation = 0 self.playing = False self.setFixedSize(120, 120) # Rotation animation self.timer = QTimer() self.timer.timeout.connect(self.rotate) def set_playing(self, playing): self.playing = playing if playing: self.timer.start(50) # 20 FPS else: self.timer.stop() self.update() def rotate(self): self.rotation = (self.rotation + 5) % 360 self.update() def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) center_x = self.width() / 2 center_y = self.height() / 2 radius = min(center_x, center_y) - 5 # Rotate if playing if self.playing: painter.translate(center_x, center_y) painter.rotate(self.rotation) painter.translate(-center_x, -center_y) # Vinyl gradient gradient = QRadialGradient(center_x, center_y, radius) gradient.setColorAt(0, QColor(34, 34, 34)) gradient.setColorAt(0.1, QColor(17, 17, 17)) gradient.setColorAt(1, QColor(0, 0, 0)) painter.setBrush(gradient) painter.setPen(QPen(QColor(51, 51, 51), 2)) painter.drawEllipse(int(center_x - radius), int(center_y - radius), int(radius * 2), int(radius * 2)) # Grooves painter.setPen(QPen(QColor(24, 24, 24), 1)) for i in range(5, int(radius), 8): painter.drawEllipse(int(center_x - i), int(center_y - i), i * 2, i * 2) # Center label label_radius = 25 label_color = PRIMARY_CYAN if self.deck_id == 'A' else SECONDARY_MAGENTA painter.setBrush(label_color) painter.setPen(QPen(label_color.darker(120), 2)) painter.drawEllipse(int(center_x - label_radius), int(center_y - label_radius), label_radius * 2, label_radius * 2) # Label text painter.setPen(QColor(0, 0, 0)) font = QFont("Orbitron", 16, QFont.Bold) painter.setFont(font) painter.drawText(self.rect(), Qt.AlignCenter, self.deck_id) # Glow effect when playing if self.playing: painter.setPen(QPen(label_color, 3)) painter.setBrush(Qt.NoBrush) painter.drawEllipse(int(center_x - radius - 3), int(center_y - radius - 3), int((radius + 3) * 2), int((radius + 3) * 2)) def mousePressEvent(self, event): self.clicked.emit() class NeonButton(QPushButton): """Neon-styled button matching web panel""" def __init__(self, text, color=PRIMARY_CYAN, parent=None): super().__init__(text, parent) self.neon_color = color self.is_active = False self.update_style() def set_active(self, active): self.is_active = active self.update_style() def update_style(self): if self.is_active: self.setStyleSheet(f""" QPushButton {{ background: rgba({self.neon_color.red()}, {self.neon_color.green()}, {self.neon_color.blue()}, 0.3); border: 2px solid rgb({self.neon_color.red()}, {self.neon_color.green()}, {self.neon_color.blue()}); color: rgb({self.neon_color.red()}, {self.neon_color.green()}, {self.neon_color.blue()}); font-family: 'Orbitron'; font-weight: bold; padding: 8px; border-radius: 4px; }} QPushButton:hover {{ background: rgba({self.neon_color.red()}, {self.neon_color.green()}, {self.neon_color.blue()}, 0.5); }} """) else: self.setStyleSheet(f""" QPushButton {{ background: #222; border: 1px solid #444; color: #666; font-family: 'Orbitron'; font-weight: bold; padding: 8px; border-radius: 4px; }} QPushButton:hover {{ background: #333; color: #888; }} """) class DeckWidget(QWidget): """Complete deck widget matching web panel layout""" def __init__(self, deck_id, audio_engine, parent=None): super().__init__(parent) self.deck_id = deck_id self.audio_engine = audio_engine self.color = PRIMARY_CYAN if deck_id == 'A' else SECONDARY_MAGENTA self.init_ui() # Update timer self.timer = QTimer() self.timer.timeout.connect(self.update_display) self.timer.start(50) def init_ui(self): layout = QVBoxLayout() layout.setSpacing(8) layout.setContentsMargins(10, 10, 10, 10) # Header header = QHBoxLayout() title = QLabel(f"DECK {self.deck_id}") title.setStyleSheet(f""" font-family: 'Orbitron'; font-size: 16px; font-weight: bold; color: rgb({self.color.red()}, {self.color.green()}, {self.color.blue()}); """) header.addWidget(title) self.track_label = QLabel("NO TRACK LOADED") self.track_label.setStyleSheet("color: #888; font-size: 12px;") header.addWidget(self.track_label, 1) layout.addLayout(header) # Waveform waveform_container = QWidget() waveform_container.setStyleSheet("background: #000; border: 1px solid #333; border-radius: 4px; padding: 3px;") waveform_layout = QVBoxLayout(waveform_container) waveform_layout.setContentsMargins(0, 0, 0, 0) self.waveform = WaveformWidget(self.deck_id, self) waveform_layout.addWidget(self.waveform) time_layout = QHBoxLayout() self.time_label = QLabel("0:00 / 0:00") self.time_label.setStyleSheet(f""" color: rgb({self.color.red()}, {self.color.green()}, {self.color.blue()}); font-family: 'Orbitron'; font-size: 11px; """) time_layout.addWidget(self.time_label) time_layout.addStretch() self.bpm_label = QLabel("") self.bpm_label.setStyleSheet("color: #f0f; font-weight: bold; font-size: 11px;") time_layout.addWidget(self.bpm_label) waveform_layout.addLayout(time_layout) layout.addWidget(waveform_container) # Vinyl disk disk_container = QHBoxLayout() disk_container.addStretch() self.vinyl_disk = VinylDiskWidget(self.deck_id) self.vinyl_disk.clicked.connect(self.toggle_play) disk_container.addWidget(self.vinyl_disk) disk_container.addStretch() layout.addLayout(disk_container) # VU Meter canvas (placeholder) self.vu_canvas = QWidget() self.vu_canvas.setFixedHeight(60) self.vu_canvas.setStyleSheet("background: #000; border: 1px solid #333; border-radius: 4px;") layout.addWidget(self.vu_canvas) # Hot Cues cue_layout = QGridLayout() cue_layout.setSpacing(3) self.cue_buttons = [] for i in range(4): btn = NeonButton(f"CUE {i+1}", self.color) btn.clicked.connect(lambda checked, num=i+1: self.handle_cue(num)) cue_layout.addWidget(btn, 0, i) self.cue_buttons.append(btn) layout.addLayout(cue_layout) # Loop Controls loop_layout = QGridLayout() loop_layout.setSpacing(3) loop_in = NeonButton("LOOP IN", QColor(255, 102, 0)) loop_out = NeonButton("LOOP OUT", QColor(255, 102, 0)) loop_exit = NeonButton("EXIT", QColor(255, 102, 0)) loop_layout.addWidget(loop_in, 0, 0) loop_layout.addWidget(loop_out, 0, 1) loop_layout.addWidget(loop_exit, 0, 2) layout.addLayout(loop_layout) # Controls Grid controls = QGridLayout() controls.setSpacing(8) # Volume vol_label = QLabel("VOLUME") vol_label.setStyleSheet("color: #888; font-size: 10px;") controls.addWidget(vol_label, 0, 0) self.volume_slider = QSlider(Qt.Horizontal) self.volume_slider.setRange(0, 100) self.volume_slider.setValue(80) self.volume_slider.valueChanged.connect(self.on_volume_change) self.volume_slider.setStyleSheet(self.get_slider_style()) controls.addWidget(self.volume_slider, 1, 0) # EQ eq_widget = QWidget() eq_layout = QHBoxLayout(eq_widget) eq_layout.setSpacing(8) for band in ['HI', 'MID', 'LO']: band_widget = QWidget() band_layout = QVBoxLayout(band_widget) band_layout.setSpacing(2) band_layout.setContentsMargins(0, 0, 0, 0) slider = QSlider(Qt.Vertical) slider.setRange(-20, 20) slider.setValue(0) slider.setFixedHeight(80) slider.setStyleSheet(self.get_slider_style()) slider.valueChanged.connect(lambda v, b=band.lower(): self.on_eq_change(b, v)) label = QLabel(band) label.setStyleSheet("color: #888; font-size: 9px;") label.setAlignment(Qt.AlignCenter) band_layout.addWidget(slider) band_layout.addWidget(label) eq_layout.addWidget(band_widget) controls.addWidget(eq_widget, 0, 1, 2, 1) # Filters filter_widget = QWidget() filter_layout = QVBoxLayout(filter_widget) filter_layout.setSpacing(4) lp_label = QLabel("LOW-PASS") lp_label.setStyleSheet("color: #888; font-size: 9px;") filter_layout.addWidget(lp_label) self.lp_slider = QSlider(Qt.Horizontal) self.lp_slider.setRange(0, 100) self.lp_slider.setValue(100) self.lp_slider.setStyleSheet(self.get_slider_style()) filter_layout.addWidget(self.lp_slider) hp_label = QLabel("HIGH-PASS") hp_label.setStyleSheet("color: #888; font-size: 9px;") filter_layout.addWidget(hp_label) self.hp_slider = QSlider(Qt.Horizontal) self.hp_slider.setRange(0, 100) self.hp_slider.setValue(0) self.hp_slider.setStyleSheet(self.get_slider_style()) filter_layout.addWidget(self.hp_slider) controls.addWidget(filter_widget, 0, 2, 2, 1) # Speed speed_widget = QWidget() speed_layout = QVBoxLayout(speed_widget) speed_layout.setSpacing(4) speed_label = QLabel("PITCH / TEMPO") speed_label.setStyleSheet("color: #888; font-size: 9px;") speed_layout.addWidget(speed_label) self.speed_slider = QSlider(Qt.Horizontal) self.speed_slider.setRange(50, 150) self.speed_slider.setValue(100) self.speed_slider.valueChanged.connect(self.on_speed_change) self.speed_slider.setStyleSheet(self.get_slider_style()) speed_layout.addWidget(self.speed_slider) bend_layout = QHBoxLayout() bend_minus = QPushButton("-") bend_minus.setFixedSize(30, 25) bend_plus = QPushButton("+") bend_plus.setFixedSize(30, 25) bend_layout.addWidget(bend_minus) bend_layout.addWidget(bend_plus) speed_layout.addLayout(bend_layout) controls.addWidget(speed_widget, 0, 3, 2, 1) layout.addLayout(controls) # Transport transport = QHBoxLayout() transport.setSpacing(4) self.play_btn = NeonButton("▶ PLAY", self.color) self.play_btn.clicked.connect(self.play) transport.addWidget(self.play_btn) self.pause_btn = NeonButton("⏸ PAUSE") self.pause_btn.clicked.connect(self.pause) transport.addWidget(self.pause_btn) sync_btn = NeonButton("SYNC", self.color) transport.addWidget(sync_btn) reset_btn = NeonButton("🔄 RESET") transport.addWidget(reset_btn) layout.addLayout(transport) self.setLayout(layout) # Deck styling self.setStyleSheet(f""" QWidget {{ background: rgba(20, 20, 30, 0.8); color: #e0e0e0; font-family: 'Rajdhani'; }} QWidget#deck {{ border: 2px solid rgb({self.color.red()}, {self.color.green()}, {self.color.blue()}); border-radius: 8px; }} """) self.setObjectName("deck") def get_slider_style(self): return """ QSlider::groove:horizontal { height: 8px; background: #333; border-radius: 4px; } QSlider::handle:horizontal { background: #ccc; border: 2px solid #888; width: 16px; margin: -4px 0; border-radius: 8px; } QSlider::groove:vertical { width: 8px; background: #333; border-radius: 4px; } QSlider::handle:vertical { background: #ccc; border: 2px solid #888; height: 16px; margin: 0 -4px; border-radius: 8px; } """ def load_track(self, filepath): if self.audio_engine.load_track(self.deck_id, filepath): filename = os.path.basename(filepath) self.track_label.setText(filename) deck = self.audio_engine.decks[self.deck_id] self.waveform.set_waveform(deck['audio_data'], deck['sample_rate']) def play(self): self.audio_engine.play(self.deck_id) self.vinyl_disk.set_playing(True) self.play_btn.set_active(True) def pause(self): self.audio_engine.pause(self.deck_id) self.vinyl_disk.set_playing(False) self.play_btn.set_active(False) def toggle_play(self): if self.audio_engine.decks[self.deck_id]['playing']: self.pause() else: self.play() def on_volume_change(self, value): self.audio_engine.set_volume(self.deck_id, value / 100.0) def on_speed_change(self, value): self.audio_engine.set_speed(self.deck_id, value / 100.0) def on_eq_change(self, band, value): self.audio_engine.set_eq(self.deck_id, band, value) def handle_cue(self, cue_num): deck = self.audio_engine.decks[self.deck_id] if cue_num in deck['cues']: self.audio_engine.jump_to_cue(self.deck_id, cue_num) else: self.audio_engine.set_cue(self.deck_id, cue_num) self.cue_buttons[cue_num-1].set_active(True) def seek_deck(self, time): self.audio_engine.seek(self.deck_id, time) def update_display(self): deck = self.audio_engine.decks[self.deck_id] position = self.audio_engine.get_position(self.deck_id) duration = deck['duration'] pos_min = int(position // 60) pos_sec = int(position % 60) dur_min = int(duration // 60) dur_sec = int(duration % 60) self.time_label.setText(f"{pos_min}:{pos_sec:02d} / {dur_min}:{dur_sec:02d}") self.waveform.set_position(position, duration) self.waveform.set_cues(deck['cues']) class TechDJMainWindow(QMainWindow): """Main window matching web panel layout""" def __init__(self): super().__init__() self.server_url = "http://54.37.246.24:5000" self.cache_dir = Path.home() / ".techdj_cache" self.cache_dir.mkdir(exist_ok=True) self.audio_engine = AudioEngine() self.library = [] self.download_threads = {} self.broadcasting = False self.broadcast_thread = None self.listener_count = 0 self.glow_enabled = {'A': False, 'B': False} self.glow_intensity = 30 self.deck_loading_target = {'A': None, 'B': None} # Socket.IO for broadcasting self.socket = None # Library settings self.library_mode = 'server' # 'server' or 'local' self.server_library = [] self.local_library = [] self.local_folder = None self.load_settings() self.init_ui() self.audio_engine.start_stream() self.fetch_library() def init_ui(self): self.setWindowTitle("TechDJ Pro - Native Edition") self.setGeometry(50, 50, 1600, 900) # Central widget with overlay support central = QWidget() self.setCentralWidget(central) # Main grid layout matching web panel main_layout = QHBoxLayout() main_layout.setSpacing(10) main_layout.setContentsMargins(10, 10, 10, 10) # Left: Library (320px) library_widget = QWidget() library_widget.setFixedWidth(320) library_widget.setStyleSheet(f""" QWidget {{ background: rgba(20, 20, 30, 0.8); border: 2px solid rgb({PRIMARY_CYAN.red()}, {PRIMARY_CYAN.green()}, {PRIMARY_CYAN.blue()}); border-radius: 10px; }} """) library_layout = QVBoxLayout(library_widget) library_layout.setSpacing(10) library_layout.setContentsMargins(15, 15, 15, 15) lib_header = QLabel("📁 LIBRARY") lib_header.setStyleSheet(f""" font-family: 'Orbitron'; font-size: 16px; font-weight: bold; color: rgb({PRIMARY_CYAN.red()}, {PRIMARY_CYAN.green()}, {PRIMARY_CYAN.blue()}); border: none; """) library_layout.addWidget(lib_header) # Library Mode Switch mode_switch_layout = QHBoxLayout() self.server_mode_btn = NeonButton("SERVER", PRIMARY_CYAN) self.server_mode_btn.set_active(True) self.server_mode_btn.clicked.connect(lambda: self.set_library_mode('server')) self.local_mode_btn = NeonButton("LOCAL", TEXT_DIM) self.local_mode_btn.clicked.connect(lambda: self.set_library_mode('local')) mode_switch_layout.addWidget(self.server_mode_btn) mode_switch_layout.addWidget(self.local_mode_btn) library_layout.addLayout(mode_switch_layout) # Local Folder Selection (hidden by default) self.local_folder_widget = QWidget() local_folder_layout = QHBoxLayout(self.local_folder_widget) local_folder_layout.setContentsMargins(0, 0, 0, 0) self.folder_label = QLabel("NO FOLDER...") self.folder_label.setStyleSheet("color: #888; font-size: 10px;") select_folder_btn = QPushButton("📁") select_folder_btn.setFixedSize(30, 30) select_folder_btn.setStyleSheet("background: #333; border-radius: 4px; color: white;") select_folder_btn.clicked.connect(self.select_local_folder) local_folder_layout.addWidget(self.folder_label, 1) local_folder_layout.addWidget(select_folder_btn) self.local_folder_widget.hide() library_layout.addWidget(self.local_folder_widget) self.search_box = QLineEdit() self.search_box.setPlaceholderText("🔍 FILTER LIBRARY...") self.search_box.textChanged.connect(self.filter_library) self.search_box.setStyleSheet(""" QLineEdit { background: rgba(0, 0, 0, 0.3); border: 1px solid #333; color: white; padding: 10px; border-radius: 4px; font-family: 'Rajdhani'; } """) library_layout.addWidget(self.search_box) self.library_list = QListWidget() self.library_list.setStyleSheet(""" QListWidget { background: rgba(0, 0, 0, 0.3); border: none; color: white; } QListWidget::item { background: rgba(255, 255, 255, 0.03); margin-bottom: 8px; padding: 10px; border-radius: 4px; border-left: 3px solid transparent; } QListWidget::item:hover { background: rgba(255, 255, 255, 0.08); border-left: 3px solid #00f3ff; } """) self.library_list.itemDoubleClicked.connect(self.on_library_double_click) library_layout.addWidget(self.library_list) refresh_btn = QPushButton("🔄 Refresh Library") refresh_btn.clicked.connect(self.fetch_library) refresh_btn.setStyleSheet(f""" QPushButton {{ background: rgba(0, 243, 255, 0.1); border: 1px solid rgb({PRIMARY_CYAN.red()}, {PRIMARY_CYAN.green()}, {PRIMARY_CYAN.blue()}); color: rgb({PRIMARY_CYAN.red()}, {PRIMARY_CYAN.green()}, {PRIMARY_CYAN.blue()}); padding: 8px 12px; border-radius: 4px; font-family: 'Orbitron'; font-weight: bold; }} QPushButton:hover {{ background: rgba(0, 243, 255, 0.2); }} """) library_layout.addWidget(refresh_btn) main_layout.addWidget(library_widget) # Right: Decks + Crossfader decks_widget = QWidget() decks_layout = QVBoxLayout(decks_widget) decks_layout.setSpacing(10) decks_layout.setContentsMargins(0, 0, 0, 0) # Decks grid decks_grid = QHBoxLayout() decks_grid.setSpacing(10) self.deck_a = DeckWidget('A', self.audio_engine) decks_grid.addWidget(self.deck_a) self.deck_b = DeckWidget('B', self.audio_engine) decks_grid.addWidget(self.deck_b) decks_layout.addLayout(decks_grid) # Crossfader xfader_widget = QWidget() xfader_widget.setFixedHeight(80) xfader_widget.setStyleSheet(""" QWidget { background: qlineargradient(x1:0, y1:0, x1:0, y1:1, stop:0 #1a1a1a, stop:1 #0a0a0a); border: 2px solid #444; border-radius: 8px; } """) xfader_layout = QHBoxLayout(xfader_widget) xfader_layout.setContentsMargins(40, 15, 40, 15) label_a = QLabel("A") label_a.setStyleSheet(f""" font-family: 'Orbitron'; font-size: 24px; font-weight: bold; color: rgb({PRIMARY_CYAN.red()}, {PRIMARY_CYAN.green()}, {PRIMARY_CYAN.blue()}); """) xfader_layout.addWidget(label_a) self.crossfader = QSlider(Qt.Horizontal) self.crossfader.setRange(0, 100) self.crossfader.setValue(50) self.crossfader.valueChanged.connect(self.on_crossfader_change) self.crossfader.setStyleSheet(""" QSlider::groove:horizontal { height: 12px; background: qlineargradient(x1:0, y1:0, x1:1, y1:0, stop:0 #00f3ff, stop:0.5 #333, stop:1 #bc13fe); border-radius: 6px; border: 2px solid #555; } QSlider::handle:horizontal { background: qlineargradient(x1:0, y1:0, x1:0, y1:1, stop:0 #aaa, stop:1 #666); border: 3px solid #ccc; width: 60px; height: 40px; margin: -15px 0; border-radius: 8px; } QSlider::handle:horizontal:hover { background: qlineargradient(x1:0, y1:0, x1:0, y1:1, stop:0 #ccc, stop:1 #888); } """) xfader_layout.addWidget(self.crossfader) label_b = QLabel("B") label_b.setStyleSheet(f""" font-family: 'Orbitron'; font-size: 24px; font-weight: bold; color: rgb({SECONDARY_MAGENTA.red()}, {SECONDARY_MAGENTA.green()}, {SECONDARY_MAGENTA.blue()}); """) xfader_layout.addWidget(label_b) decks_layout.addWidget(xfader_widget) main_layout.addWidget(decks_widget, 1) central.setLayout(main_layout) # Floating action buttons (bottom right) self.create_floating_buttons() # Streaming panel (hidden by default) self.create_streaming_panel() # Settings panel (hidden by default) self.create_settings_panel() # Window styling self.setStyleSheet(f""" QMainWindow {{ background: rgb({BG_DARK.red()}, {BG_DARK.green()}, {BG_DARK.blue()}); }} QWidget {{ color: rgb({TEXT_MAIN.red()}, {TEXT_MAIN.green()}, {TEXT_MAIN.blue()}); font-family: 'Rajdhani', sans-serif; }} """) # Glow effect timer self.glow_timer = QTimer() self.glow_timer.timeout.connect(self.update_glow_effect) self.glow_timer.start(100) def create_floating_buttons(self): """Create floating action buttons in bottom-right corner""" button_style = """ QPushButton { background: rgba(188, 19, 254, 0.2); border: 2px solid #bc13fe; color: white; font-size: 20px; border-radius: 25px; padding: 10px; } QPushButton:hover { background: rgba(188, 19, 254, 0.4); } """ # Streaming button self.streaming_btn = QPushButton("📡", self) self.streaming_btn.setFixedSize(50, 50) self.streaming_btn.setStyleSheet(button_style) self.streaming_btn.clicked.connect(self.toggle_streaming_panel) self.streaming_btn.setToolTip("Live Streaming") self.streaming_btn.move(self.width() - 70, self.height() - 280) # Settings button self.settings_btn = QPushButton("⚙️", self) self.settings_btn.setFixedSize(50, 50) self.settings_btn.setStyleSheet(button_style) self.settings_btn.clicked.connect(self.toggle_settings_panel) self.settings_btn.setToolTip("Settings") self.settings_btn.move(self.width() - 70, self.height() - 220) # Upload button self.upload_btn = QPushButton("📁", self) self.upload_btn.setFixedSize(50, 50) self.upload_btn.setStyleSheet(button_style) self.upload_btn.clicked.connect(self.upload_file) self.upload_btn.setToolTip("Upload MP3") self.upload_btn.move(self.width() - 70, self.height() - 160) # Keyboard shortcuts button self.keyboard_btn = QPushButton("⌨️", self) self.keyboard_btn.setFixedSize(50, 50) self.keyboard_btn.setStyleSheet(button_style) self.keyboard_btn.setToolTip("Keyboard Shortcuts") self.keyboard_btn.move(self.width() - 70, self.height() - 100) def create_streaming_panel(self): """Create streaming panel matching web version""" self.streaming_panel = QWidget(self) self.streaming_panel.setFixedSize(400, 500) self.streaming_panel.setStyleSheet(f""" QWidget {{ background: rgba(20, 20, 30, 0.95); border: 2px solid rgb({PRIMARY_CYAN.red()}, {PRIMARY_CYAN.green()}, {PRIMARY_CYAN.blue()}); border-radius: 10px; }} """) self.streaming_panel.hide() layout = QVBoxLayout(self.streaming_panel) layout.setSpacing(15) # Header header = QHBoxLayout() title = QLabel("📡 LIVE STREAM") title.setStyleSheet(f""" font-family: 'Orbitron'; font-size: 16px; font-weight: bold; color: rgb({PRIMARY_CYAN.red()}, {PRIMARY_CYAN.green()}, {PRIMARY_CYAN.blue()}); """) header.addWidget(title) header.addStretch() close_btn = QPushButton("✕") close_btn.setFixedSize(30, 30) close_btn.clicked.connect(self.toggle_streaming_panel) close_btn.setStyleSheet(""" QPushButton { background: transparent; border: none; color: #888; font-size: 18px; } QPushButton:hover { color: white; } """) header.addWidget(close_btn) layout.addLayout(header) # Broadcast button self.broadcast_btn = QPushButton("🔴 START BROADCAST") self.broadcast_btn.setFixedHeight(60) self.broadcast_btn.clicked.connect(self.toggle_broadcast) self.broadcast_btn.setStyleSheet(f""" QPushButton {{ background: rgba(255, 0, 0, 0.2); border: 2px solid #ff0000; color: #ff0000; font-family: 'Orbitron'; font-size: 14px; font-weight: bold; border-radius: 8px; }} QPushButton:hover {{ background: rgba(255, 0, 0, 0.3); }} """) layout.addWidget(self.broadcast_btn) # Status self.broadcast_status = QLabel("Offline") self.broadcast_status.setAlignment(Qt.AlignCenter) self.broadcast_status.setStyleSheet("color: #888; font-size: 12px;") layout.addWidget(self.broadcast_status) # Listener count listener_widget = QWidget() listener_layout = QHBoxLayout(listener_widget) listener_layout.setContentsMargins(0, 0, 0, 0) listener_icon = QLabel("👂") listener_icon.setStyleSheet("font-size: 24px;") listener_layout.addWidget(listener_icon) self.listener_count_label = QLabel("0") self.listener_count_label.setStyleSheet(f""" font-family: 'Orbitron'; font-size: 32px; font-weight: bold; color: rgb({PRIMARY_CYAN.red()}, {PRIMARY_CYAN.green()}, {PRIMARY_CYAN.blue()}); """) listener_layout.addWidget(self.listener_count_label) listener_text = QLabel("Listeners") listener_text.setStyleSheet("color: #888; font-size: 14px;") listener_layout.addWidget(listener_text) listener_layout.addStretch() layout.addWidget(listener_widget) # Stream URL url_label = QLabel("Share this URL:") url_label.setStyleSheet("color: #888; font-size: 12px;") layout.addWidget(url_label) url_widget = QWidget() url_layout = QHBoxLayout(url_widget) url_layout.setContentsMargins(0, 0, 0, 0) url_layout.setSpacing(5) self.stream_url = QLineEdit("http://localhost:5001") self.stream_url.setReadOnly(True) self.stream_url.setStyleSheet(""" QLineEdit { background: rgba(0, 0, 0, 0.3); border: 1px solid #333; color: white; padding: 8px; border-radius: 4px; } """) url_layout.addWidget(self.stream_url) copy_btn = QPushButton("📋") copy_btn.setFixedSize(40, 30) copy_btn.clicked.connect(self.copy_stream_url) copy_btn.setStyleSheet(""" QPushButton { background: rgba(0, 243, 255, 0.1); border: 1px solid #00f3ff; color: #00f3ff; } QPushButton:hover { background: rgba(0, 243, 255, 0.2); } """) url_layout.addWidget(copy_btn) layout.addWidget(url_widget) # Auto-start checkbox self.auto_start_check = QCheckBox("Auto-start on play") self.auto_start_check.setStyleSheet("color: #e0e0e0;") layout.addWidget(self.auto_start_check) # Quality selector quality_label = QLabel("Stream Quality:") quality_label.setStyleSheet("color: #888; font-size: 12px;") layout.addWidget(quality_label) self.quality_combo = QComboBox() self.quality_combo.addItems([ "High (128kbps)", "Medium (96kbps)", "Low (64kbps)", "Very Low (48kbps)", "Minimum (32kbps)" ]) self.quality_combo.setCurrentIndex(1) self.quality_combo.setStyleSheet(""" QComboBox { background: rgba(0, 0, 0, 0.3); border: 1px solid #333; color: white; padding: 5px; border-radius: 4px; } QComboBox::drop-down { border: none; } QComboBox QAbstractItemView { background: #1a1a1a; color: white; selection-background-color: #00f3ff; } """) layout.addWidget(self.quality_combo) hint = QLabel("Lower = more stable on poor connections") hint.setStyleSheet("color: #666; font-size: 10px;") layout.addWidget(hint) layout.addStretch() # Position panel self.streaming_panel.move(self.width() - 420, 20) def create_settings_panel(self): """Create settings panel with glow controls""" self.settings_panel = QWidget(self) self.settings_panel.setFixedSize(400, 600) self.settings_panel.setStyleSheet(f""" QWidget {{ background: rgba(20, 20, 30, 0.95); border: 2px solid rgb({SECONDARY_MAGENTA.red()}, {SECONDARY_MAGENTA.green()}, {SECONDARY_MAGENTA.blue()}); border-radius: 10px; }} """) self.settings_panel.hide() layout = QVBoxLayout(self.settings_panel) layout.setSpacing(10) # Header header = QHBoxLayout() title = QLabel("⚙️ SETTINGS") title.setStyleSheet(f""" font-family: 'Orbitron'; font-size: 16px; font-weight: bold; color: rgb({SECONDARY_MAGENTA.red()}, {SECONDARY_MAGENTA.green()}, {SECONDARY_MAGENTA.blue()}); """) header.addWidget(title) header.addStretch() close_btn = QPushButton("✕") close_btn.setFixedSize(30, 30) close_btn.clicked.connect(self.toggle_settings_panel) close_btn.setStyleSheet(""" QPushButton { background: transparent; border: none; color: #888; font-size: 18px; } QPushButton:hover { color: white; } """) header.addWidget(close_btn) layout.addLayout(header) # Settings checkboxes checkbox_style = """ QCheckBox { color: #e0e0e0; font-size: 13px; spacing: 8px; } QCheckBox::indicator { width: 18px; height: 18px; border: 2px solid #666; border-radius: 3px; background: rgba(0, 0, 0, 0.3); } QCheckBox::indicator:checked { background: #bc13fe; border-color: #bc13fe; } """ self.repeat_a_check = QCheckBox("🔁 Repeat Deck A") self.repeat_a_check.setStyleSheet(checkbox_style) layout.addWidget(self.repeat_a_check) self.repeat_b_check = QCheckBox("🔁 Repeat Deck B") self.repeat_b_check.setStyleSheet(checkbox_style) layout.addWidget(self.repeat_b_check) self.auto_mix_check = QCheckBox("🎛️ Auto-Crossfade") self.auto_mix_check.setStyleSheet(checkbox_style) layout.addWidget(self.auto_mix_check) self.shuffle_check = QCheckBox("🔀 Shuffle Library") self.shuffle_check.setStyleSheet(checkbox_style) layout.addWidget(self.shuffle_check) self.quantize_check = QCheckBox("📐 Quantize") self.quantize_check.setStyleSheet(checkbox_style) layout.addWidget(self.quantize_check) self.auto_play_check = QCheckBox("▶️ Auto-play next") self.auto_play_check.setChecked(True) self.auto_play_check.setStyleSheet(checkbox_style) layout.addWidget(self.auto_play_check) # Glow controls layout.addWidget(QLabel("")) # Spacer glow_title = QLabel("✨ NEON GLOW EFFECTS") glow_title.setStyleSheet(f""" font-family: 'Orbitron'; font-size: 14px; font-weight: bold; color: rgb({SECONDARY_MAGENTA.red()}, {SECONDARY_MAGENTA.green()}, {SECONDARY_MAGENTA.blue()}); """) layout.addWidget(glow_title) self.glow_a_check = QCheckBox("✨ Glow Deck A (Cyan)") self.glow_a_check.setStyleSheet(checkbox_style) self.glow_a_check.stateChanged.connect(lambda: self.toggle_glow('A')) layout.addWidget(self.glow_a_check) self.glow_b_check = QCheckBox("✨ Glow Deck B (Magenta)") self.glow_b_check.setStyleSheet(checkbox_style) self.glow_b_check.stateChanged.connect(lambda: self.toggle_glow('B')) layout.addWidget(self.glow_b_check) # Glow intensity intensity_label = QLabel("✨ Glow Intensity") intensity_label.setStyleSheet("color: #e0e0e0; font-size: 13px;") layout.addWidget(intensity_label) self.glow_slider = QSlider(Qt.Horizontal) self.glow_slider.setRange(1, 100) self.glow_slider.setValue(30) self.glow_slider.valueChanged.connect(self.update_glow_intensity) self.glow_slider.setStyleSheet(""" QSlider::groove:horizontal { height: 8px; background: #333; border-radius: 4px; } QSlider::handle:horizontal { background: #bc13fe; border: 2px solid #bc13fe; width: 16px; margin: -4px 0; border-radius: 8px; } """) layout.addWidget(self.glow_slider) # Server URL configuration layout.addWidget(QLabel("")) # Spacer server_title = QLabel("📡 SERVER CONFIGURATION") server_title.setStyleSheet(f""" font-family: 'Orbitron'; font-size: 14px; font-weight: bold; color: rgb({SECONDARY_MAGENTA.red()}, {SECONDARY_MAGENTA.green()}, {SECONDARY_MAGENTA.blue()}); """) layout.addWidget(server_title) server_url_label = QLabel("🔗 Server API URL (e.g. http://localhost:5000)") server_url_label.setStyleSheet("color: #e0e0e0; font-size: 13px;") layout.addWidget(server_url_label) self.server_url_input = QLineEdit(self.server_url) self.server_url_input.setStyleSheet(""" background: rgba(0, 0, 0, 0.4); border: 1px solid #444; color: cyan; padding: 5px; font-family: 'Rajdhani'; border-radius: 4px; """) self.server_url_input.textChanged.connect(self.on_server_url_change) layout.addWidget(self.server_url_input) layout.addStretch() # Position panel self.settings_panel.move(self.width() - 420, 20) def load_settings(self): """Load persistent settings""" settings_path = Path.home() / ".techdj_settings.json" if settings_path.exists(): try: with open(settings_path, 'r') as f: data = json.load(f) self.local_folder = data.get('local_folder') self.library_mode = data.get('library_mode', 'server') self.server_url = data.get('server_url', self.server_url) except Exception as e: print(f"Error loading settings: {e}") def save_settings(self): """Save persistent settings""" settings_path = Path.home() / ".techdj_settings.json" try: with open(settings_path, 'w') as f: json.dump({ 'local_folder': self.local_folder, 'library_mode': self.library_mode, 'server_url': self.server_url }, f) except Exception as e: print(f"Error saving settings: {e}") def set_library_mode(self, mode): """Switch between server and local library""" self.library_mode = mode if mode == 'server': self.server_mode_btn.set_active(True) self.local_mode_btn.set_active(False) self.local_folder_widget.hide() else: self.server_mode_btn.set_active(False) self.local_mode_btn.set_active(True) self.local_folder_widget.show() if self.local_folder: self.folder_label.setText(os.path.basename(self.local_folder).upper()) self.scan_local_library() self.update_library_list() self.save_settings() def select_local_folder(self): """Open dialog to select local music folder""" folder = QFileDialog.getExistingDirectory(self, "Select Music Folder") if folder: self.local_folder = folder self.folder_label.setText(os.path.basename(folder).upper()) self.scan_local_library() self.update_library_list() self.save_settings() def on_server_url_change(self, text): """Update server URL and save""" self.server_url = text self.save_settings() # Debounce the refresh to avoid spamming while typing if not hasattr(self, '_refresh_timer'): self._refresh_timer = QTimer() self._refresh_timer.timeout.connect(self.fetch_library) self._refresh_timer.setSingleShot(True) self._refresh_timer.start(1500) # Refresh library 1.5s after typing stops def scan_local_library(self): """Scan local folder for audio files""" if not self.local_folder: return self.local_library = [] extensions = ('.mp3', '.wav', '.flac', '.ogg', '.m4a') try: for root, dirs, files in os.walk(self.local_folder): for file in sorted(files): if file.lower().endswith(extensions): full_path = os.path.join(root, file) self.local_library.append({ "title": os.path.splitext(file)[0], "file": full_path, "is_local": True }) print(f"📂 Found {len(self.local_library)} local tracks") except Exception as e: print(f"Error scanning folder: {e}") def fetch_library(self): try: response = requests.get(f"{self.server_url}/library.json", timeout=5) self.server_library = response.json() # Mark server tracks for track in self.server_library: track['is_local'] = False # Initial mode setup self.set_library_mode(self.library_mode) print(f"📚 Loaded {len(self.server_library)} tracks from server") except Exception as e: print(f"❌ Error fetching library: {e}") # Still set local mode if server fails self.set_library_mode(self.library_mode) def update_library_list(self): self.library_list.clear() search_term = self.search_box.text().lower() library_to_show = self.server_library if self.library_mode == 'server' else self.local_library for track in library_to_show: if search_term and search_term not in track['title'].lower(): continue item = QListWidgetItem(track['title']) item.setData(Qt.UserRole, track) # Color coding for local vs server (optional visibility) if self.library_mode == 'local': item.setForeground(PRIMARY_CYAN) self.library_list.addItem(item) def filter_library(self): self.update_library_list() def on_library_double_click(self, item): track = item.data(Qt.UserRole) dialog = QDialog(self) dialog.setWindowTitle("Load Track") dialog.setStyleSheet(f""" QDialog {{ background: rgb({BG_DARK.red()}, {BG_DARK.green()}, {BG_DARK.blue()}); }} """) layout = QVBoxLayout() layout.addWidget(QLabel(f"Load '{track['title']}' to:")) btn_a = NeonButton(f"Deck A", PRIMARY_CYAN) btn_a.clicked.connect(lambda: self.load_to_deck('A', track, dialog)) layout.addWidget(btn_a) btn_b = NeonButton(f"Deck B", SECONDARY_MAGENTA) btn_b.clicked.connect(lambda: self.load_to_deck('B', track, dialog)) layout.addWidget(btn_b) dialog.setLayout(layout) dialog.exec_() def load_to_deck(self, deck_id, track, dialog=None): if dialog: dialog.accept() if track.get('is_local'): # Load local file directly print(f"📂 Loading local: {track['file']}") self.deck_loading_target[deck_id] = track['file'] if deck_id == 'A': self.deck_a.load_track(track['file']) else: self.deck_b.load_track(track['file']) return filename = os.path.basename(track['file']) cache_path = self.cache_dir / filename self.deck_loading_target[deck_id] = str(cache_path) if cache_path.exists(): print(f"📦 Using cached: {filename}") if deck_id == 'A': self.deck_a.load_track(str(cache_path)) else: self.deck_b.load_track(str(cache_path)) else: url = f"{self.server_url}/{track['file']}" print(f"⬇️ Downloading: {filename}") thread = DownloadThread(url, str(cache_path)) thread.finished.connect(lambda path, success: self.on_download_finished(deck_id, path, success)) thread.start() self.download_threads[filename] = thread def on_download_finished(self, deck_id, filepath, success): if success: # Check if this is still the intended track for this deck if self.deck_loading_target.get(deck_id) != filepath: print(f"⏭️ Stale download finished (ignored): {os.path.basename(filepath)}") return print(f"✅ Downloaded: {os.path.basename(filepath)}") if deck_id == 'A': self.deck_a.load_track(filepath) else: self.deck_b.load_track(filepath) else: QMessageBox.warning(self, "Download Error", "Failed to download track") def on_crossfader_change(self, value): self.audio_engine.set_crossfader(value / 100.0) def toggle_streaming_panel(self): """Toggle streaming panel visibility""" if self.streaming_panel.isVisible(): self.streaming_panel.hide() else: self.settings_panel.hide() # Hide settings if open self.streaming_panel.show() self.streaming_panel.raise_() def toggle_settings_panel(self): """Toggle settings panel visibility""" if self.settings_panel.isVisible(): self.settings_panel.hide() else: self.streaming_panel.hide() # Hide streaming if open self.settings_panel.show() self.settings_panel.raise_() def toggle_broadcast(self): """Toggle broadcast on/off""" if not self.broadcasting: # Start broadcast try: if self.socket is None: print(f"🔌 Connecting to server: {self.server_url}") self.socket = socketio.Client(logger=True, engineio_logger=False) # Add connection event handlers @self.socket.on('connect') def on_connect(): print("✅ Socket.IO connected successfully") @self.socket.on('connect_error') def on_connect_error(data): print(f"❌ Socket.IO connection error: {data}") QMessageBox.warning(self, "Connection Error", f"Failed to connect to server at {self.server_url}\n\nError: {data}") @self.socket.on('disconnect') def on_disconnect(): print("⚠️ Socket.IO disconnected") self.socket.on('listener_count', self.on_listener_count) try: self.socket.connect(self.server_url, wait_timeout=10) print("✅ Connection established") except Exception as e: print(f"❌ Connection failed: {e}") QMessageBox.critical(self, "Connection Failed", f"Could not connect to {self.server_url}\n\nError: {str(e)}\n\nMake sure the server is running.") return bitrate_map = {0: "128k", 1: "96k", 2: "64k", 3: "48k", 4: "32k"} bitrate = bitrate_map.get(self.quality_combo.currentIndex(), "96k") print(f"📡 Emitting start_broadcast with bitrate: {bitrate}") self.socket.emit('start_broadcast', { 'bitrate': bitrate, 'format': 'mp3' }) # Start local encoding thread self.audio_engine.is_broadcasting = True self.broadcast_thread = BroadcastThread(self.audio_engine.broadcast_queue, bitrate) self.broadcast_thread.chunk_ready.connect(self.on_broadcast_chunk) self.broadcast_thread.start() self.broadcasting = True self.broadcast_btn.setText("🟢 STOP BROADCAST") self.broadcast_btn.setStyleSheet(""" QPushButton { background: rgba(0, 255, 0, 0.2); border: 2px solid #00ff00; color: #00ff00; font-family: 'Orbitron'; font-size: 14px; font-weight: bold; border-radius: 8px; } QPushButton:hover { background: rgba(0, 255, 0, 0.3); } """) self.broadcast_status.setText("🔴 LIVE") self.broadcast_status.setStyleSheet("color: #00ff00; font-size: 12px; font-weight: bold;") print("🎙️ Broadcast started") except Exception as e: print(f"❌ Broadcast error: {e}") QMessageBox.warning(self, "Broadcast Error", f"Could not start broadcast:\n{e}") else: # Stop broadcast if self.socket: self.socket.emit('stop_broadcast') self.audio_engine.is_broadcasting = False if self.broadcast_thread: self.broadcast_thread.stop() self.broadcast_thread = None self.broadcasting = False self.broadcast_btn.setText("🔴 START BROADCAST") self.broadcast_btn.setStyleSheet(""" QPushButton { background: rgba(255, 0, 0, 0.2); border: 2px solid #ff0000; color: #ff0000; font-family: 'Orbitron'; font-size: 14px; font-weight: bold; border-radius: 8px; } QPushButton:hover { background: rgba(255, 0, 0, 0.3); } """) self.broadcast_status.setText("Offline") self.broadcast_status.setStyleSheet("color: #888; font-size: 12px;") print("🛑 Broadcast stopped") def on_broadcast_chunk(self, chunk): """Send encoded chunk to server via Socket.IO""" if self.socket and self.broadcasting: self.socket.emit('audio_chunk', chunk) def on_listener_count(self, data): """Update listener count from server""" self.listener_count = data.get('count', 0) # Update UI if streaming panel is visible if hasattr(self, 'listener_count_label'): self.listener_count_label.setText(f"{self.listener_count}") def copy_stream_url(self): """Copy stream URL to clipboard""" clipboard = QApplication.clipboard() clipboard.setText(self.stream_url.text()) # Show feedback original_text = self.stream_url.text() self.stream_url.setText("✅ Copied!") QTimer.singleShot(1000, lambda: self.stream_url.setText(original_text)) def toggle_glow(self, deck_id): """Toggle glow effect for a deck""" if deck_id == 'A': self.glow_enabled['A'] = self.glow_a_check.isChecked() else: self.glow_enabled['B'] = self.glow_b_check.isChecked() print(f"✨ Glow {deck_id}: {self.glow_enabled[deck_id]}") def update_glow_intensity(self, value): """Update glow intensity""" self.glow_intensity = value def update_glow_effect(self): """Update window glow effect based on settings""" # This would apply a glow effect to the window border # For now, just update deck styling for deck_id in ['A', 'B']: if self.glow_enabled[deck_id]: deck_widget = self.deck_a if deck_id == 'A' else self.deck_b color = PRIMARY_CYAN if deck_id == 'A' else SECONDARY_MAGENTA opacity = self.glow_intensity / 100.0 # Apply glow effect (simplified - could be enhanced with QGraphicsEffect) deck_widget.setStyleSheet(deck_widget.styleSheet() + f""" QWidget#deck {{ box-shadow: 0 0 {self.glow_intensity}px rgba({color.red()}, {color.green()}, {color.blue()}, {opacity}); }} """) def upload_file(self): """Upload MP3 file to server""" file_path, _ = QFileDialog.getOpenFileName( self, "Upload MP3", "", "MP3 Files (*.mp3);;All Files (*)" ) if file_path: try: filename = os.path.basename(file_path) with open(file_path, 'rb') as f: files = {'file': (filename, f, 'audio/mpeg')} response = requests.post(f"{self.server_url}/upload", files=files) if response.json().get('success'): print(f"✅ Uploaded: {filename}") QMessageBox.information(self, "Upload Success", f"Uploaded {filename}") self.fetch_library() # Refresh library else: error = response.json().get('error', 'Unknown error') QMessageBox.warning(self, "Upload Failed", error) except Exception as e: print(f"❌ Upload error: {e}") QMessageBox.warning(self, "Upload Error", str(e)) def resizeEvent(self, event): """Handle window resize to reposition floating elements""" super().resizeEvent(event) # Reposition floating buttons if hasattr(self, 'streaming_btn'): self.streaming_btn.move(self.width() - 70, self.height() - 280) self.settings_btn.move(self.width() - 70, self.height() - 220) self.upload_btn.move(self.width() - 70, self.height() - 160) self.keyboard_btn.move(self.width() - 70, self.height() - 100) # Reposition panels if hasattr(self, 'streaming_panel'): self.streaming_panel.move(self.width() - 420, 20) self.settings_panel.move(self.width() - 420, 20) def closeEvent(self, event): self.audio_engine.stop_stream() event.accept() def main(): app = QApplication(sys.argv) app.setStyle('Fusion') # Set dark palette palette = app.palette() palette.setColor(palette.Window, BG_DARK) palette.setColor(palette.WindowText, TEXT_MAIN) palette.setColor(palette.Base, QColor(15, 15, 20)) palette.setColor(palette.AlternateBase, QColor(20, 20, 30)) palette.setColor(palette.Text, TEXT_MAIN) palette.setColor(palette.Button, QColor(30, 30, 40)) palette.setColor(palette.ButtonText, TEXT_MAIN) app.setPalette(palette) window = TechDJMainWindow() window.show() sys.exit(app.exec_()) if __name__ == '__main__': main()