diff --git a/app/classes.py b/app/classes.py index 1bdf518..671577e 100644 --- a/app/classes.py +++ b/app/classes.py @@ -15,7 +15,8 @@ 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 +from music import Music import helpers @@ -116,93 +117,87 @@ class MusicMusterSignals(QObject): super().__init__() -class PlaylistTrack: +class _PlaylistTrack: """ - 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, player_name: str, plrid: 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 + with db.Session() as session: + plr = session.get(PlaylistRows, plrid) + if not plr.track: + raise ValueError("No track defined in plr passed to PlaylistTrack") + if helpers.file_is_unreadable(plr.track.path): + raise ValueError(f"_PlaylistTrack({plrid=}): {plr.track.path} is unreadable") + track = plr.track + + self.artist = track.artist + self.duration = track.duration + self.end_time: Optional[dt.datetime] = None + self.fade_at = track.fade_at + self.fade_graph: Optional[FadeCurve] = None + self.fade_graph_start_updates: Optional[dt.datetime] = None + self.fade_length: Optional[int] = None + self.intro = track.intro + self.path = track.path + self.player_name = player_name + self.playlist_id = plr.playlist_id + self.plr_id = plr.id + self.plr_rownum = plr.plr_rownum + self.resume_marker: Optional[float] + self.silence_at = track.silence_at + self.start_gap = track.start_gap + self.start_time: Optional[dt.datetime] = 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.player = Music(name=player_name) + + # Initialise and add FadeCurve in a thread as it's slow + self.fadecurve_thread = QThread() + self.worker = AddFadeCurve( + self, + track_path=track.path, + track_fade_at=track.fade_at, + track_silence_at=track.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 __repr__(self) -> str: return ( f"" + f"player_name={self.player_name}>" ) - def set_plr(self, session: scoped_session, plr: PlaylistRows) -> None: - """ - Update with new plr information - """ + def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None: + """Fade music""" - session.add(plr) - self.plr_rownum = plr.plr_rownum - if not plr.track: - return - track = plr.track + self.player.fade(fade_seconds) - self.artist = track.artist - 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 - - # Initialise and add FadeCurve in a thread as it's slow - self.fadecurve_thread = QThread() - self.worker = AddFadeCurve( - self, - track_path=track.path, - track_fade_at=track.fade_at, - track_silence_at=track.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 start(self) -> None: - """ - Called when track starts playing - """ + 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 +208,28 @@ 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) -> 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 MainPlaylistTrack(_PlaylistTrack): + def __init__(self, plrid: int) -> None: + super().__init__(player_name=Config.VLC_MAIN_PLAYER_NAME, plrid=plrid) + + +class PreviewPlaylistTrack(_PlaylistTrack): + def __init__(self, plrid: int) -> None: + super().__init__(player_name=Config.VLC_PREVIEW_PLAYER_NAME, plrid=plrid) @dataclass @@ -246,7 +256,7 @@ class AddFadeCurve(QObject): def __init__( self, - playlist_track: PlaylistTrack, + playlist_track: _PlaylistTrack, track_path: str, track_fade_at: int, track_silence_at: int, @@ -270,10 +280,11 @@ class AddFadeCurve(QObject): self.finished.emit() -class TrackSequence: - next = PlaylistTrack() - now = PlaylistTrack() - previous = PlaylistTrack() +class PlaylistTracks: + next: Optional[MainPlaylistTrack] = None + now: Optional[MainPlaylistTrack] = None + previous: Optional[MainPlaylistTrack] = None + preview: Optional[PreviewPlaylistTrack] = None -track_sequence = TrackSequence() +playlist_track = PlaylistTracks() 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..78cab75 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -55,10 +55,10 @@ import stackprinter # type: ignore # App imports from classes import ( - track_sequence, + playlist_track, FadeCurve, MusicMusterSignals, - PlaylistTrack, + _PlaylistTrack, TrackFileData, ) from config import Config @@ -430,7 +430,7 @@ class Window(QMainWindow, Ui_MainWindow): Clear next track """ - track_sequence.next = PlaylistTrack() + playlist_track.next = None self.update_headers() def clear_selection(self) -> None: @@ -521,12 +521,13 @@ class Window(QMainWindow, Ui_MainWindow): """ # Don't close current track playlist - current_track_playlist_id = track_sequence.now.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: - self.show_status_message("Can't close current track playlist", 5000) - return False + if playlist_track.now: + current_track_playlist_id = playlist_track.now.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: + self.show_status_message("Can't close current track playlist", 5000) + return False # Record playlist as closed and update remaining playlist tabs with db.Session() as session: @@ -578,7 +579,7 @@ class Window(QMainWindow, Ui_MainWindow): self.actionMoveUnplayed.triggered.connect(self.move_unplayed) self.actionSetNext.triggered.connect(self.set_selected_track_next) self.actionSkipToNext.triggered.connect(self.play_next) - self.actionStop.triggered.connect(self.stop) + self.actionStop.triggered.connect(self.stop_immediately) self.btnDrop3db.clicked.connect(self.drop3db) self.btnFade.clicked.connect(self.fade) self.btnHidePlayed.clicked.connect(self.hide_played) @@ -589,7 +590,7 @@ class Window(QMainWindow, Ui_MainWindow): self.btnPreviewFwd.clicked.connect(self.preview_fwd) self.btnPreviewMark.clicked.connect(self.preview_mark) self.btnPreviewStart.clicked.connect(self.preview_start) - self.btnStop.clicked.connect(self.stop) + self.btnStop.clicked.connect(self.stop_immediately) self.hdrCurrentTrack.clicked.connect(self.show_current) self.hdrNextTrack.clicked.connect(self.show_next) self.tabPlaylist.currentChanged.connect(self.tab_change) @@ -1147,38 +1148,34 @@ class Window(QMainWindow, Ui_MainWindow): # Check for inadvertent press of 'return' if self.catch_return_key: # Suppress inadvertent double press - if ( - track_sequence.now.start_time - and track_sequence.now.start_time - + dt.timedelta(milliseconds=Config.RETURN_KEY_DEBOUNCE_MS) - > dt.datetime.now() - ): - return - # 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 - ) - if not helpers.ask_yes_no( - "Track playing", - "Really play next track now?", - default_yes=default_yes, - parent=self, - ): - return + if playlist_track.now and playlist_track.now.start_time: + if ( + playlist_track.now.start_time + + dt.timedelta(milliseconds=Config.RETURN_KEY_DEBOUNCE_MS) + > dt.datetime.now() + ): + return + + # 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 = ( + dt.datetime.now() - playlist_track.now.start_time + ).total_seconds() * 1000 > Config.PLAY_NEXT_GUARD_MS + if not helpers.ask_yes_no( + "Track playing", + "Really play next track now?", + default_yes=default_yes, + parent=self, + ): + return log.info(f"play_next({position=})") # If there is no next track set, return. - if not track_sequence.next.track_id: + if not playlist_track.next: 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 +1192,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 + playlist_track.now = playlist_track.next # Clear next track self.clear_next() @@ -1206,10 +1203,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) + playlist_track.now.play() # Show closing volume graph if track_sequence.now.fade_graph: @@ -1252,14 +1246,14 @@ 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 = track_sequence.next.path + track_path = playlist_track.next.path if not track_path: self.btnPreview.setChecked(False) return self.preview_player.play(path=track_path) else: - self.preview_player.stop() + self.preview_player._stop() self.label_intro_timer.setText("0.0") self.btnPreviewMark.setEnabled(False) self.btnPreviewArm.setChecked(False) @@ -1417,27 +1411,27 @@ class Window(QMainWindow, Ui_MainWindow): log.info("resume()") # Return if no saved position - if not track_sequence.previous.resume_marker: + if not playlist_track.previous.resume_marker: log.error("No previous track position") return # We want to use play_next() to resume, so copy the previous # track to the next track: - track_sequence.next = track_sequence.previous + playlist_track.next = playlist_track.previous # Now resume playing the now-next track - self.play_next(track_sequence.next.resume_marker) + self.play_next(playlist_track.next.resume_marker) # Adjust track info so that clocks and graph are correct. # 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 + playlist_track.now.start_time + and playlist_track.now.duration + and playlist_track.now.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 = playlist_track.now.duration * playlist_track.now.resume_marker + playlist_track.now.start_time -= dt.timedelta(milliseconds=elapsed_ms) def save_as_template(self) -> None: """Save current playlist as template""" @@ -1563,7 +1557,7 @@ class Window(QMainWindow, Ui_MainWindow): def show_current(self) -> None: """Scroll to show current track""" - self.show_track(track_sequence.now) + self.show_track(playlist_track.now) def show_warning(self, title: str, body: str) -> None: """ @@ -1576,7 +1570,7 @@ class Window(QMainWindow, Ui_MainWindow): def show_next(self) -> None: """Scroll to show next track""" - self.show_track(track_sequence.next) + self.show_track(playlist_track.next) def show_status_message(self, message: str, timing: int) -> None: """ @@ -1585,7 +1579,7 @@ class Window(QMainWindow, Ui_MainWindow): self.statusbar.showMessage(message, timing) - def show_track(self, plt: PlaylistTrack) -> None: + def show_track(self, plt: _PlaylistTrack) -> None: """Scroll to show track in plt""" # Switch to the correct tab @@ -1636,7 +1630,7 @@ class Window(QMainWindow, Ui_MainWindow): else: return None - def stop(self) -> None: + def stop_immediately(self) -> None: """Stop playing immediately""" self.stop_playing(fade=False) @@ -1658,30 +1652,23 @@ class Window(QMainWindow, Ui_MainWindow): - Enable controls """ + if not playlist_track.now: + return + # Set flag to say we're not playing a track so that timer ticks # don't see player=None and kick off end-of-track actions if self.playing: self.playing = False - else: - # Return if not playing - log.info("stop_playing() called but not playing") - return # Stop/fade track - track_sequence.now.resume_marker = self.music.get_position() if fade: - self.music.fade() + playlist_track.now.stop_playing(fade_seconds=Config.FADEOUT_SECONDS) else: - self.music.stop() + playlist_track.now.stop_playing(fade_seconds=0) - # Reset fade graph - if track_sequence.now.fade_graph: - track_sequence.now.fade_graph.clear() - - # Reset track_sequence objects - if track_sequence.now.track_id: - track_sequence.previous = track_sequence.now - track_sequence.now = PlaylistTrack() + # Reset playlist_track objects + playlist_track.previous = playlist_track.now + playlist_track.now = None # Tell model previous track has finished self.active_proxy_model().previous_track_ended() @@ -1712,20 +1699,20 @@ class Window(QMainWindow, Ui_MainWindow): # 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() + playlist_track.now.fade_graph_start_updates is None + or playlist_track.now.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 + playlist_track.now.track_id + and playlist_track.now.fade_graph + and playlist_track.now.start_time ): play_time = ( - dt.datetime.now() - track_sequence.now.start_time + dt.datetime.now() - playlist_track.now.start_time ).total_seconds() * 1000 - track_sequence.now.fade_graph.tick(play_time) + playlist_track.now.fade_graph.tick(play_time) def tick_500ms(self) -> None: """ @@ -1800,7 +1787,7 @@ class Window(QMainWindow, Ui_MainWindow): self.label_elapsed_timer.setText( helpers.ms_to_mmss(playtime) + " / " - + helpers.ms_to_mmss(track_sequence.now.duration) + + helpers.ms_to_mmss(playlist_track.now.duration) ) # Time to fade @@ -1845,25 +1832,37 @@ class Window(QMainWindow, Ui_MainWindow): Update last / current / next track headers """ - if track_sequence.previous.title and track_sequence.previous.artist: + if ( + playlist_track.previous + and playlist_track.previous.title + and playlist_track.previous.artist + ): self.hdrPreviousTrack.setText( - f"{track_sequence.previous.title} - {track_sequence.previous.artist}" + f"{playlist_track.previous.title} - {playlist_track.previous.artist}" ) else: self.hdrPreviousTrack.setText("") - if track_sequence.now.title and track_sequence.now.artist: + if ( + playlist_track.now + and playlist_track.now.title + and playlist_track.now.artist + ): self.hdrCurrentTrack.setText( - f"{track_sequence.now.title.replace('&', '&&')} - " - f"{track_sequence.now.artist.replace('&', '&&')}" + f"{playlist_track.now.title.replace('&', '&&')} - " + f"{playlist_track.now.artist.replace('&', '&&')}" ) else: self.hdrCurrentTrack.setText("") - if track_sequence.next.title and track_sequence.next.artist: + if ( + playlist_track.next + and playlist_track.next.title + and playlist_track.next.artist + ): self.hdrNextTrack.setText( - f"{track_sequence.next.title.replace('&', '&&')} - " - f"{track_sequence.next.artist.replace('&', '&&')}" + f"{playlist_track.next.title.replace('&', '&&')} - " + f"{playlist_track.next.artist.replace('&', '&&')}" ) else: self.hdrNextTrack.setText("") diff --git a/app/playlistmodel.py b/app/playlistmodel.py index 14780fc..102c527 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -30,7 +30,13 @@ import obswebsocket # type: ignore # import snoop # type: ignore # App imports -from classes import Col, track_sequence, MusicMusterSignals, PlaylistTrack +from classes import ( + Col, + playlist_track, + MusicMusterSignals, + MainPlaylistTrack, + PreviewPlaylistTrack, +) from config import Config from helpers import ( file_is_unreadable, @@ -195,10 +201,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 playlist_track.now and prd.plrid == playlist_track.now.plr_id: return QBrush(QColor(Config.COLOUR_CURRENT_PLAYLIST)) # Next track - if prd.plrid == track_sequence.next.plr_id: + if playlist_track.next and prd.plrid == playlist_track.next.plr_id: return QBrush(QColor(Config.COLOUR_NEXT_PLAYLIST)) # Individual cell colouring @@ -250,25 +256,15 @@ class PlaylistModel(QAbstractTableModel): - find next track """ - row_number = track_sequence.now.plr_rownum + if not playlist_track.now: + return + + row_number = playlist_track.now.plr_rownum if row_number is not None: prd = self.playlist_rows[row_number] else: prd = None - # 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 - # Check for OBS scene change log.debug("Call OBS scene change") self.obs_scene_change(row_number) @@ -276,29 +272,29 @@ 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, playlist_track.now.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, playlist_track.now.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=}") + log.error(f"Can't retrieve plr, {playlist_track.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 + prd.start_time = playlist_track.now.start_time + prd.end_time = playlist_track.now.end_time # 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 playlist_track.previous and playlist_track.previous.plr_rownum: + self.invalidate_row(playlist_track.previous.plr_rownum) # Update all other track times self.update_track_times() @@ -391,7 +387,7 @@ class PlaylistModel(QAbstractTableModel): session.commit() super().endRemoveRows() - self.reset_track_sequence_row_numbers() + self.reset_playlist_track_row_numbers() def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: """ @@ -464,7 +460,7 @@ class PlaylistModel(QAbstractTableModel): with db.Session() as session: self.refresh_data(session) super().endResetModel() - self.reset_track_sequence_row_numbers() + self.reset_playlist_track_row_numbers() def edit_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: """ @@ -703,16 +699,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 + playlist_track.now and playlist_track.now.plr_rownum + and playlist_track.now.end_time and ( row_number - < track_sequence.now.plr_rownum + < playlist_track.now.plr_rownum < prd.plr_rownum ) ): section_end_time = ( - track_sequence.now.end_time + playlist_track.now.end_time + dt.timedelta(milliseconds=duration) ) end_time_str = ( @@ -793,7 +789,7 @@ class PlaylistModel(QAbstractTableModel): super().endInsertRows() self.signals.resize_rows_signal.emit(self.playlist_id) - self.reset_track_sequence_row_numbers() + self.reset_playlist_track_row_numbers() self.invalidate_rows(list(range(new_row_number, len(self.playlist_rows)))) def invalidate_row(self, modified_row: int) -> None: @@ -905,15 +901,15 @@ class PlaylistModel(QAbstractTableModel): if old_row != new_row: 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 + # Check to see whether any rows in playlist_tracks have moved + if playlist_track.previous and playlist_track.previous.plr_rownum in row_map: + playlist_track.previous.plr_rownum = row_map[ + playlist_track.previous.plr_rownum ] - 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 playlist_track.now and playlist_track.now.plr_rownum in row_map: + playlist_track.now.plr_rownum = row_map[playlist_track.now.plr_rownum] + if playlist_track.next and playlist_track.next.plr_rownum in row_map: + playlist_track.next.plr_rownum = row_map[playlist_track.next.plr_rownum] # For SQLAlchemy, build a list of dictionaries that map plrid to # new row number: @@ -929,7 +925,7 @@ class PlaylistModel(QAbstractTableModel): self.refresh_data(session) # Update display - self.reset_track_sequence_row_numbers() + self.reset_playlist_track_row_numbers() self.invalidate_rows(list(row_map.keys())) def move_rows_between_playlists( @@ -973,7 +969,7 @@ 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 playlist_track.now and plr.id == playlist_track.now.plr_id: # Don't move current track continue plr.playlist_id = to_playlist_id @@ -988,7 +984,7 @@ class PlaylistModel(QAbstractTableModel): session.commit() # Reset of model must come after session has been closed - self.reset_track_sequence_row_numbers() + self.reset_playlist_track_row_numbers() self.signals.row_order_changed_signal.emit(to_playlist_id) self.signals.end_reset_model_signal.emit(to_playlist_id) self.update_track_times() @@ -1080,18 +1076,18 @@ class PlaylistModel(QAbstractTableModel): log.info("previous_track_ended()") # Sanity check - if not track_sequence.previous.track_id: + if not playlist_track.previous: log.error("playlistmodel:previous_track_ended called with no current track") return - if track_sequence.previous.plr_rownum is None: + if playlist_track.previous.plr_rownum is None: log.error( "playlistmodel:previous_track_ended called with no row number " - f"({track_sequence.previous=})" + f"({playlist_track.previous=})" ) return # Update display - self.invalidate_row(track_sequence.previous.plr_rownum) + self.invalidate_row(playlist_track.previous.plr_rownum) def refresh_data(self, session: db.session): """Populate dicts for data calls""" @@ -1138,28 +1134,28 @@ class PlaylistModel(QAbstractTableModel): self.signals.resize_rows_signal.emit(self.playlist_id) session.commit() - def reset_track_sequence_row_numbers(self) -> None: + def reset_playlist_track_row_numbers(self) -> None: """ Signal handler for when row ordering has changed """ - log.debug("reset_track_sequence_row_numbers()") + log.debug("reset_playlist_track_row_numbers()") - # Check the track_sequence next, now and previous plrs and + # Check the playlist_track next, now 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 playlist_track.next and playlist_track.next.plr_rownum: + next_plr = session.get(PlaylistRows, playlist_track.next.plr_id) 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) + playlist_track.next.plr_rownum = next_plr.plr_rownum + if playlist_track.now and playlist_track.now.plr_rownum: + now_plr = session.get(PlaylistRows, playlist_track.now.plr_id) 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) + playlist_track.now.plr_rownum = now_plr.plr_rownum + if playlist_track.previous and playlist_track.previous.plr_rownum: + previous_plr = session.get(PlaylistRows, playlist_track.previous.plr_id) if previous_plr: - track_sequence.previous.plr_rownum = previous_plr.plr_rownum + playlist_track.previous.plr_rownum = previous_plr.plr_rownum self.update_track_times() @@ -1210,7 +1206,7 @@ class PlaylistModel(QAbstractTableModel): if playlist_id != self.playlist_id: return - self.reset_track_sequence_row_numbers() + self.reset_playlist_track_row_numbers() def selection_is_sortable(self, row_numbers: List[int]) -> bool: """ @@ -1240,59 +1236,43 @@ 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 playlist_track.next: + playlist_track.next = None + self.signals.next_track_changed_signal.emit() + 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: + # Update playlist_track try: plrid = self.playlist_rows[row_number].plrid except IndexError: log.error( - f"playlistmodel.set_next_track({row_number=}, " - f"{self.playlist_id=}" + f"playlistmodel.set_next_track({row_number=}, " f"{self.playlist_id=}" ) 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 - ) - self.invalidate_row(row_number) + try: + playlist_track.next = MainPlaylistTrack(plrid) + except ValueError as e: + log.error(f"Error creating MainPlaylistTrack({plrid=}): ({str(e)})") + return - if next_row_was is not None: - self.invalidate_row(next_row_was) + 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 @@ -1434,9 +1414,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 == playlist_track.now.plr_rownum: + prd.start_time = playlist_track.now.start_time + prd.end_time = playlist_track.now.end_time update_rows.append(row_number) if not next_start_time: next_start_time = prd.end_time @@ -1444,10 +1424,10 @@ 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 + row_number == playlist_track.next.plr_rownum + and playlist_track.now.end_time ): - prd.start_time = track_sequence.now.end_time + prd.start_time = playlist_track.now.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,11 +1440,11 @@ class PlaylistModel(QAbstractTableModel): # If we're between the current and next row, zero out # times if ( - track_sequence.now.plr_rownum is not None - and track_sequence.next.plr_rownum is not None - and track_sequence.now.plr_rownum + playlist_track.now.plr_rownum is not None + and playlist_track.next.plr_rownum is not None + and playlist_track.now.plr_rownum < row_number - < track_sequence.next.plr_rownum + < playlist_track.next.plr_rownum ): prd.start_time = None prd.end_time = None @@ -1539,16 +1519,16 @@ 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: - next_plr = session.get(PlaylistRows, track_sequence.next.plr_id) + if playlist_track.next and playlist_track.next.plr_id: + next_plr = session.get(PlaylistRows, playlist_track.next.plr_id) if ( next_plr and next_plr.plr_rownum == source_row 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 playlist_track.now and playlist_track.now.plr_id: + now_plr = session.get(PlaylistRows, playlist_track.now.plr_id) if ( now_plr and now_plr.plr_rownum == source_row @@ -1558,9 +1538,9 @@ 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 playlist_track.previous and playlist_track.previous.plr_id: previous_plr = session.get( - PlaylistRows, track_sequence.previous.plr_id + PlaylistRows, playlist_track.previous.plr_id ) if ( previous_plr @@ -1568,9 +1548,9 @@ class PlaylistProxyModel(QSortFilterProxyModel): and previous_plr.playlist_id == self.source_model.playlist_id ): - if track_sequence.now.start_time: + if playlist_track.now and playlist_track.now.start_time: if dt.datetime.now() > ( - track_sequence.now.start_time + playlist_track.now.start_time + dt.timedelta( milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET ) diff --git a/app/playlists.py b/app/playlists.py index dc10740..b1e3073 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -35,7 +35,7 @@ from PyQt6.QtWidgets import ( # Third party imports # App imports -from classes import Col, MusicMusterSignals, track_sequence +from classes import Col, MusicMusterSignals, playlist_track from config import Config from dialogs import TrackSelectDialog from helpers import ( @@ -425,8 +425,8 @@ 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 + current_row = model_row_number == playlist_track.now.plr_rownum + next_row = model_row_number == playlist_track.next.plr_rownum track_path = self.source_model.get_row_info(model_row_number).path # Open/import in/from Audacity