WIP Issue 285
This commit is contained in:
parent
3b004567df
commit
2f8afeb814
@ -11,7 +11,6 @@ import re
|
||||
from PyQt6.QtCore import (
|
||||
QAbstractTableModel,
|
||||
QModelIndex,
|
||||
QObject,
|
||||
QRegularExpression,
|
||||
QSortFilterProxyModel,
|
||||
Qt,
|
||||
@ -25,7 +24,6 @@ from PyQt6.QtGui import (
|
||||
)
|
||||
|
||||
# Third party imports
|
||||
import line_profiler
|
||||
from sqlalchemy.orm.session import Session
|
||||
import obswebsocket # type: ignore
|
||||
|
||||
@ -72,18 +70,14 @@ class PlaylistModel(QAbstractTableModel):
|
||||
database.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
playlist_id: int,
|
||||
is_template: bool,
|
||||
*args: Optional[QObject],
|
||||
**kwargs: Optional[QObject],
|
||||
) -> None:
|
||||
def __init__(self, playlist_id: int, is_template: bool,) -> None:
|
||||
|
||||
super().__init__()
|
||||
|
||||
log.debug("PlaylistModel.__init__()")
|
||||
|
||||
self.playlist_id = playlist_id
|
||||
self.is_template = is_template
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.playlist_rows: dict[int, RowAndTrack] = {}
|
||||
self.signals = MusicMusterSignals()
|
||||
@ -101,13 +95,17 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<PlaylistModel: playlist_id={self.playlist_id}, {self.rowCount()} rows>"
|
||||
f"<PlaylistModel: playlist_id={self.playlist_id}, "
|
||||
f"is_template={self.is_template}, "
|
||||
f"{self.rowCount()} rows>"
|
||||
)
|
||||
|
||||
def active_section_header(self) -> int:
|
||||
"""
|
||||
Return the row number of the first header that has either unplayed tracks
|
||||
or currently being played track below it.
|
||||
Return the row number of the first header that has any of the following below it:
|
||||
- unplayed tracks
|
||||
- the currently being played track
|
||||
- the track marked as next to play
|
||||
"""
|
||||
|
||||
header_row = 0
|
||||
@ -119,23 +117,20 @@ class PlaylistModel(QAbstractTableModel):
|
||||
if not self.is_played_row(row_number):
|
||||
break
|
||||
|
||||
# If track is played, we need to check it's not the current
|
||||
# next or previous track because we don't want to scroll them
|
||||
# out of view
|
||||
# Here means that row_number points to a played track. The
|
||||
# current track will be marked as played when we start
|
||||
# playing it. It's also possible that the track marked as
|
||||
# next has already been played. Check for either of those.
|
||||
|
||||
for ts in [
|
||||
track_sequence.next,
|
||||
track_sequence.current,
|
||||
]:
|
||||
for ts in [track_sequence.next, track_sequence.current]:
|
||||
if (
|
||||
ts
|
||||
and ts.row_number == row_number
|
||||
and ts.playlist_id == self.playlist_id
|
||||
):
|
||||
break
|
||||
else:
|
||||
continue # continue iterating over playlist_rows
|
||||
break # current row is in one of the track sequences
|
||||
# We've found the current or next track, so return
|
||||
# the last-found header row
|
||||
return header_row
|
||||
|
||||
return header_row
|
||||
|
||||
@ -152,31 +147,34 @@ class PlaylistModel(QAbstractTableModel):
|
||||
try:
|
||||
rat = self.playlist_rows[row_number]
|
||||
except KeyError:
|
||||
log.error(
|
||||
raise ApplicationError(
|
||||
f"{self}: KeyError in add_track_to_header ({row_number=}, {track_id=})"
|
||||
)
|
||||
return
|
||||
if rat.path:
|
||||
log.error(
|
||||
raise ApplicationError(
|
||||
f"{self}: Header row already has track associated ({rat=}, {track_id=})"
|
||||
)
|
||||
return
|
||||
with db.Session() as session:
|
||||
playlistrow = session.get(PlaylistRows, rat.playlistrow_id)
|
||||
if playlistrow:
|
||||
if not playlistrow:
|
||||
raise ApplicationError(
|
||||
f"{self}: Failed to retrieve playlist row ({rat.playlistrow_id=}"
|
||||
)
|
||||
# Add track to PlaylistRows
|
||||
playlistrow.track_id = track_id
|
||||
# Add any further note (header will already have a note)
|
||||
if note:
|
||||
playlistrow.note += "\n" + note
|
||||
playlistrow.note += " " + note
|
||||
session.commit()
|
||||
|
||||
# Update local copy
|
||||
self.refresh_row(session, row_number)
|
||||
# Repaint row
|
||||
self.invalidate_row(row_number)
|
||||
session.commit()
|
||||
|
||||
self.signals.resize_rows_signal.emit(self.playlist_id)
|
||||
|
||||
# @line_profiler.profile
|
||||
def background_role(self, row: int, column: int, rat: RowAndTrack) -> QBrush:
|
||||
"""Return background setting"""
|
||||
|
||||
@ -257,26 +255,28 @@ class PlaylistModel(QAbstractTableModel):
|
||||
- update track times
|
||||
"""
|
||||
|
||||
log.debug(f"{self}: current_track_started()")
|
||||
|
||||
if not track_sequence.current:
|
||||
return
|
||||
|
||||
row_number = track_sequence.current.row_number
|
||||
|
||||
# Check for OBS scene change
|
||||
log.debug(f"{self}: Call OBS scene change")
|
||||
self.obs_scene_change(row_number)
|
||||
|
||||
# Sanity check that we have a track_id
|
||||
if not track_sequence.current.track_id:
|
||||
log.error(
|
||||
f"{self}: current_track_started() called with {track_sequence.current.track_id=}"
|
||||
track_id = track_sequence.current.track_id
|
||||
if not track_id:
|
||||
raise ApplicationError(
|
||||
f"{self}: current_track_started() called with {track_id=}"
|
||||
)
|
||||
return
|
||||
|
||||
with db.Session() as session:
|
||||
# Update Playdates in database
|
||||
log.debug(f"{self}: update playdates")
|
||||
Playdates(session, track_sequence.current.track_id)
|
||||
log.debug(f"{self}: update playdates {track_id=}")
|
||||
Playdates(session, track_id)
|
||||
session.commit()
|
||||
|
||||
# Mark track as played in playlist
|
||||
log.debug(f"{self}: Mark track as played")
|
||||
@ -315,36 +315,16 @@ class PlaylistModel(QAbstractTableModel):
|
||||
if next_row is not None:
|
||||
self.set_next_row(next_row)
|
||||
|
||||
session.commit()
|
||||
|
||||
# @line_profiler.profile
|
||||
def data(
|
||||
self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole
|
||||
) -> QVariant:
|
||||
) -> QVariant | QFont | QBrush | str:
|
||||
"""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()
|
||||
# rat for playlist row data as it's used a lot
|
||||
rat = self.playlist_rows[row]
|
||||
|
||||
# Dispatch to role-specific functions
|
||||
dispatch_table = {
|
||||
int(Qt.ItemDataRole.BackgroundRole): self.background_role,
|
||||
int(Qt.ItemDataRole.DisplayRole): self.display_role,
|
||||
int(Qt.ItemDataRole.EditRole): self.edit_role,
|
||||
int(Qt.ItemDataRole.FontRole): self.font_role,
|
||||
int(Qt.ItemDataRole.ForegroundRole): self.foreground_role,
|
||||
int(Qt.ItemDataRole.ToolTipRole): self.tooltip_role,
|
||||
}
|
||||
|
||||
if role in dispatch_table:
|
||||
return QVariant(dispatch_table[role](row, column, rat))
|
||||
|
||||
# Document other roles but don't use them
|
||||
if role in [
|
||||
if (
|
||||
not index.isValid()
|
||||
or not (0 <= index.row() < len(self.playlist_rows))
|
||||
or role in [
|
||||
Qt.ItemDataRole.DecorationRole,
|
||||
Qt.ItemDataRole.StatusTipRole,
|
||||
Qt.ItemDataRole.WhatsThisRole,
|
||||
@ -352,10 +332,30 @@ class PlaylistModel(QAbstractTableModel):
|
||||
Qt.ItemDataRole.TextAlignmentRole,
|
||||
Qt.ItemDataRole.CheckStateRole,
|
||||
Qt.ItemDataRole.InitialSortOrderRole,
|
||||
]:
|
||||
]
|
||||
):
|
||||
return QVariant()
|
||||
|
||||
# Fall through to no-op
|
||||
row = index.row()
|
||||
column = index.column()
|
||||
# rat for playlist row data as it's used a lot
|
||||
rat = self.playlist_rows[row]
|
||||
|
||||
# These are ordered in approximately the frequency with which
|
||||
# they are called
|
||||
if role == Qt.ItemDataRole.BackgroundRole:
|
||||
return self.background_role(row, column, rat)
|
||||
elif role == Qt.ItemDataRole.DisplayRole:
|
||||
return self.display_role(row, column, rat)
|
||||
elif role == Qt.ItemDataRole.EditRole:
|
||||
return self.edit_role(row, column, rat)
|
||||
elif role == Qt.ItemDataRole.FontRole:
|
||||
return self.font_role(row, column, rat)
|
||||
elif role == Qt.ItemDataRole.ForegroundRole:
|
||||
return self.foreground_role(row, column, rat)
|
||||
elif role == Qt.ItemDataRole.ToolTipRole:
|
||||
return self.tooltip_role(row, column, rat)
|
||||
|
||||
return QVariant()
|
||||
|
||||
def delete_rows(self, row_numbers: list[int]) -> None:
|
||||
@ -385,8 +385,10 @@ class PlaylistModel(QAbstractTableModel):
|
||||
super().endRemoveRows()
|
||||
|
||||
self.reset_track_sequence_row_numbers()
|
||||
self.update_track_times()
|
||||
|
||||
def display_role(self, row: int, column: int, rat: RowAndTrack) -> QVariant:
|
||||
# @line_profiler.profile
|
||||
def display_role(self, row: int, column: int, rat: RowAndTrack) -> str:
|
||||
"""
|
||||
Return text for display
|
||||
"""
|
||||
@ -406,45 +408,45 @@ class PlaylistModel(QAbstractTableModel):
|
||||
if column == HEADER_NOTES_COLUMN:
|
||||
header_text = self.header_text(rat)
|
||||
if not header_text:
|
||||
return QVariant(Config.TEXT_NO_TRACK_NO_NOTE)
|
||||
return Config.SECTION_HEADER
|
||||
else:
|
||||
formatted_header = self.header_text(rat)
|
||||
trimmed_header = self.remove_section_timer_markers(formatted_header)
|
||||
return QVariant(trimmed_header)
|
||||
return trimmed_header
|
||||
else:
|
||||
return QVariant("")
|
||||
return ""
|
||||
|
||||
if column == Col.START_TIME.value:
|
||||
start_time = rat.forecast_start_time
|
||||
if start_time:
|
||||
return QVariant(start_time.strftime(Config.TRACK_TIME_FORMAT))
|
||||
return QVariant()
|
||||
return start_time.strftime(Config.TRACK_TIME_FORMAT)
|
||||
return ""
|
||||
|
||||
if column == Col.END_TIME.value:
|
||||
end_time = rat.forecast_end_time
|
||||
if end_time:
|
||||
return QVariant(end_time.strftime(Config.TRACK_TIME_FORMAT))
|
||||
return QVariant()
|
||||
return end_time.strftime(Config.TRACK_TIME_FORMAT)
|
||||
return ""
|
||||
|
||||
if column == Col.INTRO.value:
|
||||
if rat.intro:
|
||||
return QVariant(f"{rat.intro / 1000:{Config.INTRO_SECONDS_FORMAT}}")
|
||||
return f"{rat.intro / 1000:{Config.INTRO_SECONDS_FORMAT}}"
|
||||
else:
|
||||
return QVariant("")
|
||||
return ""
|
||||
|
||||
dispatch_table = {
|
||||
Col.ARTIST.value: QVariant(rat.artist),
|
||||
Col.BITRATE.value: QVariant(rat.bitrate),
|
||||
Col.DURATION.value: QVariant(ms_to_mmss(rat.duration)),
|
||||
Col.LAST_PLAYED.value: QVariant(get_relative_date(rat.lastplayed)),
|
||||
Col.NOTE.value: QVariant(rat.note),
|
||||
Col.START_GAP.value: QVariant(rat.start_gap),
|
||||
Col.TITLE.value: QVariant(rat.title),
|
||||
dispatch_table: dict[int, str] = {
|
||||
Col.ARTIST.value: rat.artist,
|
||||
Col.BITRATE.value: str(rat.bitrate),
|
||||
Col.DURATION.value: ms_to_mmss(rat.duration),
|
||||
Col.LAST_PLAYED.value: get_relative_date(rat.lastplayed),
|
||||
Col.NOTE.value: rat.note,
|
||||
Col.START_GAP.value: str(rat.start_gap),
|
||||
Col.TITLE.value: rat.title,
|
||||
}
|
||||
if column in dispatch_table:
|
||||
return dispatch_table[column]
|
||||
|
||||
return QVariant()
|
||||
return ""
|
||||
|
||||
def end_reset_model(self, playlist_id: int) -> None:
|
||||
"""
|
||||
@ -461,7 +463,8 @@ class PlaylistModel(QAbstractTableModel):
|
||||
super().endResetModel()
|
||||
self.reset_track_sequence_row_numbers()
|
||||
|
||||
def edit_role(self, row: int, column: int, rat: RowAndTrack) -> QVariant:
|
||||
# @line_profiler.profile
|
||||
def edit_role(self, row: int, column: int, rat: RowAndTrack) -> str:
|
||||
"""
|
||||
Return text for editing
|
||||
"""
|
||||
@ -469,19 +472,20 @@ class PlaylistModel(QAbstractTableModel):
|
||||
# If this is a header row and we're being asked for the
|
||||
# HEADER_NOTES_COLUMN, return the note value
|
||||
if self.is_header_row(row) and column == HEADER_NOTES_COLUMN:
|
||||
return QVariant(rat.note)
|
||||
return rat.note
|
||||
|
||||
if column == Col.INTRO.value:
|
||||
return QVariant(rat.intro)
|
||||
return str(rat.intro or "")
|
||||
if column == Col.TITLE.value:
|
||||
return QVariant(rat.title)
|
||||
return rat.title
|
||||
if column == Col.ARTIST.value:
|
||||
return QVariant(rat.artist)
|
||||
return rat.artist
|
||||
if column == Col.NOTE.value:
|
||||
return QVariant(rat.note)
|
||||
return rat.note
|
||||
|
||||
return QVariant()
|
||||
return ""
|
||||
|
||||
# @line_profiler.profile
|
||||
def foreground_role(self, row: int, column: int, rat: RowAndTrack) -> QBrush:
|
||||
"""Return header foreground colour or QBrush() if none"""
|
||||
|
||||
@ -518,19 +522,20 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
return default
|
||||
|
||||
def font_role(self, row: int, column: int, rat: RowAndTrack) -> QVariant:
|
||||
# @line_profiler.profile
|
||||
def font_role(self, row: int, column: int, rat: RowAndTrack) -> QFont:
|
||||
"""
|
||||
Return font
|
||||
"""
|
||||
|
||||
# Notes column is never bold
|
||||
if column == Col.NOTE.value:
|
||||
return QVariant()
|
||||
return QFont()
|
||||
|
||||
boldfont = QFont()
|
||||
boldfont.setBold(not self.playlist_rows[row].played)
|
||||
|
||||
return QVariant(boldfont)
|
||||
return boldfont
|
||||
|
||||
def get_duplicate_rows(self) -> list[int]:
|
||||
"""
|
||||
@ -729,7 +734,8 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.reset_track_sequence_row_numbers()
|
||||
self.invalidate_rows(list(range(new_row_number, len(self.playlist_rows))))
|
||||
|
||||
@line_profiler.profile
|
||||
# Keep this decorator for now
|
||||
# @line_profiler.profile
|
||||
def invalidate_row(self, modified_row: int) -> None:
|
||||
"""
|
||||
Signal to view to refresh invalidated row
|
||||
@ -742,7 +748,8 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.index(modified_row, self.columnCount() - 1),
|
||||
)
|
||||
|
||||
@line_profiler.profile
|
||||
# Keep this decorator for now
|
||||
# @line_profiler.profile
|
||||
def invalidate_rows(self, modified_rows: list[int]) -> None:
|
||||
"""
|
||||
Signal to view to refresh invlidated rows
|
||||
@ -1558,19 +1565,20 @@ class PlaylistModel(QAbstractTableModel):
|
||||
def supportedDropActions(self) -> Qt.DropAction:
|
||||
return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction
|
||||
|
||||
def tooltip_role(self, row: int, column: int, rat: RowAndTrack) -> QVariant:
|
||||
# @line_profiler.profile
|
||||
def tooltip_role(self, row: int, column: int, rat: RowAndTrack) -> str:
|
||||
"""
|
||||
Return tooltip. Currently only used for last_played column.
|
||||
"""
|
||||
|
||||
if column != Col.LAST_PLAYED.value:
|
||||
return QVariant()
|
||||
return ""
|
||||
with db.Session() as session:
|
||||
track_id = self.playlist_rows[row].track_id
|
||||
if not track_id:
|
||||
return QVariant()
|
||||
return ""
|
||||
playdates = Playdates.last_playdates(session, track_id)
|
||||
return QVariant(
|
||||
return (
|
||||
"<br>".join(
|
||||
[
|
||||
a.lastplayed.strftime(Config.LAST_PLAYED_TOOLTIP_DATE_FORMAT)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user