From 8ea0a0dad53c782184b2fd334dfbd06a613f41bf Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sat, 1 Jun 2024 17:41:22 +0100 Subject: [PATCH] WIP: moving player to PlaylistTrack. Player works. --- app/classes.py | 169 ++++++++++++++++++------------ app/music.py | 14 +-- app/musicmuster.py | 145 +++++++++++++------------- app/playlistmodel.py | 240 +++++++++++++++++++++---------------------- app/playlists.py | 28 +++-- 5 files changed, 320 insertions(+), 276 deletions(-) diff --git a/app/classes.py b/app/classes.py index 1bdf518..1fdecc9 100644 --- a/app/classes.py +++ b/app/classes.py @@ -10,12 +10,12 @@ from PyQt6.QtCore import pyqtSignal, QObject, QThread # Third party imports import numpy as np import pyqtgraph as pg # type: ignore -from sqlalchemy.orm import scoped_session # App imports from config import Config from log import log -from models import PlaylistRows +from models import db, PlaylistRows, Tracks +from music import Music import helpers @@ -116,68 +116,42 @@ class MusicMusterSignals(QObject): super().__init__() -class PlaylistTrack: +class _TrackPlayer: """ - Used to provide a single reference point for specific playlist tracks, + Object to manage active playlist tracks, typically the previous, current and next track. """ - def __init__(self) -> None: + def __init__(self, session: db.Session, player_name: str, track_id: int) -> None: """ - Only initialises data structure. Call set_plr to populate. + Initialises data structure. + Define a player. + Raise ValueError if no track in passed plr. """ - self.artist: Optional[str] = None - self.duration: Optional[int] = None - self.end_time: Optional[dt.datetime] = None - self.fade_at: Optional[int] = None - self.fade_graph: Optional[FadeCurve] = None - self.fade_graph_start_updates: Optional[dt.datetime] = None - self.fade_length: Optional[int] = None - self.path: Optional[str] = None - self.playlist_id: Optional[int] = None - self.plr_id: Optional[int] = None - self.plr_rownum: Optional[int] = None - self.resume_marker: Optional[float] = None - self.silence_at: Optional[int] = None - self.start_gap: Optional[int] = None - self.start_time: Optional[dt.datetime] = None - self.title: Optional[str] = None - self.track_id: Optional[int] = None - - def __repr__(self) -> str: - return ( - f"" - ) - - def set_plr(self, session: scoped_session, plr: PlaylistRows) -> None: - """ - Update with new plr information - """ - - session.add(plr) - self.plr_rownum = plr.plr_rownum - if not plr.track: - return - track = plr.track + track = session.get(Tracks, track_id) + if not track: + raise ValueError(f"_TrackPlayer: unable to retreived {track_id=}") + self.player_name = player_name self.artist = track.artist + self.bitrate = track.bitrate self.duration = track.duration - self.end_time = None self.fade_at = track.fade_at self.intro = track.intro self.path = track.path - self.playlist_id = plr.playlist_id - self.plr_id = plr.id self.silence_at = track.silence_at self.start_gap = track.start_gap - self.start_time = None self.title = track.title self.track_id = track.id - if track.silence_at and track.fade_at: - self.fade_length = track.silence_at - track.fade_at + self.end_time: Optional[dt.datetime] = None + self.fade_graph: Optional[FadeCurve] = None + self.fade_graph_start_updates: Optional[dt.datetime] = None + self.resume_marker: Optional[float] + self.start_time: Optional[dt.datetime] = None + + self.player = Music(name=player_name) # Initialise and add FadeCurve in a thread as it's slow self.fadecurve_thread = QThread() @@ -194,15 +168,25 @@ class PlaylistTrack: self.fadecurve_thread.finished.connect(self.fadecurve_thread.deleteLater) self.fadecurve_thread.start() - def start(self) -> None: - """ - Called when track starts playing - """ + def __repr__(self) -> str: + return ( + f"<_TrackPlayer(title={self.title}, artist={self.artist}, " + f"player_name={self.player_name}>" + ) + + def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None: + """Fade music""" + + self.player.fade(fade_seconds) + + def play(self, position: Optional[float] = None) -> None: + """Play track""" now = dt.datetime.now() self.start_time = now - if self.duration: - self.end_time = self.start_time + dt.timedelta(milliseconds=self.duration) + self.player.play(self.path, position) + + self.end_time = self.start_time + dt.timedelta(milliseconds=self.duration) # Calculate time fade_graph should start updating if self.fade_at: @@ -213,13 +197,68 @@ class PlaylistTrack: milliseconds=update_graph_at_ms ) - # Calculate time fade_graph should start updating - update_graph_at_ms = max( - 0, self.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1 - ) - self.fade_graph_start_updates = now + dt.timedelta( - milliseconds=update_graph_at_ms - ) + def stop_playing(self, fade_seconds: int = 0) -> None: + """ + Stop this track playing + """ + + self.resume_marker = self.player.get_position() + self.player.fade(fade_seconds) + + # Reset fade graph + if self.fade_graph: + self.fade_graph.clear() + + +class MainTrackPlayer(_TrackPlayer): + def __init__(self, session: db.Session, track_id: int) -> None: + super().__init__( + session=session, player_name=Config.VLC_MAIN_PLAYER_NAME, track_id=track_id + ) + + +class PreviewTrackPlayer(_TrackPlayer): + def __init__(self, session: db.Session, track_id: int) -> None: + super().__init__( + session=session, + player_name=Config.VLC_PREVIEW_PLAYER_NAME, + track_id=track_id, + ) + + +class PlaylistTrack: + """ + Used to provide a single reference point for specific playlist tracks, + typically the previous, current and next track. + """ + + def __init__(self, plrid: int) -> None: + """ + Initialise + """ + + with db.Session() as session: + # Ensure we have a track + plr = session.get(PlaylistRows, plrid) + if not plr: + raise ValueError(f"PlaylistTrack: unable to retreive plr {plrid=}") + + self.track_id: int = plr.track_id + + # Save non-track plr info + self.row_number: int = plr.plr_rownum + self.playlist_id: int = plr.playlist_id + self.plr_id: int = plr.id + + # Initialise player + self.track_player = MainTrackPlayer(session=session, track_id=self.track_id) + + def __repr__(self) -> str: + return ( + f"" + ) @dataclass @@ -246,13 +285,13 @@ class AddFadeCurve(QObject): def __init__( self, - playlist_track: PlaylistTrack, + track_player: _TrackPlayer, track_path: str, track_fade_at: int, track_silence_at: int, ): super().__init__() - self.playlist_track = playlist_track + self.track_player = track_player self.track_path = track_path self.track_fade_at = track_fade_at self.track_silence_at = track_silence_at @@ -266,14 +305,14 @@ class AddFadeCurve(QObject): if not fc: log.error(f"Failed to create FadeCurve for {self.track_path=}") else: - self.playlist_track.fade_graph = fc + self.track_player.fade_graph = fc self.finished.emit() class TrackSequence: - next = PlaylistTrack() - now = PlaylistTrack() - previous = PlaylistTrack() + next: Optional[PlaylistTrack] = None + current: Optional[PlaylistTrack] = None + previous: Optional[PlaylistTrack] = None track_sequence = TrackSequence() diff --git a/app/music.py b/app/music.py index 17a3619..cc8be00 100644 --- a/app/music.py +++ b/app/music.py @@ -84,7 +84,7 @@ class Music: else: self.start_dt -= dt.timedelta(milliseconds=ms) - def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None: + def fade(self, fade_seconds: int) -> None: """ Fade the currently playing track. @@ -100,6 +100,10 @@ class Music: if not self.player.get_position() > 0 and self.player.is_playing(): return + if fade_seconds <= 0: + self._stop() + return + # Take a copy of current player to allow another track to be # started without interfering here with lock: @@ -168,7 +172,7 @@ class Music: self._adjust_by_ms(ms) - def play(self, path: str, position: Optional[float] = None) -> None: + def play(self, path: str, position: Optional[float]) -> None: """ Start playing the track at path. @@ -238,7 +242,7 @@ class Music: log.debug(f"Reset from {volume=}") sleep(0.1) - def stop(self) -> float: + def _stop(self) -> None: """Immediately stop playing""" log.info(f"Music[{self.name}].stop()") @@ -246,15 +250,13 @@ class Music: self.start_dt = None if not self.player: - return 0.0 + return p = self.player self.player = None self.start_dt = None with lock: - position = p.get_position() p.stop() p.release() p = None - return position diff --git a/app/musicmuster.py b/app/musicmuster.py index 0a2eb13..f769ba0 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -59,6 +59,7 @@ from classes import ( FadeCurve, MusicMusterSignals, PlaylistTrack, + PreviewTrackPlayer, TrackFileData, ) from config import Config @@ -231,9 +232,8 @@ class Window(QMainWindow, Ui_MainWindow): self.timer1000: QTimer = QTimer() self.music: music.Music = music.Music(name=Config.VLC_MAIN_PLAYER_NAME) - self.preview_player: music.Music = music.Music( - name=Config.VLC_PREVIEW_PLAYER_NAME - ) + self.preview_track_player: Optional[PreviewTrackPlayer] = None + self.playing: bool = False self.set_main_window_size() @@ -430,7 +430,7 @@ class Window(QMainWindow, Ui_MainWindow): Clear next track """ - track_sequence.next = PlaylistTrack() + track_sequence.next = None self.update_headers() def clear_selection(self) -> None: @@ -521,7 +521,10 @@ class Window(QMainWindow, Ui_MainWindow): """ # Don't close current track playlist - current_track_playlist_id = track_sequence.now.playlist_id + if track_sequence.current is None: + return True + + current_track_playlist_id = track_sequence.current.playlist_id closing_tab_playlist_id = self.tabPlaylist.widget(tab_index).playlist_id if current_track_playlist_id: if closing_tab_playlist_id == current_track_playlist_id: @@ -1145,11 +1148,12 @@ class Window(QMainWindow, Ui_MainWindow): """ # Check for inadvertent press of 'return' - if self.catch_return_key: + if track_sequence.current and self.catch_return_key: # Suppress inadvertent double press if ( - track_sequence.now.start_time - and track_sequence.now.start_time + track_sequence.current + and track_sequence.current.track_player.start_time + and track_sequence.current.track_player.start_time + dt.timedelta(milliseconds=Config.RETURN_KEY_DEBOUNCE_MS) > dt.datetime.now() ): @@ -1157,10 +1161,16 @@ class Window(QMainWindow, Ui_MainWindow): # If return is pressed during first PLAY_NEXT_GUARD_MS then # default to NOT playing the next track, else default to # playing it. - default_yes: bool = track_sequence.now.start_time is not None and ( - (dt.datetime.now() - track_sequence.now.start_time).total_seconds() - * 1000 - > Config.PLAY_NEXT_GUARD_MS + default_yes: bool = ( + track_sequence.current.track_player.start_time is not None + and ( + ( + dt.datetime.now() + - track_sequence.current.track_player.start_time + ).total_seconds() + * 1000 + > Config.PLAY_NEXT_GUARD_MS + ) ) if not helpers.ask_yes_no( "Track playing", @@ -1173,12 +1183,9 @@ class Window(QMainWindow, Ui_MainWindow): log.info(f"play_next({position=})") # If there is no next track set, return. - if not track_sequence.next.track_id: + if track_sequence.next is None: log.error("musicmuster.play_next(): no next track selected") return - if not track_sequence.next.path: - log.error("musicmuster.play_next(): no path for next track") - return # Issue #223 concerns a very short pause (maybe 0.1s) sometimes # when starting to play at track. @@ -1195,7 +1202,7 @@ 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 - track_sequence.now = track_sequence.next + track_sequence.current = track_sequence.next # Clear next track self.clear_next() @@ -1206,21 +1213,7 @@ class Window(QMainWindow, Ui_MainWindow): self.btnDrop3db.setChecked(False) # Play (new) current track - if not track_sequence.now.path: - log.error("No path for next track") - return - self.music.play(track_sequence.now.path, position) - - # Show closing volume graph - if track_sequence.now.fade_graph: - track_sequence.now.fade_graph.plot() - else: - log.error("No fade_graph") - - # Note that track is playing - log.debug("set track_sequence") - track_sequence.now.start() - self.playing = True + track_sequence.current.track_player.play(position) # Disable play next controls self.catch_return_key = True @@ -1248,18 +1241,22 @@ class Window(QMainWindow, Ui_MainWindow): """ if self.btnPreview.isChecked(): - # Get track path for first selected track if there is one - track_path = self.active_tab().get_selected_row_track_path() - if not track_path: + # Get track_id for first selected track if there is one + track_id = self.active_tab().get_selected_row_track_id() + if not track_id: # Otherwise get path to next track to play - track_path = track_sequence.next.path - if not track_path: + if track_sequence.next: + track_id = track_sequence.next.track_id + if not track_id: self.btnPreview.setChecked(False) return - self.preview_player.play(path=track_path) + with db.Session() as session: + self.preview_track_player = PreviewTrackPlayer(session, track_id) + self.preview_track_player.play() else: - self.preview_player.stop() + if self.preview_track_player: + self.preview_track_player.stop_playing() self.label_intro_timer.setText("0.0") self.btnPreviewMark.setEnabled(False) self.btnPreviewArm.setChecked(False) @@ -1272,7 +1269,7 @@ class Window(QMainWindow, Ui_MainWindow): def preview_back(self) -> None: """Wind back preview file""" - self.preview_player.move_back(Config.PREVIEW_BACK_MS) + self.preview_track_player.move_back(Config.PREVIEW_BACK_MS) def preview_end(self) -> None: """Advance preview file to just before end of intro""" @@ -1316,7 +1313,7 @@ class Window(QMainWindow, Ui_MainWindow): def preview_start(self) -> None: """Advance preview file""" - self.preview_player.set_position(0) + self.preview_track_player.set_position(0) def rename_playlist(self) -> None: """ @@ -1432,12 +1429,14 @@ class Window(QMainWindow, Ui_MainWindow): # We need to fake the start time to reflect where we resumed the # track if ( - track_sequence.now.start_time - and track_sequence.now.duration - and track_sequence.now.resume_marker + track_sequence.current.start_time + and track_sequence.current.duration + and track_sequence.current.resume_marker ): - elapsed_ms = track_sequence.now.duration * track_sequence.now.resume_marker - track_sequence.now.start_time -= dt.timedelta(milliseconds=elapsed_ms) + elapsed_ms = ( + track_sequence.current.duration * track_sequence.current.resume_marker + ) + track_sequence.current.start_time -= dt.timedelta(milliseconds=elapsed_ms) def save_as_template(self) -> None: """Save current playlist as template""" @@ -1563,7 +1562,8 @@ class Window(QMainWindow, Ui_MainWindow): def show_current(self) -> None: """Scroll to show current track""" - self.show_track(track_sequence.now) + if track_sequence.current: + self.show_track(track_sequence.current) def show_warning(self, title: str, body: str) -> None: """ @@ -1576,7 +1576,8 @@ class Window(QMainWindow, Ui_MainWindow): def show_next(self) -> None: """Scroll to show next track""" - self.show_track(track_sequence.next) + if track_sequence.next: + self.show_track(track_sequence.next) def show_status_message(self, message: str, timing: int) -> None: """ @@ -1585,25 +1586,25 @@ class Window(QMainWindow, Ui_MainWindow): self.statusbar.showMessage(message, timing) - def show_track(self, plt: PlaylistTrack) -> None: + def show_track(self, playlist_track: PlaylistTrack) -> None: """Scroll to show track in plt""" # Switch to the correct tab - plt_playlist_id = plt.playlist_id - if not plt_playlist_id: + playlist_id = playlist_track.playlist_id + if not playlist_id: # No playlist return - if plt_playlist_id != self.active_tab().playlist_id: + if playlist_id != self.active_tab().playlist_id: for idx in range(self.tabPlaylist.count()): - if self.tabPlaylist.widget(idx).playlist_id == plt_playlist_id: + if self.tabPlaylist.widget(idx).playlist_id == playlist_id: self.tabPlaylist.setCurrentIndex(idx) break display_row = ( self.active_proxy_model() .mapFromSource( - self.active_proxy_model().source_model.index(plt.plr_rownum, 0) + self.active_proxy_model().source_model.index(playlist_track.row_number, 0) ) .row() ) @@ -1710,22 +1711,23 @@ class Window(QMainWindow, Ui_MainWindow): Called every 10ms """ + return # Update volume fade curve if ( - track_sequence.now.fade_graph_start_updates is None - or track_sequence.now.fade_graph_start_updates > dt.datetime.now() + track_sequence.current.fade_graph_start_updates is None + or track_sequence.current.fade_graph_start_updates > dt.datetime.now() ): return if ( - track_sequence.now.track_id - and track_sequence.now.fade_graph - and track_sequence.now.start_time + track_sequence.current.track_id + and track_sequence.current.fade_graph + and track_sequence.current.start_time ): play_time = ( - dt.datetime.now() - track_sequence.now.start_time + dt.datetime.now() - track_sequence.current.start_time ).total_seconds() * 1000 - track_sequence.now.fade_graph.tick(play_time) + track_sequence.current.fade_graph.tick(play_time) def tick_500ms(self) -> None: """ @@ -1845,25 +1847,26 @@ class Window(QMainWindow, Ui_MainWindow): Update last / current / next track headers """ - if track_sequence.previous.title and track_sequence.previous.artist: - self.hdrPreviousTrack.setText( - f"{track_sequence.previous.title} - {track_sequence.previous.artist}" - ) + if track_sequence.previous: + player = track_sequence.previous.track_player + self.hdrPreviousTrack.setText(f"{player.title} - {player.artist}") else: self.hdrPreviousTrack.setText("") - if track_sequence.now.title and track_sequence.now.artist: + if track_sequence.current: + player = track_sequence.current.track_player self.hdrCurrentTrack.setText( - f"{track_sequence.now.title.replace('&', '&&')} - " - f"{track_sequence.now.artist.replace('&', '&&')}" + f"{player.title.replace('&', '&&')} - " + f"{player.artist.replace('&', '&&')}" ) else: self.hdrCurrentTrack.setText("") - if track_sequence.next.title and track_sequence.next.artist: + if track_sequence.next: + player = track_sequence.next.track_player self.hdrNextTrack.setText( - f"{track_sequence.next.title.replace('&', '&&')} - " - f"{track_sequence.next.artist.replace('&', '&&')}" + f"{player.title.replace('&', '&&')} - " + f"{player.artist.replace('&', '&&')}" ) else: self.hdrNextTrack.setText("") diff --git a/app/playlistmodel.py b/app/playlistmodel.py index 14780fc..1e37608 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -47,7 +47,7 @@ HEADER_NOTES_COLUMN = 1 scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]") -class PlaylistRowData: +class _PlaylistRowData: def __init__(self, plr: PlaylistRows) -> None: """ Populate PlaylistRowData from database PlaylistRows record @@ -117,7 +117,7 @@ class PlaylistModel(QAbstractTableModel): self.playlist_id = playlist_id super().__init__(*args, **kwargs) - self.playlist_rows: dict[int, PlaylistRowData] = {} + self.playlist_rows: dict[int, _PlaylistRowData] = {} self.signals = MusicMusterSignals() self.played_tracks_hidden = False @@ -178,7 +178,7 @@ class PlaylistModel(QAbstractTableModel): self.signals.resize_rows_signal.emit(self.playlist_id) - def background_role(self, row: int, column: int, prd: PlaylistRowData) -> QBrush: + def background_role(self, row: int, column: int, prd: _PlaylistRowData) -> QBrush: """Return background setting""" # Handle entire row colouring @@ -195,10 +195,10 @@ class PlaylistModel(QAbstractTableModel): if file_is_unreadable(prd.path): return QBrush(QColor(Config.COLOUR_UNREADABLE)) # Current track - if prd.plrid == track_sequence.now.plr_id: + if track_sequence.current and track_sequence.current.track_id == prd.track_id: return QBrush(QColor(Config.COLOUR_CURRENT_PLAYLIST)) # Next track - if prd.plrid == track_sequence.next.plr_id: + if track_sequence.next and track_sequence.next.track_id == prd.track_id: return QBrush(QColor(Config.COLOUR_NEXT_PLAYLIST)) # Individual cell colouring @@ -250,24 +250,10 @@ class PlaylistModel(QAbstractTableModel): - find next track """ - row_number = track_sequence.now.plr_rownum - if row_number is not None: - prd = self.playlist_rows[row_number] - else: - prd = None + if not track_sequence.current: + return - # Sanity check - if not track_sequence.now.track_id: - log.error( - "playlistmodel:current_track_started called with no current track" - ) - return - if row_number is None: - log.error( - "playlistmodel:current_track_started called with no row number " - f"({track_sequence.now=})" - ) - return + row_number = track_sequence.current.row_number # Check for OBS scene change log.debug("Call OBS scene change") @@ -276,29 +262,23 @@ class PlaylistModel(QAbstractTableModel): with db.Session() as session: # Update Playdates in database log.debug("update playdates") - Playdates(session, track_sequence.now.track_id) + Playdates(session, track_sequence.current.track_id) # Mark track as played in playlist log.debug("Mark track as played") - plr = session.get(PlaylistRows, track_sequence.now.plr_id) + plr = session.get(PlaylistRows, track_sequence.current.plr_id) if plr: plr.played = True self.refresh_row(session, plr.plr_rownum) else: - log.error(f"Can't retrieve plr, {track_sequence.now.plr_id=}") - - # Update track times - log.debug("Update track times") - if prd: - prd.start_time = track_sequence.now.start_time - prd.end_time = track_sequence.now.end_time + log.error(f"Can't retrieve plr, {track_sequence.current.plr_id=}") # Update colour and times for current row self.invalidate_row(row_number) # Update previous row in case we're hiding played rows - if track_sequence.previous.plr_rownum: - self.invalidate_row(track_sequence.previous.plr_rownum) + if track_sequence.previous and track_sequence.previous.row_number: + self.invalidate_row(track_sequence.previous.row_number) # Update all other track times self.update_track_times() @@ -393,7 +373,7 @@ class PlaylistModel(QAbstractTableModel): self.reset_track_sequence_row_numbers() - def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: + def display_role(self, row: int, column: int, prd: _PlaylistRowData) -> QVariant: """ Return text for display """ @@ -466,7 +446,7 @@ class PlaylistModel(QAbstractTableModel): super().endResetModel() self.reset_track_sequence_row_numbers() - def edit_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: + def edit_role(self, row: int, column: int, prd: _PlaylistRowData) -> QVariant: """ Return text for editing """ @@ -510,7 +490,7 @@ class PlaylistModel(QAbstractTableModel): return default - def font_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: + def font_role(self, row: int, column: int, prd: _PlaylistRowData) -> QVariant: """ Return font """ @@ -569,7 +549,7 @@ class PlaylistModel(QAbstractTableModel): log.debug(f"get_new_row_number() return: {new_row_number=}") return new_row_number - def get_row_info(self, row_number: int) -> PlaylistRowData: + def get_row_info(self, row_number: int) -> _PlaylistRowData: """ Return info about passed row """ @@ -659,7 +639,7 @@ class PlaylistModel(QAbstractTableModel): return QVariant() - def header_text(self, prd: PlaylistRowData) -> str: + def header_text(self, prd: _PlaylistRowData) -> str: """ Process possible section timing directives embeded in header """ @@ -703,16 +683,16 @@ class PlaylistModel(QAbstractTableModel): # calculate end time if all tracks are played. end_time_str = "" if ( - track_sequence.now.plr_rownum - and track_sequence.now.end_time + track_sequence.current + and track_sequence.current.track_player.end_time and ( row_number - < track_sequence.now.plr_rownum + < track_sequence.current.row_number < prd.plr_rownum ) ): section_end_time = ( - track_sequence.now.end_time + track_sequence.current.track_player.end_time + dt.timedelta(milliseconds=duration) ) end_time_str = ( @@ -830,7 +810,7 @@ class PlaylistModel(QAbstractTableModel): return self.playlist_rows[row_number].played - def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]: + def is_track_in_playlist(self, track_id: int) -> Optional[_PlaylistRowData]: """ If this track_id is in the playlist, return the PlaylistRowData object else return None @@ -906,14 +886,16 @@ class PlaylistModel(QAbstractTableModel): row_map[old_row] = new_row # Check to see whether any rows in track_sequence have moved - if track_sequence.previous.plr_rownum in row_map: - track_sequence.previous.plr_rownum = row_map[ - track_sequence.previous.plr_rownum + if track_sequence.previous and track_sequence.previous.row_number in row_map: + track_sequence.previous.row_number = row_map[ + track_sequence.previous.row_number ] - if track_sequence.now.plr_rownum in row_map: - track_sequence.now.plr_rownum = row_map[track_sequence.now.plr_rownum] - if track_sequence.next.plr_rownum in row_map: - track_sequence.next.plr_rownum = row_map[track_sequence.next.plr_rownum] + if track_sequence.current and track_sequence.current.row_number in row_map: + track_sequence.current.row_number = row_map[ + track_sequence.current.row_number + ] + if track_sequence.next and track_sequence.next.row_number in row_map: + track_sequence.next.row_number = row_map[track_sequence.next.row_number] # For SQLAlchemy, build a list of dictionaries that map plrid to # new row number: @@ -973,7 +955,10 @@ class PlaylistModel(QAbstractTableModel): self.playlist_id, [self.playlist_rows[a].plrid for a in row_group], ): - if plr.id == track_sequence.now.plr_id: + if ( + track_sequence.current + and plr.id == track_sequence.current.plr_id + ): # Don't move current track continue plr.playlist_id = to_playlist_id @@ -994,7 +979,7 @@ class PlaylistModel(QAbstractTableModel): self.update_track_times() def move_track_add_note( - self, new_row_number: int, existing_prd: PlaylistRowData, note: str + self, new_row_number: int, existing_prd: _PlaylistRowData, note: str ) -> None: """ Move existing_prd track to new_row_number and append note to any existing note @@ -1018,7 +1003,10 @@ class PlaylistModel(QAbstractTableModel): self.signals.resize_rows_signal.emit(self.playlist_id) def move_track_to_header( - self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str] + self, + header_row_number: int, + existing_prd: _PlaylistRowData, + note: Optional[str], ) -> None: """ Add the existing_prd track details to the existing header at header_row_number @@ -1080,10 +1068,10 @@ class PlaylistModel(QAbstractTableModel): log.info("previous_track_ended()") # Sanity check - if not track_sequence.previous.track_id: + if not track_sequence.previous: log.error("playlistmodel:previous_track_ended called with no current track") return - if track_sequence.previous.plr_rownum is None: + if track_sequence.previous.row_number is None: log.error( "playlistmodel:previous_track_ended called with no row number " f"({track_sequence.previous=})" @@ -1091,7 +1079,7 @@ class PlaylistModel(QAbstractTableModel): return # Update display - self.invalidate_row(track_sequence.previous.plr_rownum) + self.invalidate_row(track_sequence.previous.row_number) def refresh_data(self, session: db.session): """Populate dicts for data calls""" @@ -1099,13 +1087,13 @@ class PlaylistModel(QAbstractTableModel): # Populate self.playlist_rows with playlist data self.playlist_rows.clear() for p in PlaylistRows.deep_rows(session, self.playlist_id): - self.playlist_rows[p.plr_rownum] = PlaylistRowData(p) + self.playlist_rows[p.plr_rownum] = _PlaylistRowData(p) def refresh_row(self, session, row_number): """Populate dict for one row from database""" p = PlaylistRows.deep_row(session, self.playlist_id, row_number) - self.playlist_rows[row_number] = PlaylistRowData(p) + self.playlist_rows[row_number] = _PlaylistRowData(p) def remove_track(self, row_number: int) -> None: """ @@ -1145,21 +1133,23 @@ class PlaylistModel(QAbstractTableModel): log.debug("reset_track_sequence_row_numbers()") - # Check the track_sequence next, now and previous plrs and + # Check the track_sequence next, current and previous plrs and # update the row number with db.Session() as session: - if track_sequence.next.plr_rownum: - next_plr = session.get(PlaylistRows, track_sequence.next.plr_id) + if track_sequence.next and track_sequence.next.row_number: + next_plr = session.get(PlaylistRows, track_sequence.next.row_number) if next_plr: - track_sequence.next.plr_rownum = next_plr.plr_rownum - if track_sequence.now.plr_rownum: - now_plr = session.get(PlaylistRows, track_sequence.now.plr_id) + track_sequence.next.row_number = next_plr.plr_rownum + if track_sequence.current and track_sequence.current.row_number: + now_plr = session.get(PlaylistRows, track_sequence.current.row_number) if now_plr: - track_sequence.now.plr_rownum = now_plr.plr_rownum - if track_sequence.previous.plr_rownum: - previous_plr = session.get(PlaylistRows, track_sequence.previous.plr_id) + track_sequence.current.row_number = now_plr.plr_rownum + if track_sequence.previous and track_sequence.previous.row_number: + previous_plr = session.get( + PlaylistRows, track_sequence.previous.row_number + ) if previous_plr: - track_sequence.previous.plr_rownum = previous_plr.plr_rownum + track_sequence.previous.row_number = previous_plr.plr_rownum self.update_track_times() @@ -1240,59 +1230,53 @@ class PlaylistModel(QAbstractTableModel): Set row_number as next track. If row_number is None, clear next track. """ - log.info(f"set_next_row({row_number=})") - - next_row_was = track_sequence.next.plr_rownum + log.debug(f"set_next_row({row_number=})") if row_number is None: - if next_row_was is None: + # Clear next track + if track_sequence.next: + track_sequence.next = None + else: return - track_sequence.next = PlaylistTrack() - self.signals.next_track_changed_signal.emit() - return - - # Update track_sequence - with db.Session() as session: - track_sequence.next = PlaylistTrack() + else: + # Get plrid of row try: - plrid = self.playlist_rows[row_number].plrid + prd = self.playlist_rows[row_number] except IndexError: log.error( f"playlistmodel.set_next_track({row_number=}, " f"{self.playlist_id=}" + "IndexError" ) return - plr = session.get(PlaylistRows, plrid) - if plr: - # Check this isn't a header row - if self.is_header_row(row_number): - log.error( - "Tried to set next row on header row: " - f"playlistmodel.set_next_track({row_number=}, " - f"{self.playlist_id=}" - ) - return - # Check track is readable - if file_is_unreadable(plr.track.path): - log.error( - "Tried to set next row on unreadable row: " - f"playlistmodel.set_next_track({row_number=}, " - f"{self.playlist_id=}" - ) - return - track_sequence.next.set_plr(session, plr) - self.signals.next_track_changed_signal.emit() - self.signals.search_wikipedia_signal.emit( - self.playlist_rows[row_number].title + if prd.track_id is None or prd.plr_rownum is None: + log.error( + f"playlistmodel.set_next_track({row_number=}, " + "No track / row number " + f"{self.playlist_id=}, {prd.track_id=}, {prd.plr_rownum=}" ) - self.invalidate_row(row_number) + return - if next_row_was is not None: - self.invalidate_row(next_row_was) + try: + track_sequence.next = PlaylistTrack(prd.plrid) + self.invalidate_row(row_number) + except ValueError as e: + log.error(f"Error creating PlaylistTrack({prd=}): ({str(e)})") + return + + self.signals.search_wikipedia_signal.emit( + self.playlist_rows[row_number].title + ) + self.invalidate_row(row_number) + + self.signals.next_track_changed_signal.emit() self.update_track_times() def setData( - self, index: QModelIndex, value: str | float, role: int = Qt.ItemDataRole.EditRole + self, + index: QModelIndex, + value: str | float, + role: int = Qt.ItemDataRole.EditRole, ) -> bool: """ Update model with edited data @@ -1396,7 +1380,7 @@ class PlaylistModel(QAbstractTableModel): def supportedDropActions(self) -> Qt.DropAction: return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction - def tooltip_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: + def tooltip_role(self, row: int, column: int, prd: _PlaylistRowData) -> QVariant: """ Return tooltip. Currently only used for last_played column. """ @@ -1434,9 +1418,9 @@ class PlaylistModel(QAbstractTableModel): prd = self.playlist_rows[row_number] # Reset start_time if this is the current row - if row_number == track_sequence.now.plr_rownum: - prd.start_time = track_sequence.now.start_time - prd.end_time = track_sequence.now.end_time + if row_number == track_sequence.current.plr_rownum: + prd.start_time = track_sequence.current.start_time + prd.end_time = track_sequence.current.end_time update_rows.append(row_number) if not next_start_time: next_start_time = prd.end_time @@ -1445,9 +1429,9 @@ class PlaylistModel(QAbstractTableModel): # Set start time for next row if we have a current track if ( row_number == track_sequence.next.plr_rownum - and track_sequence.now.end_time + and track_sequence.current.end_time ): - prd.start_time = track_sequence.now.end_time + prd.start_time = track_sequence.current.end_time prd.end_time = prd.start_time + dt.timedelta(milliseconds=prd.duration) next_start_time = prd.end_time update_rows.append(row_number) @@ -1460,9 +1444,9 @@ class PlaylistModel(QAbstractTableModel): # If we're between the current and next row, zero out # times if ( - track_sequence.now.plr_rownum is not None + track_sequence.current.plr_rownum is not None and track_sequence.next.plr_rownum is not None - and track_sequence.now.plr_rownum + and track_sequence.current.plr_rownum < row_number < track_sequence.next.plr_rownum ): @@ -1539,7 +1523,7 @@ class PlaylistProxyModel(QSortFilterProxyModel): if self.source_model.is_played_row(source_row): # Don't hide current or next track with db.Session() as session: - if track_sequence.next.plr_id: + if track_sequence.next: next_plr = session.get(PlaylistRows, track_sequence.next.plr_id) if ( next_plr @@ -1547,8 +1531,10 @@ class PlaylistProxyModel(QSortFilterProxyModel): and next_plr.playlist_id == self.source_model.playlist_id ): return True - if track_sequence.now.plr_id: - now_plr = session.get(PlaylistRows, track_sequence.now.plr_id) + if track_sequence.current: + now_plr = session.get( + PlaylistRows, track_sequence.current.plr_id + ) if ( now_plr and now_plr.plr_rownum == source_row @@ -1558,19 +1544,20 @@ class PlaylistProxyModel(QSortFilterProxyModel): # Don't hide previous track until # HIDE_AFTER_PLAYING_OFFSET milliseconds after # current track has started - if track_sequence.previous.plr_id: + if track_sequence.previous: previous_plr = session.get( PlaylistRows, track_sequence.previous.plr_id ) if ( - previous_plr + track_sequence.current + and previous_plr and previous_plr.plr_rownum == source_row and previous_plr.playlist_id == self.source_model.playlist_id ): - if track_sequence.now.start_time: + if track_sequence.current.track_player.start_time: if dt.datetime.now() > ( - track_sequence.now.start_time + track_sequence.current.track_player.start_time + dt.timedelta( milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET ) @@ -1623,7 +1610,7 @@ class PlaylistProxyModel(QSortFilterProxyModel): def get_rows_duration(self, row_numbers: List[int]) -> int: return self.source_model.get_rows_duration(row_numbers) - def get_row_info(self, row_number: int) -> PlaylistRowData: + def get_row_info(self, row_number: int) -> _PlaylistRowData: return self.source_model.get_row_info(row_number) def get_row_track_path(self, row_number: int) -> str: @@ -1649,7 +1636,7 @@ class PlaylistProxyModel(QSortFilterProxyModel): def is_played_row(self, row_number: int) -> bool: return self.source_model.is_played_row(row_number) - def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]: + def is_track_in_playlist(self, track_id: int) -> Optional[_PlaylistRowData]: return self.source_model.is_track_in_playlist(track_id) def mark_unplayed(self, row_numbers: List[int]) -> None: @@ -1666,12 +1653,15 @@ class PlaylistProxyModel(QSortFilterProxyModel): ) def move_track_add_note( - self, new_row_number: int, existing_prd: PlaylistRowData, note: str + self, new_row_number: int, existing_prd: _PlaylistRowData, note: str ) -> None: return self.source_model.move_track_add_note(new_row_number, existing_prd, note) def move_track_to_header( - self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str] + self, + header_row_number: int, + existing_prd: _PlaylistRowData, + note: Optional[str], ) -> None: return self.source_model.move_track_to_header( header_row_number, existing_prd, note diff --git a/app/playlists.py b/app/playlists.py index dc10740..9c5dad8 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -118,9 +118,13 @@ class EscapeDelegate(QStyledItemDelegate): # Close editor if no changes have been made data_modified = False if isinstance(self.editor, QPlainTextEdit): - data_modified = self.original_model_data == self.editor.toPlainText() + data_modified = ( + self.original_model_data == self.editor.toPlainText() + ) elif isinstance(self.editor, QDoubleSpinBox): - data_modified = self.original_model_data == int(self.editor.value()) * 1000 + data_modified = ( + self.original_model_data == int(self.editor.value()) * 1000 + ) if data_modified: self.closeEditor.emit(editor) return True @@ -425,12 +429,18 @@ class PlaylistTab(QTableView): header_row = proxy_model.is_header_row(model_row_number) track_row = not header_row - current_row = model_row_number == track_sequence.now.plr_rownum - next_row = model_row_number == track_sequence.next.plr_rownum + if track_sequence.current: + this_is_current_row = model_row_number == track_sequence.current.row_number + else: + this_is_current_row = False + if track_sequence.next: + this_is_next_row = model_row_number == track_sequence.next.row_number + else: + this_is_next_row = False track_path = self.source_model.get_row_info(model_row_number).path # Open/import in/from Audacity - if track_row and not current_row: + if track_row and not this_is_current_row: if track_path == self.musicmuster.audacity_file_path: # This track was opened in Audacity self._add_context_menu( @@ -447,7 +457,7 @@ class PlaylistTab(QTableView): ) # Rescan - if track_row and not current_row: + if track_row and not this_is_current_row: self._add_context_menu( "Rescan track", lambda: self._rescan(model_row_number) ) @@ -456,11 +466,11 @@ class PlaylistTab(QTableView): self.menu.addSeparator() # Delete row - if not current_row and not next_row: + if not this_is_current_row and not this_is_next_row: self._add_context_menu("Delete row", lambda: self._delete_rows()) # Remove track from row - if track_row and not current_row and not next_row: + if track_row and not this_is_current_row and not this_is_next_row: self._add_context_menu( "Remove track from row", lambda: proxy_model.remove_track(model_row_number), @@ -481,7 +491,7 @@ class PlaylistTab(QTableView): ) # Unmark as next - if next_row: + if this_is_next_row: self._add_context_menu( "Unmark as next track", lambda: self._unmark_as_next() )