diff --git a/app/models.py b/app/models.py index 907e3f9..ca115bd 100644 --- a/app/models.py +++ b/app/models.py @@ -118,9 +118,9 @@ class NoteColours(Base): for rec in ( session.query(NoteColours) - .filter(NoteColours.enabled.is_(True)) - .order_by(NoteColours.order) - .all() + .filter(NoteColours.enabled.is_(True)) + .order_by(NoteColours.order) + .all() ): if rec.is_regex: flags = re.UNICODE @@ -175,6 +175,18 @@ class Notes(Base): session.query(Notes).filter_by(id=self.id).delete() session.commit() + @classmethod + def get_by_id(cls, session: Session, note_id: int) -> Optional["Notes"]: + """Return note or None""" + + try: + DEBUG(f"Notes.get_track(track_id={note_id})") + note = session.query(cls).filter(cls.id == note_id).one() + return note + except NoResultFound: + ERROR(f"get_track({note_id}): not found") + return None + def update_note( self, session: Session, row: int, text: Optional[str] = None) -> None: @@ -215,8 +227,9 @@ class Playdates(Base): """Return datetime track last played or None""" last_played: Optional[Playdates] = session.query( - Playdates.lastplayed).filter((Playdates.track_id == track_id) - ).order_by(Playdates.lastplayed.desc()).first() + Playdates.lastplayed).filter( + (Playdates.track_id == track_id) + ).order_by(Playdates.lastplayed.desc()).first() if last_played: return last_played[0] else: @@ -246,7 +259,9 @@ class Playlists(Base): last_used: datetime = Column(DateTime, default=None, nullable=True) loaded: bool = Column(Boolean, default=True, nullable=False) notes = relationship( - "Notes", order_by="Notes.row", back_populates="playlist", lazy="joined") + "Notes", order_by="Notes.row", + back_populates="playlist", lazy="joined" + ) tracks = association_proxy('playlist_tracks', 'tracks') row = association_proxy('playlist_tracks', 'row') @@ -265,7 +280,7 @@ class Playlists(Base): return Notes(session, self.id, row, text) def add_track( - self, session: Session, track: "Tracks", + self, session: Session, track_id: int, row: Optional[int] = None) -> None: """ Add track to playlist at given row. @@ -275,7 +290,7 @@ class Playlists(Base): if not row: row = PlaylistTracks.next_free_row(session, self) - PlaylistTracks(session, self.id, track.id, row) + PlaylistTracks(session, self.id, track_id, row) def close(self, session: Session) -> None: """Record playlist as no longer loaded""" @@ -289,8 +304,7 @@ class Playlists(Base): """Returns a list of all playlists ordered by last use""" return ( - session.query(cls) - .order_by(cls.last_used.desc()) + session.query(cls).order_by(cls.last_used.desc()) ).all() @classmethod @@ -303,8 +317,8 @@ class Playlists(Base): return ( session.query(cls) - .filter(cls.loaded.is_(False)) - .order_by(cls.last_used.desc()) + .filter(cls.loaded.is_(False)) + .order_by(cls.last_used.desc()) ).all() @classmethod @@ -315,8 +329,8 @@ class Playlists(Base): return ( session.query(cls) - .filter(cls.loaded.is_(True)) - .order_by(cls.last_used.desc()) + .filter(cls.loaded.is_(True)) + .order_by(cls.last_used.desc()) ).all() def mark_open(self, session: Session) -> None: @@ -353,7 +367,7 @@ class PlaylistTracks(Base): id: int = Column(Integer, primary_key=True, autoincrement=True) playlist_id: int = Column(Integer, ForeignKey('playlists.id'), - primary_key=True) + primary_key=True) track_id: int = Column(Integer, ForeignKey('tracks.id'), primary_key=True) row: int = Column(Integer, nullable=False) tracks: RelationshipProperty = relationship("Tracks") @@ -421,7 +435,7 @@ class PlaylistTracks(Base): row: int - last_row: int = session.query( + last_row = session.query( func.max(PlaylistTracks.row) ).filter_by(playlist_id=playlist.id).first() # if there are no rows, the above returns (None, ) which is True @@ -480,11 +494,11 @@ class Tracks(Base): mtime: float = Column(Float, index=True) lastplayed: datetime = Column(DateTime, index=True, default=None) playlists: RelationshipProperty = relationship("PlaylistTracks", - back_populates="tracks", - lazy="joined") + back_populates="tracks", + lazy="joined") playdates: RelationshipProperty = relationship("Playdates", - back_populates="tracks", - lazy="joined") + back_populates="tracks", + lazy="joined") def __init__(self, session: Session, path: str) -> None: self.path = path @@ -572,10 +586,10 @@ class Tracks(Base): audio: AudioSegment = get_audio_segment(self.path) self.duration = len(audio) self.fade_at = round(fade_point(audio) / 1000, - Config.MILLISECOND_SIGFIGS) * 1000 + Config.MILLISECOND_SIGFIGS) * 1000 self.mtime = os.path.getmtime(self.path) self.silence_at = round(trailing_silence(audio) / 1000, - Config.MILLISECOND_SIGFIGS) * 1000 + Config.MILLISECOND_SIGFIGS) * 1000 self.start_gap = leading_silence(audio) session.add(self) session.commit() @@ -597,16 +611,16 @@ class Tracks(Base): return ( session.query(cls) - .filter(cls.artist.ilike(f"%{text}%")) - .order_by(cls.title) + .filter(cls.artist.ilike(f"%{text}%")) + .order_by(cls.title) ).all() @classmethod def search_titles(cls, session: Session, text: str) -> List["Tracks"]: return ( session.query(cls) - .filter(cls.title.ilike(f"%{text}%")) - .order_by(cls.title) + .filter(cls.title.ilike(f"%{text}%")) + .order_by(cls.title) ).all() def update_lastplayed(self, session: Session) -> None: diff --git a/app/music.py b/app/music.py index 1b96092..59690e1 100644 --- a/app/music.py +++ b/app/music.py @@ -86,8 +86,6 @@ class Music: p.stop() DEBUG(f"Releasing player {p=}", True) p.release() - # Ensure we don't reference player after release - p = None self.fading -= 1 diff --git a/app/musicmuster.py b/app/musicmuster.py index 3712eec..a406aa5 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -397,6 +397,9 @@ class Window(QMainWindow, Ui_MainWindow): if not dlg.plid: return + # TODO: just update dest playlist and call populate if + # visible + # If destination playlist is visible, we need to add the moved # tracks to it. If not, they will be automatically loaded when # the playlistis opened. @@ -753,7 +756,8 @@ class Window(QMainWindow, Ui_MainWindow): session.add(self.current_track) playtime: int = self.music.get_playtime() time_to_fade: int = (self.current_track.fade_at - playtime) - time_to_silence: int = (self.current_track.silence_at - playtime) + time_to_silence: int = ( + self.current_track.silence_at - playtime) time_to_end: int = (self.current_track.duration - playtime) # Elapsed time @@ -762,7 +766,8 @@ class Window(QMainWindow, Ui_MainWindow): helpers.ms_to_mmss(self.current_track.duration) ) else: - self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime)) + self.label_elapsed_timer.setText( + helpers.ms_to_mmss(playtime)) # Time to fade self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade)) @@ -935,9 +940,11 @@ class SelectPlaylistDialog(QDialog): self.plid = None with Session() as session: - record = Settings.get_int_settings(session, "select_playlist_dialog_width") + record = Settings.get_int_settings( + session, "select_playlist_dialog_width") width = record.f_int or 800 - record = Settings.get_int_settings(session, "select_playlist_dialog_height") + record = Settings.get_int_settings( + session, "select_playlist_dialog_height") height = record.f_int or 600 self.resize(width, height) @@ -949,11 +956,13 @@ class SelectPlaylistDialog(QDialog): def __del__(self): # review with Session() as session: - record = Settings.get_int_settings(session, "select_playlist_dialog_height") + record = Settings.get_int_settings( + session, "select_playlist_dialog_height") if record.f_int != self.height(): record.update(session, {'f_int': self.height()}) - record = Settings.get_int_settings(session, "select_playlist_dialog_width") + record = Settings.get_int_settings( + session, "select_playlist_dialog_width") if record.f_int != self.width(): record.update(session, {'f_int': self.width()}) diff --git a/app/playlists.py b/app/playlists.py index 1dd3f24..2646d89 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -15,8 +15,6 @@ from PyQt5.QtWidgets import ( QTableWidget, QTableWidgetItem, ) -from sqlalchemy import inspect -from sqlalchemy.orm.exc import DetachedInstanceError import helpers import os @@ -40,9 +38,9 @@ class RowMeta: CLEAR = 0 NOTE = 1 UNREADABLE = 2 - NEXT = 4 - CURRENT = 8 - PLAYED = 16 + NEXT = 3 + CURRENT = 4 + PLAYED = 5 class PlaylistTab(QTableWidget): @@ -142,7 +140,7 @@ class PlaylistTab(QTableWidget): self.cellEditingEnded.connect(self._cell_edit_ended) # Now load our tracks and notes - self._populate(session) + self._populate(session, playlist) def __repr__(self) -> str: return ( @@ -180,7 +178,7 @@ class PlaylistTab(QTableWidget): # rows. Check and fix: row = 0 # So row is defined even if there are no rows in range for row in range(drop_row, drop_row + len(rows_to_move)): - if row in self._meta_get_notes(): + if row in self._get_notes_rows(): self.setSpan( row, self.COL_NOTE, self.NOTE_ROW_SPAN, self.NOTE_COL_SPAN) @@ -217,13 +215,13 @@ class PlaylistTab(QTableWidget): if item is not None: row = item.row() DEBUG(f"playlist.eventFilter(): Right-click on row {row}") - current = row == self._meta_get_current() - next_row = row == self._meta_get_next() + current = row == self._get_current_track_row() + next_row = row == self._get_next_track_row() self.menu = QMenu(self) act_info = self.menu.addAction('Info') act_info.triggered.connect(lambda: self._info_row(row)) self.menu.addSeparator() - if row not in self._meta_get_notes(): + if row not in self._get_notes_rows(): if not current and not next_row: act_setnext = self.menu.addAction("Set next") act_setnext.triggered.connect( @@ -375,12 +373,12 @@ class PlaylistTab(QTableWidget): stop_item: QTableWidgetItem = QTableWidgetItem() self.setItem(row, self.COL_END_TIME, stop_item) - # Attach track object to row - self._set_row_content(row, track) + # Attach track.id object to row + self._set_row_content(row, track.id) # Mark track if file is unreadable if not self._file_is_readable(track.path): - self._meta_set_unreadable(row) + self._set_unreadable_row(row) # Scroll to new row self.scrollToItem(title_item, QAbstractItemView.PositionAtCenter) @@ -418,11 +416,11 @@ class PlaylistTab(QTableWidget): self.current_track_start_time = datetime.now() # Mark next-track row as current - current_row = self._meta_get_next() - self._meta_set_current(current_row) + current_row = self._get_next_track_row() + self._set_current_track_row(current_row) # Mark current row as played - self._meta_set_played(current_row) + self._set_played_row(current_row) # Scroll to put current track in middle scroll_to = self.item(current_row, self.COL_MSS) @@ -447,7 +445,7 @@ class PlaylistTab(QTableWidget): - Update display """ - self._meta_clear_current() + self._clear_current_track_row() self.current_track_start_time = None def save_playlist(self, session) -> None: @@ -470,11 +468,11 @@ class PlaylistTab(QTableWidget): # Create dictionaries indexed by note_id playlist_notes: Dict[int, Notes] = {} database_notes: Dict[int, Notes] = {} - notes_rows: List[int] = self._meta_get_notes() + notes_rows: List[int] = self._get_notes_rows() # PlaylistTab for row in notes_rows: - note: Notes = self._get_row_object(row, session) + note: Notes = self._get_row_notes_object(row, session) session.add(note) playlist_notes[note.id] = note @@ -512,9 +510,9 @@ class PlaylistTab(QTableWidget): for row in range(self.rowCount()): if row in notes_rows: continue - track: Tracks = self.item( + track_id: int = self.item( row, self.COL_USERDATA).data(self.CONTENT_OBJECT) - self.playlist.add_track(session, track, row) + self.playlist.add_track(session, track_id, row) def select_next_row(self) -> None: """ @@ -539,7 +537,7 @@ class PlaylistTab(QTableWidget): # Don't select notes wrapped: bool = False - while row in self._meta_get_notes(): + while row in self._get_notes_rows(): row += 1 if row >= self.rowCount(): if wrapped: @@ -580,7 +578,7 @@ class PlaylistTab(QTableWidget): # Don't select notes wrapped: bool = False - while row in self._meta_get_notes(): + while row in self._get_notes_rows(): row -= 1 if row < 0: if wrapped: @@ -620,17 +618,17 @@ class PlaylistTab(QTableWidget): if self.playlist not in session: session.add(self.playlist) - DEBUG(f"playlist. update_display [{self.playlist=}]") + DEBUG(f"playlist.update_display [{self.playlist=}]") # Clear selection if required if clear_selection: self.clearSelection() - current_row: Optional[int] = self._meta_get_current() - next_row: Optional[int] = self._meta_get_next() - notes: Optional[List[int]] = self._meta_get_notes() - played: Optional[List[int]] = self._meta_get_played() - unreadable: Optional[List[int]] = self._meta_get_unreadable() + current_row: Optional[int] = self._get_current_track_row() + next_row: Optional[int] = self._get_next_track_row() + notes: Optional[List[int]] = self._get_notes_rows() + played: Optional[List[int]] = self._get_played_track_rows() + unreadable: Optional[List[int]] = self._get_unreadable_track_rows() last_played_str: Optional[str] last_playedtime: Optional[datetime] @@ -697,7 +695,7 @@ class PlaylistTab(QTableWidget): last_played_str) # Calculate next_start_time - track = self._get_row_object(row, session) + track = self._get_row_track_object(row, session) next_start_time = self._calculate_track_end_time( track, self.current_track_start_time) @@ -725,7 +723,7 @@ class PlaylistTab(QTableWidget): self._set_row_start_time(row, start_time) # Set end time - track = self._get_row_object(row, session) + track = self._get_row_track_object(row, session) next_start_time = self._calculate_track_end_time( track, start_time) self._set_row_end_time(row, next_start_time) @@ -739,7 +737,7 @@ class PlaylistTab(QTableWidget): else: # This is a track row other than next or current - track = self._get_row_object(row, session) + track = self._get_row_track_object(row, session) if row in played: # Played today, so update last played column last_playedtime = Playdates.last_played( @@ -777,11 +775,11 @@ class PlaylistTab(QTableWidget): DEBUG(f"_audacity({row})") - if row in self._meta_get_notes(): + if row in self._get_notes_rows(): return None with Session() as session: - track: Tracks = self._get_row_object(row, session) + track: Tracks = self._get_row_track_object(row, session) open_in_audacity(track.path) @staticmethod @@ -810,11 +808,11 @@ class PlaylistTab(QTableWidget): DEBUG(f"_copy_path({row})") - if row in self._meta_get_notes(): + if row in self._get_notes_rows(): return None with Session() as session: - track: Optional[Tracks] = self._get_row_object(row, session) + track: Optional[Tracks] = self._get_row_track_object(row, session) if track: cb: QApplication.clipboard = QApplication.clipboard() cb.clear(mode=cb.Clipboard) @@ -832,12 +830,11 @@ class PlaylistTab(QTableWidget): DEBUG(f"_cell_changed({row=}, {column=}, {new_text=}") with Session() as session: - row_object: Union[Tracks, Notes] = self._get_row_object( - row, session) - if row in self._meta_get_notes(): + if row in self._get_notes_rows(): # Save change to database DEBUG(f"Notes.update_note: saving new note text '{new_text=}'") - row_object.update_note(session, row, new_text) + note: Notes = self._get_notes_row_object(row, session) + note.update_note(session, row, new_text) # Set/clear row start time accordingly start_time = self._get_note_text_time(new_text) if start_time: @@ -854,10 +851,11 @@ class PlaylistTab(QTableWidget): "start time" ) else: + track: Tracks = self._get_track_row_object(row, session) if column == self.COL_ARTIST: - row_object.update_artist(session, artist=new_text) + track.update_artist(session, artist=new_text) elif column == self.COL_TITLE: - row_object.update_title(session, title=new_text) + track.update_title(session, title=new_text) else: ERROR("_cell_changed(): unrecognised column") @@ -896,7 +894,7 @@ class PlaylistTab(QTableWidget): set(item.row() for item in self.selectedItems()) ) rows_to_delete: List[int] = [] - note_rows: Optional[List[int]] = self._meta_get_notes() + note_rows: Optional[List[int]] = self._get_notes_rows() row: int row_object: Union[Tracks, Notes] @@ -913,15 +911,15 @@ class PlaylistTab(QTableWidget): if msg.exec() == QMessageBox.Yes: rows_to_delete.append(row) - # delete in reverse row order so row numbers don't - # change - for row in sorted(rows_to_delete, reverse=True): - row_object = self._get_row_object(row, session) - if row in note_rows: - row_object.delete_note(session) - else: - self.remove_track(session, row) - self.removeRow(row) + # delete in reverse row order so row numbers don't + # change + for row in sorted(rows_to_delete, reverse=True): + if row in note_rows: + note: Notes = self._get_row_notes_object(row, session) + note.delete_note(session) + else: + self.remove_track(session, row) + self.removeRow(row) self.save_playlist(session) self.update_display(session) @@ -963,14 +961,21 @@ class PlaylistTab(QTableWidget): except ValueError: return None - def _get_row_object(self, row: int, session: Session) \ - -> Union[Tracks, Notes]: - """Return content associated with this row""" + def _get_row_track_object(self, row: int, session: Session) \ + -> Optional[Tracks]: + """Return track associated with this row""" - obj = self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT) - if obj not in session: - session.add(obj) - return obj + track_id = self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT) + track = Tracks.get_by_id(session, track_id) + return track + + def _get_row_notes_object(self, row: int, session: Session) \ + -> Optional[Notes]: + """Return note associated with this row""" + + note_id = self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT) + note = Notes.get_by_id(session, note_id) + return note def _get_row_start_time(self, row: int) -> Optional[datetime]: try: @@ -990,12 +995,11 @@ class PlaylistTab(QTableWidget): txt: str with Session() as session: - row_object: Union[Tracks, Notes] = self._get_row_object( - row, session) - if row in self._meta_get_notes(): - txt = row_object.note + if row in self._get_notes_rows(): + note: Notes = self._get_row_notes_object(row, session) + txt = note.note else: - track = row_object + track: Tracks = self._get_row_track_object(row, session) txt = ( f"Title: {track.title}\n" f"Artist: {track.artist}\n" @@ -1038,13 +1042,14 @@ class PlaylistTab(QTableWidget): # Add text of note from title column onwards titleitem: QTableWidgetItem = QTableWidgetItem(note.note) self.setItem(row, self.COL_NOTE, titleitem) - self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, self.NOTE_COL_SPAN) + self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, + self.NOTE_COL_SPAN) - # Attach note object to row - self._set_row_content(row, note) + # Attach note id to row + self._set_row_content(row, note.id) # Mark row as a Note row - self._meta_set_note(row) + self._set_note_row(row) # Scroll to new row self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter) @@ -1088,13 +1093,13 @@ class PlaylistTab(QTableWidget): """ if starting_row is None: - current_row = self._meta_get_current() + current_row = self._get_current_track_row() if current_row is not None: starting_row = current_row + 1 else: starting_row = 0 - notes_rows = self._meta_get_notes() - played_rows = self._meta_get_played() + notes_rows = self._get_notes_rows() + played_rows = self._get_played_track_rows() for row in range(starting_row, self.rowCount()): if row in notes_rows or row in played_rows: continue @@ -1109,16 +1114,16 @@ class PlaylistTab(QTableWidget): if row is None: raise ValueError(f"_meta_clear_attribute({row=}, {attribute=})") - new_metadata: int = self._meta_get(row) ^ attribute + new_metadata: int = self._meta_get(row) & ~(1 << attribute) self.item(row, self.COL_USERDATA).setData( self.ROW_METADATA, new_metadata) - def _meta_clear_current(self) -> None: + def _clear_current_track_row(self) -> None: """ Clear current row if there is one. """ - current_row: Optional[int] = self._meta_get_current() + current_row: Optional[int] = self._get_current_track_row() if current_row is not None: self._meta_clear_attribute(current_row, RowMeta.CURRENT) # Reset row colour @@ -1134,11 +1139,11 @@ class PlaylistTab(QTableWidget): Clear next row if there is one. """ - next_row: Optional[int] = self._meta_get_next() + next_row: Optional[int] = self._get_next_track_row() if next_row is not None: self._meta_clear_attribute(next_row, RowMeta.NEXT) - def _meta_clear_played(self, row: int) -> None: + def _clear_played_row_status(self, row: int) -> None: """Clear played status on row""" self._meta_clear_attribute(row, RowMeta.PLAYED) @@ -1148,31 +1153,51 @@ class PlaylistTab(QTableWidget): return self.item(row, self.COL_USERDATA).data(self.ROW_METADATA) - def _meta_get_current(self) -> Optional[int]: + def _get_current_track_row(self) -> Optional[int]: """Return row marked as current, or None""" return self._meta_search(RowMeta.CURRENT) - def _meta_get_next(self) -> Optional[int]: + def _get_next_track_row(self) -> Optional[int]: """Return row marked as next, or None""" return self._meta_search(RowMeta.NEXT) - def _meta_get_notes(self) -> Optional[List[int]]: + def _get_notes_rows(self) -> Optional[List[int]]: """Return rows marked as notes, or None""" return self._meta_search(RowMeta.NOTE, one=False) - def _meta_get_played(self) -> Optional[List[int]]: + def _get_track_rows(self) -> Optional[List[int]]: + """Return rows marked as tracks, or None""" + + return self._meta_notset(RowMeta.NOTE) + + def _get_played_track_rows(self) -> Optional[List[int]]: """Return rows marked as played, or None""" return self._meta_search(RowMeta.PLAYED, one=False) - def _meta_get_unreadable(self) -> Optional[List[int]]: + def _get_unreadable_track_rows(self) -> Optional[List[int]]: """Return rows marked as unreadable, or None""" return self._meta_search(RowMeta.UNREADABLE, one=False) + def _meta_notset(self, metadata: int) -> Union[List[int]]: + """ + Search rows for metadata not set. + + Return a list of matching row numbers. + """ + + matches = [] + for row in range(self.rowCount()): + if self._meta_get(row): + if not self._meta_get(row) & (1 << metadata): + matches.append(row) + + return matches + def _meta_search(self, metadata: int, one: bool = True) -> Union[ List[int], int, None]: """ @@ -1187,7 +1212,7 @@ class PlaylistTab(QTableWidget): matches = [] for row in range(self.rowCount()): if self._meta_get(row): - if self._meta_get(row) & metadata: + if self._meta_get(row) & (1 << metadata): matches.append(row) if not one: @@ -1212,40 +1237,40 @@ class PlaylistTab(QTableWidget): current_metadata: int = self._meta_get(row) if not current_metadata: - new_metadata = attribute + new_metadata = (1 << attribute) else: - new_metadata: int = self._meta_get(row) | attribute + new_metadata: int = self._meta_get(row) | (1 << attribute) self.item(row, self.COL_USERDATA).setData( self.ROW_METADATA, new_metadata) - def _meta_set_current(self, row: int) -> None: + def _set_current_track_row(self, row: int) -> None: """Mark this row as current track""" - self._meta_clear_current() + self._clear_current_track_row() self._meta_set_attribute(row, RowMeta.CURRENT) - def _meta_set_next(self, row: int) -> None: + def _set_next_track_row(self, row: int) -> None: """Mark this row as next track""" self._meta_clear_next() self._meta_set_attribute(row, RowMeta.NEXT) - def _meta_set_note(self, row: int) -> None: + def _set_note_row(self, row: int) -> None: """Mark this row as a note""" self._meta_set_attribute(row, RowMeta.NOTE) - def _meta_set_played(self, row: int) -> None: + def _set_played_row(self, row: int) -> None: """Mark this row as played""" self._meta_set_attribute(row, RowMeta.PLAYED) - def _meta_set_unreadable(self, row: int) -> None: + def _set_unreadable_row(self, row: int) -> None: """Mark this row as unreadable""" self._meta_set_attribute(row, RowMeta.UNREADABLE) - def _populate(self, session: Session) -> None: + def _populate(self, session: Session, playlist: Playlists) -> None: """ Populate from the associated playlist object @@ -1261,16 +1286,11 @@ class PlaylistTab(QTableWidget): row: int track: Tracks - # Make sure the database object is usable - insp = inspect(self.playlist) - if insp.detached: - session.add(self.playlist) - assert insp.persistent + if playlist not in session: + session.add(playlist) for row, track in self.playlist.tracks.items(): data.append(([row], track)) - # Add track to session to expose attributes - session.add(track) for note in self.playlist.notes: data.append(([note.row], note)) @@ -1302,14 +1322,12 @@ class PlaylistTab(QTableWidget): DEBUG(f"_rescan({row=})") - if row in self._meta_get_notes(): - return None - with Session() as session: - track: Tracks = self._get_row_object(row, session) - if track: - track.rescan(session) - self._update_row(session, row, track) + for row in self._get_track_rows(): + track: Tracks = self._get_row_track_object(row, session) + if track: + track.rescan(session) + self._update_row(session, row, track) def _select_event(self) -> None: """ @@ -1317,24 +1335,21 @@ class PlaylistTab(QTableWidget): If multiple rows are selected, display sum of durations in status bar. """ - row_set: Set[int] = set([item.row() for item in self.selectedItems()]) - note_row_set: Set[int] = set(self._meta_get_notes()) - track_rows = list(row_set - note_row_set) - tracks: List[Tracks] + # Get the row number of all selected items and put into a set + # to deduplicate + sel_rows: Set[int] = set([item.row() for item in self.selectedItems()]) + notes_rows: Set[int] = set(self._get_notes_rows()) + ms: int = 0 + with Session() as session: + for row in (sel_rows - notes_rows): + ms += self._get_row_track_object(row, session).duration - with Session() as session: # checked - tracks = [self._get_row_object(row, session) for row in track_rows] - for track in tracks: - if track not in session: - session.add(track) - ms: int = sum([track.duration for track in tracks]) - - # Only paint message if there are selected track rows - if ms > 0: - self.parent.lblSumPlaytime.setText( - f"Selected duration: {helpers.ms_to_mmss(ms)}") - else: - self.parent.lblSumPlaytime.setText("") + # Only paint message if there are selected track rows + if ms > 0: + self.parent.lblSumPlaytime.setText( + f"Selected duration: {helpers.ms_to_mmss(ms)}") + else: + self.parent.lblSumPlaytime.setText("") def _set_column_widths(self) -> None: """Column widths from settings""" @@ -1342,7 +1357,7 @@ class PlaylistTab(QTableWidget): with Session() as session: for column in range(self.columnCount()): name: str = f"playlist_col_{str(column)}_width" - record: int = Settings.get_int_settings(session, name) + record: Settings = Settings.get_int_settings(session, name) if record and record.f_int is not None: self.setColumnWidth(column, record.f_int) else: @@ -1364,19 +1379,19 @@ class PlaylistTab(QTableWidget): with Session() as session: # Check row is a track row - if row in self._meta_get_notes(): + if row in self._get_notes_rows(): return None - track: Tracks = self._get_row_object(row, session) + track: Tracks = self._get_row_track_object(row, session) if not track: return None # Check track is readable if not self._file_is_readable(track.path): - self._meta_set_unreadable(row) + self._set_unreadable_row(row) return None # Mark as next track - self._meta_set_next(row) + self._set_next_track_row(row) # Notify musicmuster self.parent.this_is_the_next_track(self, track) @@ -1405,13 +1420,13 @@ class PlaylistTab(QTableWidget): if self.item(row, j): self.item(row, j).setBackground(colour) - def _set_row_content(self, row: int, content: Union[Tracks, Notes]) \ - -> None: + def _set_row_content(self, row: int, object_id: int) -> None: """Set content associated with this row""" assert self.item(row, self.COL_USERDATA) - self.item(row, self.COL_USERDATA).setData(self.CONTENT_OBJECT, content) + self.item(row, self.COL_USERDATA).setData( + self.CONTENT_OBJECT, object_id) def _set_row_end_time(self, row: int, time: Optional[datetime]) -> None: """Set passed row end time to passed time""" @@ -1446,10 +1461,10 @@ class PlaylistTab(QTableWidget): # Need to allow multiple rows to be selected self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) - notes_rows: List[int] = self._meta_get_notes() + notes_rows: List[int] = self._get_notes_rows() self.clearSelection() - played_rows: List[int] = self._meta_get_played() + played_rows: List[int] = self._get_played_track_rows() for row in range(self.rowCount()): if row in notes_rows: continue diff --git a/app/ui_helpers.py b/app/ui_helpers.py index caeb12a..d842f1e 100644 --- a/app/ui_helpers.py +++ b/app/ui_helpers.py @@ -1,5 +1,6 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QFontMetrics, QPainter +from PyQt5.QtWidgets import QLabel class ElideLabel(QLabel): diff --git a/app/utilities.py b/app/utilities.py index 3a2f5fc..17bf05e 100755 --- a/app/utilities.py +++ b/app/utilities.py @@ -6,12 +6,11 @@ import shutil import tempfile from config import Config -from helpers import show_warning from log import DEBUG, INFO from models import Notes, Playdates, Session, Tracks from mutagen.flac import FLAC from mutagen.mp3 import MP3 -from pydub import AudioSegment, effects +from pydub import effects # Globals (I know) messages = [] diff --git a/test_helpers.py b/test_helpers.py index a8ffc6c..e6251d4 100644 --- a/test_helpers.py +++ b/test_helpers.py @@ -5,8 +5,8 @@ from models import Tracks def test_fade_point(): - test_track_path = "../testdata/isa.mp3" - test_track_data = "../testdata/isa.py" + test_track_path = "testdata/isa.mp3" + test_track_data = "testdata/isa.py" audio_segment = get_audio_segment(test_track_path) assert audio_segment @@ -23,8 +23,8 @@ def test_fade_point(): def test_get_tags(): - test_track_path = "../testdata/mom.mp3" - test_track_data = "../testdata/mom.py" + test_track_path = "testdata/mom.mp3" + test_track_data = "testdata/mom.py" tags = get_tags(test_track_path) @@ -49,8 +49,8 @@ def test_get_relative_date(): def test_leading_silence(): - test_track_path = "../testdata/isa.mp3" - test_track_data = "../testdata/isa.py" + test_track_path = "testdata/isa.mp3" + test_track_data = "testdata/isa.py" audio_segment = get_audio_segment(test_track_path) assert audio_segment @@ -69,4 +69,4 @@ def test_leading_silence(): def test_ms_to_mmss(): assert ms_to_mmss(None) == "-" assert ms_to_mmss(59600) == "0:59" - assert ms_to_mmss((5 * 60 * 1000) + 23000) == "5:23" \ No newline at end of file + assert ms_to_mmss((5 * 60 * 1000) + 23000) == "5:23" diff --git a/test_models.py b/test_models.py index 4bb2b6e..2f6254e 100644 --- a/test_models.py +++ b/test_models.py @@ -172,7 +172,7 @@ def test_playlist_add_track(session): row = 17 - playlist.add_track(session, track, row) + playlist.add_track(session, track.id, row) assert len(playlist.tracks) == 1 playlist_track = playlist.tracks[row] @@ -192,8 +192,8 @@ def test_playlist_tracks(session): track2_row = 29 track2 = Tracks(session, track2_path) - playlist.add_track(session, track1, track1_row) - playlist.add_track(session, track2, track2_row) + playlist.add_track(session, track1.id, track1_row) + playlist.add_track(session, track2.id, track2_row) tracks = playlist.tracks assert tracks[track1_row] == track1 @@ -269,7 +269,7 @@ def test_playlist_remove_tracks(session): # Add all tracks to both playlists for p in [playlist1, playlist2]: for t in [track1, track2, track3]: - p.add_track(session, t) + p.add_track(session, t.id) assert len(playlist1.tracks) == 3 assert len(playlist2.tracks) == 3 @@ -295,9 +295,9 @@ def test_playlist_get_track_playlists(session): track2 = Tracks(session, track2_path) # Put track1 in both playlists, track2 only in playlist1 - playlist1.add_track(session, track1) - playlist2.add_track(session, track1) - playlist1.add_track(session, track2) + playlist1.add_track(session, track1.id) + playlist2.add_track(session, track1.id) + playlist1.add_track(session, track2.id) playlists_track1 = track1.playlists playlists_track2 = track2.playlists @@ -324,8 +324,8 @@ def test_playlisttracks_move_track(session): track1 = Tracks(session, track1_path) # Add both to playlist1 and check - playlist1.add_track(session, track1, track1_row) - playlist1.add_track(session, track2, track2_row) + playlist1.add_track(session, track1.id, track1_row) + playlist1.add_track(session, track2.id, track2_row) tracks = playlist1.tracks assert tracks[track1_row] == track1 diff --git a/test_playlists.py b/test_playlists.py index 01e4ce3..9594f32 100644 --- a/test_playlists.py +++ b/test_playlists.py @@ -2,7 +2,6 @@ from PyQt5.QtCore import Qt from app.playlists import Notes, PlaylistTab, Tracks from app.models import Playlists -# from musicmuster import Window from musicmuster import Window @@ -60,11 +59,11 @@ def test_meta_all_clear(qtbot, session): track3 = Tracks(session, track3_path) playlist_tab.insert_track(session, track3) - assert playlist_tab._meta_get_current() is None - assert playlist_tab._meta_get_next() is None - assert playlist_tab._meta_get_notes() == [] - assert playlist_tab._meta_get_played() == [] - assert len(playlist_tab._meta_get_unreadable()) == 3 + assert playlist_tab._get_current_track_row() is None + assert playlist_tab._get_next_track_row() is None + assert playlist_tab._get_notes_rows() == [] + assert playlist_tab._get_played_track_rows() == [] + assert len(playlist_tab._get_unreadable_track_rows()) == 3 def test_meta(qtbot, session): @@ -84,18 +83,18 @@ def test_meta(qtbot, session): track3 = Tracks(session, track3_path) playlist_tab.insert_track(session, track3) - assert len(playlist_tab._meta_get_unreadable()) == 3 + assert len(playlist_tab._get_unreadable_track_rows()) == 3 - assert playlist_tab._meta_get_played() == [] - assert playlist_tab._meta_get_current() is None - assert playlist_tab._meta_get_next() is None - assert playlist_tab._meta_get_notes() == [] + assert playlist_tab._get_played_track_rows() == [] + assert playlist_tab._get_current_track_row() is None + assert playlist_tab._get_next_track_row() is None + assert playlist_tab._get_notes_rows() == [] - playlist_tab._meta_set_played(0) - assert playlist_tab._meta_get_played() == [0] - assert playlist_tab._meta_get_current() is None - assert playlist_tab._meta_get_next() is None - assert playlist_tab._meta_get_notes() == [] + playlist_tab._set_played_row(0) + assert playlist_tab._get_played_track_rows() == [0] + assert playlist_tab._get_current_track_row() is None + assert playlist_tab._get_next_track_row() is None + assert playlist_tab._get_notes_rows() == [] # Add a note note_text = "my note" @@ -103,40 +102,47 @@ def test_meta(qtbot, session): note = Notes(session, playlist.id, note_row, note_text) playlist_tab._insert_note(session, note) - assert playlist_tab._meta_get_played() == [0] - assert playlist_tab._meta_get_current() is None - assert playlist_tab._meta_get_next() is None - assert playlist_tab._meta_get_notes() == [3] + assert playlist_tab._get_played_track_rows() == [0] + assert playlist_tab._get_current_track_row() is None + assert playlist_tab._get_next_track_row() is None + assert playlist_tab._get_notes_rows() == [3] - playlist_tab._meta_set_next(1) - assert playlist_tab._meta_get_played() == [0] - assert playlist_tab._meta_get_current() is None - assert playlist_tab._meta_get_next() == 1 - assert playlist_tab._meta_get_notes() == [3] + playlist_tab._set_next_track_row(1) + assert playlist_tab._get_played_track_rows() == [0] + assert playlist_tab._get_current_track_row() is None + assert playlist_tab._get_next_track_row() == 1 + assert playlist_tab._get_notes_rows() == [3] - playlist_tab._meta_set_current(2) - assert playlist_tab._meta_get_played() == [0] - assert playlist_tab._meta_get_current() == 2 - assert playlist_tab._meta_get_next() == 1 - assert playlist_tab._meta_get_notes() == [3] + playlist_tab._set_current_track_row(2) + assert playlist_tab._get_played_track_rows() == [0] + assert playlist_tab._get_current_track_row() == 2 + assert playlist_tab._get_next_track_row() == 1 + assert playlist_tab._get_notes_rows() == [3] - playlist_tab._meta_clear_played(0) - assert playlist_tab._meta_get_played() == [] - assert playlist_tab._meta_get_current() == 2 - assert playlist_tab._meta_get_next() == 1 - assert playlist_tab._meta_get_notes() == [3] + playlist_tab._clear_played_row_status(0) + assert playlist_tab._get_played_track_rows() == [] + assert playlist_tab._get_current_track_row() == 2 + assert playlist_tab._get_next_track_row() == 1 + assert playlist_tab._get_notes_rows() == [3] playlist_tab._meta_clear_next() - assert playlist_tab._meta_get_played() == [] - assert playlist_tab._meta_get_current() == 2 - assert playlist_tab._meta_get_next() is None - assert playlist_tab._meta_get_notes() == [3] + assert playlist_tab._get_played_track_rows() == [] + assert playlist_tab._get_current_track_row() == 2 + assert playlist_tab._get_next_track_row() is None + assert playlist_tab._get_notes_rows() == [3] - playlist_tab._meta_clear_current() - assert playlist_tab._meta_get_played() == [] - assert playlist_tab._meta_get_current() is None - assert playlist_tab._meta_get_next() is None - assert playlist_tab._meta_get_notes() == [3] + playlist_tab._clear_current_track_row() + assert playlist_tab._get_played_track_rows() == [] + assert playlist_tab._get_current_track_row() is None + assert playlist_tab._get_next_track_row() is None + assert playlist_tab._get_notes_rows() == [3] + + # Test clearing again has no effect + playlist_tab._clear_current_track_row() + assert playlist_tab._get_played_track_rows() == [] + assert playlist_tab._get_current_track_row() is None + assert playlist_tab._get_next_track_row() is None + assert playlist_tab._get_notes_rows() == [3] def test_clear_next(qtbot, session): @@ -152,11 +158,11 @@ def test_clear_next(qtbot, session): track2 = Tracks(session, track2_path) playlist_tab.insert_track(session, track2) - playlist_tab._meta_set_next(1) - assert playlist_tab._meta_get_next() == 1 + playlist_tab._set_next_track_row(1) + assert playlist_tab._get_next_track_row() == 1 - playlist_tab.clear_next() - assert playlist_tab._meta_get_next() is None + playlist_tab.clear_next(session) + assert playlist_tab._get_next_track_row() is None def test_get_selected_row(qtbot, session): @@ -174,10 +180,9 @@ def test_get_selected_row(qtbot, session): playlist_tab.insert_track(session, track2) window = Window() - window.show() qtbot.addWidget(playlist_tab) - qtbot.wait_for_window_shown(playlist_tab) - + with qtbot.waitExposed(window): + window.show() row0_item0 = playlist_tab.item(0, 0) assert row0_item0 is not None rect = playlist_tab.visualItemRect(row0_item0)