# Standard library imports # Allow forward reference to PlaylistModel from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from typing import Optional import datetime as dt # PyQt imports from PyQt6.QtCore import ( QAbstractTableModel, QModelIndex, Qt, QVariant, ) from PyQt6.QtGui import ( QColor, QFont, ) # Third party imports from sqlalchemy.orm.session import Session # import snoop # type: ignore # App imports from classes import ( ApplicationError, Filter, QueryCol, ) from config import Config from helpers import ( file_is_unreadable, get_relative_date, ms_to_mmss, show_warning, ) from log import log from models import db, Playdates, Tracks from music_manager import RowAndTrack @dataclass class QueryRow: artist: str bitrate: int duration: int lastplayed: Optional[dt.datetime] path: str title: str track_id: int class QuerylistModel(QAbstractTableModel): """ The Querylist Model Used to support query lists. The underlying database is never updated. We just present tracks that match a query and allow the user to copy those to a playlist. """ def __init__(self, session: Session, filter: Filter) -> None: """ Load query """ log.debug(f"QuerylistModel.__init__({filter=})") super().__init__() self.session = session self.filter = filter self.querylist_rows: dict[int, QueryRow] = {} self._selected_rows: set[int] = set() self.load_data() def __repr__(self) -> str: return f"" def background_role(self, row: int, column: int, qrow: QueryRow) -> QVariant: """Return background setting""" # Unreadable track file if file_is_unreadable(qrow.path): return QVariant(QColor(Config.COLOUR_UNREADABLE)) # Selected row if row in self._selected_rows: return QVariant(QColor(Config.COLOUR_QUERYLIST_SELECTED)) # Individual cell colouring if column == QueryCol.BITRATE.value: if not qrow.bitrate or qrow.bitrate < Config.BITRATE_LOW_THRESHOLD: return QVariant(QColor(Config.COLOUR_BITRATE_LOW)) elif qrow.bitrate < Config.BITRATE_OK_THRESHOLD: return QVariant(QColor(Config.COLOUR_BITRATE_MEDIUM)) else: return QVariant(QColor(Config.COLOUR_BITRATE_OK)) return QVariant() def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: """Standard function for view""" return len(QueryCol) def data( self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole ) -> QVariant: """Return data to view""" if not index.isValid() or not (0 <= index.row() < len(self.querylist_rows)): return QVariant() row = index.row() column = index.column() # rat for playlist row data as it's used a lot qrow = self.querylist_rows[row] # Dispatch to role-specific functions dispatch_table: dict[int, Callable] = { int(Qt.ItemDataRole.BackgroundRole): self.background_role, int(Qt.ItemDataRole.DisplayRole): self.display_role, int(Qt.ItemDataRole.ToolTipRole): self.tooltip_role, } if role in dispatch_table: return QVariant(dispatch_table[role](row, column, qrow)) # Document other roles but don't use them if role in [ Qt.ItemDataRole.DecorationRole, Qt.ItemDataRole.EditRole, Qt.ItemDataRole.FontRole, Qt.ItemDataRole.ForegroundRole, Qt.ItemDataRole.InitialSortOrderRole, Qt.ItemDataRole.SizeHintRole, Qt.ItemDataRole.StatusTipRole, Qt.ItemDataRole.TextAlignmentRole, Qt.ItemDataRole.ToolTipRole, Qt.ItemDataRole.WhatsThisRole, ]: return QVariant() # Fall through to no-op return QVariant() def display_role(self, row: int, column: int, qrow: QueryRow) -> QVariant: """ Return text for display """ dispatch_table = { QueryCol.ARTIST.value: QVariant(qrow.artist), QueryCol.BITRATE.value: QVariant(qrow.bitrate), QueryCol.DURATION.value: QVariant(ms_to_mmss(qrow.duration)), QueryCol.LAST_PLAYED.value: QVariant(get_relative_date(qrow.lastplayed)), QueryCol.TITLE.value: QVariant(qrow.title), } if column in dispatch_table: return dispatch_table[column] return QVariant() def flags(self, index: QModelIndex) -> Qt.ItemFlag: """ Standard model flags """ if not index.isValid(): return Qt.ItemFlag.NoItemFlags return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable def get_selected_track_ids(self) -> list[int]: """ Return a list of track_ids from selected tracks """ return [self.querylist_rows[row].track_id for row in self._selected_rows] def headerData( self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole, ) -> QVariant: """ Return text for headers """ display_dispatch_table = { QueryCol.TITLE.value: QVariant(Config.HEADER_TITLE), QueryCol.ARTIST.value: QVariant(Config.HEADER_ARTIST), QueryCol.DURATION.value: QVariant(Config.HEADER_DURATION), QueryCol.LAST_PLAYED.value: QVariant(Config.HEADER_LAST_PLAYED), QueryCol.BITRATE.value: QVariant(Config.HEADER_BITRATE), } if role == Qt.ItemDataRole.DisplayRole: if orientation == Qt.Orientation.Horizontal: return display_dispatch_table[section] else: if Config.ROWS_FROM_ZERO: return QVariant(str(section)) else: return QVariant(str(section + 1)) elif role == Qt.ItemDataRole.FontRole: boldfont = QFont() boldfont.setBold(True) return QVariant(boldfont) return QVariant() def load_data(self) -> None: """ Populate self.querylist_rows """ # Clear any exsiting rows self.querylist_rows = {} row = 0 try: results = Tracks.get_filtered_tracks(self.session, self.filter) for result in results: if hasattr(result, "lastplayed"): lastplayed = result["lastplayed"] else: lastplayed = None queryrow = QueryRow( artist=result.artist, bitrate=result.bitrate or 0, duration=result.duration, lastplayed=lastplayed, path=result.path, title=result.title, track_id=result.id, ) self.querylist_rows[row] = queryrow row += 1 except ApplicationError as e: show_warning(None, "Query error", f"Error loading query data ({e})") def rowCount(self, index: QModelIndex = QModelIndex()) -> int: """Standard function for view""" return len(self.querylist_rows) def toggle_row_selection(self, row: int) -> None: if row in self._selected_rows: self._selected_rows.discard(row) else: self._selected_rows.add(row) # Emit dataChanged for the entire row top_left = self.index(row, 0) bottom_right = self.index(row, self.columnCount() - 1) self.dataChanged.emit(top_left, bottom_right, [Qt.ItemDataRole.BackgroundRole]) def tooltip_role(self, row: int, column: int, rat: RowAndTrack) -> QVariant: """ Return tooltip. Currently only used for last_played column. """ if column != QueryCol.LAST_PLAYED.value: return QVariant() with db.Session() as session: track_id = self.querylist_rows[row].track_id if not track_id: return QVariant() playdates = Playdates.last_playdates(session, track_id) return QVariant( "
".join( [ a.lastplayed.strftime(Config.LAST_PLAYED_TOOLTIP_DATE_FORMAT) for a in reversed(playdates) ] ) )