From bc6a4c11cfb5d2884f1fb0bb21d5660353a93c61 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Wed, 2 Mar 2022 09:27:10 +0000 Subject: [PATCH] Rebase dev onto v2_id --- app/models.py | 44 ++--- app/music.py | 2 + app/musicmuster.py | 157 ++++++++-------- app/playlists.py | 452 +++++++++++++++++++++++++-------------------- 4 files changed, 356 insertions(+), 299 deletions(-) diff --git a/app/models.py b/app/models.py index 9da98cc..690588f 100644 --- a/app/models.py +++ b/app/models.py @@ -51,8 +51,10 @@ engine = sqlalchemy.create_engine( echo=Config.DISPLAY_SQL, pool_pre_ping=True) -sm: sessionmaker = sessionmaker(bind=engine) -Session = scoped_session(sm) +# Create a Session factory +Session = scoped_session(sessionmaker(bind=engine)) +# sm: sessionmaker = sessionmaker(bind=engine) # , expire_on_commit=False) +# Session = scoped_session(sm) Base: DeclarativeMeta = declarative_base() Base.metadata.create_all(engine) @@ -105,7 +107,7 @@ class NoteColours(Base): Optional["NoteColours"]: """Return record identified by id, or None if not found""" - return session.query(NoteColours).local_filter( + return session.query(NoteColours).filter( NoteColours.id == note_id).first() @staticmethod @@ -116,7 +118,7 @@ class NoteColours(Base): for rec in ( session.query(NoteColours) - .local_filter(NoteColours.enabled.is_(True)) + .filter(NoteColours.enabled.is_(True)) .order_by(NoteColours.order) .all() ): @@ -213,7 +215,7 @@ class Playdates(Base): """Return datetime track last played or None""" last_played: Optional[Playdates] = session.query( - Playdates.lastplayed).local_filter((Playdates.track_id == track_id) + Playdates.lastplayed).filter((Playdates.track_id == track_id) ).order_by(Playdates.lastplayed.desc()).first() if last_played: return last_played[0] @@ -226,7 +228,7 @@ class Playdates(Base): Remove all records of track_id """ - session.query(Playdates).local_filter( + session.query(Playdates).filter( Playdates.track_id == track_id, ).delete() session.commit() @@ -293,7 +295,7 @@ class Playlists(Base): @classmethod def get_by_id(cls, session: Session, playlist_id: int) -> "Playlists": - return (session.query(cls).local_filter(cls.id == playlist_id)).one() + return (session.query(cls).filter(cls.id == playlist_id)).one() @classmethod def get_closed(cls, session: Session) -> List["Playlists"]: @@ -301,7 +303,7 @@ class Playlists(Base): return ( session.query(cls) - .local_filter(cls.loaded.is_(False)) + .filter(cls.loaded.is_(False)) .order_by(cls.last_used.desc()) ).all() @@ -313,7 +315,7 @@ class Playlists(Base): return ( session.query(cls) - .local_filter(cls.loaded.is_(True)) + .filter(cls.loaded.is_(True)) .order_by(cls.last_used.desc()) ).all() @@ -330,7 +332,7 @@ class Playlists(Base): Remove all tracks from this playlist """ - session.query(PlaylistTracks).local_filter( + session.query(PlaylistTracks).filter( PlaylistTracks.playlist_id == self.id, ).delete() session.commit() @@ -338,7 +340,7 @@ class Playlists(Base): def remove_track(self, session: Session, row: int) -> None: DEBUG(f"Playlist.remove_track({self.id=}, {row=})") - session.query(PlaylistTracks).local_filter( + session.query(PlaylistTracks).filter( PlaylistTracks.playlist_id == self.id, PlaylistTracks.row == row ).delete() @@ -389,7 +391,7 @@ class PlaylistTracks(Base): new_row: int max_row: Optional[int] = session.query( - func.max(PlaylistTracks.row)).local_filter( + func.max(PlaylistTracks.row)).filter( PlaylistTracks.playlist_id == to_playlist_id).scalar() if max_row is None: # Destination playlist is empty; use row 0 @@ -398,7 +400,7 @@ class PlaylistTracks(Base): # Destination playlist has tracks; add to end new_row = max_row + 1 try: - record: PlaylistTracks = session.query(PlaylistTracks).local_filter( + record: PlaylistTracks = session.query(PlaylistTracks).filter( PlaylistTracks.playlist_id == from_playlist_id, PlaylistTracks.row == row).one() except NoResultFound: @@ -446,7 +448,7 @@ class Settings(Base): int_setting: Settings try: - int_setting = session.query(cls).local_filter( + int_setting = session.query(cls).filter( cls.name == name).one() except NoResultFound: int_setting = Settings() @@ -516,7 +518,7 @@ class Tracks(Base): DEBUG(f"Tracks.get_or_create({path=})") try: - track = session.query(cls).local_filter(cls.path == path).one() + track = session.query(cls).filter(cls.path == path).one() except NoResultFound: track = Tracks(session, path) @@ -533,7 +535,7 @@ class Tracks(Base): DEBUG(f"Tracks.get_track_from_filename({filename=})") try: - track = session.query(Tracks).local_filter(Tracks.path.ilike( + track = session.query(Tracks).filter(Tracks.path.ilike( f'%{os.path.sep}{filename}')).one() return track except (NoResultFound, MultipleResultsFound): @@ -547,7 +549,7 @@ class Tracks(Base): DEBUG(f"Tracks.get_track_from_path({path=})") - return session.query(Tracks).local_filter(Tracks.path == path).first() + return session.query(Tracks).filter(Tracks.path == path).first() @classmethod def get_by_id(cls, session: Session, track_id: int) -> Optional["Tracks"]: @@ -555,7 +557,7 @@ class Tracks(Base): try: DEBUG(f"Tracks.get_track(track_id={track_id})") - track = session.query(Tracks).local_filter(Tracks.id == track_id).one() + track = session.query(Tracks).filter(Tracks.id == track_id).one() return track except NoResultFound: ERROR(f"get_track({track_id}): not found") @@ -584,7 +586,7 @@ class Tracks(Base): DEBUG(f"Tracks.remove_path({path=})") try: - session.query(Tracks).local_filter(Tracks.path == path).delete() + session.query(Tracks).filter(Tracks.path == path).delete() session.commit() except IntegrityError as exception: ERROR(f"Can't remove track with {path=} ({exception=})") @@ -594,7 +596,7 @@ class Tracks(Base): return ( session.query(cls) - .local_filter(cls.artist.ilike(f"%{text}%")) + .filter(cls.artist.ilike(f"%{text}%")) .order_by(cls.title) ).all() @@ -602,7 +604,7 @@ class Tracks(Base): def search_titles(cls, session: Session, text: str) -> List["Tracks"]: return ( session.query(cls) - .local_filter(cls.title.ilike(f"%{text}%")) + .filter(cls.title.ilike(f"%{text}%")) .order_by(cls.title) ).all() diff --git a/app/music.py b/app/music.py index 59690e1..1b96092 100644 --- a/app/music.py +++ b/app/music.py @@ -86,6 +86,8 @@ class Music: p.stop() DEBUG(f"Releasing player {p=}", True) p.release() + # Ensure we don't reference player after release + p = None self.fading -= 1 diff --git a/app/musicmuster.py b/app/musicmuster.py index ef3c30c..3712eec 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -412,7 +412,8 @@ class Window(QMainWindow, Ui_MainWindow): rows = [] for (row, track) in ( - self.visible_playlist_tab().get_selected_rows_and_tracks() + self.visible_playlist_tab().get_selected_rows_and_tracks( + session) ): rows.append(row) if destination_visible_playlist_tab: @@ -436,8 +437,14 @@ class Window(QMainWindow, Ui_MainWindow): Ensure we have info tabs for next and current track titles """ - title_list: List[str, str] = [self.previous_track.title, - self.current_track.title] + title_list: List[str] = [] + + if self.previous_track: + title_list.append(self.previous_track.title) + if self.current_track: + title_list.append(self.current_track.title) + if self.next_track: + title_list.append(self.next_track.title) for title in title_list: if title in self.info_tabs.keys(): @@ -533,7 +540,7 @@ class Window(QMainWindow, Ui_MainWindow): Playdates(session, self.current_track) # Tell playlist track is now playing - self.current_track_playlist_tab.play_started() + self.current_track_playlist_tab.play_started(session) # Disable play next controls self.disable_play_next_controls() @@ -680,38 +687,39 @@ class Window(QMainWindow, Ui_MainWindow): """ - # Clear next track if on another tab - if self.next_track_playlist_tab != playlist_tab: - # We need to reset the ex-next-track playlist - if self.next_track_playlist_tab: - self.next_track_playlist_tab.clear_next() + with Session() as session: + # Clear next track if on another tab + if self.next_track_playlist_tab != playlist_tab: + # We need to reset the ex-next-track playlist + if self.next_track_playlist_tab: + self.next_track_playlist_tab.clear_next(session) - # Reset tab colour if on other tab - if (self.next_track_playlist_tab != - self.current_track_playlist_tab): - self.set_tab_colour( - self.next_track_playlist_tab, - QColor(Config.COLOUR_NORMAL_TAB)) + # Reset tab colour if on other tab + if (self.next_track_playlist_tab != + self.current_track_playlist_tab): + self.set_tab_colour( + self.next_track_playlist_tab, + QColor(Config.COLOUR_NORMAL_TAB)) - # Note next playlist tab - self.next_track_playlist_tab = playlist_tab + # Note next playlist tab + self.next_track_playlist_tab = playlist_tab - # Set next playlist_tab tab colour if it isn't the - # currently-playing tab - if (self.next_track_playlist_tab != - self.current_track_playlist_tab): - self.set_tab_colour( - self.next_track_playlist_tab, - QColor(Config.COLOUR_NEXT_TAB)) + # Set next playlist_tab tab colour if it isn't the + # currently-playing tab + if (self.next_track_playlist_tab != + self.current_track_playlist_tab): + self.set_tab_colour( + self.next_track_playlist_tab, + QColor(Config.COLOUR_NEXT_TAB)) - # Note next track - self.next_track = track + # Note next track + self.next_track = track - # Update headers - self.update_headers() + # Update headers + self.update_headers() - # Populate 'info' tabs - self.open_info_tabs() + # Populate 'info' tabs + self.open_info_tabs() def tick(self) -> None: """ @@ -739,52 +747,55 @@ class Window(QMainWindow, Ui_MainWindow): # If track is playing, update track clocks time and colours if self.music.player and self.music.playing(): - self.playing = True - playtime: int = self.music.get_playtime() - time_to_fade: int = (self.current_track.fade_at - playtime) - time_to_silence: int = (self.current_track.silence_at - playtime) - time_to_end: int = (self.current_track.duration - playtime) + with Session() as session: + self.playing = True + if self.current_track not in session: + session.add(self.current_track) + playtime: int = self.music.get_playtime() + time_to_fade: int = (self.current_track.fade_at - playtime) + time_to_silence: int = (self.current_track.silence_at - playtime) + time_to_end: int = (self.current_track.duration - playtime) - # Elapsed time - if time_to_end < 500: - self.label_elapsed_timer.setText( - helpers.ms_to_mmss(self.current_track.duration) + # Elapsed time + if time_to_end < 500: + self.label_elapsed_timer.setText( + helpers.ms_to_mmss(self.current_track.duration) + ) + else: + self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime)) + + # Time to fade + self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade)) + + # If silent in the next 5 seconds, put warning colour on + # time to silence box and enable play controls + if time_to_silence <= 5500: + self.frame_silent.setStyleSheet( + f"background: {Config.COLOUR_ENDING_TIMER}" + ) + self.enable_play_next_controls() + # Set warning colour on time to silence box when fade starts + elif time_to_fade <= 500: + self.frame_silent.setStyleSheet( + f"background: {Config.COLOUR_WARNING_TIMER}" + ) + # Five seconds before fade starts, set warning colour on + # time to silence box and enable play controls + elif time_to_fade <= 5500: + self.frame_fade.setStyleSheet( + f"background: {Config.COLOUR_WARNING_TIMER}" + ) + self.enable_play_next_controls() + else: + self.frame_silent.setStyleSheet("") + self.frame_fade.setStyleSheet("") + + self.label_silent_timer.setText( + helpers.ms_to_mmss(time_to_silence) ) - else: - self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime)) - # Time to fade - self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade)) - - # If silent in the next 5 seconds, put warning colour on - # time to silence box and enable play controls - if time_to_silence <= 5500: - self.frame_silent.setStyleSheet( - f"background: {Config.COLOUR_ENDING_TIMER}" - ) - self.enable_play_next_controls() - # Set warning colour on time to silence box when fade starts - elif time_to_fade <= 500: - self.frame_silent.setStyleSheet( - f"background: {Config.COLOUR_WARNING_TIMER}" - ) - # Five seconds before fade starts, set warning colour on - # time to silence box and enable play controls - elif time_to_fade <= 5500: - self.frame_fade.setStyleSheet( - f"background: {Config.COLOUR_WARNING_TIMER}" - ) - self.enable_play_next_controls() - else: - self.frame_silent.setStyleSheet("") - self.frame_fade.setStyleSheet("") - - self.label_silent_timer.setText( - helpers.ms_to_mmss(time_to_silence) - ) - - # Time to end - self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end)) + # Time to end + self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end)) else: if self.playing: diff --git a/app/playlists.py b/app/playlists.py index 9a6dd01..1dd3f24 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -16,6 +16,7 @@ from PyQt5.QtWidgets import ( QTableWidgetItem, ) from sqlalchemy import inspect +from sqlalchemy.orm.exc import DetachedInstanceError import helpers import os @@ -192,9 +193,9 @@ class PlaylistTab(QTableWidget): f"Moved row(s) {rows} to become row {drop_row}" ) - with Session() as session: + with Session() as session: # checked self.save_playlist(session) - self.update_display() + self.update_display(session) def edit(self, index, trigger, event): # review result = super(PlaylistTab, self).edit(index, trigger, event) @@ -249,7 +250,7 @@ class PlaylistTab(QTableWidget): def closeEvent(self, event) -> None: """Save column widths""" - with Session() as session: + with Session() as session: # checked for column in range(self.columnCount()): width = self.columnWidth(column) name = f"playlist_col_{str(column)}_width" @@ -259,11 +260,11 @@ class PlaylistTab(QTableWidget): event.accept() - def clear_next(self) -> None: + def clear_next(self, session) -> None: """Clear next track""" self._meta_clear_next() - self.update_display() + self.update_display(session) def create_note(self) -> None: """ @@ -287,7 +288,7 @@ class PlaylistTab(QTableWidget): with Session() as session: note: Notes = Notes( session, self.playlist.id, row, dlg.textValue()) - self._insert_note(session, note, row, True) + self._insert_note(session, note, row, True) # checked def get_selected_row(self) -> Optional[int]: """Return row number of first selected row, or None if none selected""" @@ -297,7 +298,7 @@ class PlaylistTab(QTableWidget): else: return self.selectionModel().selectedRows()[0].row() - def get_selected_rows_and_tracks(self) \ + def get_selected_rows_and_tracks(self, session: Session) \ -> Optional[List[Tuple[int, Tracks]]]: """Return a list of selected (row-number, track) tuples""" @@ -307,7 +308,7 @@ class PlaylistTab(QTableWidget): result = [] for row in [r.row() for r in self.selectionModel().selectedRows()]: - result.append((row, self._get_row_object(row))) + result.append((row, self._get_row_object(row, session))) return result @@ -378,14 +379,14 @@ class PlaylistTab(QTableWidget): self._set_row_content(row, track) # Mark track if file is unreadable - if not self._track_is_readable(track): + if not self._file_is_readable(track.path): self._meta_set_unreadable(row) # Scroll to new row self.scrollToItem(title_item, QAbstractItemView.PositionAtCenter) if repaint: self.save_playlist(session) - self.update_display(clear_selection=False) + self.update_display(session, clear_selection=False) def remove_rows(self, rows) -> None: """Remove rows passed in rows list""" @@ -398,10 +399,9 @@ class PlaylistTab(QTableWidget): with Session() as session: self.save_playlist(session) + self.update_display(session) - self.update_display() - - def play_started(self) -> None: + def play_started(self, session: Session) -> None: """ Notification from musicmuster that track has started playing. @@ -425,16 +425,17 @@ class PlaylistTab(QTableWidget): self._meta_set_played(current_row) # Scroll to put current track in middle - scroll_to = self.item(current_row, self.COL_INDEX) + scroll_to = self.item(current_row, self.COL_MSS) self.scrollToItem(scroll_to, QAbstractItemView.PositionAtCenter) # Set next track search_from = current_row + 1 next_row = self._find_next_track_row(search_from) - self._set_next(next_row) + if next_row: + self._set_next(next_row) # Update display - self.update_display() + self.update_display(session) def play_stopped(self) -> None: """ @@ -448,7 +449,6 @@ class PlaylistTab(QTableWidget): self._meta_clear_current() self.current_track_start_time = None - self.update_display() def save_playlist(self, session) -> None: """ @@ -474,7 +474,7 @@ class PlaylistTab(QTableWidget): # PlaylistTab for row in notes_rows: - note: Notes = self._get_row_object(row) + note: Notes = self._get_row_object(row, session) session.add(note) playlist_notes[note.id] = note @@ -604,9 +604,9 @@ class PlaylistTab(QTableWidget): if row is None: return None - self.set_next_track(row) + self._set_next(row) - def update_display(self, clear_selection: bool = True) -> None: + def update_display(self, session, clear_selection: bool = True) -> None: """ Set row colours, fonts, etc @@ -618,6 +618,8 @@ class PlaylistTab(QTableWidget): - Show unplayed tracks in bold """ + if self.playlist not in session: + session.add(self.playlist) DEBUG(f"playlist. update_display [{self.playlist=}]") # Clear selection if required @@ -654,120 +656,119 @@ class PlaylistTab(QTableWidget): if not start_times_row: start_times_row = 0 - with Session() as session: - # Cycle through all rows - for row in range(self.rowCount()): + # Cycle through all rows + for row in range(self.rowCount()): - # Render notes in correct colour - if row in notes: - # Extract note text - note_text = self.item(row, self.COL_TITLE).text() - # Does the note have a start time? - row_time = self._get_note_text_time(note_text) - if row_time: - next_start_time = row_time - # Set colour - note_colour = NoteColours.get_colour(session, note_text) - if not note_colour: - note_colour = Config.COLOUR_NOTES_PLAYLIST - self._set_row_colour( - row, QColor(note_colour) - ) - # Notes are always bold - self._set_row_bold(row) + # Render notes in correct colour + if row in notes: + # Extract note text + note_text = self.item(row, self.COL_TITLE).text() + # Does the note have a start time? + row_time = self._get_note_text_time(note_text) + if row_time: + next_start_time = row_time + # Set colour + note_colour = NoteColours.get_colour(session, note_text) + if not note_colour: + note_colour = Config.COLOUR_NOTES_PLAYLIST + self._set_row_colour( + row, QColor(note_colour) + ) + # Notes are always bold + self._set_row_bold(row) - # Render unplayable tracks in correct colour - elif row in unreadable: - self._set_row_colour( - row, QColor(Config.COLOUR_UNREADABLE) - ) - self._set_row_bold(row) + # Render unplayable tracks in correct colour + elif row in unreadable: + self._set_row_colour( + row, QColor(Config.COLOUR_UNREADABLE) + ) + self._set_row_bold(row) - # Render current track - elif row == current_row: - # Set start time - self._set_row_start_time( - row, self.current_track_start_time) + # Render current track + elif row == current_row: + # Set start time + self._set_row_start_time( + row, self.current_track_start_time) - # Set last played time - last_played_str = get_relative_date( - self.current_track_start_time) + # Set last played time + last_played_str = get_relative_date( + self.current_track_start_time) + self.item(row, self.COL_LAST_PLAYED).setText( + last_played_str) + + # Calculate next_start_time + track = self._get_row_object(row, session) + next_start_time = self._calculate_track_end_time( + track, self.current_track_start_time) + + # Set end time + self._set_row_end_time(row, next_start_time) + + # Set colour + self._set_row_colour(row, QColor( + Config.COLOUR_CURRENT_PLAYLIST)) + + # Make bold + self._set_row_bold(row) + + # Render next track + elif row == next_row: + # if there's a track playing, set start time from that + if current_row: + start_time = self.current_track_start_time + else: + # No current track to base from, but don't change + # time if it's already set + start_time = self._get_row_start_time(row) + if not start_time: + start_time = next_start_time + self._set_row_start_time(row, start_time) + + # Set end time + track = self._get_row_object(row, session) + next_start_time = self._calculate_track_end_time( + track, start_time) + self._set_row_end_time(row, next_start_time) + + # Set colour + self._set_row_colour( + row, QColor(Config.COLOUR_NEXT_PLAYLIST)) + + # Make bold + self._set_row_bold(row) + + else: + # This is a track row other than next or current + track = self._get_row_object(row, session) + if row in played: + # Played today, so update last played column + last_playedtime = Playdates.last_played( + session, track.id) + last_played_str = get_relative_date(last_playedtime) self.item(row, self.COL_LAST_PLAYED).setText( last_played_str) - - # Calculate next_start_time - track = self._get_row_object(row) - next_start_time = self._calculate_track_end_time( - track, self.current_track_start_time) - - # Set end time - self._set_row_end_time(row, next_start_time) - - # Set colour - self._set_row_colour(row, QColor( - Config.COLOUR_CURRENT_PLAYLIST)) - - # Make bold - self._set_row_bold(row) - - # Render next track - elif row == next_row: - # if there's a track playing, set start time from that - if current_row: - start_time = self.current_track_start_time - else: - # No current track to base from, but don't change - # time if it's already set - start_time = self._get_row_start_time(row) - if not start_time: - start_time = next_start_time - self._set_row_start_time(row, start_time) - - # Set end time - track = self._get_row_object(row) - next_start_time = self._calculate_track_end_time( - track, start_time) - self._set_row_end_time(row, next_start_time) - - # Set colour - self._set_row_colour( - row, QColor(Config.COLOUR_NEXT_PLAYLIST)) - - # Make bold - self._set_row_bold(row) - + self._set_row_not_bold(row) else: - # This is a track row other than next or current - track = self._get_row_object(row) - if row in played: - # Played today, so update last played column - last_playedtime = Playdates.last_played( - session, track.id) - last_played_str = get_relative_date(last_playedtime) - self.item(row, self.COL_LAST_PLAYED).setText( - last_played_str) - self._set_row_not_bold(row) + # Set start/end times as we haven't played it yet + if next_start_time and row >= start_times_row: + self._set_row_start_time(row, next_start_time) + next_start_time = self._calculate_track_end_time( + track, next_start_time) + # Set end time + self._set_row_end_time(row, next_start_time) else: - # Set start/end times as we haven't played it yet - if next_start_time and row >= start_times_row: - self._set_row_start_time(row, next_start_time) - next_start_time = self._calculate_track_end_time( - track, next_start_time) - # Set end time - self._set_row_end_time(row, next_start_time) - else: - # Clear start and end time - self._set_row_start_time(row, None) - self._set_row_end_time(row, None) - # Don't dim unplayed tracks - self._set_row_bold(row) - # Stripe rows - if row % 2: - self._set_row_colour( - row, QColor(Config.COLOUR_ODD_PLAYLIST)) - else: - self._set_row_colour( - row, QColor(Config.COLOUR_EVEN_PLAYLIST)) + # Clear start and end time + self._set_row_start_time(row, None) + self._set_row_end_time(row, None) + # Don't dim unplayed tracks + self._set_row_bold(row) + # Stripe rows + if row % 2: + self._set_row_colour( + row, QColor(Config.COLOUR_ODD_PLAYLIST)) + else: + self._set_row_colour( + row, QColor(Config.COLOUR_EVEN_PLAYLIST)) # ########## Internally called functions ########## @@ -779,8 +780,9 @@ class PlaylistTab(QTableWidget): if row in self._meta_get_notes(): return None - track: Tracks = self._get_row_object(row) - open_in_audacity(track.path) + with Session() as session: + track: Tracks = self._get_row_object(row, session) + open_in_audacity(track.path) @staticmethod def _calculate_track_end_time(track: Tracks, @@ -811,11 +813,12 @@ class PlaylistTab(QTableWidget): if row in self._meta_get_notes(): return None - track: Optional[Tracks] = self._get_row_object(row) - if track: - cb: QApplication.clipboard = QApplication.clipboard() - cb.clear(mode=cb.Clipboard) - cb.setText(track.path, mode=cb.Clipboard) + with Session() as session: + track: Optional[Tracks] = self._get_row_object(row, session) + if track: + cb: QApplication.clipboard = QApplication.clipboard() + cb.clear(mode=cb.Clipboard) + cb.setText(track.path, mode=cb.Clipboard) def _cell_changed(self, row: int, column: int) -> None: """Called when cell content has changed""" @@ -828,8 +831,9 @@ class PlaylistTab(QTableWidget): new_text: str = self.item(row, column).text() DEBUG(f"_cell_changed({row=}, {column=}, {new_text=}") - row_object: Union[Tracks, Notes] = self._get_row_object(row) with Session() as session: + row_object: Union[Tracks, Notes] = self._get_row_object( + row, session) if row in self._meta_get_notes(): # Save change to database DEBUG(f"Notes.update_note: saving new note text '{new_text=}'") @@ -866,7 +870,8 @@ class PlaylistTab(QTableWidget): # update_display to update start times, such as when a note has # been edited - self.update_display() + with Session() as session: + self.update_display(session) self.parent.enable_play_next_controls() @@ -908,18 +913,18 @@ class PlaylistTab(QTableWidget): if msg.exec() == QMessageBox.Yes: rows_to_delete.append(row) - # delete in reverse row order so row numbers don't - # change - for row in sorted(rows_to_delete, reverse=True): - row_object = self._get_row_object(row) - if row in note_rows: - row_object.delete_note(session) - else: - self.remove_track(session, row) - self.removeRow(row) + # delete in reverse row order so row numbers don't + # change + for row in sorted(rows_to_delete, reverse=True): + row_object = self._get_row_object(row, session) + if row in note_rows: + row_object.delete_note(session) + else: + self.remove_track(session, row) + self.removeRow(row) - self.save_playlist(session) - self.update_display() + self.save_playlist(session) + self.update_display(session) def _drop_on(self, event): # review index = self.indexAt(event.pos()) @@ -958,10 +963,14 @@ class PlaylistTab(QTableWidget): except ValueError: return None - def _get_row_object(self, row: int) -> Union[Tracks, Notes]: + def _get_row_object(self, row: int, session: Session) \ + -> Union[Tracks, Notes]: """Return content associated with this row""" - return self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT) + obj = self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT) + if obj not in session: + session.add(obj) + return obj def _get_row_start_time(self, row: int) -> Optional[datetime]: try: @@ -980,27 +989,29 @@ class PlaylistTab(QTableWidget): txt: str - row_object: Union[Tracks, Notes] = self._get_row_object(row) - if row in self._meta_get_notes(): - txt = row_object.note - else: - track = row_object - txt = ( - f"Title: {track.title}\n" - f"Artist: {track.artist}\n" - f"Track ID: {track.id}\n" - f"Track duration: {helpers.ms_to_mmss(track.duration)}\n" - f"Track fade at: {helpers.ms_to_mmss(track.fade_at)}\n" - f"Track silence at: {helpers.ms_to_mmss(track.silence_at)}" - "\n\n" - f"Path: {track.path}\n" - ) - info: QMessageBox = QMessageBox(self) - info.setIcon(QMessageBox.Information) - info.setText(txt) - info.setStandardButtons(QMessageBox.Ok) - info.setDefaultButton(QMessageBox.Cancel) - info.exec() + with Session() as session: + row_object: Union[Tracks, Notes] = self._get_row_object( + row, session) + if row in self._meta_get_notes(): + txt = row_object.note + else: + track = row_object + txt = ( + f"Title: {track.title}\n" + f"Artist: {track.artist}\n" + f"Track ID: {track.id}\n" + f"Track duration: {helpers.ms_to_mmss(track.duration)}\n" + f"Track fade at: {helpers.ms_to_mmss(track.fade_at)}\n" + f"Track silence at: {helpers.ms_to_mmss(track.silence_at)}" + "\n\n" + f"Path: {track.path}\n" + ) + info: QMessageBox = QMessageBox(self) + info.setIcon(QMessageBox.Information) + info.setText(txt) + info.setStandardButtons(QMessageBox.Ok) + info.setDefaultButton(QMessageBox.Cancel) + info.exec() def _insert_note(self, session: Session, note: Notes, row: Optional[int] = None, repaint: bool = True) -> None: @@ -1040,7 +1051,7 @@ class PlaylistTab(QTableWidget): if repaint: self.save_playlist(session) - self.update_display(clear_selection=False) + self.update_display(session, clear_selection=False) def _is_below(self, pos, index): # review rect = self.visualRect(index) @@ -1110,6 +1121,13 @@ class PlaylistTab(QTableWidget): current_row: Optional[int] = self._meta_get_current() if current_row is not None: self._meta_clear_attribute(current_row, RowMeta.CURRENT) + # Reset row colour + if current_row % 2: + self._set_row_colour( + current_row, QColor(Config.COLOUR_ODD_PLAYLIST)) + else: + self._set_row_colour( + current_row, QColor(Config.COLOUR_EVEN_PLAYLIST)) def _meta_clear_next(self) -> None: """ @@ -1274,7 +1292,7 @@ class PlaylistTab(QTableWidget): # We possibly don't need to save the playlist here, but row # numbers may have changed during population, and it's cheap to do self.save_playlist(session) - self.update_display() + self.update_display(session) def _rescan(self, row: int) -> None: """ @@ -1287,11 +1305,11 @@ class PlaylistTab(QTableWidget): if row in self._meta_get_notes(): return None - track: Tracks = self._get_row_object(row) - if track: - with Session() as session: + with Session() as session: + track: Tracks = self._get_row_object(row, session) + if track: track.rescan(session) - self._update_row(row, track) + self._update_row(session, row, track) def _select_event(self) -> None: """ @@ -1304,15 +1322,19 @@ class PlaylistTab(QTableWidget): track_rows = list(row_set - note_row_set) tracks: List[Tracks] - tracks = [self._get_row_object(row) for row in track_rows] - ms: int = sum([track.duration for track in tracks]) + with Session() as session: # checked + tracks = [self._get_row_object(row, session) for row in track_rows] + for track in tracks: + if track not in session: + session.add(track) + ms: int = sum([track.duration for track in tracks]) - # Only paint message if there are selected track rows - if ms > 0: - self.parent.lblSumPlaytime.setText( - f"Selected duration: {helpers.ms_to_mmss(ms)}") - else: - self.parent.lblSumPlaytime.setText("") + # Only paint message if there are selected track rows + if ms > 0: + self.parent.lblSumPlaytime.setText( + f"Selected duration: {helpers.ms_to_mmss(ms)}") + else: + self.parent.lblSumPlaytime.setText("") def _set_column_widths(self) -> None: """Column widths from settings""" @@ -1340,26 +1362,27 @@ class PlaylistTab(QTableWidget): DEBUG(f"_set_next({row=})") - # Check row is a track row - if row in self._meta_get_notes(): - return None - track: Tracks = self._get_row_object(row) - if not track: - return None + with Session() as session: + # Check row is a track row + if row in self._meta_get_notes(): + return None + track: Tracks = self._get_row_object(row, session) + if not track: + return None - # Check track is readable - if not self._track_is_readable(track): - self._meta_set_unreadable(row) - return None + # Check track is readable + if not self._file_is_readable(track.path): + self._meta_set_unreadable(row) + return None - # Mark as next track - self._meta_set_next(row) + # Mark as next track + self._meta_set_next(row) - # Notify musicmuster - self.parent.this_is_the_next_track(self, track) + # Notify musicmuster + self.parent.this_is_the_next_track(self, track) - # Update display - self.update_display() + # Update display + self.update_display(session) def _set_row_bold(self, row: int, bold: bool = True) -> None: """Make row bold (bold=True) or not bold""" @@ -1415,23 +1438,42 @@ class PlaylistTab(QTableWidget): item: QTableWidgetItem = QTableWidgetItem(time_str) self.setItem(row, self.COL_START_TIME, item) - def _track_path_is_readable(self, track_id): + def _select_tracks(self, played: bool) -> None: + """ + Select all played (played=True) or unplayed (played=False) + tracks in playlist + """ + + # Need to allow multiple rows to be selected + self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) + notes_rows: List[int] = self._meta_get_notes() + self.clearSelection() + + played_rows: List[int] = self._meta_get_played() + for row in range(self.rowCount()): + if row in notes_rows: + continue + if row in played_rows == played: + self.selectRow(row) + + # Reset extended selection + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + + @staticmethod + def _file_is_readable(path: str) -> bool: """ Returns True if track path is readable, else False vlc cannot read files with a colon in the path """ - with Session() as session: - path = Tracks.get_path(session, track_id) - if os.access(path, os.R_OK): if ':' not in path: return True return False - def _update_row(self, row: int, track: Tracks) -> None: + def _update_row(self, session, row: int, track: Tracks) -> None: """ Update the passed row with info from the passed track. """ @@ -1454,4 +1496,4 @@ class PlaylistTab(QTableWidget): item_duration: QTableWidgetItem = self.item(row, self.COL_DURATION) item_duration.setText(helpers.ms_to_mmss(track.duration)) - self.update_display() + self.update_display(session)