From d113b9fc2004be9c57d94b766c0223290c1b5a76 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sun, 25 Apr 2021 09:44:32 +0100 Subject: [PATCH] Name internal functions with underscore in playlists.py --- app/playlists.py | 981 +++++++++++++++++++++++------------------------ 1 file changed, 489 insertions(+), 492 deletions(-) diff --git a/app/playlists.py b/app/playlists.py index f3ab5b5..f1f5119 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -26,6 +26,7 @@ from model import Notes, Playdates, Playlists, PlaylistTracks, Settings, Tracks from ui.dlg_search_database_ui import Ui_Dialog from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist + class Playlist(QTableWidget): # Column names COL_INDEX = 0 @@ -58,7 +59,7 @@ class Playlist(QTableWidget): # This signal is emitted when the widget's contextMenuPolicy is # Qt::CustomContextMenu, and the user has requested a context # menu on the widget. - self.customContextMenuRequested.connect(self.context_menu) + self.customContextMenuRequested.connect(self._context_menu) self.viewport().installEventFilter(self) self.music = music.Music() @@ -70,6 +71,8 @@ class Playlist(QTableWidget): self.previous_track_position = None self.played_tracks = [] + # ########## Events ########## + def closeEvent(self, event): "Save column widths" @@ -82,6 +85,48 @@ class Playlist(QTableWidget): event.accept() + def dropEvent(self, event: QDropEvent): + if not event.isAccepted() and event.source() == self: + drop_row = self._drop_on(event) + + rows = sorted(set(item.row() for item in self.selectedItems())) + rows_to_move = [ + [QTableWidgetItem(self.item(row_index, column_index)) for + column_index in range(self.columnCount())] + for row_index in rows + ] + for row_index in reversed(rows): + self.removeRow(row_index) + if row_index < drop_row: + drop_row -= 1 + + for row_index, data in enumerate(rows_to_move): + row_index += drop_row + self.insertRow(row_index) + for column_index, column_data in enumerate(data): + self.setItem(row_index, column_index, column_data) + event.accept() + # We don't want rows to be selected after move + # for row_index in range(len(rows_to_move)): + # for column_index in range(self.columnCount()): + # self.item(drop_row + row_index, + # column_index).setSelected(True) + # The above doesn't handle column spans, which we use in note + # rows. Check and fix: + for row in range(drop_row, drop_row + len(rows_to_move)): + if row in self._meta_get_notes(): + self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, + self.NOTE_COL_SPAN) + + super().dropEvent(event) + + DEBUG( + "playlist.dropEvent(): " + f"Moved row(s) {rows} to become row {drop_row}" + ) + + self._repaint() + def eventFilter(self, source, event): "Used to process context (right-click) menu" @@ -93,48 +138,16 @@ class Playlist(QTableWidget): row = item.row() DEBUG(f"playlist.eventFilter(): Right-click on row {row}") self.menu = QMenu(self) - if row not in self.meta_get_notes(): + if row not in self._meta_get_notes(): act_setnext = self.menu.addAction("Set next") - act_setnext.triggered.connect(lambda: self.set_next(row)) + act_setnext.triggered.connect(lambda: self._set_next(row)) self.menu.addSeparator() act_delete = self.menu.addAction('Delete') - act_delete.triggered.connect(lambda: self.delete_row(row)) + act_delete.triggered.connect(lambda: self._delete_row(row)) return super(Playlist, self).eventFilter(source, event) - def context_menu(self, pos): - - self.menu.exec_(self.mapToGlobal(pos)) - - def delete_row(self, row): - "Delete row" - - if row == self.meta_get_current(): - # TODO - DEBUG("playlist.delete_row(): Can't delete playing track") - elif row == self.meta_get_next(): - # TODO - DEBUG("playlist.delete_row(): Can't delete next track") - - else: - title = self.item(row, self.COL_TITLE).text() - - msg = QMessageBox(self) - msg.setIcon(QMessageBox.Warning) - msg.setText(f"Delete '{title}'?") - msg.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel) - msg.setDefaultButton(QMessageBox.Cancel) - msg.setWindowTitle("Delete row") - if msg.exec() == QMessageBox.Yes: - DEBUG(f"playlist.delete_row(): Delete row {row}") - id = int(self.item(row, self.COL_INDEX).text()) - if row in self.meta_get_notes(): - Notes.delete_note(id) - else: - PlaylistTracks.remove_track(self.playlist_id, id) - self.removeRow(row) - - self.repaint() + # ########## Externally called functions ########## def add_note(self, text): """ @@ -209,22 +222,17 @@ class Playlist(QTableWidget): # Add start times or empty items, otherwise background # colour won't be set for columns without items - self.set_row_time(row, start_time) + self._set_row_time(row, start_time) item = QTableWidgetItem() self.setItem(row, self.COL_PATH, item) - self.meta_set_note(row) + self._meta_set_note(row) # Scroll to new row self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter) if repaint: - self.repaint(clear_selection=False) - - def can_read_track(self, track): - "Check track file is readable" - - return os.access(track.path, os.R_OK) + self._repaint(clear_selection=False) def create_playlist(self, name): "Create new playlist" @@ -232,56 +240,6 @@ class Playlist(QTableWidget): new_id = Playlists.new(name) self.load_playlist(new_id) - def drop_on(self, event): - index = self.indexAt(event.pos()) - if not index.isValid(): - return self.rowCount() - - return (index.row() + 1 if self.is_below(event.pos(), index) - else index.row()) - - def dropEvent(self, event: QDropEvent): - if not event.isAccepted() and event.source() == self: - drop_row = self.drop_on(event) - - rows = sorted(set(item.row() for item in self.selectedItems())) - rows_to_move = [ - [QTableWidgetItem(self.item(row_index, column_index)) for - column_index in range(self.columnCount())] - for row_index in rows - ] - for row_index in reversed(rows): - self.removeRow(row_index) - if row_index < drop_row: - drop_row -= 1 - - for row_index, data in enumerate(rows_to_move): - row_index += drop_row - self.insertRow(row_index) - for column_index, column_data in enumerate(data): - self.setItem(row_index, column_index, column_data) - event.accept() - # We don't want rows to be selected after move - # for row_index in range(len(rows_to_move)): - # for column_index in range(self.columnCount()): - # self.item(drop_row + row_index, - # column_index).setSelected(True) - # The above doesn't handle column spans, which we use in note - # rows. Check and fix: - for row in range(drop_row, drop_row + len(rows_to_move)): - if row in self.meta_get_notes(): - self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, - self.NOTE_COL_SPAN) - - super().dropEvent(event) - - DEBUG( - "playlist.dropEvent(): " - f"Moved row(s) {rows} to become row {drop_row}" - ) - - self.repaint() - def fade(self): self.previous_track = self.current_track self.previous_track_position = self.music.fade() @@ -304,10 +262,6 @@ class Playlist(QTableWidget): except AttributeError: return 0 - def get_current_playtime(self): - - return self.music.get_playtime() - def get_current_silence_at(self): try: return self.current_track.silence_at @@ -332,12 +286,6 @@ class Playlist(QTableWidget): except AttributeError: return "" - def get_next_track_id(self): - try: - return self.next_track.id - except AttributeError: - return 0 - def get_previous_artist(self): try: return self.previous_track.artist @@ -350,44 +298,6 @@ class Playlist(QTableWidget): except AttributeError: return "" - def get_row_time(self, row): - try: - if self.item(row, self.COL_START_TIME): - return datetime.strptime(self.item( - row, self.COL_START_TIME).text(), "%H:%M:%S" - ) - else: - return None - except ValueError: - return None - - def calculate_next_start_time(self, row, start): - "Return this row's end time given its start time" - - if start is None: - return None - if row is None: - DEBUG("calculate_next_start_time() called with row=None") - return None - - duration = Tracks.get_duration( - int(self.item(row, self.COL_INDEX).text())) - return start + timedelta(milliseconds=duration) - - def is_below(self, pos, index): - rect = self.visualRect(index) - margin = 2 - if pos.y() - rect.top() < margin: - return False - elif rect.bottom() - pos.y() < margin: - return True - # noinspection PyTypeChecker - return ( - rect.contains(pos, True) and not - (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) - and pos.y() >= rect.center().y() # noqa W503 - ) - def load_playlist(self, plid): """ Load tracks and notes from playlist id. @@ -424,114 +334,24 @@ class Playlist(QTableWidget): # a next track set in which case don't change it. Otherwise, set # the first non-notes row as next track to play. if not self.next_track: - notes_rows = self.meta_get_notes() + notes_rows = self._meta_get_notes() for row in range(self.rowCount()): if row in notes_rows: continue - self.cue_next_track(row) + self._cue_next_track(row) break # Scroll to top scroll_to = self.item(0, self.COL_INDEX) self.scrollToItem(scroll_to, QAbstractItemView.PositionAtTop) - def meta_clear(self, row): - "Clear metadata for row" + def music_ended(self): + "Update display" - self.meta_set(row, None) - - def meta_clear_current(self): - """ - Clear current row if there is one. There may not be if - we've changed playlists - """ - - current_row = self.meta_get_current() - if current_row: - self.meta_clear(current_row) - - def meta_find(self, metadata, one=True): - """ - Search rows for metadata. - - If one is True, check that only one row matches and return - the row number. - - If one is False, return a list of matching row numbers. - """ - - matches = [] - for row in range(self.rowCount()): - if self.meta_get(row) == metadata: - matches.append(row) - - if not one: - return matches - - if len(matches) == 0: - return None - elif len(matches) == 1: - return matches[0] - else: - ERROR( - f"Multiple matches for metadata '{metadata}' found " - f"in rows: {', '.join([str(x) for x in matches])}" - ) - raise AttributeError(f"Multiple '{metadata}' metadata {matches}") - - def meta_get(self, row): - "Return row metadata" - - return self.item(row, self.COL_INDEX).data(Qt.UserRole) - - def meta_get_current(self): - "Return row marked as current, or None" - - return self.meta_find("current") - - def meta_get_next(self): - "Return row marked as next, or None" - - return self.meta_find("next") - - def meta_get_notes(self): - "Return rows marked as notes, or None" - - return self.meta_find("note", one=False) - - def meta_set_current(self, row): - "Mark row as current track" - - old_current = self.meta_get_current() - if old_current is not None: - self.meta_clear(old_current) - self.meta_set(row, "current") - - def meta_set_next(self, row): - "Mark row as next track" - - old_next = self.meta_get_next() - if old_next is not None: - self.meta_clear(old_next) - self.meta_set(row, "next") - - def meta_set_note(self, row): - "Mark row as note" - - self.meta_set(row, "note") - - def meta_set(self, row, metadata): - "Set row metadata" - - if self.item(row, self.COL_TITLE): - title = self.item(row, self.COL_TITLE).text() - else: - title = "" - DEBUG(f"meta_set(row={row}, title={title}, metadata={metadata})") - if row is None: - raise ValueError(f"meta_set() with row=None") - - self.item(row, self.COL_INDEX).setData(Qt.UserRole, metadata) + self.previous_track = self.current_track + self.previous_track_position = 0 + self._meta_clear_current() + self._repaint() def play_next(self): """ @@ -573,20 +393,20 @@ class Playlist(QTableWidget): self.current_track.start_time = datetime.now() # Update metadata - self.meta_set_current(self.meta_get_next()) + self._meta_set_current(self._meta_get_next()) # Set up metadata for next track in playlist if there is one. - current_row = self.meta_get_current() + current_row = self._meta_get_current() if current_row is not None: start = current_row + 1 else: start = 0 - notes_rows = self.meta_get_notes() + notes_rows = self._meta_get_notes() for row in range(start, self.rowCount()): if row in notes_rows: continue if self.item(row, self.COL_INDEX): - self.cue_next_track(row) + self._cue_next_track(row) break # Tell database to record it as played @@ -597,7 +417,7 @@ class Playlist(QTableWidget): self.played_tracks.append(self.current_track.id) # Update display - self.repaint() + self._repaint() def search_database(self): dlg = DbDialog(self) @@ -607,229 +427,6 @@ class Playlist(QTableWidget): dlg = SelectPlaylistDialog(self) dlg.exec() - def set_selected_as_next(self): - """ - Sets the selected track as the next track. - """ - - if not self.selectionModel().hasSelection(): - return - - self.set_next(self.currentRow()) - - def set_next(self, row): - """ - If passed row is track row, set that track as the next track to - be played and return True. Otherwise return False. - """ - - if row in self.meta_get_notes(): - return False - - if self.item(row, self.COL_INDEX): - self.cue_next_track(row) - return True - - return False - - def stop(self): - "Stop playing immediately" - - self.previous_track = self.current_track - self.previous_track_position = self.music.stop() - - def music_ended(self): - "Update display" - - self.previous_track = self.current_track - self.previous_track_position = 0 - self.meta_clear_current() - self.repaint() - - def repaint(self, clear_selection=True): - "Set row colours, fonts, etc, and save playlist" - - DEBUG(f"repaint(clear_selection={clear_selection})") - - self.save_playlist() - - if clear_selection: - self.clearSelection() - current = self.meta_get_current() - next = self.meta_get_next() or 0 - notes = self.meta_get_notes() - - # Set colours and start times - next_start_time = None - - # Cycle through all rows - for row in range(self.rowCount()): - # We can't calculate start times until next_start_time is - # set. That can be set by either a note with a time, or the - # current track. - - if row in notes: - row_time = self.get_row_time(row) - if row_time: - next_start_time = row_time - # Set colour - self.set_row_colour( - row, QColor(Config.COLOUR_NOTES_PLAYLIST) - ) - self.set_row_bold(row) - - elif row == current: - # Set start time - self.set_row_time(row, self.current_track.start_time) - # Calculate next_start_time - next_start_time = self.calculate_next_start_time( - row, self.current_track.start_time) - # Set colour - self.set_row_colour(row, QColor( - Config.COLOUR_CURRENT_PLAYLIST)) - # Make bold - self.set_row_bold(row) - - elif row == next: - # if there's a current row, set start time from that - if self.current_track: - start_time = self.calculate_next_start_time( - current, 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_time(row) - if not start_time: - start_time = next_start_time - # Now set it - self.set_row_time(row, start_time) - next_start_time = self.calculate_next_start_time( - row, start_time) - # Set colour - self.set_row_colour(row, QColor(Config.COLOUR_NEXT_PLAYLIST)) - # Make bold - self.set_row_bold(row) - - else: - # Stripe remaining rows - if row % 2: - colour = QColor(Config.COLOUR_ODD_PLAYLIST) - else: - colour = QColor(Config.COLOUR_EVEN_PLAYLIST) - self.set_row_colour(row, colour) - - if (int(self.item(row, self.COL_INDEX).text()) in - self.played_tracks): - self.set_row_not_bold(row) - else: - # Set time only if we haven't played it yet - if next_start_time: - self.set_row_time(row, next_start_time) - next_start_time = self.calculate_next_start_time( - row, next_start_time) - # Don't dim unplayed tracks - self.set_row_bold(row) - - # Headers might need updating - self.parent().parent().update_headers() - - def save_playlist(self): - """ - Save playlist to database. Add missing notes/tracks; remove any that - are in database but not playlist. Correct row number in database if - necessary. - """ - - note_rows = self.meta_get_notes() - playlist = Playlists.get_playlist_by_id(self.playlist_id) - - # Create dictionaries indexed by track/note id - playlist_notes = {} - playlist_tracks = {} - database_notes = {} - database_tracks = {} - - # Playlist - for row in range(self.rowCount()): - # Get id of item - if self.item(row, self.COL_INDEX): - id = int(self.item(row, self.COL_INDEX).text()) - else: - DEBUG(f"(save_playlist(): no COL_INDEX data in row {row}") - continue - if row in note_rows: - playlist_notes[id] = row - else: - playlist_tracks[id] = row - - # Database - for note in playlist.notes: - database_notes[note.id] = note.row - for track in playlist.tracks: - database_tracks[track.track_id] = track.row - - # Notes to remove from playlist in database - for note_id in set(database_notes.keys()) - set(playlist_notes.keys()): - DEBUG( - f"save_playlist(): Delete note.id={id} " - f"from playlist {playlist} in database" - ) - Notes.delete_note(note_id) - - # Tracks to remove from playlist in database - for track_id in ( - set(database_tracks.keys()) - set(playlist_tracks.keys()) - ): - DEBUG( - f"save_playlist(): Delete track.id={track_id} " - f"from playlist {playlist} in database" - ) - PlaylistTracks.remove_track(playlist.id, track_id) - - # Notes to add to playlist database - # This should never be needed as notes are added to a specific - # playlist upon creation - for note_id in set(playlist_notes.keys()) - set(database_notes.keys()): - ERROR( - f"save_playlist(): Note.id={note_id} " - f"missing from playlist {playlist} in database" - ) - - # Notes to remove from playlist database - for note_id in set(database_notes.keys()) - set(playlist_notes.keys()): - DEBUG( - f"save_playlist(): Remove note.id={note_id} " - f"from playlist {playlist} in database" - ) - Notes.delete_note(note_id) - - # Note rows to update in playlist database - for note_id in set(playlist_notes.keys()) & set(database_notes.keys()): - if playlist_notes[note_id] != database_notes[note_id]: - DEBUG( - f"save_playlist(): Set database note.id {note_id} " - f"row={playlist_notes[note_id]} " - f"in playlist {playlist} in database" - ) - Notes.update_note(note_id, playlist_notes[note_id]) - - # Track rows to update in playlist database - for track_id in ( - set(playlist_tracks.keys()) & set(database_tracks.keys()) - ): - if playlist_tracks[track_id] != database_tracks[track_id]: - DEBUG( - f"save_playlist(): Set database track.id {track_id} " - f"row={playlist_tracks[track_id]} " - f"in playlist {playlist} in database" - ) - PlaylistTracks.update_track_row( - playlist_id=self.playlist_id, - track_id=track_id, - old_row=database_tracks[track_id], - new_row=playlist_tracks[track_id] - ) - def set_column_widths(self): # Column widths from settings @@ -844,41 +441,58 @@ class Playlist(QTableWidget): print("setting column width") self.setColumnWidth(column, record.f_int) - def set_row_bold(self, row, bold=True): - boldfont = QFont() - boldfont.setBold(bold) - for j in range(self.columnCount()): - if self.item(row, j): - self.item(row, j).setFont(boldfont) + def set_selected_as_next(self): + """ + Sets the selected track as the next track. + """ - def set_row_colour(self, row, colour): - for j in range(self.columnCount()): - if self.item(row, j): - self.item(row, j).setBackground(colour) + if not self.selectionModel().hasSelection(): + return - def set_row_not_bold(self, row): - self.set_row_bold(row, False) + self._set_next(self.currentRow()) - def set_row_time(self, row, time): - try: - time_str = time.strftime("%H:%M:%S") - except AttributeError: - time_str = "" - item = QTableWidgetItem(time_str) - self.setItem(row, self.COL_START_TIME, item) + def stop(self): + "Stop playing immediately" - def cue_next_track(self, next_row): + self.previous_track = self.current_track + self.previous_track_position = self.music.stop() + + # ########## Internally called functions ########## + + def _calculate_next_start_time(self, row, start): + "Return this row's end time given its start time" + + if start is None: + return None + if row is None: + DEBUG("_calculate_next_start_time() called with row=None") + return None + + duration = Tracks.get_duration( + int(self.item(row, self.COL_INDEX).text())) + return start + timedelta(milliseconds=duration) + + def _can_read_track(self, track): + "Check track file is readable" + + return os.access(track.path, os.R_OK) + + def _context_menu(self, pos): + + self.menu.exec_(self.mapToGlobal(pos)) + + def _cue_next_track(self, next_row): """ Set the passed row as the next track to play """ if next_row is not None: - self.meta_set_next(next_row) + self._meta_set_next(next_row) track_id = int(self.item(next_row, self.COL_INDEX).text()) if not self.next_track or self.next_track.id != track_id: self.next_track = Tracks.get_track(track_id) # Check we can read it - if not self.can_read_track(self.next_track): + if not self._can_read_track(self.next_track): self.parent().parent().show_warning( "Can't read next track", self.next_track.path) @@ -886,7 +500,390 @@ class Playlist(QTableWidget): self.next_track = None # Update display - self.repaint() + self._repaint() + + def _delete_row(self, row): + "Delete row" + + if row == self._meta_get_current(): + # TODO + DEBUG("playlist._delete_row(): Can't delete playing track") + elif row == self._meta_get_next(): + # TODO + DEBUG("playlist._delete_row(): Can't delete next track") + + else: + title = self.item(row, self.COL_TITLE).text() + + msg = QMessageBox(self) + msg.setIcon(QMessageBox.Warning) + msg.setText(f"Delete '{title}'?") + msg.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel) + msg.setDefaultButton(QMessageBox.Cancel) + msg.setWindowTitle("Delete row") + if msg.exec() == QMessageBox.Yes: + DEBUG(f"playlist._delete_row(): Delete row {row}") + id = int(self.item(row, self.COL_INDEX).text()) + if row in self._meta_get_notes(): + Notes.delete_note(id) + else: + PlaylistTracks.remove_track(self.playlist_id, id) + self.removeRow(row) + + self._repaint() + + def _drop_on(self, event): + index = self.indexAt(event.pos()) + if not index.isValid(): + return self.rowCount() + + return (index.row() + 1 if self._is_below(event.pos(), index) + else index.row()) + + def _get_row_time(self, row): + try: + if self.item(row, self.COL_START_TIME): + return datetime.strptime(self.item( + row, self.COL_START_TIME).text(), "%H:%M:%S" + ) + else: + return None + except ValueError: + return None + + def _is_below(self, pos, index): + rect = self.visualRect(index) + margin = 2 + if pos.y() - rect.top() < margin: + return False + elif rect.bottom() - pos.y() < margin: + return True + # noinspection PyTypeChecker + return ( + rect.contains(pos, True) and not + (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) + and pos.y() >= rect.center().y() # noqa W503 + ) + + def _meta_clear(self, row): + "Clear metadata for row" + + self._meta_set(row, None) + + def _meta_clear_current(self): + """ + Clear current row if there is one. There may not be if + we've changed playlists + """ + + current_row = self._meta_get_current() + if current_row: + self._meta_clear(current_row) + + def _meta_find(self, metadata, one=True): + """ + Search rows for metadata. + + If one is True, check that only one row matches and return + the row number. + + If one is False, return a list of matching row numbers. + """ + + matches = [] + for row in range(self.rowCount()): + if self._meta_get(row) == metadata: + matches.append(row) + + if not one: + return matches + + if len(matches) == 0: + return None + elif len(matches) == 1: + return matches[0] + else: + ERROR( + f"Multiple matches for metadata '{metadata}' found " + f"in rows: {', '.join([str(x) for x in matches])}" + ) + raise AttributeError(f"Multiple '{metadata}' metadata {matches}") + + def _meta_get(self, row): + "Return row metadata" + + return self.item(row, self.COL_INDEX).data(Qt.UserRole) + + def _meta_get_current(self): + "Return row marked as current, or None" + + return self._meta_find("current") + + def _meta_get_next(self): + "Return row marked as next, or None" + + return self._meta_find("next") + + def _meta_get_notes(self): + "Return rows marked as notes, or None" + + return self._meta_find("note", one=False) + + def _meta_set_current(self, row): + "Mark row as current track" + + old_current = self._meta_get_current() + if old_current is not None: + self._meta_clear(old_current) + self._meta_set(row, "current") + + def _meta_set_next(self, row): + "Mark row as next track" + + old_next = self._meta_get_next() + if old_next is not None: + self._meta_clear(old_next) + self._meta_set(row, "next") + + def _meta_set_note(self, row): + "Mark row as note" + + self._meta_set(row, "note") + + def _meta_set(self, row, metadata): + "Set row metadata" + + if self.item(row, self.COL_TITLE): + title = self.item(row, self.COL_TITLE).text() + else: + title = "" + DEBUG(f"_meta_set(row={row}, title={title}, metadata={metadata})") + if row is None: + raise ValueError(f"_meta_set() with row=None") + + self.item(row, self.COL_INDEX).setData(Qt.UserRole, metadata) + + def _set_next(self, row): + """ + If passed row is track row, set that track as the next track to + be played and return True. Otherwise return False. + """ + + if row in self._meta_get_notes(): + return False + + if self.item(row, self.COL_INDEX): + self._cue_next_track(row) + return True + + return False + + def _repaint(self, clear_selection=True): + "Set row colours, fonts, etc, and save playlist" + + DEBUG(f"_repaint(clear_selection={clear_selection})") + + self._save_playlist() + + if clear_selection: + self.clearSelection() + current = self._meta_get_current() + next = self._meta_get_next() or 0 + notes = self._meta_get_notes() + + # Set colours and start times + next_start_time = None + + # Cycle through all rows + for row in range(self.rowCount()): + # We can't calculate start times until next_start_time is + # set. That can be set by either a note with a time, or the + # current track. + + if row in notes: + row_time = self._get_row_time(row) + if row_time: + next_start_time = row_time + # Set colour + self._set_row_colour( + row, QColor(Config.COLOUR_NOTES_PLAYLIST) + ) + self._set_row_bold(row) + + elif row == current: + # Set start time + self._set_row_time(row, self.current_track.start_time) + # Calculate next_start_time + next_start_time = self._calculate_next_start_time( + row, self.current_track.start_time) + # Set colour + self._set_row_colour(row, QColor( + Config.COLOUR_CURRENT_PLAYLIST)) + # Make bold + self._set_row_bold(row) + + elif row == next: + # if there's a current row, set start time from that + if self.current_track: + start_time = self._calculate_next_start_time( + current, 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_time(row) + if not start_time: + start_time = next_start_time + # Now set it + self._set_row_time(row, start_time) + next_start_time = self._calculate_next_start_time( + row, start_time) + # Set colour + self._set_row_colour(row, QColor(Config.COLOUR_NEXT_PLAYLIST)) + # Make bold + self._set_row_bold(row) + + else: + # Stripe remaining rows + if row % 2: + colour = QColor(Config.COLOUR_ODD_PLAYLIST) + else: + colour = QColor(Config.COLOUR_EVEN_PLAYLIST) + self._set_row_colour(row, colour) + + if (int(self.item(row, self.COL_INDEX).text()) in + self.played_tracks): + self._set_row_not_bold(row) + else: + # Set time only if we haven't played it yet + if next_start_time: + self._set_row_time(row, next_start_time) + next_start_time = self._calculate_next_start_time( + row, next_start_time) + # Don't dim unplayed tracks + self._set_row_bold(row) + + # Headers might need updating + self.parent().parent().update_headers() + + def _save_playlist(self): + """ + Save playlist to database. Add missing notes/tracks; remove any that + are in database but not playlist. Correct row number in database if + necessary. + """ + + note_rows = self._meta_get_notes() + playlist = Playlists.get_playlist_by_id(self.playlist_id) + + # Create dictionaries indexed by track/note id + playlist_notes = {} + playlist_tracks = {} + database_notes = {} + database_tracks = {} + + # Playlist + for row in range(self.rowCount()): + # Get id of item + if self.item(row, self.COL_INDEX): + id = int(self.item(row, self.COL_INDEX).text()) + else: + DEBUG(f"(_save_playlist(): no COL_INDEX data in row {row}") + continue + if row in note_rows: + playlist_notes[id] = row + else: + playlist_tracks[id] = row + + # Database + for note in playlist.notes: + database_notes[note.id] = note.row + for track in playlist.tracks: + database_tracks[track.track_id] = track.row + + # Notes to remove from playlist in database + for note_id in set(database_notes.keys()) - set(playlist_notes.keys()): + DEBUG( + f"_save_playlist(): Delete note.id={id} " + f"from playlist {playlist} in database" + ) + Notes.delete_note(note_id) + + # Tracks to remove from playlist in database + for track_id in ( + set(database_tracks.keys()) - set(playlist_tracks.keys()) + ): + DEBUG( + f"_save_playlist(): Delete track.id={track_id} " + f"from playlist {playlist} in database" + ) + PlaylistTracks.remove_track(playlist.id, track_id) + + # Notes to add to playlist database + # This should never be needed as notes are added to a specific + # playlist upon creation + for note_id in set(playlist_notes.keys()) - set(database_notes.keys()): + ERROR( + f"_save_playlist(): Note.id={note_id} " + f"missing from playlist {playlist} in database" + ) + + # Notes to remove from playlist database + for note_id in set(database_notes.keys()) - set(playlist_notes.keys()): + DEBUG( + f"_save_playlist(): Remove note.id={note_id} " + f"from playlist {playlist} in database" + ) + Notes.delete_note(note_id) + + # Note rows to update in playlist database + for note_id in set(playlist_notes.keys()) & set(database_notes.keys()): + if playlist_notes[note_id] != database_notes[note_id]: + DEBUG( + f"_save_playlist(): Set database note.id {note_id} " + f"row={playlist_notes[note_id]} " + f"in playlist {playlist} in database" + ) + Notes.update_note(note_id, playlist_notes[note_id]) + + # Track rows to update in playlist database + for track_id in ( + set(playlist_tracks.keys()) & set(database_tracks.keys()) + ): + if playlist_tracks[track_id] != database_tracks[track_id]: + DEBUG( + f"_save_playlist(): Set database track.id {track_id} " + f"row={playlist_tracks[track_id]} " + f"in playlist {playlist} in database" + ) + PlaylistTracks.update_track_row( + playlist_id=self.playlist_id, + track_id=track_id, + old_row=database_tracks[track_id], + new_row=playlist_tracks[track_id] + ) + + def _set_row_bold(self, row, bold=True): + boldfont = QFont() + boldfont.setBold(bold) + for j in range(self.columnCount()): + if self.item(row, j): + self.item(row, j).setFont(boldfont) + + def _set_row_colour(self, row, colour): + for j in range(self.columnCount()): + if self.item(row, j): + self.item(row, j).setBackground(colour) + + def _set_row_not_bold(self, row): + self._set_row_bold(row, False) + + def _set_row_time(self, row, time): + try: + time_str = time.strftime("%H:%M:%S") + except AttributeError: + time_str = "" + item = QTableWidgetItem(time_str) + self.setItem(row, self.COL_START_TIME, item) class DbDialog(QDialog):