✨ | Add ability to load previous track
All checks were successful
SonarQube Code Quality Scan / SonarQube Scan (push) Successful in 26s
All checks were successful
SonarQube Code Quality Scan / SonarQube Scan (push) Successful in 26s
This commit is contained in:
59
gui.py
59
gui.py
@@ -972,7 +972,8 @@ class PlaybackBar(QWidget):
|
|||||||
"QPushButton:hover { color: #ffffff; }"
|
"QPushButton:hover { color: #ffffff; }"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
btn_base = icon_style["normal"] + icon_style["hover"]
|
self._btn_base = icon_style["normal"] + icon_style["hover"]
|
||||||
|
btn_base = self._btn_base
|
||||||
|
|
||||||
self._loop_btn = QPushButton("↻")
|
self._loop_btn = QPushButton("↻")
|
||||||
self._loop_btn.setFixedSize(btn_size, btn_size)
|
self._loop_btn.setFixedSize(btn_size, btn_size)
|
||||||
@@ -986,13 +987,17 @@ class PlaybackBar(QWidget):
|
|||||||
|
|
||||||
self._prev_btn = QPushButton("⏮")
|
self._prev_btn = QPushButton("⏮")
|
||||||
self._prev_btn.setFixedSize(btn_size, btn_size)
|
self._prev_btn.setFixedSize(btn_size, btn_size)
|
||||||
self._prev_btn.setToolTip("Restart / Previous")
|
self._prev_btn.setToolTip("Previous")
|
||||||
self._prev_btn.setStyleSheet(btn_base)
|
self._prev_btn.setStyleSheet(btn_base)
|
||||||
self._prev_btn.clicked.connect(self._restart)
|
self._prev_btn.clicked.connect(self._restart)
|
||||||
|
|
||||||
|
self._prev_btn.setEnabled(False)
|
||||||
|
|
||||||
self._play_btn = QPushButton("▶")
|
self._play_btn = QPushButton("▶")
|
||||||
self._play_btn.setFixedSize(40, 40)
|
self._play_btn.setFixedSize(40, 40)
|
||||||
self._play_btn.setToolTip("Play / Pause")
|
self._play_btn.setToolTip("Play / Pause")
|
||||||
|
|
||||||
|
self._play_btn.setEnabled(False)
|
||||||
self._play_btn.setStyleSheet(
|
self._play_btn.setStyleSheet(
|
||||||
"QPushButton { background: #ffffff; border: none;"
|
"QPushButton { background: #ffffff; border: none;"
|
||||||
" border-radius: 20px; color: #0a0814;"
|
" border-radius: 20px; color: #0a0814;"
|
||||||
@@ -1008,6 +1013,8 @@ class PlaybackBar(QWidget):
|
|||||||
self._next_btn.setStyleSheet(btn_base)
|
self._next_btn.setStyleSheet(btn_base)
|
||||||
self._next_btn.clicked.connect(self._next)
|
self._next_btn.clicked.connect(self._next)
|
||||||
|
|
||||||
|
self._next_btn.setEnabled(False)
|
||||||
|
|
||||||
self._stop_btn = QPushButton("⏹")
|
self._stop_btn = QPushButton("⏹")
|
||||||
self._stop_btn.setFixedSize(btn_size, btn_size)
|
self._stop_btn.setFixedSize(btn_size, btn_size)
|
||||||
self._stop_btn.setToolTip("Stop")
|
self._stop_btn.setToolTip("Stop")
|
||||||
@@ -1017,6 +1024,8 @@ class PlaybackBar(QWidget):
|
|||||||
)
|
)
|
||||||
self._stop_btn.clicked.connect(self._stop)
|
self._stop_btn.clicked.connect(self._stop)
|
||||||
|
|
||||||
|
self._stop_btn.setEnabled(False)
|
||||||
|
|
||||||
btn_row.addWidget(self._loop_btn)
|
btn_row.addWidget(self._loop_btn)
|
||||||
btn_row.addWidget(self._prev_btn)
|
btn_row.addWidget(self._prev_btn)
|
||||||
btn_row.addStretch()
|
btn_row.addStretch()
|
||||||
@@ -1024,6 +1033,19 @@ class PlaybackBar(QWidget):
|
|||||||
btn_row.addStretch()
|
btn_row.addStretch()
|
||||||
btn_row.addWidget(self._next_btn)
|
btn_row.addWidget(self._next_btn)
|
||||||
btn_row.addWidget(self._stop_btn)
|
btn_row.addWidget(self._stop_btn)
|
||||||
|
|
||||||
|
self._disabled_style = """
|
||||||
|
QPushButton { background: transparent; border: none;
|
||||||
|
color: #44445a; font-size: 15pt; }
|
||||||
|
"""
|
||||||
|
self._disabled_extra = """
|
||||||
|
QPushButton { background: transparent; border: none;
|
||||||
|
color: #44445a; font-size: 15pt; }
|
||||||
|
QPushButton:hover { color: #44445a; }
|
||||||
|
"""
|
||||||
|
self._disabled_loop_checked = """
|
||||||
|
QPushButton:checked { color: #44445a; }
|
||||||
|
"""
|
||||||
center_layout.addLayout(btn_row)
|
center_layout.addLayout(btn_row)
|
||||||
|
|
||||||
# ── Progress bar ──
|
# ── Progress bar ──
|
||||||
@@ -1263,8 +1285,34 @@ class PlaybackBar(QWidget):
|
|||||||
self._player.set_loop(enabled)
|
self._player.set_loop(enabled)
|
||||||
self._loop_btn.setChecked(enabled)
|
self._loop_btn.setChecked(enabled)
|
||||||
|
|
||||||
|
def set_buttons_enabled(self, enabled: bool) -> None:
|
||||||
|
"""Enable or disable playback control buttons."""
|
||||||
|
if enabled:
|
||||||
|
normal = self._btn_base
|
||||||
|
loop_checked = "QPushButton:checked { color: #a78bfa; }"
|
||||||
|
else:
|
||||||
|
normal = self._disabled_extra
|
||||||
|
loop_checked = self._disabled_loop_checked
|
||||||
|
self._prev_btn.setEnabled(enabled)
|
||||||
|
self._prev_btn.setStyleSheet(normal)
|
||||||
|
self._play_btn.setEnabled(enabled)
|
||||||
|
self._next_btn.setEnabled(enabled)
|
||||||
|
self._next_btn.setStyleSheet(normal)
|
||||||
|
self._stop_btn.setEnabled(enabled)
|
||||||
|
self._stop_btn.setStyleSheet(normal)
|
||||||
|
self._loop_btn.setEnabled(enabled)
|
||||||
|
if enabled:
|
||||||
|
self._loop_btn.setStyleSheet(self._btn_base + loop_checked)
|
||||||
|
else:
|
||||||
|
self._loop_btn.setStyleSheet(self._disabled_style + loop_checked)
|
||||||
|
self._vol_slider.setEnabled(enabled)
|
||||||
|
self._vol_btn.setEnabled(enabled)
|
||||||
|
|
||||||
def _restart(self) -> None:
|
def _restart(self) -> None:
|
||||||
self._player.seek(0)
|
if self._player.has_previous() and self._player.get_position() < 3000:
|
||||||
|
self._player.previous()
|
||||||
|
else:
|
||||||
|
self._player.seek(0)
|
||||||
|
|
||||||
def _next(self) -> None:
|
def _next(self) -> None:
|
||||||
self._player.skip()
|
self._player.skip()
|
||||||
@@ -2214,6 +2262,7 @@ class TunettiWindow(QMainWindow):
|
|||||||
self.player.playback_state_changed.connect(self._on_playback_state_changed)
|
self.player.playback_state_changed.connect(self._on_playback_state_changed)
|
||||||
self.player.error.connect(self._on_player_error)
|
self.player.error.connect(self._on_player_error)
|
||||||
self.player.seeked.connect(self._on_player_seeked)
|
self.player.seeked.connect(self._on_player_seeked)
|
||||||
|
self.player.song_loaded.connect(self._on_song_loaded)
|
||||||
|
|
||||||
# ── Visualizer audio buffer input ──
|
# ── Visualizer audio buffer input ──
|
||||||
# Connect the player's audio buffer output to our visualizer
|
# Connect the player's audio buffer output to our visualizer
|
||||||
@@ -2714,6 +2763,10 @@ class TunettiWindow(QMainWindow):
|
|||||||
log.error("Player error: %s", err.get("error", "unknown"))
|
log.error("Player error: %s", err.get("error", "unknown"))
|
||||||
self._playback.show_error(err.get("error", "Playback error"))
|
self._playback.show_error(err.get("error", "Playback error"))
|
||||||
|
|
||||||
|
@Slot(bool)
|
||||||
|
def _on_song_loaded(self, loaded: bool) -> None:
|
||||||
|
self._playback.set_buttons_enabled(loaded)
|
||||||
|
|
||||||
@Slot(int)
|
@Slot(int)
|
||||||
def _on_player_seeked(self, position_ms: int) -> None:
|
def _on_player_seeked(self, position_ms: int) -> None:
|
||||||
"""Recalculate Discord RPC start timestamp after a seek."""
|
"""Recalculate Discord RPC start timestamp after a seek."""
|
||||||
|
|||||||
30
player.py
30
player.py
@@ -355,6 +355,7 @@ class AudioPlayer(QObject):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
loading = Signal(str)
|
loading = Signal(str)
|
||||||
|
song_loaded = Signal(bool)
|
||||||
song_started = Signal(dict)
|
song_started = Signal(dict)
|
||||||
song_ended = Signal(str)
|
song_ended = Signal(str)
|
||||||
paused = Signal(dict)
|
paused = Signal(dict)
|
||||||
@@ -383,6 +384,7 @@ class AudioPlayer(QObject):
|
|||||||
# ── Queue & state ─────────────────────────────────────────────
|
# ── Queue & state ─────────────────────────────────────────────
|
||||||
self._queue: list[dict] = []
|
self._queue: list[dict] = []
|
||||||
self._current: Optional[dict] = None
|
self._current: Optional[dict] = None
|
||||||
|
self._history: list[dict] = []
|
||||||
self._loop_mode = False
|
self._loop_mode = False
|
||||||
|
|
||||||
# ── Download worker (single persistent thread) ────────────────
|
# ── Download worker (single persistent thread) ────────────────
|
||||||
@@ -447,20 +449,42 @@ class AudioPlayer(QObject):
|
|||||||
|
|
||||||
def skip(self) -> None:
|
def skip(self) -> None:
|
||||||
"""Skip to the next song."""
|
"""Skip to the next song."""
|
||||||
# Cancel any in-progress download (the skip is our new intent).
|
# Push current song to history before moving on.
|
||||||
|
if self._current is not None:
|
||||||
|
self._history.append(self._current)
|
||||||
self._cancel_active_downloads()
|
self._cancel_active_downloads()
|
||||||
self._player.stop()
|
self._player.stop()
|
||||||
self._advance()
|
self._advance()
|
||||||
|
|
||||||
|
def previous(self) -> None:
|
||||||
|
"""Go back to the previous song, if available."""
|
||||||
|
if not self._history:
|
||||||
|
return
|
||||||
|
prev = self._history.pop()
|
||||||
|
self._cancel_active_downloads()
|
||||||
|
self._player.stop()
|
||||||
|
self._queue.insert(0, prev)
|
||||||
|
self._try_next_song()
|
||||||
|
|
||||||
|
def has_previous(self) -> bool:
|
||||||
|
"""Whether a previous song is available."""
|
||||||
|
return len(self._history) > 0
|
||||||
|
|
||||||
|
def is_loaded(self) -> bool:
|
||||||
|
"""Whether any song is currently loaded (playing or paused)."""
|
||||||
|
return self._current is not None
|
||||||
|
|
||||||
def stop_playback(self) -> None:
|
def stop_playback(self) -> None:
|
||||||
"""Soft stop: halt playback, clear queue, cancel downloads."""
|
"""Soft stop: halt playback, clear queue, cancel downloads."""
|
||||||
self._cancel_active_downloads()
|
self._cancel_active_downloads()
|
||||||
self._cleanup_temp_files()
|
self._cleanup_temp_files()
|
||||||
self._queue.clear()
|
self._queue.clear()
|
||||||
self._prefetch_cache.clear()
|
self._prefetch_cache.clear()
|
||||||
|
self._history.clear()
|
||||||
self._loop_mode = False
|
self._loop_mode = False
|
||||||
self._current = None
|
self._current = None
|
||||||
self._player.stop()
|
self._player.stop()
|
||||||
|
self.song_loaded.emit(False)
|
||||||
self.playback_state_changed.emit("stopped")
|
self.playback_state_changed.emit("stopped")
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
@@ -523,6 +547,9 @@ class AudioPlayer(QObject):
|
|||||||
def get_loop(self) -> bool:
|
def get_loop(self) -> bool:
|
||||||
return self._loop_mode
|
return self._loop_mode
|
||||||
|
|
||||||
|
def get_position(self) -> int:
|
||||||
|
return self._player.position()
|
||||||
|
|
||||||
def set_loop(self, enabled: bool) -> None:
|
def set_loop(self, enabled: bool) -> None:
|
||||||
self._loop_mode = enabled
|
self._loop_mode = enabled
|
||||||
|
|
||||||
@@ -658,6 +685,7 @@ class AudioPlayer(QObject):
|
|||||||
self._player.setSource(QUrl.fromLocalFile(local_path))
|
self._player.setSource(QUrl.fromLocalFile(local_path))
|
||||||
self._player.play()
|
self._player.play()
|
||||||
self.song_started.emit(song)
|
self.song_started.emit(song)
|
||||||
|
self.song_loaded.emit(True)
|
||||||
|
|
||||||
# Kick off prefetch for the next queued song.
|
# Kick off prefetch for the next queued song.
|
||||||
self._start_prefetch()
|
self._start_prefetch()
|
||||||
|
|||||||
Reference in New Issue
Block a user