Add loop/repeat and queue features for both decks

This commit is contained in:
ComputerTech 2026-01-20 17:57:19 +00:00
parent ee26294106
commit de973b2a0e
1 changed files with 146 additions and 5 deletions

View File

@ -54,7 +54,9 @@ class AudioEngine:
'cues': {},
'loop_start': None,
'loop_end': None,
'loop_active': False
'loop_active': False,
'repeat': False,
'queue': [],
},
'B': {
'audio_data': None,
@ -70,7 +72,9 @@ class AudioEngine:
'cues': {},
'loop_start': None,
'loop_end': None,
'loop_active': False
'loop_active': False,
'repeat': False,
'queue': [],
}
}
@ -185,7 +189,16 @@ class AudioEngine:
# Auto-stop at end
if deck['position'] >= len(deck['audio_data']):
deck['playing'] = False
if deck['repeat']:
# Loop current track
deck['position'] = 0
elif len(deck['queue']) > 0:
# Mark that we need to load next track
# Can't load here (wrong thread), UI will handle it
deck['playing'] = False
deck['needs_next_track'] = True
else:
deck['playing'] = False
output = output * self.master_volume
outdata[:] = output
@ -264,6 +277,39 @@ class AudioEngine:
def set_filter(self, deck_id, filter_type, value):
with self.lock:
self.decks[deck_id]['filters'][filter_type] = value
def set_repeat(self, deck_id, enabled):
"""Toggle repeat/loop for a deck"""
with self.lock:
self.decks[deck_id]['repeat'] = enabled
def add_to_queue(self, deck_id, filepath):
"""Add track to deck's queue"""
with self.lock:
self.decks[deck_id]['queue'].append(filepath)
def remove_from_queue(self, deck_id, index):
"""Remove track from queue by index"""
with self.lock:
if 0 <= index < len(self.decks[deck_id]['queue']):
self.decks[deck_id]['queue'].pop(index)
def clear_queue(self, deck_id):
"""Clear all tracks from queue"""
with self.lock:
self.decks[deck_id]['queue'].clear()
def get_queue(self, deck_id):
"""Get current queue (returns a copy)"""
with self.lock:
return list(self.decks[deck_id]['queue'])
def pop_next_from_queue(self, deck_id):
"""Get and remove next track from queue"""
with self.lock:
if len(self.decks[deck_id]['queue']) > 0:
return self.decks[deck_id]['queue'].pop(0)
return None
class DownloadThread(QThread):
@ -854,6 +900,11 @@ class DeckWidget(QWidget):
reset_btn.clicked.connect(self.reset_deck)
transport.addWidget(reset_btn)
self.loop_btn = NeonButton("🔁 LOOP")
self.loop_btn.setCheckable(True)
self.loop_btn.clicked.connect(self.toggle_loop)
transport.addWidget(self.loop_btn)
layout.addLayout(transport)
self.setLayout(layout)
@ -962,11 +1013,51 @@ class DeckWidget(QWidget):
print(f"🔄 Deck {self.deck_id} reset to defaults")
def toggle_loop(self):
"""Toggle loop/repeat for this deck"""
is_looping = self.loop_btn.isChecked()
self.audio_engine.set_repeat(self.deck_id, is_looping)
if is_looping:
self.loop_btn.setStyleSheet(f"""
QPushButton {{
background: rgba({self.color.red()}, {self.color.green()}, {self.color.blue()}, 0.3);
border: 2px solid rgb({self.color.red()}, {self.color.green()}, {self.color.blue()});
color: rgb({self.color.red()}, {self.color.green()}, {self.color.blue()});
font-family: 'Orbitron';
font-size: 12px;
font-weight: bold;
border-radius: 6px;
}}
""")
print(f"🔁 Deck {self.deck_id} loop enabled")
else:
self.loop_btn.setStyleSheet("""
QPushButton {
background: rgba(0, 0, 0, 0.3);
border: 2px solid #666;
color: #888;
font-family: 'Orbitron';
font-size: 12px;
border-radius: 6px;
}
""")
print(f"⏹️ Deck {self.deck_id} loop disabled")
def update_display(self):
deck = self.audio_engine.decks[self.deck_id]
position = self.audio_engine.get_position(self.deck_id)
duration = deck['duration']
# Check if we need to load next track from queue
if deck.get('needs_next_track', False):
deck['needs_next_track'] = False
next_track = self.audio_engine.pop_next_from_queue(self.deck_id)
if next_track:
print(f"📋 Auto-loading next track from queue: {os.path.basename(next_track)}")
self.load_track(next_track)
self.play()
pos_min = int(position // 60)
pos_sec = int(position % 60)
dur_min = int(duration // 60)
@ -1794,14 +1885,23 @@ class TechDJMainWindow(QMainWindow):
layout = QVBoxLayout()
layout.addWidget(QLabel(f"Load '{track['title']}' to:"))
btn_a = NeonButton(f"Deck A", PRIMARY_CYAN)
btn_a = NeonButton(f"▶ Play on 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 = NeonButton(f"▶ Play on Deck B", SECONDARY_MAGENTA)
btn_b.clicked.connect(lambda: self.load_to_deck('B', track, dialog))
layout.addWidget(btn_b)
# Add to queue buttons
queue_a = NeonButton(f"📋 Add to Queue A", PRIMARY_CYAN)
queue_a.clicked.connect(lambda: self.add_to_queue('A', track, dialog))
layout.addWidget(queue_a)
queue_b = NeonButton(f"📋 Add to Queue B", SECONDARY_MAGENTA)
queue_b.clicked.connect(lambda: self.add_to_queue('B', track, dialog))
layout.addWidget(queue_b)
dialog.setLayout(layout)
dialog.exec_()
@ -1853,6 +1953,47 @@ class TechDJMainWindow(QMainWindow):
else:
QMessageBox.warning(self, "Download Error", "Failed to download track")
def add_to_queue(self, deck_id, track, dialog=None):
"""Add track to deck's queue"""
if dialog:
dialog.accept()
# Determine file path
if self.library_mode == 'local':
filepath = track['file']
else:
filename = track['file'].split('/')[-1]
cache_path = self.cache_dir / filename
if cache_path.exists():
filepath = str(cache_path)
else:
# Download to cache first
url = f"{self.server_url}/{track['file']}"
print(f"⬇️ Downloading for queue: {filename}")
thread = DownloadThread(url, str(cache_path))
thread.finished.connect(lambda path, success: self.on_queue_download_finished(deck_id, path, success))
thread.start()
self.download_threads[filename] = thread
return
# Add to queue
self.audio_engine.add_to_queue(deck_id, filepath)
queue_len = len(self.audio_engine.get_queue(deck_id))
print(f"📋 Added to Deck {deck_id} queue: {track['title']} (Queue: {queue_len})")
QMessageBox.information(self, "Added to Queue",
f"Added '{track['title']}' to Deck {deck_id} queue\n\nQueue length: {queue_len}")
def on_queue_download_finished(self, deck_id, filepath, success):
"""Handle download completion for queued tracks"""
if success:
self.audio_engine.add_to_queue(deck_id, filepath)
queue_len = len(self.audio_engine.get_queue(deck_id))
print(f"✅ Downloaded and queued: {os.path.basename(filepath)} (Queue: {queue_len})")
else:
print(f"❌ Failed to download for queue: {os.path.basename(filepath)}")
def on_crossfader_change(self, value):
self.audio_engine.set_crossfader(value / 100.0)