diff --git a/app/classes.py b/app/classes.py index 908d89d..94417bb 100644 --- a/app/classes.py +++ b/app/classes.py @@ -5,7 +5,7 @@ import datetime as dt from enum import auto, Enum import functools import threading -from typing import NamedTuple +from typing import Any, NamedTuple # Third party imports @@ -21,6 +21,7 @@ from PyQt6.QtWidgets import ( ) # App imports +# from music_manager import FadeCurve # Define singleton first as it's needed below @@ -91,31 +92,6 @@ class Filter: duration_unit: str = "minutes" -@singleton -@dataclass -class MusicMusterSignals(QObject): - """ - Class for all MusicMuster signals. See: - - https://zetcode.com/gui/pyqt5/eventssignals/ - - https://stackoverflow.com/questions/62654525/emit-a-signal-from-another-class-to-main-class - """ - - begin_reset_model_signal = pyqtSignal(int) - enable_escape_signal = pyqtSignal(bool) - end_reset_model_signal = pyqtSignal(int) - next_track_changed_signal = pyqtSignal() - resize_rows_signal = pyqtSignal(int) - search_songfacts_signal = pyqtSignal(str) - search_wikipedia_signal = pyqtSignal(str) - show_warning_signal = pyqtSignal(str, str) - span_cells_signal = pyqtSignal(int, int, int, int, int) - status_message_signal = pyqtSignal(str, int) - track_ended_signal = pyqtSignal() - - def __post_init__(self): - super().__init__() - - class PlaylistStyle(QProxyStyle): def drawPrimitive(self, element, option, painter, widget=None): """ @@ -165,7 +141,7 @@ class TrackDTO: @dataclass -class PlaylistRowDTO(TrackDTO): +class PlaylistRowObj(TrackDTO): note: str played: bool playlist_id: int @@ -177,14 +153,51 @@ class PlaylistRowDTO(TrackDTO): note_bg: str | None = None end_of_track_signalled: bool = False end_time: dt.datetime | None = None - # fade_graph: FadeCurve | None = None + fade_graph: Any | None = None fade_graph_start_updates: dt.datetime | None = None resume_marker: float = 0.0 forecast_end_time: dt.datetime | None = None forecast_start_time: dt.datetime | None = None start_time: dt.datetime | None = None + def is_playing(self) -> bool: + """ + Return True if we're currently playing else False + """ + + if self.start_time is None: + return False + + return True + class TrackInfo(NamedTuple): track_id: int row_number: int + + +@singleton +@dataclass +class MusicMusterSignals(QObject): + """ + Class for all MusicMuster signals. See: + - https://zetcode.com/gui/pyqt5/eventssignals/ + - https://stackoverflow.com/questions/62654525/emit-a-signal-from-another-class-to-main-class + """ + + begin_reset_model_signal = pyqtSignal(int) + enable_escape_signal = pyqtSignal(bool) + end_reset_model_signal = pyqtSignal(int) + next_track_changed_signal = pyqtSignal() + resize_rows_signal = pyqtSignal(int) + search_songfacts_signal = pyqtSignal(str) + search_wikipedia_signal = pyqtSignal(str) + show_warning_signal = pyqtSignal(str, str) + signal_set_next_row = pyqtSignal(int) + signal_set_next_track = pyqtSignal(PlaylistRowObj) + span_cells_signal = pyqtSignal(int, int, int, int, int) + status_message_signal = pyqtSignal(str, int) + track_ended_signal = pyqtSignal() + + def __post_init__(self): + super().__init__() diff --git a/app/models.py b/app/models.py index 411788a..44cf141 100644 --- a/app/models.py +++ b/app/models.py @@ -395,6 +395,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): given playlist_id and row """ + # TODO: use selectinload? stmt = ( select(PlaylistRows) .options(joinedload(cls.track)) diff --git a/app/music_manager.py b/app/music_manager.py index 9f34529..89ded2d 100644 --- a/app/music_manager.py +++ b/app/music_manager.py @@ -238,27 +238,6 @@ class _Music: # except Exception as e: # log.error(f"Failed to set up VLC logging: {e}") - def adjust_by_ms(self, ms: int) -> None: - """Move player position by ms milliseconds""" - - if not self.player: - return - - elapsed_ms = self.get_playtime() - position = self.get_position() - if not position: - position = 0.0 - new_position = max(0.0, position + ((position * ms) / elapsed_ms)) - self.set_position(new_position) - # Adjus start time so elapsed time calculations are correct - if new_position == 0: - self.start_dt = dt.datetime.now() - else: - if self.start_dt: - self.start_dt -= dt.timedelta(milliseconds=ms) - else: - self.start_dt = dt.datetime.now() - dt.timedelta(milliseconds=ms) - def fade(self, fade_seconds: int) -> None: """ Fade the currently playing track. @@ -353,21 +332,6 @@ class _Music: self.player.set_position(position) self.start_dt = start_time - # For as-yet unknown reasons. sometimes the volume gets - # reset to zero within 200mS or so of starting play. This - # only happened since moving to Debian 12, which uses - # Pipewire for sound (which may be irrelevant). - # It has been known for the volume to need correcting more - # than once in the first 200mS. - # Update August 2024: This no longer seems to be an issue - # for _ in range(3): - # if self.player: - # volume = self.player.audio_get_volume() - # if volume < Config.VLC_VOLUME_DEFAULT: - # self.set_volume(Config.VLC_VOLUME_DEFAULT) - # log.error(f"Reset from {volume=}") - # sleep(0.1) - def set_position(self, position: float) -> None: """ Set player position @@ -519,25 +483,6 @@ class RowAndTrack: self.signals.track_ended_signal.emit() self.end_of_track_signalled = True - def create_fade_graph(self) -> None: - """ - Initialise and add FadeCurve in a thread as it's slow - """ - - self.fadecurve_thread = QThread() - self.worker = _AddFadeCurve( - self, - track_path=self.path, - track_fade_at=self.fade_at, - track_silence_at=self.silence_at, - ) - self.worker.moveToThread(self.fadecurve_thread) - self.fadecurve_thread.started.connect(self.worker.run) - self.worker.finished.connect(self.fadecurve_thread.quit) - self.worker.finished.connect(self.worker.deleteLater) - self.fadecurve_thread.finished.connect(self.fadecurve_thread.deleteLater) - self.fadecurve_thread.start() - def drop3db(self, enable: bool) -> None: """ If enable is true, drop output by 3db else restore to full volume @@ -565,20 +510,6 @@ class RowAndTrack: return self.music.is_playing() - def move_back(self, ms: int = Config.PREVIEW_BACK_MS) -> None: - """ - Rewind player by ms milliseconds - """ - - self.music.adjust_by_ms(ms * -1) - - def move_forward(self, ms: int = Config.PREVIEW_ADVANCE_MS) -> None: - """ - Rewind player by ms milliseconds - """ - - self.music.adjust_by_ms(ms) - def play(self, position: Optional[float] = None) -> None: """Play track""" @@ -599,13 +530,6 @@ class RowAndTrack: milliseconds=update_graph_at_ms ) - def restart(self) -> None: - """ - Restart player - """ - - self.music.adjust_by_ms(self.time_playing() * -1) - def set_forecast_start_time( self, modified_rows: list[int], start: Optional[dt.datetime] ) -> Optional[dt.datetime]: @@ -743,7 +667,28 @@ class TrackSequence: self.next = None else: self.next = rat - self.next.create_fade_graph() + self.create_fade_graph() + + def create_fade_graph(self) -> None: + """ + Initialise and add FadeCurve in a thread as it's slow + """ + + self.fadecurve_thread = QThread() + if self.next is None: + raise ApplicationError("hell in a handcart") + self.worker = _AddFadeCurve( + self.next, + track_path=self.next.path, + track_fade_at=self.next.fade_at, + track_silence_at=self.next.silence_at, + ) + self.worker.moveToThread(self.fadecurve_thread) + self.fadecurve_thread.started.connect(self.worker.run) + self.worker.finished.connect(self.fadecurve_thread.quit) + self.worker.finished.connect(self.worker.deleteLater) + self.fadecurve_thread.finished.connect(self.fadecurve_thread.deleteLater) + self.fadecurve_thread.start() track_sequence = TrackSequence() diff --git a/app/musicmuster.py b/app/musicmuster.py index 3bd821b..62e1613 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -2523,11 +2523,13 @@ class Window(QMainWindow): Set currently-selected row on visible playlist tab as next track """ - playlist_tab = self.active_tab() - if playlist_tab: - playlist_tab.set_row_as_next_track() - else: - log.error("No active tab") + self.signals.signal_set_next_row.emit(self.current.playlist_id) + self.clear_selection() + # playlist_tab = self.active_tab() + # if playlist_tab: + # playlist_tab.set_row_as_next_track() + # else: + # log.error("No active tab") def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None: """ diff --git a/app/playlistmodel.py b/app/playlistmodel.py index c35e028..a71dbc7 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -35,7 +35,7 @@ from classes import ( ApplicationError, Col, MusicMusterSignals, - PlaylistRowDTO, + PlaylistRowObj, ) from config import Config from helpers import ( @@ -85,9 +85,10 @@ class PlaylistModel(QAbstractTableModel): self.playlist_id = playlist_id self.is_template = is_template - self.playlist_rows: dict[int, PlaylistRowDTO] = {} - self.selected_rows: list[PlaylistRowDTO] = [] + self.playlist_rows: dict[int, PlaylistRowObj] = {} + self.selected_rows: list[PlaylistRowObj] = [] self.signals = MusicMusterSignals() + self.signals.signal_set_next_row.connect(self.set_next_row) self.played_tracks_hidden = False self.signals.begin_reset_model_signal.connect(self.begin_reset_model) @@ -306,9 +307,7 @@ class PlaylistModel(QAbstractTableModel): # Update colour and times for current row # only invalidate required roles - roles = [ - Qt.ItemDataRole.DisplayRole - ] + roles = [Qt.ItemDataRole.DisplayRole] self.invalidate_row(row_number, roles) # Update previous row in case we're hiding played rows @@ -759,7 +758,9 @@ class PlaylistModel(QAbstractTableModel): Qt.ItemDataRole.FontRole, Qt.ItemDataRole.ForegroundRole, ] - self.invalidate_rows(list(range(new_row_number, len(self.playlist_rows))), roles) + self.invalidate_rows( + list(range(new_row_number, len(self.playlist_rows))), roles + ) def invalidate_row(self, modified_row: int, roles: list[Qt.ItemDataRole]) -> None: """ @@ -771,10 +772,12 @@ class PlaylistModel(QAbstractTableModel): self.dataChanged.emit( self.index(modified_row, 0), self.index(modified_row, self.columnCount() - 1), - roles + roles, ) - def invalidate_rows(self, modified_rows: list[int], roles: list[Qt.ItemDataRole]) -> None: + def invalidate_rows( + self, modified_rows: list[int], roles: list[Qt.ItemDataRole] + ) -> None: """ Signal to view to refresh invlidated rows """ @@ -838,7 +841,7 @@ class PlaylistModel(QAbstractTableModel): # new_playlist_rows[p.row_number] = self.playlist_rows[plid_to_row[p.id]] # new_playlist_rows[p.row_number].row_number = p.row_number # build a new playlist_rows - new_playlist_rows: dict[int, PlaylistRowDTO] = {} + new_playlist_rows: dict[int, PlaylistRowObj] = {} for p in get_playlist_rows(self.playlist_id): new_playlist_rows[p.row_number] = p @@ -1432,57 +1435,47 @@ class PlaylistModel(QAbstractTableModel): """ self.selected_rows = [self.playlist_rows[a] for a in selected_rows] - import pdb; pdb.set_trace() - def set_next_row(self, row_number: Optional[int]) -> None: + def set_next_row(self, playlist_id: int) -> None: """ - Set row_number as next track. If row_number is None, clear next track. - - Return True if successful else False. + Handle signal_set_next_row """ - log.debug(f"{self}: set_next_row({row_number=})") + log.debug(f"{self}: set_next_row({playlist_id=})") + if playlist_id != self.playlist_id: + # Not for us + return - if row_number is None: - # Clear next track + if len(self.selected_rows) == 0: + # No row selected so clear next track if track_sequence.next is not None: track_sequence.set_next(None) - else: - # Get playlistrow_id of row - try: - rat = self.playlist_rows[row_number] - except IndexError: - log.error(f"{self} set_track_sequence.next({row_number=}, IndexError") - return - if rat.track_id is None or rat.row_number is None: - log.error( - f"{self} .set_track_sequence.next({row_number=}, " - f"No track / row number {rat.track_id=}, {rat.row_number=}" - ) - return + return - old_next_row: Optional[int] = None - if track_sequence.next: - old_next_row = track_sequence.next.row_number + if len(self.selected_rows) > 1: + self.signals.show_warning_signal.emit( + "Too many rows selected", "Select one row for next row" + ) + return - track_sequence.set_next(rat) + rat = self.selected_rows[0] + if rat.track_id is None: + raise ApplicationError(f"set_next_row: no track_id ({rat=})") - if Config.WIKIPEDIA_ON_NEXT: - self.signals.search_wikipedia_signal.emit( - self.playlist_rows[row_number].title - ) - if Config.SONGFACTS_ON_NEXT: - self.signals.search_songfacts_signal.emit( - self.playlist_rows[row_number].title - ) - roles = [ - Qt.ItemDataRole.BackgroundRole, - ] - if old_next_row is not None: - # only invalidate required roles - self.invalidate_row(old_next_row, roles) + old_next_row: Optional[int] = None + if track_sequence.next: + old_next_row = track_sequence.next.row_number + + track_sequence.set_next(rat) + + roles = [ + Qt.ItemDataRole.BackgroundRole, + ] + if old_next_row is not None: # only invalidate required roles - self.invalidate_row(row_number, roles) + self.invalidate_row(old_next_row, roles) + # only invalidate required roles + self.invalidate_row(rat.row_number, roles) self.signals.next_track_changed_signal.emit() self.update_track_times() @@ -1826,7 +1819,9 @@ class PlaylistProxyModel(QSortFilterProxyModel): ] QTimer.singleShot( Config.HIDE_AFTER_PLAYING_OFFSET + 100, - lambda: self.sourceModel().invalidate_row(source_row, roles), + lambda: self.sourceModel().invalidate_row( + source_row, roles + ), ) return True # Next track not playing yet so don't hide previous diff --git a/app/repository.py b/app/repository.py index 8ea7bc4..98d3c99 100644 --- a/app/repository.py +++ b/app/repository.py @@ -5,7 +5,7 @@ # Third party imports from sqlalchemy import select, func from sqlalchemy.orm import aliased -from classes import PlaylistRowDTO +from classes import PlaylistRowObj # App imports from classes import TrackDTO @@ -29,7 +29,7 @@ def tracks_like_title(filter_str: str) -> list[TrackDTO]: ] -def get_playlist_rows(playlist_id: int) -> list[PlaylistRowDTO]: +def get_playlist_rows(playlist_id: int) -> list[PlaylistRowObj]: # Alias PlaydatesTable for subquery LatestPlaydate = aliased(Playdates) @@ -75,7 +75,7 @@ def get_playlist_rows(playlist_id: int) -> list[PlaylistRowDTO]: for row in results: # Handle cases where track_id is None (no track associated) if row.track_id is None: - dto = PlaylistRowDTO( + dto = PlaylistRowObj( artist="", bitrate=0, duration=0, @@ -95,7 +95,7 @@ def get_playlist_rows(playlist_id: int) -> list[PlaylistRowDTO]: # Additional fields like row_fg, row_bg, etc., use default None values ) else: - dto = PlaylistRowDTO( + dto = PlaylistRowObj( artist=row.artist, bitrate=row.bitrate, duration=row.duration,