From 4eaab987451471b4478e29b96d946ac6931b9bbb Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Fri, 4 Apr 2025 16:19:17 +0100 Subject: [PATCH] WIP: progressing no sessions in app files --- app/file_importer.py | 27 +++-- app/musicmuster.py | 2 +- app/playlistmodel.py | 263 +++++++++++++++--------------------------- app/querylistmodel.py | 4 +- app/repository.py | 27 +++++ 5 files changed, 133 insertions(+), 190 deletions(-) diff --git a/app/file_importer.py b/app/file_importer.py index 60e3365..388769b 100644 --- a/app/file_importer.py +++ b/app/file_importer.py @@ -68,7 +68,7 @@ class TrackFileData: destination_path: str = "" import_this_file: bool = False error: str = "" - file_path_to_remove: Optional[str] = None + file_path_to_remove: str | None = None track_id: int = 0 track_match_data: list[TrackMatchData] = field(default_factory=list) @@ -251,7 +251,7 @@ class FileImporter: artist_match=artist_score, title=existing_track.title, title_match=title_score, - track_id=existing_track.id, + track_id=existing_track.track_id, ) ) @@ -384,12 +384,12 @@ class FileImporter: else: tfd.destination_path = existing_track_path - def _get_existing_track(self, track_id: int) -> Tracks: + def _get_existing_track(self, track_id: int) -> TrackDTO: """ Lookup in existing track in the local cache and return it """ - existing_track_records = [a for a in self.existing_tracks if a.id == track_id] + existing_track_records = [a for a in self.existing_tracks if a.track_id == track_id] if len(existing_track_records) != 1: raise ApplicationError( f"Internal error in _get_existing_track: {existing_track_records=}" @@ -462,13 +462,12 @@ class FileImporter: # file). Check that because the path field in the database is # unique and so adding a duplicate will give a db integrity # error. - with db.Session() as session: - if Tracks.get_by_path(session, tfd.destination_path): - tfd.error = ( - "Importing a new track but destination path already exists " - f"in database ({tfd.destination_path})" - ) - return False + if repository.track_with_path(tfd.destination_path): + tfd.error = ( + "Importing a new track but destination path already exists " + f"in database ({tfd.destination_path})" + ) + return False # Check track_id if tfd.track_id < 0: @@ -590,7 +589,7 @@ class DoTrackImport(QThread): tags: Tags, destination_path: str, track_id: int, - file_path_to_remove: Optional[str] = None, + file_path_to_remove: str | None = None, ) -> None: """ Save parameters @@ -621,7 +620,7 @@ class DoTrackImport(QThread): ) # Get audio metadata in this thread rather than calling function to save interactive time - self.audio_metadata = helpers.get_audio_metadata(self.import_file_path) + self.audio_metadata = get_audio_metadata(self.import_file_path) # Remove old file if so requested if self.file_path_to_remove and os.path.exists(self.file_path_to_remove): @@ -660,7 +659,7 @@ class DoTrackImport(QThread): return session.commit() - helpers.normalise_track(self.destination_track_path) + normalise_track(self.destination_track_path) self.signals.status_message_signal.emit( f"{os.path.basename(self.import_file_path)} imported", 10000 diff --git a/app/musicmuster.py b/app/musicmuster.py index d6cfcce..0b7fba9 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -2321,7 +2321,7 @@ class Window(QMainWindow): track.intro = intro session.commit() self.preview_manager.set_intro(intro) - self.current.base_model.refresh_row(session, row_number) + self.current.base_model.refresh_row(row_number) roles = [ Qt.ItemDataRole.DisplayRole, ] diff --git a/app/playlistmodel.py b/app/playlistmodel.py index b8e6d75..b132c8a 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -285,12 +285,7 @@ class PlaylistModel(QAbstractTableModel): f"current_track_started() called with no track_id ({playlist_dto=})" ) - # TODO: ensure Playdates is updated - # with db.Session() as session: - # # Update Playdates in database - # log.debug(f"{self}: update playdates {track_id=}") - # Playdates(session, track_id) - # session.commit() + repository.update_playdates(track_id) # Mark track as played in playlist playlist_dto.played = True @@ -356,23 +351,23 @@ class PlaylistModel(QAbstractTableModel): row = index.row() column = index.column() - # rat for playlist row data as it's used a lot - rat = self.playlist_rows[row] + # plr for playlist row data as it's used a lot + plr = self.playlist_rows[row] # These are ordered in approximately the frequency with which # they are called if role == Qt.ItemDataRole.BackgroundRole: - return self._background_role(row, column, rat) + return self._background_role(row, column, plr) elif role == Qt.ItemDataRole.DisplayRole: - return self._display_role(row, column, rat) + return self._display_role(row, column, plr) elif role == Qt.ItemDataRole.EditRole: - return self._edit_role(row, column, rat) + return self._edit_role(row, column, plr) elif role == Qt.ItemDataRole.FontRole: - return self._font_role(row, column, rat) + return self._font_role(row, column, plr) elif role == Qt.ItemDataRole.ForegroundRole: - return self._foreground_role(row, column, rat) + return self._foreground_role(row, column, plr) elif role == Qt.ItemDataRole.ToolTipRole: - return self._tooltip_role(row, column, rat) + return self._tooltip_role(row, column, plr) return QVariant() @@ -399,7 +394,7 @@ class PlaylistModel(QAbstractTableModel): self.track_sequence.update() self.update_track_times() - def _display_role(self, row: int, column: int, rat: PlaylistRow) -> str: + def _display_role(self, row: int, column: int, plr: PlaylistRow) -> str: """ Return text for display """ @@ -417,42 +412,42 @@ class PlaylistModel(QAbstractTableModel): if header_row: if column == HEADER_NOTES_COLUMN: - header_text = self.header_text(rat) + header_text = self.header_text(plr) if not header_text: return Config.SECTION_HEADER else: - formatted_header = self.header_text(rat) + formatted_header = self.header_text(plr) trimmed_header = self.remove_section_timer_markers(formatted_header) return trimmed_header else: return "" if column == Col.START_TIME.value: - start_time = rat.forecast_start_time + start_time = plr.forecast_start_time if start_time: return start_time.strftime(Config.TRACK_TIME_FORMAT) return "" if column == Col.END_TIME.value: - end_time = rat.forecast_end_time + end_time = plr.forecast_end_time if end_time: return end_time.strftime(Config.TRACK_TIME_FORMAT) return "" if column == Col.INTRO.value: - if rat.intro: - return f"{rat.intro / 1000:{Config.INTRO_SECONDS_FORMAT}}" + if plr.intro: + return f"{plr.intro / 1000:{Config.INTRO_SECONDS_FORMAT}}" else: return "" dispatch_table: dict[int, str] = { - Col.ARTIST.value: rat.artist, - Col.BITRATE.value: str(rat.bitrate), - Col.DURATION.value: ms_to_mmss(rat.duration), - Col.LAST_PLAYED.value: get_relative_date(rat.lastplayed), - Col.NOTE.value: rat.note, - Col.START_GAP.value: str(rat.start_gap), - Col.TITLE.value: rat.title, + Col.ARTIST.value: plr.artist, + Col.BITRATE.value: str(plr.bitrate), + Col.DURATION.value: ms_to_mmss(plr.duration), + Col.LAST_PLAYED.value: get_relative_date(plr.lastplayed), + Col.NOTE.value: plr.note, + Col.START_GAP.value: str(plr.start_gap), + Col.TITLE.value: plr.title, } if column in dispatch_table: return dispatch_table[column] @@ -467,13 +462,12 @@ class PlaylistModel(QAbstractTableModel): if playlist_id != self.playlist_id: return - with db.Session() as session: - self.refresh_data(session) + self.refresh_data() super().endResetModel() self.track_sequence.update() self.update_track_times() - def _edit_role(self, row: int, column: int, rat: PlaylistRow) -> str | int: + def _edit_role(self, row: int, column: int, plr: PlaylistRow) -> str | int: """ Return value for editing """ @@ -481,31 +475,25 @@ class PlaylistModel(QAbstractTableModel): # If this is a header row and we're being asked for the # HEADER_NOTES_COLUMN, return the note value if self.is_header_row(row) and column == HEADER_NOTES_COLUMN: - return rat.note + return plr.note if column == Col.INTRO.value: - return rat.intro or 0 + return plr.intro or 0 if column == Col.TITLE.value: - return rat.title + return plr.title if column == Col.ARTIST.value: - return rat.artist + return plr.artist if column == Col.NOTE.value: - return rat.note + return plr.note return "" - def _foreground_role(self, row: int, column: int, rat: PlaylistRow) -> QBrush: + def _foreground_role(self, row: int, column: int, plr: PlaylistRow) -> QBrush: """Return header foreground colour or QBrush() if none""" - if self.is_header_row(row): - if rat.row_fg is None: - with db.Session() as session: - rat.row_fg = NoteColours.get_colour( - session, rat.note, foreground=True - ) - if rat.row_fg: - return QBrush(QColor(rat.row_fg)) - + plr.row_fg = repository.get_colour(plr.note, foreground=True) + if plr.row_fg: + return QBrush(QColor(plr.row_fg)) return QBrush() def flags(self, index: QModelIndex) -> Qt.ItemFlag: @@ -531,7 +519,7 @@ class PlaylistModel(QAbstractTableModel): return default - def _font_role(self, row: int, column: int, rat: PlaylistRow) -> QFont: + def _font_role(self, row: int, column: int, plr: PlaylistRow) -> QFont: """ Return font """ @@ -670,21 +658,21 @@ class PlaylistModel(QAbstractTableModel): return QVariant() - def header_text(self, rat: PlaylistRow) -> str: + def header_text(self, plr: PlaylistRow) -> str: """ Process possible section timing directives embeded in header """ - if rat.note.endswith(Config.SECTION_STARTS): - return self.start_of_timed_section_header(rat) + if plr.note.endswith(Config.SECTION_STARTS): + return self.start_of_timed_section_header(plr) - elif rat.note.endswith("="): - return self.section_subtotal_header(rat) + elif plr.note.endswith("="): + return self.section_subtotal_header(plr) - elif rat.note == "-": + elif plr.note == "-": # If the hyphen is the only thing on the line, echo the note # that started the section without the trailing "+". - for row_number in range(rat.row_number - 1, -1, -1): + for row_number in range(plr.row_number - 1, -1, -1): row_rat = self.playlist_rows[row_number] if self.is_header_row(row_number): if row_rat.note.endswith("-"): @@ -694,7 +682,7 @@ class PlaylistModel(QAbstractTableModel): return f"[End: {row_rat.note[:-1]}]" return "-" - return rat.note + return plr.note def hide_played_tracks(self, hide: bool) -> None: """ @@ -852,31 +840,6 @@ class PlaylistModel(QAbstractTableModel): if to_row_number in from_rows: return False # Destination within rows to be moved - # # Remove rows from bottom to top to avoid index shifting - # for row in sorted(from_rows, reverse=True): - # self.beginRemoveRows(QModelIndex(), row, row) - # del self.playlist_rows[row] - # # At this point self.playlist_rows has been updated but the - # # underlying database has not (that's done below after - # # inserting the rows) - # self.endRemoveRows() - - # # Adjust insertion point after removal - # if to_row_number > max(from_rows): - # rows_below_dest = len([r for r in from_rows if r < to_row_number]) - # insertion_point = to_row_number - rows_below_dest - # else: - # insertion_point = to_row_number - - # # Insert rows at the destination - # plrid_to_new_row_number: list[dict[int, int]] = [] - # for offset, row_data in enumerate(rows_to_move): - # row_number = insertion_point + offset - # self.beginInsertRows(QModelIndex(), row_number, row_number) - # self.playlist_rows[row_number] = row_data - # plrid_to_new_row_number.append({row_data.playlistrow_id: row_number}) - # self.endInsertRows() - # Notify model going to change self.beginResetModel() # Update database @@ -884,25 +847,6 @@ class PlaylistModel(QAbstractTableModel): # Notify model changed self.endResetModel() - # Handle the moves in row_group chunks - for row_group in row_groups: - # Tell model we will be moving rows - # See https://doc.qt.io/qt-6/qabstractitemmodel.html#beginMoveRows - # for how destination is calculated - destination = to_row_number - if to_row_number > max(row_group): - destination = to_row_number - max(row_group) + 1 - super().beginMoveRows(QModelIndex(), - min(row_group), - max(row_group), - QModelIndex(), - destination - ) - # Update database - repository.move_rows_within_playlist(self.playlist_id, row_group, to_row_number) - # Tell model we have finished moving rows - super().endMoveRows() - # Update display self.refresh_data() self.track_sequence.update() @@ -913,26 +857,7 @@ class PlaylistModel(QAbstractTableModel): # Qt.ItemDataRole.DisplayRole, # ] # self.invalidate_rows(list(row_map.keys()), roles) - - def begin_insert_rows(self, insert_rows: InsertRows) -> None: - """ - Prepare model to insert rows - """ - - if insert_rows.playlist_id != self.playlist_id: - return - - super().beginInsertRows(QModelIndex(), insert_rows.from_row, insert_rows.to_row) - - def end_insert_rows(self, playlist_id: int) -> None: - """ - End insert rows - """ - - if playlist_id != self.playlist_id: - return - - super().endInsertRows() + return True @log_call def move_rows_between_playlists( @@ -971,11 +896,10 @@ class PlaylistModel(QAbstractTableModel): to_row_number + len(row_group) ) self.signals.signal_begin_insert_rows.emit(insert_rows) - repository.move_rows_to_playlist(from_rows=row_group, - from_playlist_id=self.playlist_id, - to_row=to_row_number, - to_playlist_id=to_playlist_id - ) + repository.move_rows(from_rows=row_group, + from_playlist_id=self.playlist_id, + to_row=to_row_number, + to_playlist_id=to_playlist_id) self.signals.signal_end_insert_rows.emit(to_playlist_id) super().endRemoveRows() @@ -983,6 +907,26 @@ class PlaylistModel(QAbstractTableModel): self.track_sequence.update() self.update_track_times() + def begin_insert_rows(self, insert_rows: InsertRows) -> None: + """ + Prepare model to insert rows + """ + + if insert_rows.playlist_id != self.playlist_id: + return + + super().beginInsertRows(QModelIndex(), insert_rows.from_row, insert_rows.to_row) + + def end_insert_rows(self, playlist_id: int) -> None: + """ + End insert rows + """ + + if playlist_id != self.playlist_id: + return + + super().endInsertRows() + @log_call def move_track_add_note( self, new_row_number: int, existing_plr: PlaylistRow, note: str @@ -1004,23 +948,6 @@ class PlaylistModel(QAbstractTableModel): self.move_rows([existing_plr.row_number], new_row_number) self.signals.resize_rows_signal.emit(self.playlist_id) - @log_call - def move_track_to_header( - self, - header_row_number: int, - existing_rat: RowAndTrack, - note: Optional[str], - ) -> None: - """ - Add the existing_rat track details to the existing header at header_row_number - """ - - if existing_rat.track_id: - if note and existing_rat.note: - note += "\n" + existing_rat.note - self.add_track_to_header(header_row_number, existing_rat.track_id, note) - self.delete_rows([existing_rat.row_number]) - @log_call def obs_scene_change(self, row_number: int) -> None: """ @@ -1167,17 +1094,7 @@ class PlaylistModel(QAbstractTableModel): looking up the playlistrow_id and retrieving the row number from the database. """ - # Check the track_sequence.next, current and previous plrs and - # update the row number - with db.Session() as session: - for ts in [ - track_sequence.next, - track_sequence.current, - track_sequence.previous, - ]: - if ts: - ts.update_playlist_and_row(session) - session.commit() + self.track_sequence.update() self.update_track_times() @@ -1299,7 +1216,7 @@ class PlaylistModel(QAbstractTableModel): return len(self.playlist_rows) - def section_subtotal_header(self, rat: PlaylistRow) -> str: + def section_subtotal_header(self, plr: PlaylistRow) -> str: """ Process this row as subtotal within a timed section and return display text for this row @@ -1309,12 +1226,12 @@ class PlaylistModel(QAbstractTableModel): unplayed_count: int = 0 duration: int = 0 - if rat.row_number == 0: + if plr.row_number == 0: # Meaningless to have a subtotal on row 0 return Config.SUBTOTAL_ON_ROW_ZERO # Show subtotal - for row_number in range(rat.row_number - 1, -1, -1): + for row_number in range(plr.row_number - 1, -1, -1): row_rat = self.playlist_rows[row_number] if self.is_header_row(row_number) or row_number == 0: if row_rat.note.endswith(Config.SECTION_STARTS) or row_number == 0: @@ -1327,7 +1244,7 @@ class PlaylistModel(QAbstractTableModel): and ( row_number < self.track_sequence.current.row_number - < rat.row_number + < plr.row_number ) ): section_end_time = ( @@ -1338,7 +1255,7 @@ class PlaylistModel(QAbstractTableModel): ", section end time " + section_end_time.strftime(Config.TRACK_TIME_FORMAT) ) - clean_header = self.remove_section_timer_markers(rat.note) + clean_header = self.remove_section_timer_markers(plr.note) if clean_header: return ( f"{clean_header} [" @@ -1418,15 +1335,15 @@ class PlaylistModel(QAbstractTableModel): ) return - rat = self.selected_rows[0] - if rat.track_id is None: - raise ApplicationError(f"set_next_row: no track_id ({rat=})") + plr = self.selected_rows[0] + if plr.track_id is None: + raise ApplicationError(f"set_next_row: no track_id ({plr=})") old_next_row: Optional[int] = None if self.track_sequence.next: old_next_row = self.track_sequence.next.row_number - self.track_sequence.set_next(rat) + self.track_sequence.set_next(plr) roles = [ Qt.ItemDataRole.BackgroundRole, @@ -1435,7 +1352,7 @@ class PlaylistModel(QAbstractTableModel): # only invalidate required roles self.invalidate_row(old_next_row, roles) # only invalidate required roles - self.invalidate_row(rat.row_number, roles) + self.invalidate_row(plr.row_number, roles) self.signals.next_track_changed_signal.emit() self.update_track_times() @@ -1549,7 +1466,7 @@ class PlaylistModel(QAbstractTableModel): self.sort_by_attribute(row_numbers, "title") - def start_of_timed_section_header(self, rat: PlaylistRow) -> str: + def start_of_timed_section_header(self, plr: PlaylistRow) -> str: """ Process this row as the start of a timed section and return display text for this row @@ -1559,9 +1476,9 @@ class PlaylistModel(QAbstractTableModel): unplayed_count: int = 0 duration: int = 0 - clean_header = self.remove_section_timer_markers(rat.note) + clean_header = self.remove_section_timer_markers(plr.note) - for row_number in range(rat.row_number + 1, len(self.playlist_rows)): + for row_number in range(plr.row_number + 1, len(self.playlist_rows)): row_rat = self.playlist_rows[row_number] if self.is_header_row(row_number): if row_rat.note.endswith(Config.SECTION_ENDINGS): @@ -1585,7 +1502,7 @@ class PlaylistModel(QAbstractTableModel): def supportedDropActions(self) -> Qt.DropAction: return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction - def _tooltip_role(self, row: int, column: int, rat: PlaylistRow) -> str: + def _tooltip_role(self, row: int, column: int, plr: PlaylistRow) -> str: """ Return tooltip. Currently only used for last_played column. """ @@ -1656,20 +1573,20 @@ class PlaylistModel(QAbstractTableModel): next_track_row = self.track_sequence.next.row_number for row_number in range(row_count): - rat = self.playlist_rows[row_number] + plr = self.playlist_rows[row_number] # Don't update times for tracks that have been played, for # unreadable tracks or for the current track, handled above. if ( - rat.played + plr.played or row_number == current_track_row - or (rat.path and file_is_unreadable(rat.path)) + or (plr.path and file_is_unreadable(plr.path)) ): continue # Reset start time if timing in header if self.is_header_row(row_number): - header_time = get_embedded_time(rat.note) + header_time = get_embedded_time(plr.note) if header_time: next_start_time = header_time continue @@ -1680,7 +1597,7 @@ class PlaylistModel(QAbstractTableModel): and self.track_sequence.current and self.track_sequence.current.end_time ): - next_start_time = rat.set_forecast_start_time( + next_start_time = plr.set_forecast_start_time( update_rows, self.track_sequence.current.end_time ) continue @@ -1688,11 +1605,11 @@ class PlaylistModel(QAbstractTableModel): # If we're between the current and next row, zero out # times if (current_track_row or row_count) < row_number < (next_track_row or 0): - rat.set_forecast_start_time(update_rows, None) + plr.set_forecast_start_time(update_rows, None) continue # Set start/end - rat.forecast_start_time = next_start_time + plr.forecast_start_time = next_start_time # Update start/stop times of rows that have changed for updated_row in update_rows: diff --git a/app/querylistmodel.py b/app/querylistmodel.py index 800f6de..90567ad 100644 --- a/app/querylistmodel.py +++ b/app/querylistmodel.py @@ -136,7 +136,7 @@ class QuerylistModel(QAbstractTableModel): row = index.row() column = index.column() - # rat for playlist row data as it's used a lot + # plr for playlist row data as it's used a lot qrow = self.querylist_rows[row] # Dispatch to role-specific functions @@ -268,7 +268,7 @@ class QuerylistModel(QAbstractTableModel): bottom_right = self.index(row, self.columnCount() - 1) self.dataChanged.emit(top_left, bottom_right, [Qt.ItemDataRole.BackgroundRole]) - def _tooltip_role(self, row: int, column: int, rat: PlaylistRow) -> str | QVariant: + def _tooltip_role(self, row: int, column: int, plr: PlaylistRow) -> str | QVariant: """ Return tooltip. Currently only used for last_played column. """ diff --git a/app/repository.py b/app/repository.py index e66308f..a438b23 100644 --- a/app/repository.py +++ b/app/repository.py @@ -238,6 +238,24 @@ def _tracks_where(where: BinaryExpression | ColumnElement[bool]) -> list[TrackDT return results +def track_with_path(path: str) -> bool: + """ + Return True if a track with passed path exists, else False + """ + + with db.Session() as session: + track = ( + session.execute( + select(Tracks) + .where(Tracks.path == path) + ) + .scalars() + .one_or_none() + ) + + return track is not None + + def tracks_like_artist(filter_str: str) -> list[TrackDTO]: """ Return tracks where artist is like filter @@ -399,6 +417,15 @@ def move_rows( _check_playlist_integrity(session, to_playlist_id, fix=False) +def update_playdates(track_id: int) -> None: + """ + Update playdates for passed track + """ + + with db.Session() as session: + _ = Playdates(session, track_id) + + def update_row_numbers( playlist_id: int, id_to_row_number: list[dict[int, int]] ) -> None: