From a35905dee8861917dbce73da2cbd8514642b615e Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Fri, 3 Nov 2023 15:16:27 +0000 Subject: [PATCH] WIP V3: play track working --- app/classes.py | 20 +++----- app/musicmuster.py | 109 ++++++++++++++++++++----------------------- app/playlistmodel.py | 81 +++++++++++++++++++++++++++----- app/playlists.py | 62 ++++++++++++------------ 4 files changed, 157 insertions(+), 115 deletions(-) diff --git a/app/classes.py b/app/classes.py index ad776b8..28f3bf1 100644 --- a/app/classes.py +++ b/app/classes.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Optional +from typing import Any, Optional from PyQt6.QtCore import pyqtSignal, QObject, QThread import numpy as np @@ -212,16 +212,8 @@ class AddFadeCurve(QObject): self.finished.emit() -@helpers.singleton -class CurrentTrack(PlaylistTrack): - pass - - -@helpers.singleton -class NextTrack(PlaylistTrack): - pass - - -@helpers.singleton -class PreviousTrack(PlaylistTrack): - pass +cnp_tracks = dict( + CurrentTrack = PlaylistTrack(), + NextTrack = PlaylistTrack(), + PreviousTrack = PlaylistTrack(), +) diff --git a/app/musicmuster.py b/app/musicmuster.py index 7c8ef74..b358c97 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -52,12 +52,10 @@ from sqlalchemy import text import stackprinter # type: ignore from classes import ( - CurrentTrack, + cnp_tracks, FadeCurve, MusicMusterSignals, - NextTrack, PlaylistTrack, - PreviousTrack, ) from config import Config from dbconfig import ( @@ -199,10 +197,6 @@ class Window(QMainWindow, Ui_MainWindow): self.music: music.Music = music.Music() self.playing: bool = False - self.current_track = CurrentTrack() - self.next_track = NextTrack() - self.previous_track = PreviousTrack() - self.previous_track_position: Optional[float] = None self.selected_plrs: Optional[List[PlaylistRows]] = None @@ -393,7 +387,7 @@ class Window(QMainWindow, Ui_MainWindow): Clear next track """ - self.next_track = NextTrack() + cnp_tracks['NextTrack'] = PlaylistTrack() self.update_headers() def clear_selection(self) -> None: @@ -714,12 +708,13 @@ class Window(QMainWindow, Ui_MainWindow): # self.current_track.playlist_tab.play_ended() # Reset fade graph - self.current_track.fade_graph.clear() + if cnp_tracks['CurrentTrack'].fade_graph: + cnp_tracks['CurrentTrack'].fade_graph.clear() # Reset PlaylistTrack objects - if self.current_track.track_id: - self.previous_track = cast(PreviousTrack, self.current_track) - self.current_track = CurrentTrack() + if cnp_tracks['CurrentTrack'].track_id: + cnp_tracks['PreviousTrack'] = cnp_tracks['CurrentTrack'] + cnp_tracks['CurrentTrack'] = PlaylistTrack() # Reset clocks self.frame_fade.setStyleSheet("") @@ -790,11 +785,11 @@ class Window(QMainWindow, Ui_MainWindow): times a second; this function has much better resolution. """ - if self.current_track.track_id is None or self.current_track.start_time is None: + if cnp_tracks['CurrentTrack'].track_id is None or cnp_tracks['CurrentTrack'].start_time is None: return 0 now = datetime.now() - track_start = self.current_track.start_time + track_start = cnp_tracks['CurrentTrack'].start_time elapsed_seconds = (now - track_start).total_seconds() return int(elapsed_seconds * 1000) @@ -947,7 +942,7 @@ class Window(QMainWindow, Ui_MainWindow): plrs_to_move = [ plr for plr in playlistrows - if plr.id not in [self.current_track.plr_id, self.next_track.plr_id] + if plr.id not in [cnp_tracks['CurrentTrack'].plr_id, cnp_tracks['NextTrack'].plr_id] ] rows_to_delete = [ @@ -1143,21 +1138,24 @@ class Window(QMainWindow, Ui_MainWindow): - If there is no next track set, return. - If there's currently a track playing, fade it. - Move next track to current track. - - Ensure playlist tabs are the correct colour + - Clear next track - Restore volume if -3dB active - Play (new) current track. - - Tell database to record it as played - - Tell playlist track is now playing + - Ensure 100% volume + - Show closing volume graph + - Notify model - Note that track is now playing - Disable play next controls - Update headers - - Update clocks """ # If there is no next track set, return. - if not self.next_track.track_id: + if not cnp_tracks['NextTrack'].track_id: log.debug("musicmuster.play_next(): no next track selected") return + if not cnp_tracks['NextTrack'].path: + log.debug("musicmuster.play_next(): no path for next track") + return with Session() as session: # If there's currently a track playing, fade it. @@ -1166,15 +1164,10 @@ class Window(QMainWindow, Ui_MainWindow): # Move next track to current track. # stop_playing() above has called end_of_track_actions() # which will have populated self.previous_track - self.current_track = cast(CurrentTrack, self.next_track) - self.clear_next() + cnp_tracks['CurrentTrack'] = cnp_tracks['NextTrack'] - if not self.current_track.track_id: - log.debug("musicmuster.play_next(): no id for next track") - return - if not self.current_track.path: - log.debug("musicmuster.play_next(): no path for next track") - return + # Clear next track + self.clear_next() # Set current track playlist_tab colour # TODO Reimplement without reference to self.current_track.playlist_tab @@ -1187,8 +1180,12 @@ class Window(QMainWindow, Ui_MainWindow): self.btnDrop3db.setChecked(False) # Play (new) current track - self.current_track.start() - self.music.play(self.current_track.path, position) + if not cnp_tracks['CurrentTrack'].path: + return + cnp_tracks['CurrentTrack'].start() + self.music.play(cnp_tracks['CurrentTrack'].path, position) + + # Ensure 100% volume # 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 @@ -1203,15 +1200,11 @@ class Window(QMainWindow, Ui_MainWindow): sleep(0.1) # Show closing volume graph - self.current_track.fade_graph.plot() + if cnp_tracks['CurrentTrack'].fade_graph: + cnp_tracks['CurrentTrack'].fade_graph.plot() - # Tell database to record it as played - Playdates(session, self.current_track.track_id) - - # Tell playlist track is now playing - # TODO Reimplement without reference to self.current_track.playlist_tab: - # if self.current_track.playlist_tab: - # self.current_track.playlist_tab.play_started(session) + # Notify model + self.active_model().current_track_started() # Note that track is now playing self.playing = True @@ -1234,7 +1227,7 @@ class Window(QMainWindow, Ui_MainWindow): track_path = self.active_tab().get_selected_row_track_path() if not track_path: # Otherwise get path to next track to play - track_path = self.next_track.path + track_path = cnp_tracks['NextTrack'].path if not track_path: self.btnPreview.setChecked(False) return @@ -1531,7 +1524,7 @@ class Window(QMainWindow, Ui_MainWindow): Set passed plr_id as next track to play, or clear next track if None Actions required: - - Update self.next_track PlaylistTrack structure + - Update cnp_tracks - Tell playlist tabs to update their 'next track' highlighting - Update headers - Set playlist tab colours @@ -1610,14 +1603,14 @@ class Window(QMainWindow, Ui_MainWindow): # Update volume fade curve if ( - self.current_track.track_id - and self.current_track.fade_graph - and self.current_track.start_time + cnp_tracks['CurrentTrack'].track_id + and cnp_tracks['CurrentTrack'].fade_graph + and cnp_tracks['CurrentTrack'].start_time ): play_time = ( - datetime.now() - self.current_track.start_time + datetime.now() - cnp_tracks['CurrentTrack'].start_time ).total_seconds() * 1000 - self.current_track.fade_graph.tick(play_time) + cnp_tracks['CurrentTrack'].fade_graph.tick(play_time) def tick_500ms(self) -> None: """ @@ -1649,22 +1642,22 @@ class Window(QMainWindow, Ui_MainWindow): # starting play. if ( self.music.player - and self.current_track.start_time + and cnp_tracks['CurrentTrack'].start_time and ( self.music.player.is_playing() - or (datetime.now() - self.current_track.start_time) + or (datetime.now() - cnp_tracks['CurrentTrack'].start_time) < timedelta(microseconds=Config.PLAY_SETTLE) ) ): playtime = self.get_playtime() - time_to_fade = self.current_track.fade_at - playtime - time_to_silence = self.current_track.silence_at - playtime + time_to_fade = cnp_tracks['CurrentTrack'].fade_at - playtime + time_to_silence = cnp_tracks['CurrentTrack'].silence_at - playtime # Elapsed time self.label_elapsed_timer.setText( helpers.ms_to_mmss(playtime) + " / " - + helpers.ms_to_mmss(self.current_track.duration) + + helpers.ms_to_mmss(cnp_tracks['CurrentTrack'].duration) ) # Time to fade @@ -1707,25 +1700,25 @@ class Window(QMainWindow, Ui_MainWindow): Update last / current / next track headers """ - if self.previous_track.title and self.previous_track.artist: + if cnp_tracks['PreviousTrack'].title and cnp_tracks['PreviousTrack'].artist: self.hdrPreviousTrack.setText( - f"{self.previous_track.title} - {self.previous_track.artist}" + f"{cnp_tracks['PreviousTrack'].title} - {cnp_tracks['PreviousTrack'].artist}" ) else: self.hdrPreviousTrack.setText("") - if self.current_track.title and self.current_track.artist: + if cnp_tracks['CurrentTrack'].title and cnp_tracks['CurrentTrack'].artist: self.hdrCurrentTrack.setText( - f"{self.current_track.title.replace('&', '&&')} - " - f"{self.current_track.artist.replace('&', '&&')}" + f"{cnp_tracks['CurrentTrack'].title.replace('&', '&&')} - " + f"{cnp_tracks['CurrentTrack'].artist.replace('&', '&&')}" ) else: self.hdrCurrentTrack.setText("") - if self.next_track.title and self.next_track.artist: + if cnp_tracks['NextTrack'].title and cnp_tracks['NextTrack'].artist: self.hdrNextTrack.setText( - f"{self.next_track.title.replace('&', '&&')} - " - f"{self.next_track.artist.replace('&', '&&')}" + f"{cnp_tracks['NextTrack'].title.replace('&', '&&')} - " + f"{cnp_tracks['NextTrack'].artist.replace('&', '&&')}" ) else: self.hdrNextTrack.setText("") diff --git a/app/playlistmodel.py b/app/playlistmodel.py index 0d06e15..75d4b1b 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -14,12 +14,12 @@ from PyQt6.QtGui import ( QFont, ) -from classes import CurrentTrack, MusicMusterSignals, NextTrack +from classes import cnp_tracks, MusicMusterSignals, PlaylistTrack from config import Config from dbconfig import scoped_session, Session from helpers import file_is_unreadable from log import log -from models import PlaylistRows, Tracks +from models import Playdates, PlaylistRows, Tracks HEADER_NOTES_COLUMN = 1 @@ -104,9 +104,6 @@ class PlaylistModel(QAbstractTableModel): self.signals.add_track_to_header_signal.connect(self.add_track_to_header) self.signals.add_track_to_playlist_signal.connect(self.add_track) - self.current_track = CurrentTrack() - self.next_track = NextTrack() - with Session() as session: self.refresh_data(session) @@ -194,10 +191,10 @@ class PlaylistModel(QAbstractTableModel): if file_is_unreadable(prd.path): return QBrush(QColor(Config.COLOUR_UNREADABLE)) # Current track - if prd.plrid == self.current_track.plr_id: + if prd.plrid == cnp_tracks['CurrentTrack'].plr_id: return QBrush(QColor(Config.COLOUR_CURRENT_PLAYLIST)) # Next track - if prd.plrid == self.next_track.plr_id: + if prd.plrid == cnp_tracks['NextTrack'].plr_id: return QBrush(QColor(Config.COLOUR_NEXT_PLAYLIST)) # Individual cell colouring @@ -219,6 +216,66 @@ class PlaylistModel(QAbstractTableModel): return 9 + def current_track_started(self) -> None: + """ + Notification from musicmuster that the current track has just + started playing + + Actions required: + - sanity check + - update display + - update track times + - update Playdates in database + - update PlaylistRows in database + - find next track + """ + + # Sanity check + if not cnp_tracks['CurrentTrack'].track_id: + log.error( + "playlistmodel:current_track_started called with no current track" + ) + return + if cnp_tracks['CurrentTrack'].plr_rownum is None: + log.error( + "playlistmodel:current_track_started called with no row number " + f"({self.current_track=})" + ) + return + + # Update display + self.invalidate_row(cnp_tracks['CurrentTrack'].plr_rownum) + + # Update track times + # TODO + + # Update Playdates in database + with Session() as session: + Playdates(session, cnp_tracks['CurrentTrack'].track_id) + plr = session.get(PlaylistRows, cnp_tracks['CurrentTrack'].plr_id) + if plr: + plr.played = True + + # Find next track + # Get all unplayed track rows + next_row = None + unplayed_rows = [ + a.plr_rownum + for a in PlaylistRows.get_unplayed_rows(session, self.playlist_id) + ] + + if unplayed_rows: + try: + # Find next row after current track + next_row = min( + [a for a in unplayed_rows if a > cnp_tracks['CurrentTrack'].plr_rownum] + ) + except ValueError: + # Find first unplayed track + next_row = min(unplayed_rows) + if next_row is not None: + self.set_next_row(next_row) + def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole): """Return data to view""" @@ -523,14 +580,14 @@ class PlaylistModel(QAbstractTableModel): return len(self.playlist_rows) - def set_next_track(self, row_number: int) -> None: + def set_next_row(self, row_number: int) -> None: """ - Update display if necessary + Set row_number as next track """ - # Update self.next_track PlaylistTrack structure + # Update cnp_tracks with Session() as session: - self.next_track = NextTrack() + cnp_tracks['NextTrack'] = PlaylistTrack() try: plrid = self.playlist_rows[row_number].plrid except IndexError: @@ -547,7 +604,7 @@ class PlaylistModel(QAbstractTableModel): # Check track is readable if file_is_unreadable(plr.track.path): return - self.next_track.set_plr(session, plr) + cnp_tracks['NextTrack'].set_plr(session, plr) self.signals.next_track_changed_signal.emit() self.invalidate_row(row_number) diff --git a/app/playlists.py b/app/playlists.py index a3fd28c..c45cf65 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -1028,7 +1028,7 @@ class PlaylistTab(QTableView): if selected_row is None: return model = cast(PlaylistModel, self.model()) - model.set_next_track(selected_row) + model.set_next_row(selected_row) self.clearSelection() # def tab_visible(self) -> None: @@ -1266,41 +1266,41 @@ class PlaylistTab(QTableView): self._update_start_end_times(session) - def _find_next_track_row( - self, session: scoped_session, starting_row: Optional[int] = None - ) -> Optional[int]: - """ - Find next track to play. If a starting row is given, start there; - otherwise, start from top. Skip rows already played. + # def _find_next_track_row( + # self, session: scoped_session, starting_row: Optional[int] = None + # ) -> Optional[int]: + # """ + # Find next track to play. If a starting row is given, start there; + # otherwise, start from top. Skip rows already played. - If not found, return None. + # If not found, return None. - If found, return row number. - """ + # If found, return row number. + # """ - if starting_row is None: - starting_row = 0 + # if starting_row is None: + # starting_row = 0 - track_rows = [ - p.plr_rownum - for p in PlaylistRows.get_rows_with_tracks(session, self.playlist_id) - ] - played_rows = [ - p.plr_rownum - for p in PlaylistRows.get_played_rows(session, self.playlist_id) - ] - for row_number in range(starting_row, self.rowCount()): - if row_number not in track_rows or row_number in played_rows: - continue - plr = self._get_row_plr(session, row_number) - if not plr: - continue - if file_is_unreadable(plr.track.path): - continue - else: - return row_number + # track_rows = [ + # p.plr_rownum + # for p in PlaylistRows.get_rows_with_tracks(session, self.playlist_id) + # ] + # played_rows = [ + # p.plr_rownum + # for p in PlaylistRows.get_played_rows(session, self.playlist_id) + # ] + # for row_number in range(starting_row, self.rowCount()): + # if row_number not in track_rows or row_number in played_rows: + # continue + # plr = self._get_row_plr(session, row_number) + # if not plr: + # continue + # if file_is_unreadable(plr.track.path): + # continue + # else: + # return row_number - return None + # return None def _get_current_track_row_number(self) -> Optional[int]: """Return current track row or None"""