diff --git a/app/playlists.py b/app/playlists.py index b9914bd..6406be1 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -725,5 +725,559 @@ class PlaylistTab(QTableView): def _unmark_as_next(self) -> None: """Rescan track""" +<<<<<<< HEAD self.data_model.set_next_row(None) self.clear_selection() +||||||| parent of 705f3ea (Fix bug with unended timed section) + # Reorder rows + new_order = [a[0] for a in sorted_rows] + self.sort_undo = [ + new_order.index(x) + min(new_order) + for x in range(min(new_order), max(new_order) + 1) + ] + self._reorder_rows(new_order) + + # Reset drag mode to allow row selection by dragging + self.setDragEnabled(False) + + # Save playlist + with Session() as session: + self.save_playlist(session) + self._update_start_end_times(session) + + def _sort_undo(self): + """Undo last sort""" + + if not self.sort_undo: + return + + new_order = self.sort_undo + + self._reorder_rows(new_order) + + self.sort_undo = [ + new_order.index(x) + min(new_order) + for x in range(min(new_order), max(new_order) + 1) + ] + + # Reset drag mode to allow row selection by dragging + self.setDragEnabled(False) + + # Save playlist + with Session() as session: + self.save_playlist(session) + self._update_start_end_times(session) + + def _unplayed_track_time_between_rows( + self, session: scoped_session, from_plr: PlaylistRows, to_plr: PlaylistRows + ) -> int: + """ + Returns the total unplayed 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.plr_rownum, to_plr.plr_rownum + ) + + unplayed_time = 0 + unplayed_time = sum( + [a.track.duration for a in plr_tracks if a.track.duration and not a.played] + ) + + return unplayed_time + + 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_artist(row, track.artist) + _ = self._set_row_bitrate(row, track.bitrate) + _ = self._set_row_duration(row, track.duration) + _ = self._set_row_end_time(row, None) + if track.playdates: + last_play = max([a.lastplayed for a in track.playdates]) + else: + last_play = Config.EPOCH + _ = self._set_row_last_played_time(row, last_play) + _ = 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 file_is_unreadable(track.path): + self._set_row_colour_unreadable(row) + + def _update_section_headers(self, session: scoped_session) -> None: + """ + Update section headers with run time of section + """ + + section_start_rows: List[PlaylistRows] = [] + subtotal_from: Optional[PlaylistRows] = None + active_row: Optional[int] = None + active_endtime: Optional[datetime] = None + current_row_prlid = self.musicmuster.current_track.plr_id + if current_row_prlid: + current_row = self._plrid_to_row_number(current_row_prlid) + if current_row: + active_row = current_row + active_end = self.musicmuster.current_track.end_time + else: + previous_row_plrid = self.musicmuster.previous_track.plr_id + if previous_row_plrid: + previous_row = self._plrid_to_row_number(previous_row_plrid) + if previous_row: + active_row = previous_row + active_end = self.musicmuster.previous_track.end_time + + header_rows = [ + self._get_row_plr_id(row_number) + for row_number in range(self.rowCount()) + if self._get_row_track_id(row_number) == 0 + ] + plrs = PlaylistRows.plrids_to_plrs(session, self.playlist_id, header_rows) + for plr in plrs: + # Start of timed section + if plr.note.endswith("+"): + section_start_rows.append(plr) + subtotal_from = plr + continue + # End of timed section + elif plr.note.endswith("-"): + try: + from_plr = section_start_rows.pop() + to_plr = plr + unplayed_time = self._unplayed_track_time_between_rows( + session, from_plr, to_plr + ) + if ( + active_row + and active_row >= from_plr.plr_rownum + and active_row <= to_plr.plr_rownum + ): + time_str = self._get_section_timing_string( + unplayed_time, active_end + ) + else: + time_str = self._get_section_timing_string(unplayed_time, None) + self._set_row_header_text( + session, from_plr.plr_rownum, from_plr.note + time_str + ) + + # Update section end + if to_plr.note.strip() == "-": + new_text = ( + "[End " + + re.sub( + section_header_cleanup_re, + "", + from_plr.note, + ).strip() + + "]" + ) + self._set_row_header_text(session, to_plr.plr_rownum, new_text) + subtotal_from = None + except IndexError: + # This ending row may have a time left from before a + # starting row above was deleted, so replace content + self._set_row_header_text(session, plr.plr_rownum, plr.note) + continue + # Subtotal + elif plr.note.endswith("="): + if not subtotal_from: + return + from_plr = subtotal_from + to_plr = plr + unplayed_time = self._unplayed_track_time_between_rows( + session, subtotal_from, to_plr + ) + if ( + active_row + and active_row >= from_plr.plr_rownum + and active_row <= to_plr.plr_rownum + ): + time_str = self._get_section_timing_string( + unplayed_time, active_end + ) + else: + time_str = self._get_section_timing_string(unplayed_time, None) + + if to_plr.note.strip() == "=": + leader_text = "Subtotal: " + else: + leader_text = to_plr.note[:-1] + " " + new_text = leader_text + time_str + self._set_row_header_text(session, to_plr.plr_rownum, new_text) + subtotal_from = to_plr + + # If we still have plrs in section_start_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 section_start_rows: + unplayed_time = self._unplayed_track_time_between_rows( + session, from_plr, to_plr + ) + time_str = self._get_section_timing_string( + total_time, unplayed_time, no_end=True + ) + self._set_row_header_text( + session, from_plr.plr_rownum, from_plr.note + time_str + ) + + def _update_start_end_times(self, session: scoped_session) -> None: + """Update track start and end times""" + + current_track_end_time = self.musicmuster.current_track.end_time + current_track_row = self._get_current_track_row_number() + current_track_start_time = self.musicmuster.current_track.start_time + next_start_time = None + next_track_row = self._get_next_track_row_number() + played_rows = self._get_played_rows(session) + + for row_number in range(self.rowCount()): + # Don't change start times for tracks that have been + # played other than current/next row + if row_number in played_rows and row_number not in [ + current_track_row, + next_track_row, + ]: + continue + + # Get any timing from header row (that's all we need) + if self._get_row_track_id(row_number) == 0: + note_time = self._get_note_text_time(self._get_row_note(row_number)) + if note_time: + next_start_time = note_time + continue + + # We have a track. Skip if it is unreadable + if file_is_unreadable(self._get_row_path(row_number)): + continue + + # Set next track start from end of current track + if row_number == next_track_row: + if current_track_end_time: + next_start_time = self._set_row_times( + row_number, + current_track_end_time, + self._get_row_duration(row_number), + ) + continue + # Else set track times below + + if row_number == current_track_row: + if not current_track_start_time: + continue + self._set_row_start_time(row_number, current_track_start_time) + self._set_row_end_time(row_number, current_track_end_time) + # Next track may be above us so only reset + # next_start_time if it's not set + if not next_start_time: + next_start_time = current_track_end_time + continue + + if not next_start_time: + # Clear any existing times + self._set_row_start_time(row_number, None) + self._set_row_end_time(row_number, None) + continue + + # If we're between the current and next row, zero out + # times + if ( + current_track_row + and next_track_row + and current_track_row < row_number < next_track_row + ): + self._set_row_start_time(row_number, None) + self._set_row_end_time(row_number, None) + else: + next_start_time = self._set_row_times( + row_number, next_start_time, self._get_row_duration(row_number) + ) + + self._update_section_headers(session) +======= + # Reorder rows + new_order = [a[0] for a in sorted_rows] + self.sort_undo = [ + new_order.index(x) + min(new_order) + for x in range(min(new_order), max(new_order) + 1) + ] + self._reorder_rows(new_order) + + # Reset drag mode to allow row selection by dragging + self.setDragEnabled(False) + + # Save playlist + with Session() as session: + self.save_playlist(session) + self._update_start_end_times(session) + + def _sort_undo(self): + """Undo last sort""" + + if not self.sort_undo: + return + + new_order = self.sort_undo + + self._reorder_rows(new_order) + + self.sort_undo = [ + new_order.index(x) + min(new_order) + for x in range(min(new_order), max(new_order) + 1) + ] + + # Reset drag mode to allow row selection by dragging + self.setDragEnabled(False) + + # Save playlist + with Session() as session: + self.save_playlist(session) + self._update_start_end_times(session) + + def _unplayed_track_time_between_rows( + self, session: scoped_session, from_plr: PlaylistRows, to_plr: PlaylistRows + ) -> int: + """ + Returns the total unplayed 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.plr_rownum, to_plr.plr_rownum + ) + + unplayed_time = 0 + unplayed_time = sum( + [a.track.duration for a in plr_tracks if a.track.duration and not a.played] + ) + + return unplayed_time + + 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_artist(row, track.artist) + _ = self._set_row_bitrate(row, track.bitrate) + _ = self._set_row_duration(row, track.duration) + _ = self._set_row_end_time(row, None) + if track.playdates: + last_play = max([a.lastplayed for a in track.playdates]) + else: + last_play = Config.EPOCH + _ = self._set_row_last_played_time(row, last_play) + _ = 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 file_is_unreadable(track.path): + self._set_row_colour_unreadable(row) + + def _update_section_headers(self, session: scoped_session) -> None: + """ + Update section headers with run time of section + """ + + section_start_rows: List[PlaylistRows] = [] + subtotal_from: Optional[PlaylistRows] = None + active_row: Optional[int] = None + active_endtime: Optional[datetime] = None + current_row_prlid = self.musicmuster.current_track.plr_id + if current_row_prlid: + current_row = self._plrid_to_row_number(current_row_prlid) + if current_row: + active_row = current_row + active_end = self.musicmuster.current_track.end_time + else: + previous_row_plrid = self.musicmuster.previous_track.plr_id + if previous_row_plrid: + previous_row = self._plrid_to_row_number(previous_row_plrid) + if previous_row: + active_row = previous_row + active_end = self.musicmuster.previous_track.end_time + + header_rows = [ + self._get_row_plr_id(row_number) + for row_number in range(self.rowCount()) + if self._get_row_track_id(row_number) == 0 + ] + plrs = PlaylistRows.plrids_to_plrs(session, self.playlist_id, header_rows) + for plr in plrs: + # Start of timed section + if plr.note.endswith("+"): + section_start_rows.append(plr) + subtotal_from = plr + continue + # End of timed section + elif plr.note.endswith("-"): + try: + from_plr = section_start_rows.pop() + to_plr = plr + unplayed_time = self._unplayed_track_time_between_rows( + session, from_plr, to_plr + ) + if ( + active_row + and active_row >= from_plr.plr_rownum + and active_row <= to_plr.plr_rownum + ): + time_str = self._get_section_timing_string( + unplayed_time, active_end + ) + else: + time_str = self._get_section_timing_string(unplayed_time, None) + self._set_row_header_text( + session, from_plr.plr_rownum, from_plr.note + time_str + ) + + # Update section end + if to_plr.note.strip() == "-": + new_text = ( + "[End " + + re.sub( + section_header_cleanup_re, + "", + from_plr.note, + ).strip() + + "]" + ) + self._set_row_header_text(session, to_plr.plr_rownum, new_text) + subtotal_from = None + except IndexError: + # This ending row may have a time left from before a + # starting row above was deleted, so replace content + self._set_row_header_text(session, plr.plr_rownum, plr.note) + continue + # Subtotal + elif plr.note.endswith("="): + if not subtotal_from: + return + from_plr = subtotal_from + to_plr = plr + unplayed_time = self._unplayed_track_time_between_rows( + session, subtotal_from, to_plr + ) + if ( + active_row + and active_row >= from_plr.plr_rownum + and active_row <= to_plr.plr_rownum + ): + time_str = self._get_section_timing_string( + unplayed_time, active_end + ) + else: + time_str = self._get_section_timing_string(unplayed_time, None) + + if to_plr.note.strip() == "=": + leader_text = "Subtotal: " + else: + leader_text = to_plr.note[:-1] + " " + new_text = leader_text + time_str + self._set_row_header_text(session, to_plr.plr_rownum, new_text) + subtotal_from = to_plr + + # If we still have plrs in section_start_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 section_start_rows: + unplayed_time = self._unplayed_track_time_between_rows( + session, from_plr, to_plr + ) + time_str = self._get_section_timing_string( + unplayed_time, no_end=True + ) + self._set_row_header_text( + session, from_plr.plr_rownum, from_plr.note + time_str + ) + + def _update_start_end_times(self, session: scoped_session) -> None: + """Update track start and end times""" + + current_track_end_time = self.musicmuster.current_track.end_time + current_track_row = self._get_current_track_row_number() + current_track_start_time = self.musicmuster.current_track.start_time + next_start_time = None + next_track_row = self._get_next_track_row_number() + played_rows = self._get_played_rows(session) + + for row_number in range(self.rowCount()): + # Don't change start times for tracks that have been + # played other than current/next row + if row_number in played_rows and row_number not in [ + current_track_row, + next_track_row, + ]: + continue + + # Get any timing from header row (that's all we need) + if self._get_row_track_id(row_number) == 0: + note_time = self._get_note_text_time(self._get_row_note(row_number)) + if note_time: + next_start_time = note_time + continue + + # We have a track. Skip if it is unreadable + if file_is_unreadable(self._get_row_path(row_number)): + continue + + # Set next track start from end of current track + if row_number == next_track_row: + if current_track_end_time: + next_start_time = self._set_row_times( + row_number, + current_track_end_time, + self._get_row_duration(row_number), + ) + continue + # Else set track times below + + if row_number == current_track_row: + if not current_track_start_time: + continue + self._set_row_start_time(row_number, current_track_start_time) + self._set_row_end_time(row_number, current_track_end_time) + # Next track may be above us so only reset + # next_start_time if it's not set + if not next_start_time: + next_start_time = current_track_end_time + continue + + if not next_start_time: + # Clear any existing times + self._set_row_start_time(row_number, None) + self._set_row_end_time(row_number, None) + continue + + # If we're between the current and next row, zero out + # times + if ( + current_track_row + and next_track_row + and current_track_row < row_number < next_track_row + ): + self._set_row_start_time(row_number, None) + self._set_row_end_time(row_number, None) + else: + next_start_time = self._set_row_times( + row_number, next_start_time, self._get_row_duration(row_number) + ) + + self._update_section_headers(session) +>>>>>>> 705f3ea (Fix bug with unended timed section)