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

@ -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:

View File

@ -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

View File

@ -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()})

View File

@ -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

View File

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

View File

@ -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 = []

View File

@ -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"
assert ms_to_mmss((5 * 60 * 1000) + 23000) == "5:23"

View File

@ -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

View File

@ -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)