from datetime import datetime from enum import auto, Enum from typing import Optional, TYPE_CHECKING from PyQt6.QtCore import ( QAbstractTableModel, QModelIndex, Qt, QVariant, ) from PyQt6.QtGui import ( QBrush, QColor, QFont, ) from config import Config from dbconfig import Session from helpers import ( file_is_unreadable, ) from models import PlaylistRows, Tracks if TYPE_CHECKING: from musicmuster import MusicMusterSignals class Col(Enum): START_GAP = 0 TITLE = auto() ARTIST = auto() DURATION = auto() START_TIME = auto() END_TIME = auto() LAST_PLAYED = auto() BITRATE = auto() NOTE = auto() HEADER_NOTES_COLUMN = 1 class PlaylistRowData: def __init__(self, plr: PlaylistRows) -> None: """ Populate PlaylistRowData from database PlaylistRows record """ self.start_gap: Optional[int] = None self.title: str = "" self.artist: str = "" self.duration: int = 0 self.lastplayed: datetime = Config.EPOCH self.bitrate = 0 self.path = "" self.plrid: int = plr.id self.plr_rownum: int = plr.plr_rownum self.note: str = plr.note if plr.track: self.start_gap = plr.track.start_gap self.title = plr.track.title self.artist = plr.track.artist self.duration = plr.track.duration if plr.track.playdates: self.lastplayed = max([a.lastplayed for a in plr.track.playdates]) else: self.lastplayed = Config.EPOCH self.bitrate = plr.track.bitrate or 0 self.path = plr.track.path def __repr__(self) -> str: return ( f"" ) class PlaylistModel(QAbstractTableModel): def __init__( self, playlist_id: int, signals: "MusicMusterSignals", *args, **kwargs ): self.playlist_id = playlist_id self.signals = signals super().__init__(*args, **kwargs) self.playlist_rows: dict[int, PlaylistRowData] = {} self.refresh_data() def __repr__(self) -> str: return f"" def background_role(self, row: int, column: int, prd: PlaylistRowData) -> QBrush: """Return background setting""" # Handle entire row colouring # Header row if not prd.path: return QBrush(QColor(Config.COLOUR_NOTES_PLAYLIST)) # Unreadable track file if file_is_unreadable(prd.path): return QBrush(QColor(Config.COLOUR_UNREADABLE)) if column == Col.START_GAP.value: if prd.start_gap and prd.start_gap >= Config.START_GAP_WARNING_THRESHOLD: return QBrush(QColor(Config.COLOUR_LONG_START)) if column == Col.BITRATE.value: if prd.bitrate < Config.BITRATE_LOW_THRESHOLD: return QBrush(QColor(Config.COLOUR_BITRATE_LOW)) elif prd.bitrate and prd.bitrate < Config.BITRATE_OK_THRESHOLD: return QBrush(QColor(Config.COLOUR_BITRATE_MEDIUM)) else: return QBrush(QColor(Config.COLOUR_BITRATE_OK)) return QBrush() def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: """Standard function for view""" return 9 def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole): """Return data to view""" if not index.isValid() or not (0 <= index.row() < len(self.playlist_rows)): return QVariant() row = index.row() column = index.column() # prd for playlist row data as it's used a lot prd = self.playlist_rows[row] # Dispatch to role-specific functions if role == Qt.ItemDataRole.DisplayRole: return self.display_role(row, column, prd) elif role == Qt.ItemDataRole.DecorationRole: pass elif role == Qt.ItemDataRole.EditRole: return self.edit_role(row, column, prd) elif role == Qt.ItemDataRole.ToolTipRole: pass elif role == Qt.ItemDataRole.StatusTipRole: pass elif role == Qt.ItemDataRole.WhatsThisRole: pass elif role == Qt.ItemDataRole.SizeHintRole: pass elif role == Qt.ItemDataRole.FontRole: pass elif role == Qt.ItemDataRole.TextAlignmentRole: pass elif role == Qt.ItemDataRole.BackgroundRole: return self.background_role(row, column, prd) elif role == Qt.ItemDataRole.ForegroundRole: pass elif role == Qt.ItemDataRole.CheckStateRole: pass elif role == Qt.ItemDataRole.InitialSortOrderRole: pass # Fall through to no-op return QVariant() def edit_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: """ Return text for editing """ if column == Col.TITLE.value: return QVariant(prd.title) if column == Col.ARTIST.value: return QVariant(prd.artist) if column == Col.NOTE.value: return QVariant(prd.note) return QVariant() def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: """ Return text for display """ # Detect whether this is a header row if not prd.path: header = prd.note else: header = "" if header: if column == HEADER_NOTES_COLUMN: self.signals.span_cells_signal.emit( row, HEADER_NOTES_COLUMN, 1, self.columnCount() - 1 ) return QVariant(prd.note) else: return QVariant() if column == Col.START_GAP.value: return QVariant(prd.start_gap) if column == Col.TITLE.value: return QVariant(prd.title) if column == Col.ARTIST.value: return QVariant(prd.artist) if column == Col.DURATION.value: return QVariant(prd.duration) if column == Col.START_TIME.value: return QVariant("FIXME") if column == Col.END_TIME.value: return QVariant("FIXME") if column == Col.LAST_PLAYED.value: return QVariant(prd.lastplayed) if column == Col.BITRATE.value: return QVariant(prd.bitrate) if column == Col.NOTE.value: return QVariant(prd.note) return QVariant() def edit_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: """ Return text for editing """ if column == Col.TITLE.value: return QVariant(prd.title) if column == Col.ARTIST.value: return QVariant(prd.artist) if column == Col.NOTE.value: return QVariant(prd.note) return QVariant() def flags(self, index: QModelIndex) -> Qt.ItemFlag: """ Standard model flags """ if not index.isValid(): return Qt.ItemFlag.ItemIsEnabled default = Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable if index.column() in [Col.TITLE.value, Col.ARTIST.value, Col.NOTE.value]: return default | Qt.ItemFlag.ItemIsEditable return default def headerData( self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole, ) -> QVariant: """ Return text for headers """ if role == Qt.ItemDataRole.DisplayRole: if orientation == Qt.Orientation.Horizontal: if section == Col.START_GAP.value: return QVariant(Config.HEADER_START_GAP) elif section == Col.TITLE.value: return QVariant(Config.HEADER_TITLE) elif section == Col.ARTIST.value: return QVariant(Config.HEADER_ARTIST) elif section == Col.DURATION.value: return QVariant(Config.HEADER_DURATION) elif section == Col.START_TIME.value: return QVariant(Config.HEADER_START_TIME) elif section == Col.END_TIME.value: return QVariant(Config.HEADER_END_TIME) elif section == Col.LAST_PLAYED.value: return QVariant(Config.HEADER_LAST_PLAYED) elif section == Col.BITRATE.value: return QVariant(Config.HEADER_BITRATE) elif section == Col.NOTE.value: return QVariant(Config.HEADER_NOTE) else: return QVariant(str(section + 1)) elif role == Qt.ItemDataRole.FontRole: boldfont = QFont() boldfont.setBold(True) return QVariant(boldfont) return QVariant() def refresh_data(self): """Populate dicts for data calls""" # Populate self.playlist_rows with playlist data with Session() as session: for p in PlaylistRows.deep_rows(session, self.playlist_id): self.playlist_rows[p.plr_rownum] = PlaylistRowData(p) def refresh_row(self, session, row_number): """Populate dict for one row for data calls""" p = PlaylistRows.deep_row(session, self.playlist_id, row_number) self.playlist_rows[p.plr_rownum] = PlaylistRowData(p) def rowCount(self, index: QModelIndex = QModelIndex()) -> int: """Standard function for view""" return len(self.playlist_rows) def setData( self, index: QModelIndex, value: QVariant, role: int = Qt.ItemDataRole.EditRole ) -> bool: """ Update model with edited data """ if index.isValid() and role == Qt.ItemDataRole.EditRole: row_number = index.row() column = index.column() with Session() as session: plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid) if not plr: print( f"Error saving data: {row_number=}, {column=}, " f"{value=}, {self.playlist_id=}" ) return False if column == Col.TITLE.value or column == Col.ARTIST.value: track = session.get(Tracks, plr.track_id) if not track: print(f"Error retreiving track: {plr=}") return False if column == Col.TITLE.value: track.title = str(value) elif column == Col.ARTIST.value: track.artist = str(value) else: print(f"Error updating track: {column=}, {value=}") return False elif column == Col.NOTE.value: plr.note = str(value) # Flush changes before refreshing data session.flush() self.refresh_row(session, row_number) self.dataChanged.emit(index, index, [Qt.ItemDataRole.DisplayRole, role]) return True return False