V2 using ids rather than objects. Looking good.

This commit is contained in:
Keith Edmunds 2022-03-01 21:35:03 +00:00
parent 26edd5a2d0
commit e8211414f9
9 changed files with 276 additions and 235 deletions

View File

@ -175,6 +175,18 @@ class Notes(Base):
session.query(Notes).filter_by(id=self.id).delete() session.query(Notes).filter_by(id=self.id).delete()
session.commit() 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( def update_note(
self, session: Session, row: int, self, session: Session, row: int,
text: Optional[str] = None) -> None: text: Optional[str] = None) -> None:
@ -215,7 +227,8 @@ class Playdates(Base):
"""Return datetime track last played or None""" """Return datetime track last played or None"""
last_played: Optional[Playdates] = session.query( last_played: Optional[Playdates] = session.query(
Playdates.lastplayed).filter((Playdates.track_id == track_id) Playdates.lastplayed).filter(
(Playdates.track_id == track_id)
).order_by(Playdates.lastplayed.desc()).first() ).order_by(Playdates.lastplayed.desc()).first()
if last_played: if last_played:
return last_played[0] return last_played[0]
@ -246,7 +259,9 @@ class Playlists(Base):
last_used: datetime = Column(DateTime, default=None, nullable=True) last_used: datetime = Column(DateTime, default=None, nullable=True)
loaded: bool = Column(Boolean, default=True, nullable=False) loaded: bool = Column(Boolean, default=True, nullable=False)
notes = relationship( 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') tracks = association_proxy('playlist_tracks', 'tracks')
row = association_proxy('playlist_tracks', 'row') row = association_proxy('playlist_tracks', 'row')
@ -265,7 +280,7 @@ class Playlists(Base):
return Notes(session, self.id, row, text) return Notes(session, self.id, row, text)
def add_track( def add_track(
self, session: Session, track: "Tracks", self, session: Session, track_id: int,
row: Optional[int] = None) -> None: row: Optional[int] = None) -> None:
""" """
Add track to playlist at given row. Add track to playlist at given row.
@ -275,7 +290,7 @@ class Playlists(Base):
if not row: if not row:
row = PlaylistTracks.next_free_row(session, self) 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: def close(self, session: Session) -> None:
"""Record playlist as no longer loaded""" """Record playlist as no longer loaded"""
@ -289,8 +304,7 @@ class Playlists(Base):
"""Returns a list of all playlists ordered by last use""" """Returns a list of all playlists ordered by last use"""
return ( return (
session.query(cls) session.query(cls).order_by(cls.last_used.desc())
.order_by(cls.last_used.desc())
).all() ).all()
@classmethod @classmethod
@ -421,7 +435,7 @@ class PlaylistTracks(Base):
row: int row: int
last_row: int = session.query( last_row = session.query(
func.max(PlaylistTracks.row) func.max(PlaylistTracks.row)
).filter_by(playlist_id=playlist.id).first() ).filter_by(playlist_id=playlist.id).first()
# if there are no rows, the above returns (None, ) which is True # if there are no rows, the above returns (None, ) which is True

View File

@ -86,8 +86,6 @@ class Music:
p.stop() p.stop()
DEBUG(f"Releasing player {p=}", True) DEBUG(f"Releasing player {p=}", True)
p.release() p.release()
# Ensure we don't reference player after release
p = None
self.fading -= 1 self.fading -= 1

View File

@ -397,6 +397,9 @@ class Window(QMainWindow, Ui_MainWindow):
if not dlg.plid: if not dlg.plid:
return return
# TODO: just update dest playlist and call populate if
# visible
# If destination playlist is visible, we need to add the moved # If destination playlist is visible, we need to add the moved
# tracks to it. If not, they will be automatically loaded when # tracks to it. If not, they will be automatically loaded when
# the playlistis opened. # the playlistis opened.
@ -753,7 +756,8 @@ class Window(QMainWindow, Ui_MainWindow):
session.add(self.current_track) session.add(self.current_track)
playtime: int = self.music.get_playtime() playtime: int = self.music.get_playtime()
time_to_fade: int = (self.current_track.fade_at - 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) time_to_end: int = (self.current_track.duration - playtime)
# Elapsed time # Elapsed time
@ -762,7 +766,8 @@ class Window(QMainWindow, Ui_MainWindow):
helpers.ms_to_mmss(self.current_track.duration) helpers.ms_to_mmss(self.current_track.duration)
) )
else: else:
self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime)) self.label_elapsed_timer.setText(
helpers.ms_to_mmss(playtime))
# Time to fade # Time to fade
self.label_fade_timer.setText(helpers.ms_to_mmss(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 self.plid = None
with Session() as session: 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 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 height = record.f_int or 600
self.resize(width, height) self.resize(width, height)
@ -949,11 +956,13 @@ class SelectPlaylistDialog(QDialog):
def __del__(self): # review def __del__(self): # review
with Session() as session: 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(): if record.f_int != self.height():
record.update(session, {'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(): if record.f_int != self.width():
record.update(session, {'f_int': self.width()}) record.update(session, {'f_int': self.width()})

View File

@ -15,8 +15,6 @@ from PyQt5.QtWidgets import (
QTableWidget, QTableWidget,
QTableWidgetItem, QTableWidgetItem,
) )
from sqlalchemy import inspect
from sqlalchemy.orm.exc import DetachedInstanceError
import helpers import helpers
import os import os
@ -40,9 +38,9 @@ class RowMeta:
CLEAR = 0 CLEAR = 0
NOTE = 1 NOTE = 1
UNREADABLE = 2 UNREADABLE = 2
NEXT = 4 NEXT = 3
CURRENT = 8 CURRENT = 4
PLAYED = 16 PLAYED = 5
class PlaylistTab(QTableWidget): class PlaylistTab(QTableWidget):
@ -142,7 +140,7 @@ class PlaylistTab(QTableWidget):
self.cellEditingEnded.connect(self._cell_edit_ended) self.cellEditingEnded.connect(self._cell_edit_ended)
# Now load our tracks and notes # Now load our tracks and notes
self._populate(session) self._populate(session, playlist)
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -180,7 +178,7 @@ class PlaylistTab(QTableWidget):
# rows. Check and fix: # rows. Check and fix:
row = 0 # So row is defined even if there are no rows in range 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)): 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( self.setSpan(
row, self.COL_NOTE, self.NOTE_ROW_SPAN, self.NOTE_COL_SPAN) row, self.COL_NOTE, self.NOTE_ROW_SPAN, self.NOTE_COL_SPAN)
@ -217,13 +215,13 @@ class PlaylistTab(QTableWidget):
if item is not None: if item is not None:
row = item.row() row = item.row()
DEBUG(f"playlist.eventFilter(): Right-click on row {row}") DEBUG(f"playlist.eventFilter(): Right-click on row {row}")
current = row == self._meta_get_current() current = row == self._get_current_track_row()
next_row = row == self._meta_get_next() next_row = row == self._get_next_track_row()
self.menu = QMenu(self) self.menu = QMenu(self)
act_info = self.menu.addAction('Info') act_info = self.menu.addAction('Info')
act_info.triggered.connect(lambda: self._info_row(row)) act_info.triggered.connect(lambda: self._info_row(row))
self.menu.addSeparator() 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: if not current and not next_row:
act_setnext = self.menu.addAction("Set next") act_setnext = self.menu.addAction("Set next")
act_setnext.triggered.connect( act_setnext.triggered.connect(
@ -375,12 +373,12 @@ class PlaylistTab(QTableWidget):
stop_item: QTableWidgetItem = QTableWidgetItem() stop_item: QTableWidgetItem = QTableWidgetItem()
self.setItem(row, self.COL_END_TIME, stop_item) self.setItem(row, self.COL_END_TIME, stop_item)
# Attach track object to row # Attach track.id object to row
self._set_row_content(row, track) self._set_row_content(row, track.id)
# Mark track if file is unreadable # Mark track if file is unreadable
if not self._file_is_readable(track.path): if not self._file_is_readable(track.path):
self._meta_set_unreadable(row) self._set_unreadable_row(row)
# Scroll to new row # Scroll to new row
self.scrollToItem(title_item, QAbstractItemView.PositionAtCenter) self.scrollToItem(title_item, QAbstractItemView.PositionAtCenter)
@ -418,11 +416,11 @@ class PlaylistTab(QTableWidget):
self.current_track_start_time = datetime.now() self.current_track_start_time = datetime.now()
# Mark next-track row as current # Mark next-track row as current
current_row = self._meta_get_next() current_row = self._get_next_track_row()
self._meta_set_current(current_row) self._set_current_track_row(current_row)
# Mark current row as played # 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 put current track in middle
scroll_to = self.item(current_row, self.COL_MSS) scroll_to = self.item(current_row, self.COL_MSS)
@ -447,7 +445,7 @@ class PlaylistTab(QTableWidget):
- Update display - Update display
""" """
self._meta_clear_current() self._clear_current_track_row()
self.current_track_start_time = None self.current_track_start_time = None
def save_playlist(self, session) -> None: def save_playlist(self, session) -> None:
@ -470,11 +468,11 @@ class PlaylistTab(QTableWidget):
# Create dictionaries indexed by note_id # Create dictionaries indexed by note_id
playlist_notes: Dict[int, Notes] = {} playlist_notes: Dict[int, Notes] = {}
database_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 # PlaylistTab
for row in notes_rows: 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) session.add(note)
playlist_notes[note.id] = note playlist_notes[note.id] = note
@ -512,9 +510,9 @@ class PlaylistTab(QTableWidget):
for row in range(self.rowCount()): for row in range(self.rowCount()):
if row in notes_rows: if row in notes_rows:
continue continue
track: Tracks = self.item( track_id: int = self.item(
row, self.COL_USERDATA).data(self.CONTENT_OBJECT) 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: def select_next_row(self) -> None:
""" """
@ -539,7 +537,7 @@ class PlaylistTab(QTableWidget):
# Don't select notes # Don't select notes
wrapped: bool = False wrapped: bool = False
while row in self._meta_get_notes(): while row in self._get_notes_rows():
row += 1 row += 1
if row >= self.rowCount(): if row >= self.rowCount():
if wrapped: if wrapped:
@ -580,7 +578,7 @@ class PlaylistTab(QTableWidget):
# Don't select notes # Don't select notes
wrapped: bool = False wrapped: bool = False
while row in self._meta_get_notes(): while row in self._get_notes_rows():
row -= 1 row -= 1
if row < 0: if row < 0:
if wrapped: if wrapped:
@ -620,17 +618,17 @@ class PlaylistTab(QTableWidget):
if self.playlist not in session: if self.playlist not in session:
session.add(self.playlist) session.add(self.playlist)
DEBUG(f"playlist. update_display [{self.playlist=}]") DEBUG(f"playlist.update_display [{self.playlist=}]")
# Clear selection if required # Clear selection if required
if clear_selection: if clear_selection:
self.clearSelection() self.clearSelection()
current_row: Optional[int] = self._meta_get_current() current_row: Optional[int] = self._get_current_track_row()
next_row: Optional[int] = self._meta_get_next() next_row: Optional[int] = self._get_next_track_row()
notes: Optional[List[int]] = self._meta_get_notes() notes: Optional[List[int]] = self._get_notes_rows()
played: Optional[List[int]] = self._meta_get_played() played: Optional[List[int]] = self._get_played_track_rows()
unreadable: Optional[List[int]] = self._meta_get_unreadable() unreadable: Optional[List[int]] = self._get_unreadable_track_rows()
last_played_str: Optional[str] last_played_str: Optional[str]
last_playedtime: Optional[datetime] last_playedtime: Optional[datetime]
@ -697,7 +695,7 @@ class PlaylistTab(QTableWidget):
last_played_str) last_played_str)
# Calculate next_start_time # 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( next_start_time = self._calculate_track_end_time(
track, self.current_track_start_time) track, self.current_track_start_time)
@ -725,7 +723,7 @@ class PlaylistTab(QTableWidget):
self._set_row_start_time(row, start_time) self._set_row_start_time(row, start_time)
# Set end 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( next_start_time = self._calculate_track_end_time(
track, start_time) track, start_time)
self._set_row_end_time(row, next_start_time) self._set_row_end_time(row, next_start_time)
@ -739,7 +737,7 @@ class PlaylistTab(QTableWidget):
else: else:
# This is a track row other than next or current # 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: if row in played:
# Played today, so update last played column # Played today, so update last played column
last_playedtime = Playdates.last_played( last_playedtime = Playdates.last_played(
@ -777,11 +775,11 @@ class PlaylistTab(QTableWidget):
DEBUG(f"_audacity({row})") DEBUG(f"_audacity({row})")
if row in self._meta_get_notes(): if row in self._get_notes_rows():
return None return None
with Session() as session: 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) open_in_audacity(track.path)
@staticmethod @staticmethod
@ -810,11 +808,11 @@ class PlaylistTab(QTableWidget):
DEBUG(f"_copy_path({row})") DEBUG(f"_copy_path({row})")
if row in self._meta_get_notes(): if row in self._get_notes_rows():
return None return None
with Session() as session: 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: if track:
cb: QApplication.clipboard = QApplication.clipboard() cb: QApplication.clipboard = QApplication.clipboard()
cb.clear(mode=cb.Clipboard) cb.clear(mode=cb.Clipboard)
@ -832,12 +830,11 @@ class PlaylistTab(QTableWidget):
DEBUG(f"_cell_changed({row=}, {column=}, {new_text=}") DEBUG(f"_cell_changed({row=}, {column=}, {new_text=}")
with Session() as session: with Session() as session:
row_object: Union[Tracks, Notes] = self._get_row_object( if row in self._get_notes_rows():
row, session)
if row in self._meta_get_notes():
# Save change to database # Save change to database
DEBUG(f"Notes.update_note: saving new note text '{new_text=}'") 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 # Set/clear row start time accordingly
start_time = self._get_note_text_time(new_text) start_time = self._get_note_text_time(new_text)
if start_time: if start_time:
@ -854,10 +851,11 @@ class PlaylistTab(QTableWidget):
"start time" "start time"
) )
else: else:
track: Tracks = self._get_track_row_object(row, session)
if column == self.COL_ARTIST: 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: elif column == self.COL_TITLE:
row_object.update_title(session, title=new_text) track.update_title(session, title=new_text)
else: else:
ERROR("_cell_changed(): unrecognised column") ERROR("_cell_changed(): unrecognised column")
@ -896,7 +894,7 @@ class PlaylistTab(QTableWidget):
set(item.row() for item in self.selectedItems()) set(item.row() for item in self.selectedItems())
) )
rows_to_delete: List[int] = [] 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: int
row_object: Union[Tracks, Notes] row_object: Union[Tracks, Notes]
@ -916,9 +914,9 @@ class PlaylistTab(QTableWidget):
# delete in reverse row order so row numbers don't # delete in reverse row order so row numbers don't
# change # change
for row in sorted(rows_to_delete, reverse=True): for row in sorted(rows_to_delete, reverse=True):
row_object = self._get_row_object(row, session)
if row in note_rows: if row in note_rows:
row_object.delete_note(session) note: Notes = self._get_row_notes_object(row, session)
note.delete_note(session)
else: else:
self.remove_track(session, row) self.remove_track(session, row)
self.removeRow(row) self.removeRow(row)
@ -963,14 +961,21 @@ class PlaylistTab(QTableWidget):
except ValueError: except ValueError:
return None return None
def _get_row_object(self, row: int, session: Session) \ def _get_row_track_object(self, row: int, session: Session) \
-> Union[Tracks, Notes]: -> Optional[Tracks]:
"""Return content associated with this row""" """Return track associated with this row"""
obj = self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT) track_id = self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT)
if obj not in session: track = Tracks.get_by_id(session, track_id)
session.add(obj) return track
return obj
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]: def _get_row_start_time(self, row: int) -> Optional[datetime]:
try: try:
@ -990,12 +995,11 @@ class PlaylistTab(QTableWidget):
txt: str txt: str
with Session() as session: with Session() as session:
row_object: Union[Tracks, Notes] = self._get_row_object( if row in self._get_notes_rows():
row, session) note: Notes = self._get_row_notes_object(row, session)
if row in self._meta_get_notes(): txt = note.note
txt = row_object.note
else: else:
track = row_object track: Tracks = self._get_row_track_object(row, session)
txt = ( txt = (
f"Title: {track.title}\n" f"Title: {track.title}\n"
f"Artist: {track.artist}\n" f"Artist: {track.artist}\n"
@ -1038,13 +1042,14 @@ class PlaylistTab(QTableWidget):
# Add text of note from title column onwards # Add text of note from title column onwards
titleitem: QTableWidgetItem = QTableWidgetItem(note.note) titleitem: QTableWidgetItem = QTableWidgetItem(note.note)
self.setItem(row, self.COL_NOTE, titleitem) 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 # Attach note id to row
self._set_row_content(row, note) self._set_row_content(row, note.id)
# Mark row as a Note row # Mark row as a Note row
self._meta_set_note(row) self._set_note_row(row)
# Scroll to new row # Scroll to new row
self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter) self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter)
@ -1088,13 +1093,13 @@ class PlaylistTab(QTableWidget):
""" """
if starting_row is None: if starting_row is None:
current_row = self._meta_get_current() current_row = self._get_current_track_row()
if current_row is not None: if current_row is not None:
starting_row = current_row + 1 starting_row = current_row + 1
else: else:
starting_row = 0 starting_row = 0
notes_rows = self._meta_get_notes() notes_rows = self._get_notes_rows()
played_rows = self._meta_get_played() played_rows = self._get_played_track_rows()
for row in range(starting_row, self.rowCount()): for row in range(starting_row, self.rowCount()):
if row in notes_rows or row in played_rows: if row in notes_rows or row in played_rows:
continue continue
@ -1109,16 +1114,16 @@ class PlaylistTab(QTableWidget):
if row is None: if row is None:
raise ValueError(f"_meta_clear_attribute({row=}, {attribute=})") 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.item(row, self.COL_USERDATA).setData(
self.ROW_METADATA, new_metadata) 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. 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: if current_row is not None:
self._meta_clear_attribute(current_row, RowMeta.CURRENT) self._meta_clear_attribute(current_row, RowMeta.CURRENT)
# Reset row colour # Reset row colour
@ -1134,11 +1139,11 @@ class PlaylistTab(QTableWidget):
Clear next row if there is one. 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: if next_row is not None:
self._meta_clear_attribute(next_row, RowMeta.NEXT) 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""" """Clear played status on row"""
self._meta_clear_attribute(row, RowMeta.PLAYED) 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) 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 row marked as current, or None"""
return self._meta_search(RowMeta.CURRENT) 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 row marked as next, or None"""
return self._meta_search(RowMeta.NEXT) 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 rows marked as notes, or None"""
return self._meta_search(RowMeta.NOTE, one=False) 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 rows marked as played, or None"""
return self._meta_search(RowMeta.PLAYED, one=False) 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 rows marked as unreadable, or None"""
return self._meta_search(RowMeta.UNREADABLE, one=False) 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[ def _meta_search(self, metadata: int, one: bool = True) -> Union[
List[int], int, None]: List[int], int, None]:
""" """
@ -1187,7 +1212,7 @@ class PlaylistTab(QTableWidget):
matches = [] matches = []
for row in range(self.rowCount()): for row in range(self.rowCount()):
if self._meta_get(row): if self._meta_get(row):
if self._meta_get(row) & metadata: if self._meta_get(row) & (1 << metadata):
matches.append(row) matches.append(row)
if not one: if not one:
@ -1212,40 +1237,40 @@ class PlaylistTab(QTableWidget):
current_metadata: int = self._meta_get(row) current_metadata: int = self._meta_get(row)
if not current_metadata: if not current_metadata:
new_metadata = attribute new_metadata = (1 << attribute)
else: 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.item(row, self.COL_USERDATA).setData(
self.ROW_METADATA, new_metadata) 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""" """Mark this row as current track"""
self._meta_clear_current() self._clear_current_track_row()
self._meta_set_attribute(row, RowMeta.CURRENT) 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""" """Mark this row as next track"""
self._meta_clear_next() self._meta_clear_next()
self._meta_set_attribute(row, RowMeta.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""" """Mark this row as a note"""
self._meta_set_attribute(row, RowMeta.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""" """Mark this row as played"""
self._meta_set_attribute(row, RowMeta.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""" """Mark this row as unreadable"""
self._meta_set_attribute(row, RowMeta.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 Populate from the associated playlist object
@ -1261,16 +1286,11 @@ class PlaylistTab(QTableWidget):
row: int row: int
track: Tracks track: Tracks
# Make sure the database object is usable if playlist not in session:
insp = inspect(self.playlist) session.add(playlist)
if insp.detached:
session.add(self.playlist)
assert insp.persistent
for row, track in self.playlist.tracks.items(): for row, track in self.playlist.tracks.items():
data.append(([row], track)) data.append(([row], track))
# Add track to session to expose attributes
session.add(track)
for note in self.playlist.notes: for note in self.playlist.notes:
data.append(([note.row], note)) data.append(([note.row], note))
@ -1302,11 +1322,9 @@ class PlaylistTab(QTableWidget):
DEBUG(f"_rescan({row=})") DEBUG(f"_rescan({row=})")
if row in self._meta_get_notes():
return None
with Session() as session: with Session() as session:
track: Tracks = self._get_row_object(row, session) for row in self._get_track_rows():
track: Tracks = self._get_row_track_object(row, session)
if track: if track:
track.rescan(session) track.rescan(session)
self._update_row(session, row, track) self._update_row(session, row, track)
@ -1317,17 +1335,14 @@ class PlaylistTab(QTableWidget):
If multiple rows are selected, display sum of durations in status bar. If multiple rows are selected, display sum of durations in status bar.
""" """
row_set: Set[int] = set([item.row() for item in self.selectedItems()]) # Get the row number of all selected items and put into a set
note_row_set: Set[int] = set(self._meta_get_notes()) # to deduplicate
track_rows = list(row_set - note_row_set) sel_rows: Set[int] = set([item.row() for item in self.selectedItems()])
tracks: List[Tracks] notes_rows: Set[int] = set(self._get_notes_rows())
ms: int = 0
with Session() as session: # checked with Session() as session:
tracks = [self._get_row_object(row, session) for row in track_rows] for row in (sel_rows - notes_rows):
for track in tracks: ms += self._get_row_track_object(row, session).duration
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 # Only paint message if there are selected track rows
if ms > 0: if ms > 0:
@ -1342,7 +1357,7 @@ class PlaylistTab(QTableWidget):
with Session() as session: with Session() as session:
for column in range(self.columnCount()): for column in range(self.columnCount()):
name: str = f"playlist_col_{str(column)}_width" 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: if record and record.f_int is not None:
self.setColumnWidth(column, record.f_int) self.setColumnWidth(column, record.f_int)
else: else:
@ -1364,19 +1379,19 @@ class PlaylistTab(QTableWidget):
with Session() as session: with Session() as session:
# Check row is a track row # Check row is a track row
if row in self._meta_get_notes(): if row in self._get_notes_rows():
return None return None
track: Tracks = self._get_row_object(row, session) track: Tracks = self._get_row_track_object(row, session)
if not track: if not track:
return None return None
# Check track is readable # Check track is readable
if not self._file_is_readable(track.path): if not self._file_is_readable(track.path):
self._meta_set_unreadable(row) self._set_unreadable_row(row)
return None return None
# Mark as next track # Mark as next track
self._meta_set_next(row) self._set_next_track_row(row)
# Notify musicmuster # Notify musicmuster
self.parent.this_is_the_next_track(self, track) self.parent.this_is_the_next_track(self, track)
@ -1405,13 +1420,13 @@ class PlaylistTab(QTableWidget):
if self.item(row, j): if self.item(row, j):
self.item(row, j).setBackground(colour) self.item(row, j).setBackground(colour)
def _set_row_content(self, row: int, content: Union[Tracks, Notes]) \ def _set_row_content(self, row: int, object_id: int) -> None:
-> None:
"""Set content associated with this row""" """Set content associated with this row"""
assert self.item(row, self.COL_USERDATA) 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: def _set_row_end_time(self, row: int, time: Optional[datetime]) -> None:
"""Set passed row end time to passed time""" """Set passed row end time to passed time"""
@ -1446,10 +1461,10 @@ class PlaylistTab(QTableWidget):
# Need to allow multiple rows to be selected # Need to allow multiple rows to be selected
self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
notes_rows: List[int] = self._meta_get_notes() notes_rows: List[int] = self._get_notes_rows()
self.clearSelection() 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()): for row in range(self.rowCount()):
if row in notes_rows: if row in notes_rows:
continue continue

View File

@ -1,5 +1,6 @@
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFontMetrics, QPainter from PyQt5.QtGui import QFontMetrics, QPainter
from PyQt5.QtWidgets import QLabel
class ElideLabel(QLabel): class ElideLabel(QLabel):

View File

@ -6,12 +6,11 @@ import shutil
import tempfile import tempfile
from config import Config from config import Config
from helpers import show_warning
from log import DEBUG, INFO from log import DEBUG, INFO
from models import Notes, Playdates, Session, Tracks from models import Notes, Playdates, Session, Tracks
from mutagen.flac import FLAC from mutagen.flac import FLAC
from mutagen.mp3 import MP3 from mutagen.mp3 import MP3
from pydub import AudioSegment, effects from pydub import effects
# Globals (I know) # Globals (I know)
messages = [] messages = []

View File

@ -5,8 +5,8 @@ from models import Tracks
def test_fade_point(): def test_fade_point():
test_track_path = "../testdata/isa.mp3" test_track_path = "testdata/isa.mp3"
test_track_data = "../testdata/isa.py" test_track_data = "testdata/isa.py"
audio_segment = get_audio_segment(test_track_path) audio_segment = get_audio_segment(test_track_path)
assert audio_segment assert audio_segment
@ -23,8 +23,8 @@ def test_fade_point():
def test_get_tags(): def test_get_tags():
test_track_path = "../testdata/mom.mp3" test_track_path = "testdata/mom.mp3"
test_track_data = "../testdata/mom.py" test_track_data = "testdata/mom.py"
tags = get_tags(test_track_path) tags = get_tags(test_track_path)
@ -49,8 +49,8 @@ def test_get_relative_date():
def test_leading_silence(): def test_leading_silence():
test_track_path = "../testdata/isa.mp3" test_track_path = "testdata/isa.mp3"
test_track_data = "../testdata/isa.py" test_track_data = "testdata/isa.py"
audio_segment = get_audio_segment(test_track_path) audio_segment = get_audio_segment(test_track_path)
assert audio_segment assert audio_segment

View File

@ -172,7 +172,7 @@ def test_playlist_add_track(session):
row = 17 row = 17
playlist.add_track(session, track, row) playlist.add_track(session, track.id, row)
assert len(playlist.tracks) == 1 assert len(playlist.tracks) == 1
playlist_track = playlist.tracks[row] playlist_track = playlist.tracks[row]
@ -192,8 +192,8 @@ def test_playlist_tracks(session):
track2_row = 29 track2_row = 29
track2 = Tracks(session, track2_path) track2 = Tracks(session, track2_path)
playlist.add_track(session, track1, track1_row) playlist.add_track(session, track1.id, track1_row)
playlist.add_track(session, track2, track2_row) playlist.add_track(session, track2.id, track2_row)
tracks = playlist.tracks tracks = playlist.tracks
assert tracks[track1_row] == track1 assert tracks[track1_row] == track1
@ -269,7 +269,7 @@ def test_playlist_remove_tracks(session):
# Add all tracks to both playlists # Add all tracks to both playlists
for p in [playlist1, playlist2]: for p in [playlist1, playlist2]:
for t in [track1, track2, track3]: for t in [track1, track2, track3]:
p.add_track(session, t) p.add_track(session, t.id)
assert len(playlist1.tracks) == 3 assert len(playlist1.tracks) == 3
assert len(playlist2.tracks) == 3 assert len(playlist2.tracks) == 3
@ -295,9 +295,9 @@ def test_playlist_get_track_playlists(session):
track2 = Tracks(session, track2_path) track2 = Tracks(session, track2_path)
# Put track1 in both playlists, track2 only in playlist1 # Put track1 in both playlists, track2 only in playlist1
playlist1.add_track(session, track1) playlist1.add_track(session, track1.id)
playlist2.add_track(session, track1) playlist2.add_track(session, track1.id)
playlist1.add_track(session, track2) playlist1.add_track(session, track2.id)
playlists_track1 = track1.playlists playlists_track1 = track1.playlists
playlists_track2 = track2.playlists playlists_track2 = track2.playlists
@ -324,8 +324,8 @@ def test_playlisttracks_move_track(session):
track1 = Tracks(session, track1_path) track1 = Tracks(session, track1_path)
# Add both to playlist1 and check # Add both to playlist1 and check
playlist1.add_track(session, track1, track1_row) playlist1.add_track(session, track1.id, track1_row)
playlist1.add_track(session, track2, track2_row) playlist1.add_track(session, track2.id, track2_row)
tracks = playlist1.tracks tracks = playlist1.tracks
assert tracks[track1_row] == track1 assert tracks[track1_row] == track1

View File

@ -2,7 +2,6 @@ from PyQt5.QtCore import Qt
from app.playlists import Notes, PlaylistTab, Tracks from app.playlists import Notes, PlaylistTab, Tracks
from app.models import Playlists from app.models import Playlists
# from musicmuster import Window
from musicmuster import Window from musicmuster import Window
@ -60,11 +59,11 @@ def test_meta_all_clear(qtbot, session):
track3 = Tracks(session, track3_path) track3 = Tracks(session, track3_path)
playlist_tab.insert_track(session, track3) playlist_tab.insert_track(session, track3)
assert playlist_tab._meta_get_current() is None assert playlist_tab._get_current_track_row() is None
assert playlist_tab._meta_get_next() is None assert playlist_tab._get_next_track_row() is None
assert playlist_tab._meta_get_notes() == [] assert playlist_tab._get_notes_rows() == []
assert playlist_tab._meta_get_played() == [] assert playlist_tab._get_played_track_rows() == []
assert len(playlist_tab._meta_get_unreadable()) == 3 assert len(playlist_tab._get_unreadable_track_rows()) == 3
def test_meta(qtbot, session): def test_meta(qtbot, session):
@ -84,18 +83,18 @@ def test_meta(qtbot, session):
track3 = Tracks(session, track3_path) track3 = Tracks(session, track3_path)
playlist_tab.insert_track(session, track3) 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._get_played_track_rows() == []
assert playlist_tab._meta_get_current() is None assert playlist_tab._get_current_track_row() is None
assert playlist_tab._meta_get_next() is None assert playlist_tab._get_next_track_row() is None
assert playlist_tab._meta_get_notes() == [] assert playlist_tab._get_notes_rows() == []
playlist_tab._meta_set_played(0) playlist_tab._set_played_row(0)
assert playlist_tab._meta_get_played() == [0] assert playlist_tab._get_played_track_rows() == [0]
assert playlist_tab._meta_get_current() is None assert playlist_tab._get_current_track_row() is None
assert playlist_tab._meta_get_next() is None assert playlist_tab._get_next_track_row() is None
assert playlist_tab._meta_get_notes() == [] assert playlist_tab._get_notes_rows() == []
# Add a note # Add a note
note_text = "my note" note_text = "my note"
@ -103,40 +102,47 @@ def test_meta(qtbot, session):
note = Notes(session, playlist.id, note_row, note_text) note = Notes(session, playlist.id, note_row, note_text)
playlist_tab._insert_note(session, note) playlist_tab._insert_note(session, note)
assert playlist_tab._meta_get_played() == [0] assert playlist_tab._get_played_track_rows() == [0]
assert playlist_tab._meta_get_current() is None assert playlist_tab._get_current_track_row() is None
assert playlist_tab._meta_get_next() is None assert playlist_tab._get_next_track_row() is None
assert playlist_tab._meta_get_notes() == [3] assert playlist_tab._get_notes_rows() == [3]
playlist_tab._meta_set_next(1) playlist_tab._set_next_track_row(1)
assert playlist_tab._meta_get_played() == [0] assert playlist_tab._get_played_track_rows() == [0]
assert playlist_tab._meta_get_current() is None assert playlist_tab._get_current_track_row() is None
assert playlist_tab._meta_get_next() == 1 assert playlist_tab._get_next_track_row() == 1
assert playlist_tab._meta_get_notes() == [3] assert playlist_tab._get_notes_rows() == [3]
playlist_tab._meta_set_current(2) playlist_tab._set_current_track_row(2)
assert playlist_tab._meta_get_played() == [0] assert playlist_tab._get_played_track_rows() == [0]
assert playlist_tab._meta_get_current() == 2 assert playlist_tab._get_current_track_row() == 2
assert playlist_tab._meta_get_next() == 1 assert playlist_tab._get_next_track_row() == 1
assert playlist_tab._meta_get_notes() == [3] assert playlist_tab._get_notes_rows() == [3]
playlist_tab._meta_clear_played(0) playlist_tab._clear_played_row_status(0)
assert playlist_tab._meta_get_played() == [] assert playlist_tab._get_played_track_rows() == []
assert playlist_tab._meta_get_current() == 2 assert playlist_tab._get_current_track_row() == 2
assert playlist_tab._meta_get_next() == 1 assert playlist_tab._get_next_track_row() == 1
assert playlist_tab._meta_get_notes() == [3] assert playlist_tab._get_notes_rows() == [3]
playlist_tab._meta_clear_next() playlist_tab._meta_clear_next()
assert playlist_tab._meta_get_played() == [] assert playlist_tab._get_played_track_rows() == []
assert playlist_tab._meta_get_current() == 2 assert playlist_tab._get_current_track_row() == 2
assert playlist_tab._meta_get_next() is None assert playlist_tab._get_next_track_row() is None
assert playlist_tab._meta_get_notes() == [3] assert playlist_tab._get_notes_rows() == [3]
playlist_tab._meta_clear_current() playlist_tab._clear_current_track_row()
assert playlist_tab._meta_get_played() == [] assert playlist_tab._get_played_track_rows() == []
assert playlist_tab._meta_get_current() is None assert playlist_tab._get_current_track_row() is None
assert playlist_tab._meta_get_next() is None assert playlist_tab._get_next_track_row() is None
assert playlist_tab._meta_get_notes() == [3] 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): def test_clear_next(qtbot, session):
@ -152,11 +158,11 @@ def test_clear_next(qtbot, session):
track2 = Tracks(session, track2_path) track2 = Tracks(session, track2_path)
playlist_tab.insert_track(session, track2) playlist_tab.insert_track(session, track2)
playlist_tab._meta_set_next(1) playlist_tab._set_next_track_row(1)
assert playlist_tab._meta_get_next() == 1 assert playlist_tab._get_next_track_row() == 1
playlist_tab.clear_next() playlist_tab.clear_next(session)
assert playlist_tab._meta_get_next() is None assert playlist_tab._get_next_track_row() is None
def test_get_selected_row(qtbot, session): def test_get_selected_row(qtbot, session):
@ -174,10 +180,9 @@ def test_get_selected_row(qtbot, session):
playlist_tab.insert_track(session, track2) playlist_tab.insert_track(session, track2)
window = Window() window = Window()
window.show()
qtbot.addWidget(playlist_tab) qtbot.addWidget(playlist_tab)
qtbot.wait_for_window_shown(playlist_tab) with qtbot.waitExposed(window):
window.show()
row0_item0 = playlist_tab.item(0, 0) row0_item0 = playlist_tab.item(0, 0)
assert row0_item0 is not None assert row0_item0 is not None
rect = playlist_tab.visualItemRect(row0_item0) rect = playlist_tab.visualItemRect(row0_item0)