diff --git a/app/models.py b/app/models.py index 2c037e3..65428af 100644 --- a/app/models.py +++ b/app/models.py @@ -52,11 +52,11 @@ class Carts(Base): __tablename__ = 'carts' id: int = Column(Integer, primary_key=True, autoincrement=True) - cart_number = Column(Integer, nullable=False, unique=True) + cart_number: int = Column(Integer, nullable=False, unique=True) name = Column(String(256), index=True) duration = Column(Integer, index=True) path = Column(String(2048), index=False) - enabled = Column(Boolean, default=False, nullable=False) + enabled: bool = Column(Boolean, default=False, nullable=False) def __repr__(self) -> str: return ( @@ -192,13 +192,13 @@ class Playlists(Base): __tablename__ = "playlists" id = Column(Integer, primary_key=True, autoincrement=True, nullable=False) - name = Column(String(32), nullable=False, unique=True) + name: str = Column(String(32), nullable=False, unique=True) last_used = Column(DateTime, default=None, nullable=True) tab = Column(Integer, default=None, nullable=True, unique=True) sort_column = Column(Integer, default=None, nullable=True, unique=False) - is_template = Column(Boolean, default=False, nullable=False) + is_template: bool = Column(Boolean, default=False, nullable=False) query = Column(String(256), default=None, nullable=True, unique=False) - deleted = Column(Boolean, default=False, nullable=False) + deleted: bool = Column(Boolean, default=False, nullable=False) rows: List["PlaylistRows"] = relationship( "PlaylistRows", back_populates="playlist", @@ -371,14 +371,15 @@ class Playlists(Base): class PlaylistRows(Base): __tablename__ = 'playlist_rows' - id = Column(Integer, primary_key=True, autoincrement=True) - row_number = Column(Integer, nullable=False) - note = Column(String(2048), index=False) - playlist_id = Column(Integer, ForeignKey('playlists.id'), nullable=False) + id: int = Column(Integer, primary_key=True, autoincrement=True) + row_number: int = Column(Integer, nullable=False) + note: str = Column(String(2048), index=False, default="", nullable=False) + playlist_id: int = Column(Integer, ForeignKey('playlists.id'), + nullable=False) playlist: Playlists = relationship(Playlists, back_populates="rows") track_id = Column(Integer, ForeignKey('tracks.id'), nullable=True) track: "Tracks" = relationship("Tracks", back_populates="playlistrows") - played = Column(Boolean, nullable=False, index=False, default=False) + played: bool = Column(Boolean, nullable=False, index=False, default=False) def __repr__(self) -> str: return ( @@ -392,7 +393,7 @@ class PlaylistRows(Base): playlist_id: int, track_id: Optional[int], row_number: int, - note: Optional[str] = None + note: str = "" ) -> None: """Create PlaylistRows object""" @@ -479,8 +480,7 @@ class PlaylistRows(Base): cls.note.endswith("-") | cls.note.endswith("+") ) - ) - .order_by(cls.row_number)).scalars().all() + ).order_by(cls.row_number)).scalars().all() return plrs @@ -531,7 +531,7 @@ class PlaylistRows(Base): def get_rows_with_tracks( cls, session: scoped_session, playlist_id: int, from_row: Optional[int] = None, - to_row: Optional[int] = None) -> List["PlaylistRows"]: + to_row: Optional[int] = None) -> List["PlaylistRows"]: """ For passed playlist, return a list of rows that contain tracks @@ -546,7 +546,9 @@ class PlaylistRows(Base): if to_row is not None: query = query.where(cls.row_number <= to_row) - plrs = session.execute((query).order_by(cls.row_number)).scalars().all() + plrs = ( + session.execute((query).order_by(cls.row_number)).scalars().all() + ) return plrs @@ -661,7 +663,7 @@ class Tracks(Base): start_gap = Column(Integer, index=False) fade_at = Column(Integer, index=False) silence_at = Column(Integer, index=False) - path = Column(String(2048), index=False, nullable=False, unique=True) + path: str = Column(String(2048), index=False, nullable=False, unique=True) mtime = Column(Float, index=True) bitrate = Column(Integer, nullable=True, default=None) playlistrows: PlaylistRows = relationship("PlaylistRows", diff --git a/app/musicmuster.py b/app/musicmuster.py index 5f0bdab..0c981cf 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -447,6 +447,7 @@ class Window(QMainWindow, Ui_MainWindow): """ self.next_track = PlaylistTrack() + self.update_headers() def clear_selection(self) -> None: """ Clear selected row""" @@ -598,12 +599,13 @@ class Window(QMainWindow, Ui_MainWindow): def create_playlist(self, session: scoped_session, - playlist_name: Optional[str] = None) -> Playlists: + playlist_name: Optional[str] = None) \ + -> Optional[Playlists]: """Create new playlist""" playlist_name = self.solicit_playlist_name() if not playlist_name: - return + return None playlist = Playlists(session, playlist_name) return playlist diff --git a/app/playlists.py b/app/playlists.py index abe69ee..5e8da7f 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -266,8 +266,7 @@ class PlaylistTab(QTableWidget): if item is not None: with Session() as session: row_number = item.row() - plr_id = self._get_playlistrow_id(row_number) - plr = session.get(PlaylistRows, plr_id) + plr = self._get_row_plr(session, row_number) track_id = plr.track_id track_row = track_id is not None header_row = not track_row @@ -445,7 +444,7 @@ class PlaylistTab(QTableWidget): # Determine cell type changed with Session() as session: # Get playlistrow object - plr_id = self._get_playlistrow_id(row) + plr_id = self._get_row_plr_id(row) plr_item = session.get(PlaylistRows, plr_id) if not plr_item: return @@ -500,16 +499,16 @@ class PlaylistTab(QTableWidget): play controls and update display. """ - # Update start times in case a start time in a note has been - # edited - self._update_start_end_times() - self.edit_cell_type = None self.musicmuster.enable_play_next_controls() self.musicmuster.actionSetNext.setEnabled(True) super(PlaylistTab, self).closeEditor(editor, hint) + # Update start times in case a start time in a note has been + # edited + self._update_start_end_times() + def edit(self, index: QModelIndex, # type: ignore # FIXME trigger: QAbstractItemView.EditTrigger, event: QEvent) -> bool: @@ -556,8 +555,7 @@ class PlaylistTab(QTableWidget): # editing a note. if note_column: with Session() as session: - plr_id = self._get_playlistrow_id(row) - plr_item = session.get(PlaylistRows, plr_id) + plr_item = self._get_row_plr(session, row) item = self.item(row, note_column) if not item: return False @@ -596,7 +594,7 @@ class PlaylistTab(QTableWidget): Return a list of PlaylistRow ids of the selected rows """ - return [self._get_playlistrow_id(a) for a in self._get_selected_rows()] + return [self._get_row_plr_id(a) for a in self._get_selected_rows()] def get_selected_playlistrows( self, session: scoped_session) -> List[PlaylistRows]: @@ -648,34 +646,14 @@ class PlaylistTab(QTableWidget): Insert passed playlist row (plr) into playlist tab. """ - if plr.row_number is None: - return - row = plr.row_number self.insertRow(row) + _ = self._set_row_plr_id(row, plr.id) - # Add row metadata to userdata column - self._set_row_userdata(row, self.PLAYLISTROW_ID, plr.id) - - if plr.track_id: - _ = self._set_row_userdata(row, self.ROW_TRACK_ID, plr.track_id) - _ = self._set_row_userdata(row, self.TRACK_PATH, plr.track.path) - _ = self._set_row_start_gap(row, plr.track.start_gap) - _ = self._set_row_title(row, plr.track.title) - _ = self._set_row_artist(row, plr.track.artist) - _ = self._set_row_duration(row, plr.track.duration) - _ = self._set_row_start_time(row, None) - _ = self._set_row_end_time(row, None) - _ = self._set_row_bitrate(row, plr.track.bitrate) - _ = self._set_row_note(session, row, plr.note) - _ = self._set_row_last_played( - row, Playdates.last_played(session, plr.track.id)) - - if not file_is_readable(plr.track.path): - self._set_row_colour(row, QColor(Config.COLOUR_UNREADABLE)) + if plr.track: + self._update_row_track_info(session, row, plr.track) if not played: self._set_row_bold(row) - else: # This is a section header so it must have note text if plr.note is None: @@ -684,34 +662,22 @@ class PlaylistTab(QTableWidget): ) return - # In order to colour the row, we need items in every column. - # Bug in PyQt5 means that required height of row considers - # text to be wrapped in one column and ignores any spanned - # columns, hence putting notes in HEADER_NOTES_COLUMN which - # is typically reasonably wide and thus minimises - # unneccessary row height increases. + # In order to colour the row, we need items in every column for i in range(1, len(columns)): - if i == HEADER_NOTES_COLUMN: - continue self._set_item_text(row, i, None) self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1) - _ = self._set_item_text(row, HEADER_NOTES_COLUMN, plr.note) - - note_colour = NoteColours.get_colour(session, plr.note) - if not note_colour: - note_colour = Config.COLOUR_NOTES_PLAYLIST - self._set_row_colour(row, QColor(note_colour)) + _ = self._set_row_note(session, row, plr.note, section_header=True) # Save (or clear) track_id - _ = self._set_row_userdata(row, self.ROW_TRACK_ID, 0) + _ = self._set_row_track_id(row, 0) if update_track_times: # Queue time updates so playlist updates first QTimer.singleShot(0, lambda: self._update_start_end_times()) def insert_track(self, session: scoped_session, track: Tracks, - note: Optional[str] = None, repaint: bool = True) -> None: + note: str = "", repaint: bool = True) -> None: """ Insert track into playlist tab. @@ -762,7 +728,6 @@ class PlaylistTab(QTableWidget): self.musicmuster.clear_next() self.clear_selection() self._set_row_colour(row, None) - self.musicmuster.update_headers() def play_started(self, session: scoped_session) -> None: """ @@ -873,7 +838,7 @@ class PlaylistTab(QTableWidget): # Ensure all row plrs have correct row number and playlist_id for row in range(self.rowCount()): - plr = self._get_playlistrow_object(session, row) + plr = self._get_row_plr(session, row) if not plr: continue plr.row_number = row @@ -1008,225 +973,14 @@ class PlaylistTab(QTableWidget): self.resizeRowsToContents() self.setColumnWidth(len(columns) - 1, 0) - # def update_display(self, session: scoped_session) -> None: - # """ - # Set row colours, fonts, etc - - # Actions required: - # - Render notes in correct colour - # - Render current, next and unplayable tracks in correct colour - # - Set start and end times - # - Show unplayed tracks in bold - # """ - - # current_row = self._get_current_track_row_number() - # next_row = self._get_next_track_row_number() - # played = [ - # p.row_number for p in PlaylistRows.get_played_rows( - # session, self.playlist_id) - # ] - - # # next_start_time = None - # section_start_plr = None - # section_time = 0 - - # # Start time calculations - # # Don't change start times for tracks that have been played. - # # For unplayed tracks, if there's a 'current' or 'next' - # # track marked, populate start times from then onwards. A note - # # with a start time will reset the next track start time. - - # # Cycle through all rows - # for row in range(self.rowCount()): - - # # Extract note text from database to ignore section timings - # playlist_row = session.get(PlaylistRows, - # self._get_playlistrow_id(row)) - # if not playlist_row: - # continue - # note_text = playlist_row.note - # note_colour = None - # if not note_text: - # note_text = "" - # # Get note colour - # else: - # note_colour = NoteColours.get_colour(session, note_text) - - # # Get track if there is one - # track_id = self._get_row_track_id(row) - # track = None - # if track_id: - # track = session.get(Tracks, track_id) - # if not track: - # # We have a track_id but we can't find the track. - # # Update playlist_row accordingly - # missing_track = playlist_row.track_id - # playlist_row.track_id = None - # if note_text: - # note_text += f"track_id {missing_track} not found" - # else: - # note_text = f"track_id {missing_track} not found" - # playlist_row.note = note_text - # session.flush() - # _ = self._set_item_text(row, HEADER_NOTES_COLUMN, - # note_text) - - # if track: - # # Reset colour in case it was current/next/unplayable - # self._set_row_colour(row, None) - - # # Render unplayable tracks in correct colour - # if not file_is_readable(track.path): - # self._set_row_colour(row, - # QColor(Config.COLOUR_UNREADABLE)) - # self._set_row_bold(row) - # continue - - # # Add track time to section time if in timed section - # if section_start_plr is not None: - # section_time += track.duration - - # # Colour any note - # if note_colour: - # notes_item = self.item(row, ROW_NOTES) - # if notes_item: - # notes_item.setBackground(QColor(note_colour)) - - # # Highlight low bitrates - # if track.bitrate: - # bitrate_str = str(track.bitrate) - # bitrate_item = self._set_item_text( - # row, BITRATE, str(track.bitrate)) - # if bitrate_item: - # if track.bitrate < Config.BITRATE_LOW_THRESHOLD: - # cell_colour = Config.COLOUR_BITRATE_LOW - # elif track.bitrate < Config.BITRATE_OK_THRESHOLD: - # cell_colour = Config.COLOUR_BITRATE_MEDIUM - # else: - # cell_colour = Config.COLOUR_BITRATE_OK - # brush = QBrush(QColor(cell_colour)) - # bitrate_item.setBackground(brush) - - # # Render playing track - # if row == current_row: - # # Set last played time to "Today" - # self._set_item_text( - # row, LASTPLAYED, Config.LAST_PLAYED_TODAY_STRING) - # # Calculate next_start_time - # # if track.duration: - # # next_start_time = self._calculate_end_time( - # # self.musicmuster.current_track.start_time, - # # track.duration - # # ) - # # 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) - # continue - - # # Render next track - # if row == next_row: - # # Set start time - # # if there's a track playing, set start time from - # # that. It may be on a different tab, so we get - # # start time from musicmuster. - # # start_time = self.musicmuster.current_track.end_time - # # if start_time is None: - # # # 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) - # # # Calculate next_start_time - # # if track.duration: - # # next_start_time = self._calculate_end_time( - # # start_time, track.duration) - # # # Set end 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) - # continue - - # if row in played: - # # Played today, so update last played column - # self._set_item_text( - # row, LASTPLAYED, Config.LAST_PLAYED_TODAY_STRING) - # if self.musicmuster.hide_played_tracks: - # self.hideRow(row) - # else: - # self.showRow(row) - # self._set_row_not_bold(row) - # else: - # # Set start/end times as we haven't played it yet - # # if next_start_time: - # # self._set_row_start_time(row, next_start_time) - # # if track.duration: - # # next_start_time = self._calculate_end_time( - # # next_start_time, track.duration) - # # # 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) - - # continue - - # # No track associated, so this row is a section header - # # Does the note have a start time? - # # row_time = self._get_note_text_time(note_text) - # # if row_time: - # # next_start_time = row_time - # # Does it delimit a section? - # if section_start_plr is not None: - # if note_text.endswith("-"): - # self._update_note_text( - # section_start_plr, - # self._get_section_timing_string(section_time) - # ) - # section_start_plr = None - # section_time = 0 - # elif note_text.endswith("+"): - # section_start_plr = playlist_row - # section_time = 0 - # if not note_colour: - # note_colour = Config.COLOUR_NOTES_PLAYLIST - # self._set_row_colour(row, QColor(note_colour)) - # # Section headers are always bold - # self._set_row_bold(row) - # continue - - # # Set row heights - # self.resizeRowsToContents() - - # # Have we had a section start but not end? - # if section_start_plr is not None: - # self._update_note_text( - # section_start_plr, - # self._get_section_timing_string(section_time, no_end=True) - # ) - # # ########## Internally called functions ########## def _add_track(self, row: int) -> None: """Add a track to a section header making it a normal track row""" with Session() as session: - track = self.musicmuster.get_one_track(session) - if not track: - return - # Add track to playlist row - plr = session.get(PlaylistRows, self._get_playlistrow_id(row)) + plr = self._get_row_plr(session, row) if not plr: return @@ -1234,6 +988,11 @@ class PlaylistTab(QTableWidget): if plr.track_id is not None: return + # Get track + track = self.musicmuster.get_one_track(session) + if not track: + return + plr.track_id = track.id session.flush() @@ -1242,13 +1001,8 @@ class PlaylistTab(QTableWidget): self.setSpan(row, column, 1, 1) # Update attributes of row - _ = self._set_row_userdata(row, self.ROW_TRACK_ID, plr.track_id) - _ = self._set_row_last_played( - row, Playdates.last_played(session, plr.track.id)) _ = self._set_row_note(session, row, plr.note) - _ = self._set_row_start_gap(row, plr.track.start_gap) - - self._update_row(session, row, track) + self._update_row_track_info(session, row, track) def _calculate_end_time(self, start: Optional[datetime], duration: int) -> Optional[datetime]: @@ -1291,27 +1045,21 @@ class PlaylistTab(QTableWidget): to the clipboard. Otherwise, return None. """ - track_id = self._get_row_track_id(row) - if track_id is None: + track_path = self._get_row_track_path(row) + if not track_path: return - with Session() as session: - track = session.get(Tracks, track_id) - if track and track.path: - # Escape single quotes and spaces in name - path = track.path - pathq = path.replace("'", "\\'") - pathqs = pathq.replace(" ", "\\ ") - cb = QApplication.clipboard() - cb.clear(mode=cb.Clipboard) - cb.setText(pathqs, mode=cb.Clipboard) + pathq = track_path.replace("'", "\\'") + pathqs = pathq.replace(" ", "\\ ") + cb = QApplication.clipboard() + cb.clear(mode=cb.Clipboard) + cb.setText(pathqs, mode=cb.Clipboard) def _deferred_save(self) -> None: """ Create session and save playlist """ - print("_deferred_save() called") with Session() as session: self.save_playlist(session) @@ -1322,8 +1070,10 @@ class PlaylistTab(QTableWidget): Actions required: - Remove the rows from the display - Save the playlist + - Update track start/end times """ + rows_to_delete: List[int] = [] with Session() as session: plrs = self.get_selected_playlistrows(session) row_count = len(plrs) @@ -1336,7 +1086,7 @@ class PlaylistTab(QTableWidget): f"Really delete {row_count} row{plural}?"): return - rows_to_delete = [a.row_number for a in plrs] + rows_to_delete = [plr.row_number for plr in plrs] # Delete rows from database. Would be more efficient to # query then have a single delete. @@ -1349,8 +1099,10 @@ class PlaylistTab(QTableWidget): # Reset drag mode self.setDragEnabled(False) - # QTimer.singleShot(0, lambda: self._deferred_save()) - self.signals.save_playlist_signal.emit() + self.save_playlist(session) + + # Queue time updates so playlist updates first + QTimer.singleShot(0, lambda: self._update_start_end_times()) def _drop_on(self, event): """ @@ -1390,7 +1142,7 @@ class PlaylistTab(QTableWidget): for row in range(starting_row, self.rowCount()): if row not in track_rows or row in played_rows: continue - plr = self._get_playlistrow_object(session, row) + plr = self._get_row_plr(session, row) if not plr: continue if not file_is_readable(plr.track.path): @@ -1442,7 +1194,7 @@ class PlaylistTab(QTableWidget): @staticmethod def _get_note_text_time(text: str) -> Optional[datetime]: - """Return time specified as @hh:mm:ss in text""" + """Return datetime specified as @hh:mm:ss in text""" try: match = start_time_re.search(text) @@ -1467,30 +1219,12 @@ class PlaylistTab(QTableWidget): session, self.playlist_id) if p.row_number is not None ] - def _get_playlistrow_id(self, row: int) -> Optional[int]: - """Return the playlistrow_id associated with this row""" - - plrid = self._get_row_userdata(row, self.PLAYLISTROW_ID) - if plrid is None: - return None - return int(plrid) - - def _get_playlistrow_object(self, session: scoped_session, - row: int) -> Optional[PlaylistRows]: - """Return the playlistrow object associated with this row""" - - playlistrow_id = self._get_playlistrow_id(row) - if not playlistrow_id: - return None - - return session.get(PlaylistRows, playlistrow_id) - - def _get_row_artist(self, row: int) -> Optional[str]: + def _get_row_artist(self, row: int) -> str: """Return artist on this row or None if none""" item_artist = self.item(row, ARTIST) if not item_artist: - return None + return "" return item_artist.text() @@ -1503,8 +1237,8 @@ class PlaylistTab(QTableWidget): else: return int(duration_userdata) - def _get_row_note(self, row: int) -> Optional[str]: - """Return note on this row or None if none""" + def _get_row_note(self, row: int) -> str: + """Return note on this row or null string if none""" track_id = self._get_row_track_id(row) if track_id: @@ -1512,16 +1246,37 @@ class PlaylistTab(QTableWidget): else: item_note = self.item(row, HEADER_NOTES_COLUMN) if not item_note: - return None + return "" return item_note.text() - def _get_row_path(self, row: int) -> Optional[str]: + def _get_row_path(self, row: int) -> str: """ - Return path of track associated with this row or None + Return path of track associated with this row or null string """ - return str(self._get_row_userdata(row, self.TRACK_PATH)) + path = str(self._get_row_userdata(row, self.TRACK_PATH)) + if not path: + return "" + + return path + + def _get_row_plr(self, session: scoped_session, + row: int) -> Optional[PlaylistRows]: + """ + Return PlaylistRows object for this row + """ + + return session.get(PlaylistRows, self._get_row_plr_id(row)) + + def _get_row_plr_id(self, row: int) -> int: + """Return the plr_id associated with this row or 0""" + + plr_id = self._get_row_userdata(row, self.PLAYLISTROW_ID) + if not plr_id: + return 0 + else: + return int(plr_id) def _get_row_start_time(self, row: int) -> Optional[datetime]: """Return row start time as string or None""" @@ -1568,6 +1323,15 @@ class PlaylistTab(QTableWidget): else: return int(track_id) + def _get_row_track_path(self, row: int) -> str: + """Return the track path associated with this row or '' """ + + path = self._get_row_userdata(row, self.TRACK_PATH) + if not path: + return "" + else: + return str(path) + def _get_row_userdata(self, row: int, role: int) -> Optional[Union[str, int]]: """ @@ -1580,6 +1344,16 @@ class PlaylistTab(QTableWidget): return userdata_item.data(role) + def _get_section_timing_string(self, ms: int, + no_end: bool = False) -> str: + """Return string describing section duration""" + + duration = ms_to_mmss(ms) + caveat = "" + if no_end: + caveat = " (to end of playlist)" + return ' [' + duration + caveat + ']' + def _get_selected_row(self) -> Optional[int]: """Return row number of first selected row, or None if none selected""" @@ -1714,7 +1488,7 @@ class PlaylistTab(QTableWidget): """ for row_number in range(self.rowCount()): - if self._get_playlistrow_id(row_number) == plrid: + if self._get_row_plr_id(row_number) == plrid: return row_number return None @@ -1729,7 +1503,7 @@ class PlaylistTab(QTableWidget): # Update playlist_rows record with Session() as session: - plr = session.get(PlaylistRows, self._get_playlistrow_id(row)) + plr = self._get_row_plr(session, row) if not plr: return @@ -1766,8 +1540,15 @@ class PlaylistTab(QTableWidget): row_colour = QColor(Config.COLOUR_UNREADABLE) else: set_track_metadata(session, track) - self._update_row(session, row, track) + self._update_row_track_info(session, row, track) else: + _ = self._set_row_track_id(row, 0) + note_text = self._get_row_note(row) + if note_text is None: + note_text = "" + else: + note_text += f"{track_id=} not found" + self._set_row_note(session, row, note_text) log.error( f"playlists._rescan({track_id=}): " "Track not found" @@ -1970,7 +1751,7 @@ class PlaylistTab(QTableWidget): self._set_row_colour(next_track_row, None) # Notify musicmuster - plr = session.get(PlaylistRows, self._get_playlistrow_id(row_number)) + plr = self._get_row_plr(session, row_number) if not plr: log.debug(f"playists._set_next({row_number=}) can't retrieve plr") else: @@ -1986,7 +1767,7 @@ class PlaylistTab(QTableWidget): def _set_played_row(self, session: scoped_session, row: int) -> None: """Mark this row as played""" - plr = session.get(PlaylistRows, self._get_playlistrow_id(row)) + plr = self._get_row_plr(session, row) if not plr: return @@ -2101,19 +1882,38 @@ class PlaylistTab(QTableWidget): self._set_row_bold(row, False) def _set_row_note(self, session: scoped_session, row: int, - note_text: Optional[str]) -> QTableWidgetItem: + note_text: Optional[str], + section_header: bool = False) -> QTableWidgetItem: """Set row note""" + if section_header: + column = HEADER_NOTES_COLUMN + else: + column - ROW_NOTES + if not note_text: note_text = "" - notes_item = self._set_item_text(row, ROW_NOTES, note_text) + + notes_item = self._set_item_text(row, column, note_text) note_colour = NoteColours.get_colour(session, note_text) + if section_header: + note_colour = Config.COLOUR_NOTES_PLAYLIST + else: + note_colour = NoteColours.get_colour(session, note_text) + if note_colour: notes_item.setBackground(QColor(note_colour)) return notes_item + def _set_row_plr_id(self, row: int, plr_id: int) -> QTableWidgetItem: + """ + Set PlaylistRows id + """ + + return self._set_row_userdata(row, self.PLAYLISTROW_ID, plr_id) + def _set_row_start_gap(self, row: int, start_gap: Optional[int]) -> QTableWidgetItem: """ @@ -2147,7 +1947,7 @@ class PlaylistTab(QTableWidget): return self._set_item_text(row, START_TIME, time_str) def _set_row_times(self, row: int, start: datetime, - duration: int) -> datetime: + duration: int) -> Optional[datetime]: """ Set row start and end times, return end time """ @@ -2162,8 +1962,6 @@ class PlaylistTab(QTableWidget): title: Optional[str]) -> QTableWidgetItem: """ Set row title. - - Return QTableWidgetItem. """ if not title: @@ -2171,6 +1969,20 @@ class PlaylistTab(QTableWidget): return self._set_item_text(row, TITLE, title) + def _set_row_track_id(self, row: int, track_id: int) -> QTableWidgetItem: + """ + Set track id + """ + + return self._set_row_userdata(row, self.ROW_TRACK_ID, track_id) + + def _set_row_track_path(self, row: int, path: str) -> QTableWidgetItem: + """ + Set track path + """ + + return self._set_row_userdata(row, self.TRACK_PATH, path) + def _set_row_userdata(self, row: int, role: int, value: Optional[Union[str, int]]) \ -> QTableWidgetItem: @@ -2187,16 +1999,6 @@ class PlaylistTab(QTableWidget): return item - def _get_section_timing_string(self, ms: int, - no_end: bool = False) -> str: - """Return string describing section duration""" - - duration = ms_to_mmss(ms) - caveat = "" - if no_end: - caveat = " (to end of playlist)" - return ' [' + duration + caveat + ']' - def _songfacts(self, row_number: int) -> None: """Look up passed row title in songfacts and display info tab""" @@ -2204,6 +2006,23 @@ class PlaylistTab(QTableWidget): self.musicmuster.tabInfolist.open_in_songfacts(title) + def _track_time_between_rows(self, session: scoped_session, + from_plr: PlaylistRows, + to_plr: PlaylistRows) -> int: + """ + Returns the total duration of all tracks in rows between + from_row and to_row inclusive + """ + + plr_tracks = PlaylistRows.get_rows_with_tracks( + session, self.playlist_id, from_plr.row_number, to_plr.row_number) + + total_time = 0 + total_time = sum([a.track.duration for a in plr_tracks + if a.track.duration]) + + return total_time + def _update_note_text(self, playlist_row: PlaylistRows, new_text: str) -> None: """Update note text""" @@ -2217,16 +2036,26 @@ class PlaylistTab(QTableWidget): _ = self._set_item_text(playlist_row.row_number, column, new_text) - def _update_row(self, session, row: int, track: Tracks) -> None: + def _update_row_track_info(self, session: scoped_session, row: int, + track: Tracks) -> None: """ Update the passed row with info from the passed track. """ - _ = self._set_row_start_gap(row, track.start_gap) - _ = self._set_row_title(row, track.title) _ = self._set_row_artist(row, track.artist) - _ = self._set_row_duration(row, track.duration) _ = self._set_row_bitrate(row, track.bitrate) + _ = self._set_row_duration(row, track.duration) + _ = self._set_row_end_time(row, None) + _ = self._set_row_last_played( + row, Playdates.last_played(session, track.id)) + _ = self._set_row_start_gap(row, track.start_gap) + _ = self._set_row_start_time(row, None) + _ = self._set_row_title(row, track.title) + _ = self._set_row_track_id(row, track.id) + _ = self._set_row_track_path(row, track.path) + + if not file_is_readable(track.path): + self._set_row_colour(row, QColor(Config.COLOUR_UNREADABLE)) def _update_section_headers(self, session: scoped_session) -> None: """ @@ -2240,34 +2069,42 @@ class PlaylistTab(QTableWidget): for plr in plrs: if plr.note.endswith("+"): header_rows.append(plr) - else: - try: - from_plr = header_rows.pop() - except IndexError: - pass - # section runs from from_plr to plr - from_row = from_plr.row_number - plr_tracks = PlaylistRows.get_rows_with_tracks( - session, self.playlist_id, from_row, plr.row_number) - - total_time = 0 - total_time = sum([a.track.duration for a in plr_tracks]) + continue + assert plr.note.endswith("-") + try: + from_plr = header_rows.pop() + to_plr = plr + total_time = self._track_time_between_rows(session, + from_plr, to_plr) time_str = self._get_section_timing_string(total_time) - self._update_note_text(from_plr, from_plr.note + time_str) # Update section end - if plr.note.strip() == "-": + if to_plr.note.strip() == "-": new_text = ( "[End " + from_plr.note.strip()[:-1].strip() + "]" ) self._update_note_text(plr, new_text) + except IndexError: + continue + + # If we still have plrs in header_rows, there isn't an end + # section row for them + possible_plr = self._get_row_plr(session, self.rowCount() - 1) + if possible_plr: + to_plr = possible_plr + for from_plr in header_rows: + total_time = self._track_time_between_rows(session, + from_plr, to_plr) + time_str = self._get_section_timing_string(total_time, + no_end=True) + self._update_note_text(from_plr, from_plr.note + time_str) def _update_start_end_times(self) -> None: """ Update track start and end times """ with Session() as session: - section_start_rows = [] + section_start_rows: List[PlaylistRows] = [] current_track_end_time = self._get_current_track_end_time() current_track_row = self._get_current_track_row_number() current_track_start_time = self._get_current_track_start_time()