From de973b2a0ef02e61f6e686af29e594afea494d03 Mon Sep 17 00:00:00 2001 From: ComputerTech Date: Tue, 20 Jan 2026 17:57:19 +0000 Subject: [PATCH] Add loop/repeat and queue features for both decks --- techdj_qt.py | 151 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 146 insertions(+), 5 deletions(-) diff --git a/techdj_qt.py b/techdj_qt.py index ff7ba26..e2a72aa 100644 --- a/techdj_qt.py +++ b/techdj_qt.py @@ -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)