Merge branch 'issue285' into dev
This commit is contained in:
commit
5f9fd31dfd
@ -123,11 +123,11 @@ class Config(object):
|
|||||||
ROWS_FROM_ZERO = True
|
ROWS_FROM_ZERO = True
|
||||||
SCROLL_TOP_MARGIN = 3
|
SCROLL_TOP_MARGIN = 3
|
||||||
SECTION_ENDINGS = ("-", "+-", "-+")
|
SECTION_ENDINGS = ("-", "+-", "-+")
|
||||||
|
SECTION_HEADER = "[Section header]"
|
||||||
SECTION_STARTS = ("+", "+-", "-+")
|
SECTION_STARTS = ("+", "+-", "-+")
|
||||||
SONGFACTS_ON_NEXT = False
|
SONGFACTS_ON_NEXT = False
|
||||||
START_GAP_WARNING_THRESHOLD = 300
|
START_GAP_WARNING_THRESHOLD = 300
|
||||||
SUBTOTAL_ON_ROW_ZERO = "[No subtotal on first row]"
|
SUBTOTAL_ON_ROW_ZERO = "[No subtotal on first row]"
|
||||||
TEXT_NO_TRACK_NO_NOTE = "[Section header]"
|
|
||||||
TOD_TIME_FORMAT = "%H:%M:%S"
|
TOD_TIME_FORMAT = "%H:%M:%S"
|
||||||
TRACK_TIME_FORMAT = "%H:%M:%S"
|
TRACK_TIME_FORMAT = "%H:%M:%S"
|
||||||
VLC_MAIN_PLAYER_NAME = "MusicMuster Main Player"
|
VLC_MAIN_PLAYER_NAME = "MusicMuster Main Player"
|
||||||
|
|||||||
@ -53,7 +53,7 @@ class NoteColoursTable(Model):
|
|||||||
__tablename__ = "notecolours"
|
__tablename__ = "notecolours"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
substring: Mapped[str] = mapped_column(String(256), index=True)
|
substring: Mapped[str] = mapped_column(String(256), index=True, unique=True)
|
||||||
colour: Mapped[str] = mapped_column(String(21), index=False)
|
colour: Mapped[str] = mapped_column(String(21), index=False)
|
||||||
enabled: Mapped[bool] = mapped_column(default=True, index=True)
|
enabled: Mapped[bool] = mapped_column(default=True, index=True)
|
||||||
foreground: Mapped[Optional[str]] = mapped_column(String(21), index=False)
|
foreground: Mapped[Optional[str]] = mapped_column(String(21), index=False)
|
||||||
|
|||||||
36
app/log.py
36
app/log.py
@ -6,16 +6,18 @@ import logging.config
|
|||||||
import logging.handlers
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from traceback import print_exception
|
import traceback
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
# PyQt imports
|
# PyQt imports
|
||||||
|
from PyQt6.QtWidgets import QApplication, QMessageBox
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
import stackprinter # type: ignore
|
import stackprinter # type: ignore
|
||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
from config import Config
|
from config import Config
|
||||||
|
from classes import ApplicationError
|
||||||
|
|
||||||
|
|
||||||
class FunctionFilter(logging.Filter):
|
class FunctionFilter(logging.Filter):
|
||||||
@ -76,26 +78,32 @@ with open("app/logging.yaml", "r") as f:
|
|||||||
log = logging.getLogger(Config.LOG_NAME)
|
log = logging.getLogger(Config.LOG_NAME)
|
||||||
|
|
||||||
|
|
||||||
def log_uncaught_exceptions(type_, value, traceback):
|
def handle_exception(exc_type, exc_value, exc_traceback):
|
||||||
|
error = str(exc_value)
|
||||||
|
if QApplication.instance() is not None:
|
||||||
|
QMessageBox.critical(None, "Application Error", error)
|
||||||
|
if issubclass(exc_type, ApplicationError):
|
||||||
|
log.error(error)
|
||||||
|
else:
|
||||||
|
# Handle unexpected errors (log and display)
|
||||||
|
error_msg = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
|
||||||
|
|
||||||
|
print(stackprinter.format(exc_value, suppressed_paths=['/.venv'], style='darkbg'))
|
||||||
|
|
||||||
|
msg = stackprinter.format(exc_value)
|
||||||
|
log.error(msg)
|
||||||
|
log.error(error_msg)
|
||||||
|
print("Critical error:", error_msg) # Consider logging instead of print
|
||||||
|
|
||||||
|
if os.environ["MM_ENV"] == "PRODUCTION":
|
||||||
from helpers import send_mail
|
from helpers import send_mail
|
||||||
|
|
||||||
print("\033[1;31;47m")
|
|
||||||
print_exception(type_, value, traceback)
|
|
||||||
print("\033[1;37;40m")
|
|
||||||
print(
|
|
||||||
stackprinter.format(
|
|
||||||
value, suppressed_paths=["/pypoetry/virtualenvs/"], style="darkbg"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if os.environ["MM_ENV"] == "PRODUCTION":
|
|
||||||
msg = stackprinter.format(value)
|
|
||||||
send_mail(
|
send_mail(
|
||||||
Config.ERRORS_TO,
|
Config.ERRORS_TO,
|
||||||
Config.ERRORS_FROM,
|
Config.ERRORS_FROM,
|
||||||
"Exception (log_uncaught_exceptions) from musicmuster",
|
"Exception (log_uncaught_exceptions) from musicmuster",
|
||||||
msg,
|
msg,
|
||||||
)
|
)
|
||||||
log.debug(msg)
|
|
||||||
|
|
||||||
|
|
||||||
sys.excepthook = log_uncaught_exceptions
|
sys.excepthook = handle_exception
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import sys
|
|||||||
# PyQt imports
|
# PyQt imports
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
|
from dogpile.cache import make_region
|
||||||
|
from dogpile.cache.api import NO_VALUE
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
bindparam,
|
bindparam,
|
||||||
delete,
|
delete,
|
||||||
@ -40,6 +42,11 @@ if "unittest" in sys.modules and "sqlite" not in DATABASE_URL:
|
|||||||
raise ValueError("Unit tests running on non-Sqlite database")
|
raise ValueError("Unit tests running on non-Sqlite database")
|
||||||
db = DatabaseManager.get_instance(DATABASE_URL, engine_options=Config.ENGINE_OPTIONS).db
|
db = DatabaseManager.get_instance(DATABASE_URL, engine_options=Config.ENGINE_OPTIONS).db
|
||||||
|
|
||||||
|
# Configure the cache region
|
||||||
|
cache_region = make_region().configure(
|
||||||
|
'dogpile.cache.memory', # Use in-memory caching for now (switch to Redis if needed)
|
||||||
|
expiration_time=600 # Cache expires after 10 minutes
|
||||||
|
)
|
||||||
|
|
||||||
def run_sql(session: Session, sql: str) -> Sequence[RowMapping]:
|
def run_sql(session: Session, sql: str) -> Sequence[RowMapping]:
|
||||||
"""
|
"""
|
||||||
@ -54,6 +61,7 @@ def run_sql(session: Session, sql: str) -> Sequence[RowMapping]:
|
|||||||
|
|
||||||
# Database classes
|
# Database classes
|
||||||
class NoteColours(dbtables.NoteColoursTable):
|
class NoteColours(dbtables.NoteColoursTable):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session: Session,
|
session: Session,
|
||||||
@ -80,13 +88,28 @@ class NoteColours(dbtables.NoteColoursTable):
|
|||||||
Return all records
|
Return all records
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result = session.scalars(select(cls)).all()
|
cache_key = "note_colours_all"
|
||||||
|
cached_result = cache_region.get(cache_key)
|
||||||
|
|
||||||
|
if cached_result is NO_VALUE:
|
||||||
|
# Query the database
|
||||||
|
result = session.scalars(
|
||||||
|
select(cls)
|
||||||
|
.where(
|
||||||
|
cls.enabled.is_(True),
|
||||||
|
)
|
||||||
|
.order_by(cls.order)
|
||||||
|
).all()
|
||||||
|
cache_region.set(cache_key, result)
|
||||||
|
else:
|
||||||
|
result = cached_result
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_colour(
|
def get_colour(
|
||||||
session: Session, text: str, foreground: bool = False
|
session: Session, text: str, foreground: bool = False
|
||||||
) -> Optional[str]:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Parse text and return background (foreground if foreground==True) colour
|
Parse text and return background (foreground if foreground==True) colour
|
||||||
string if matched, else None
|
string if matched, else None
|
||||||
@ -94,16 +117,10 @@ class NoteColours(dbtables.NoteColoursTable):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not text:
|
if not text:
|
||||||
return None
|
return ""
|
||||||
|
|
||||||
match = False
|
match = False
|
||||||
for rec in session.scalars(
|
for rec in NoteColours.get_all(session):
|
||||||
select(NoteColours)
|
|
||||||
.where(
|
|
||||||
NoteColours.enabled.is_(True),
|
|
||||||
)
|
|
||||||
.order_by(NoteColours.order)
|
|
||||||
).all():
|
|
||||||
if rec.is_regex:
|
if rec.is_regex:
|
||||||
flags = re.UNICODE
|
flags = re.UNICODE
|
||||||
if not rec.is_casesensitive:
|
if not rec.is_casesensitive:
|
||||||
@ -121,10 +138,15 @@ class NoteColours(dbtables.NoteColoursTable):
|
|||||||
|
|
||||||
if match:
|
if match:
|
||||||
if foreground:
|
if foreground:
|
||||||
return rec.foreground
|
return rec.foreground or ""
|
||||||
else:
|
else:
|
||||||
return rec.colour
|
return rec.colour
|
||||||
return None
|
return ""
|
||||||
|
|
||||||
|
def invalidate_cache(self) -> None:
|
||||||
|
"""Invalidate dogpile cache"""
|
||||||
|
|
||||||
|
cache_region.delete("note_colours_all")
|
||||||
|
|
||||||
|
|
||||||
class Playdates(dbtables.PlaydatesTable):
|
class Playdates(dbtables.PlaydatesTable):
|
||||||
|
|||||||
@ -439,6 +439,12 @@ class RowAndTrack:
|
|||||||
self.row_number = playlist_row.row_number
|
self.row_number = playlist_row.row_number
|
||||||
self.track_id = playlist_row.track_id
|
self.track_id = playlist_row.track_id
|
||||||
|
|
||||||
|
# Playlist display data
|
||||||
|
self.row_fg: Optional[str] = None
|
||||||
|
self.row_bg: Optional[str] = None
|
||||||
|
self.note_fg: Optional[str] = None
|
||||||
|
self.note_bg: Optional[str] = None
|
||||||
|
|
||||||
# Collect track data if there's a track
|
# Collect track data if there's a track
|
||||||
if playlist_row.track_id:
|
if playlist_row.track_id:
|
||||||
self.artist = playlist_row.track.artist
|
self.artist = playlist_row.track.artist
|
||||||
|
|||||||
@ -2315,7 +2315,10 @@ class Window(QMainWindow):
|
|||||||
session.commit()
|
session.commit()
|
||||||
self.preview_manager.set_intro(intro)
|
self.preview_manager.set_intro(intro)
|
||||||
self.current.base_model.refresh_row(session, row_number)
|
self.current.base_model.refresh_row(session, row_number)
|
||||||
self.current.base_model.invalidate_row(row_number)
|
roles = [
|
||||||
|
Qt.ItemDataRole.DisplayRole,
|
||||||
|
]
|
||||||
|
self.current.base_model.invalidate_row(row_number, roles)
|
||||||
|
|
||||||
def preview_start(self) -> None:
|
def preview_start(self) -> None:
|
||||||
"""Restart preview"""
|
"""Restart preview"""
|
||||||
@ -2822,8 +2825,8 @@ if __name__ == "__main__":
|
|||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
update_bitrates(session)
|
update_bitrates(session)
|
||||||
else:
|
else:
|
||||||
try:
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
try:
|
||||||
# PyQt6 defaults to a grey for labels
|
# PyQt6 defaults to a grey for labels
|
||||||
palette = app.palette()
|
palette = app.palette()
|
||||||
palette.setColor(
|
palette.setColor(
|
||||||
@ -2841,6 +2844,7 @@ if __name__ == "__main__":
|
|||||||
win.show()
|
win.show()
|
||||||
status = app.exec()
|
status = app.exec()
|
||||||
sys.exit(status)
|
sys.exit(status)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if os.environ["MM_ENV"] == "PRODUCTION":
|
if os.environ["MM_ENV"] == "PRODUCTION":
|
||||||
from helpers import send_mail
|
from helpers import send_mail
|
||||||
@ -2854,10 +2858,8 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
log.debug(msg)
|
log.debug(msg)
|
||||||
else:
|
else:
|
||||||
print("\033[1;31;47mUnhandled exception starts")
|
|
||||||
print(
|
print(
|
||||||
stackprinter.format(
|
stackprinter.format(
|
||||||
exc, suppressed_paths=["/pypoetry/virtualenvs/"], style="darkbg"
|
exc, suppressed_paths=["/pypoetry/virtualenvs/"], style="darkbg"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
print("Unhandled exception ends\033[1;37;40m")
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
# Standard library imports
|
# Standard library imports
|
||||||
# Allow forward reference to PlaylistModel
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
@ -12,7 +11,6 @@ import re
|
|||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
QAbstractTableModel,
|
QAbstractTableModel,
|
||||||
QModelIndex,
|
QModelIndex,
|
||||||
QObject,
|
|
||||||
QRegularExpression,
|
QRegularExpression,
|
||||||
QSortFilterProxyModel,
|
QSortFilterProxyModel,
|
||||||
Qt,
|
Qt,
|
||||||
@ -26,7 +24,6 @@ from PyQt6.QtGui import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
import line_profiler
|
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
import obswebsocket # type: ignore
|
import obswebsocket # type: ignore
|
||||||
|
|
||||||
@ -77,14 +74,13 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self,
|
self,
|
||||||
playlist_id: int,
|
playlist_id: int,
|
||||||
is_template: bool,
|
is_template: bool,
|
||||||
*args: Optional[QObject],
|
|
||||||
**kwargs: Optional[QObject],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
log.debug("PlaylistModel.__init__()")
|
log.debug("PlaylistModel.__init__()")
|
||||||
|
|
||||||
self.playlist_id = playlist_id
|
self.playlist_id = playlist_id
|
||||||
self.is_template = is_template
|
self.is_template = is_template
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.playlist_rows: dict[int, RowAndTrack] = {}
|
self.playlist_rows: dict[int, RowAndTrack] = {}
|
||||||
self.signals = MusicMusterSignals()
|
self.signals = MusicMusterSignals()
|
||||||
@ -102,13 +98,17 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
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:
|
def active_section_header(self) -> int:
|
||||||
"""
|
"""
|
||||||
Return the row number of the first header that has either unplayed tracks
|
Return the row number of the first header that has any of the following below it:
|
||||||
or currently being played track below it.
|
- unplayed tracks
|
||||||
|
- the currently being played track
|
||||||
|
- the track marked as next to play
|
||||||
"""
|
"""
|
||||||
|
|
||||||
header_row = 0
|
header_row = 0
|
||||||
@ -120,23 +120,20 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
if not self.is_played_row(row_number):
|
if not self.is_played_row(row_number):
|
||||||
break
|
break
|
||||||
|
|
||||||
# If track is played, we need to check it's not the current
|
# Here means that row_number points to a played track. The
|
||||||
# next or previous track because we don't want to scroll them
|
# current track will be marked as played when we start
|
||||||
# out of view
|
# playing it. It's also possible that the track marked as
|
||||||
|
# next has already been played. Check for either of those.
|
||||||
|
|
||||||
for ts in [
|
for ts in [track_sequence.next, track_sequence.current]:
|
||||||
track_sequence.next,
|
|
||||||
track_sequence.current,
|
|
||||||
]:
|
|
||||||
if (
|
if (
|
||||||
ts
|
ts
|
||||||
and ts.row_number == row_number
|
and ts.row_number == row_number
|
||||||
and ts.playlist_id == self.playlist_id
|
and ts.playlist_id == self.playlist_id
|
||||||
):
|
):
|
||||||
break
|
# We've found the current or next track, so return
|
||||||
else:
|
# the last-found header row
|
||||||
continue # continue iterating over playlist_rows
|
return header_row
|
||||||
break # current row is in one of the track sequences
|
|
||||||
|
|
||||||
return header_row
|
return header_row
|
||||||
|
|
||||||
@ -153,28 +150,37 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
try:
|
try:
|
||||||
rat = self.playlist_rows[row_number]
|
rat = self.playlist_rows[row_number]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
log.error(
|
raise ApplicationError(
|
||||||
f"{self}: KeyError in add_track_to_header ({row_number=}, {track_id=})"
|
f"{self}: KeyError in add_track_to_header ({row_number=}, {track_id=})"
|
||||||
)
|
)
|
||||||
return
|
|
||||||
if rat.path:
|
if rat.path:
|
||||||
log.error(
|
raise ApplicationError(
|
||||||
f"{self}: Header row already has track associated ({rat=}, {track_id=})"
|
f"{self}: Header row already has track associated ({rat=}, {track_id=})"
|
||||||
)
|
)
|
||||||
return
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
playlistrow = session.get(PlaylistRows, rat.playlistrow_id)
|
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
|
# Add track to PlaylistRows
|
||||||
playlistrow.track_id = track_id
|
playlistrow.track_id = track_id
|
||||||
# Add any further note (header will already have a note)
|
# Add any further note (header will already have a note)
|
||||||
if note:
|
if note:
|
||||||
playlistrow.note += "\n" + note
|
playlistrow.note += " " + note
|
||||||
|
session.commit()
|
||||||
|
|
||||||
# Update local copy
|
# Update local copy
|
||||||
self.refresh_row(session, row_number)
|
self.refresh_row(session, row_number)
|
||||||
# Repaint row
|
# Repaint row
|
||||||
self.invalidate_row(row_number)
|
roles = [
|
||||||
session.commit()
|
Qt.ItemDataRole.BackgroundRole,
|
||||||
|
Qt.ItemDataRole.DisplayRole,
|
||||||
|
Qt.ItemDataRole.FontRole,
|
||||||
|
Qt.ItemDataRole.ForegroundRole,
|
||||||
|
]
|
||||||
|
# only invalidate required roles
|
||||||
|
self.invalidate_row(row_number, roles)
|
||||||
|
|
||||||
self.signals.resize_rows_signal.emit(self.playlist_id)
|
self.signals.resize_rows_signal.emit(self.playlist_id)
|
||||||
|
|
||||||
@ -185,10 +191,11 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# Header row
|
# Header row
|
||||||
if self.is_header_row(row):
|
if self.is_header_row(row):
|
||||||
# Check for specific header colouring
|
# Check for specific header colouring
|
||||||
|
if rat.row_bg is None:
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
note_background = NoteColours.get_colour(session, rat.note)
|
rat.row_bg = NoteColours.get_colour(session, rat.note)
|
||||||
if note_background:
|
if rat.row_bg:
|
||||||
return QBrush(QColor(note_background))
|
return QBrush(QColor(rat.row_bg))
|
||||||
else:
|
else:
|
||||||
return QBrush(QColor(Config.COLOUR_NOTES_PLAYLIST))
|
return QBrush(QColor(Config.COLOUR_NOTES_PLAYLIST))
|
||||||
# Unreadable track file
|
# Unreadable track file
|
||||||
@ -222,10 +229,11 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
return QBrush(QColor(Config.COLOUR_BITRATE_OK))
|
return QBrush(QColor(Config.COLOUR_BITRATE_OK))
|
||||||
if column == Col.NOTE.value:
|
if column == Col.NOTE.value:
|
||||||
if rat.note:
|
if rat.note:
|
||||||
|
if rat.note_bg is None:
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
note_background = NoteColours.get_colour(session, rat.note)
|
rat.note_bg = NoteColours.get_colour(session, rat.note)
|
||||||
if note_background:
|
if rat.note_bg:
|
||||||
return QBrush(QColor(note_background))
|
return QBrush(QColor(rat.note_bg))
|
||||||
|
|
||||||
return QBrush()
|
return QBrush()
|
||||||
|
|
||||||
@ -258,26 +266,28 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
- update track times
|
- update track times
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.debug(f"{self}: current_track_started()")
|
||||||
|
|
||||||
if not track_sequence.current:
|
if not track_sequence.current:
|
||||||
return
|
return
|
||||||
|
|
||||||
row_number = track_sequence.current.row_number
|
row_number = track_sequence.current.row_number
|
||||||
|
|
||||||
# Check for OBS scene change
|
# Check for OBS scene change
|
||||||
log.debug(f"{self}: Call OBS scene change")
|
|
||||||
self.obs_scene_change(row_number)
|
self.obs_scene_change(row_number)
|
||||||
|
|
||||||
# Sanity check that we have a track_id
|
# Sanity check that we have a track_id
|
||||||
if not track_sequence.current.track_id:
|
track_id = track_sequence.current.track_id
|
||||||
log.error(
|
if not track_id:
|
||||||
f"{self}: current_track_started() called with {track_sequence.current.track_id=}"
|
raise ApplicationError(
|
||||||
|
f"{self}: current_track_started() called with {track_id=}"
|
||||||
)
|
)
|
||||||
return
|
|
||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
# Update Playdates in database
|
# Update Playdates in database
|
||||||
log.debug(f"{self}: update playdates")
|
log.debug(f"{self}: update playdates {track_id=}")
|
||||||
Playdates(session, track_sequence.current.track_id)
|
Playdates(session, track_id)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
# Mark track as played in playlist
|
# Mark track as played in playlist
|
||||||
log.debug(f"{self}: Mark track as played")
|
log.debug(f"{self}: Mark track as played")
|
||||||
@ -291,11 +301,16 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Update colour and times for current row
|
# Update colour and times for current row
|
||||||
self.invalidate_row(row_number)
|
# only invalidate required roles
|
||||||
|
roles = [
|
||||||
|
Qt.ItemDataRole.DisplayRole
|
||||||
|
]
|
||||||
|
self.invalidate_row(row_number, roles)
|
||||||
|
|
||||||
# Update previous row in case we're hiding played rows
|
# Update previous row in case we're hiding played rows
|
||||||
if track_sequence.previous and track_sequence.previous.row_number:
|
if track_sequence.previous and track_sequence.previous.row_number:
|
||||||
self.invalidate_row(track_sequence.previous.row_number)
|
# only invalidate required roles
|
||||||
|
self.invalidate_row(track_sequence.previous.row_number, roles)
|
||||||
|
|
||||||
# Update all other track times
|
# Update all other track times
|
||||||
self.update_track_times()
|
self.update_track_times()
|
||||||
@ -316,36 +331,16 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
if next_row is not None:
|
if next_row is not None:
|
||||||
self.set_next_row(next_row)
|
self.set_next_row(next_row)
|
||||||
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
def data(
|
def data(
|
||||||
self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole
|
self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole
|
||||||
) -> QVariant:
|
) -> QVariant | QFont | QBrush | str:
|
||||||
"""Return data to view"""
|
"""Return data to view"""
|
||||||
|
|
||||||
if not index.isValid() or not (0 <= index.row() < len(self.playlist_rows)):
|
if (
|
||||||
return QVariant()
|
not index.isValid()
|
||||||
|
or not (0 <= index.row() < len(self.playlist_rows))
|
||||||
row = index.row()
|
or role
|
||||||
column = index.column()
|
in [
|
||||||
# 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 [
|
|
||||||
Qt.ItemDataRole.DecorationRole,
|
Qt.ItemDataRole.DecorationRole,
|
||||||
Qt.ItemDataRole.StatusTipRole,
|
Qt.ItemDataRole.StatusTipRole,
|
||||||
Qt.ItemDataRole.WhatsThisRole,
|
Qt.ItemDataRole.WhatsThisRole,
|
||||||
@ -353,10 +348,30 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Qt.ItemDataRole.TextAlignmentRole,
|
Qt.ItemDataRole.TextAlignmentRole,
|
||||||
Qt.ItemDataRole.CheckStateRole,
|
Qt.ItemDataRole.CheckStateRole,
|
||||||
Qt.ItemDataRole.InitialSortOrderRole,
|
Qt.ItemDataRole.InitialSortOrderRole,
|
||||||
]:
|
]
|
||||||
|
):
|
||||||
return QVariant()
|
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()
|
return QVariant()
|
||||||
|
|
||||||
def delete_rows(self, row_numbers: list[int]) -> None:
|
def delete_rows(self, row_numbers: list[int]) -> None:
|
||||||
@ -386,8 +401,9 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
super().endRemoveRows()
|
super().endRemoveRows()
|
||||||
|
|
||||||
self.reset_track_sequence_row_numbers()
|
self.reset_track_sequence_row_numbers()
|
||||||
|
self.update_track_times()
|
||||||
|
|
||||||
def display_role(self, row: int, column: int, rat: RowAndTrack) -> QVariant:
|
def display_role(self, row: int, column: int, rat: RowAndTrack) -> str:
|
||||||
"""
|
"""
|
||||||
Return text for display
|
Return text for display
|
||||||
"""
|
"""
|
||||||
@ -407,45 +423,45 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
if column == HEADER_NOTES_COLUMN:
|
if column == HEADER_NOTES_COLUMN:
|
||||||
header_text = self.header_text(rat)
|
header_text = self.header_text(rat)
|
||||||
if not header_text:
|
if not header_text:
|
||||||
return QVariant(Config.TEXT_NO_TRACK_NO_NOTE)
|
return Config.SECTION_HEADER
|
||||||
else:
|
else:
|
||||||
formatted_header = self.header_text(rat)
|
formatted_header = self.header_text(rat)
|
||||||
trimmed_header = self.remove_section_timer_markers(formatted_header)
|
trimmed_header = self.remove_section_timer_markers(formatted_header)
|
||||||
return QVariant(trimmed_header)
|
return trimmed_header
|
||||||
else:
|
else:
|
||||||
return QVariant("")
|
return ""
|
||||||
|
|
||||||
if column == Col.START_TIME.value:
|
if column == Col.START_TIME.value:
|
||||||
start_time = rat.forecast_start_time
|
start_time = rat.forecast_start_time
|
||||||
if start_time:
|
if start_time:
|
||||||
return QVariant(start_time.strftime(Config.TRACK_TIME_FORMAT))
|
return start_time.strftime(Config.TRACK_TIME_FORMAT)
|
||||||
return QVariant()
|
return ""
|
||||||
|
|
||||||
if column == Col.END_TIME.value:
|
if column == Col.END_TIME.value:
|
||||||
end_time = rat.forecast_end_time
|
end_time = rat.forecast_end_time
|
||||||
if end_time:
|
if end_time:
|
||||||
return QVariant(end_time.strftime(Config.TRACK_TIME_FORMAT))
|
return end_time.strftime(Config.TRACK_TIME_FORMAT)
|
||||||
return QVariant()
|
return ""
|
||||||
|
|
||||||
if column == Col.INTRO.value:
|
if column == Col.INTRO.value:
|
||||||
if rat.intro:
|
if rat.intro:
|
||||||
return QVariant(f"{rat.intro / 1000:{Config.INTRO_SECONDS_FORMAT}}")
|
return f"{rat.intro / 1000:{Config.INTRO_SECONDS_FORMAT}}"
|
||||||
else:
|
else:
|
||||||
return QVariant("")
|
return ""
|
||||||
|
|
||||||
dispatch_table = {
|
dispatch_table: dict[int, str] = {
|
||||||
Col.ARTIST.value: QVariant(rat.artist),
|
Col.ARTIST.value: rat.artist,
|
||||||
Col.BITRATE.value: QVariant(rat.bitrate),
|
Col.BITRATE.value: str(rat.bitrate),
|
||||||
Col.DURATION.value: QVariant(ms_to_mmss(rat.duration)),
|
Col.DURATION.value: ms_to_mmss(rat.duration),
|
||||||
Col.LAST_PLAYED.value: QVariant(get_relative_date(rat.lastplayed)),
|
Col.LAST_PLAYED.value: get_relative_date(rat.lastplayed),
|
||||||
Col.NOTE.value: QVariant(rat.note),
|
Col.NOTE.value: rat.note,
|
||||||
Col.START_GAP.value: QVariant(rat.start_gap),
|
Col.START_GAP.value: str(rat.start_gap),
|
||||||
Col.TITLE.value: QVariant(rat.title),
|
Col.TITLE.value: rat.title,
|
||||||
}
|
}
|
||||||
if column in dispatch_table:
|
if column in dispatch_table:
|
||||||
return dispatch_table[column]
|
return dispatch_table[column]
|
||||||
|
|
||||||
return QVariant()
|
return ""
|
||||||
|
|
||||||
def end_reset_model(self, playlist_id: int) -> None:
|
def end_reset_model(self, playlist_id: int) -> None:
|
||||||
"""
|
"""
|
||||||
@ -462,7 +478,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
super().endResetModel()
|
super().endResetModel()
|
||||||
self.reset_track_sequence_row_numbers()
|
self.reset_track_sequence_row_numbers()
|
||||||
|
|
||||||
def edit_role(self, row: int, column: int, rat: RowAndTrack) -> QVariant:
|
def edit_role(self, row: int, column: int, rat: RowAndTrack) -> str:
|
||||||
"""
|
"""
|
||||||
Return text for editing
|
Return text for editing
|
||||||
"""
|
"""
|
||||||
@ -470,29 +486,30 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# If this is a header row and we're being asked for the
|
# If this is a header row and we're being asked for the
|
||||||
# HEADER_NOTES_COLUMN, return the note value
|
# HEADER_NOTES_COLUMN, return the note value
|
||||||
if self.is_header_row(row) and column == HEADER_NOTES_COLUMN:
|
if self.is_header_row(row) and column == HEADER_NOTES_COLUMN:
|
||||||
return QVariant(rat.note)
|
return rat.note
|
||||||
|
|
||||||
if column == Col.INTRO.value:
|
if column == Col.INTRO.value:
|
||||||
return QVariant(rat.intro)
|
return str(rat.intro or "")
|
||||||
if column == Col.TITLE.value:
|
if column == Col.TITLE.value:
|
||||||
return QVariant(rat.title)
|
return rat.title
|
||||||
if column == Col.ARTIST.value:
|
if column == Col.ARTIST.value:
|
||||||
return QVariant(rat.artist)
|
return rat.artist
|
||||||
if column == Col.NOTE.value:
|
if column == Col.NOTE.value:
|
||||||
return QVariant(rat.note)
|
return rat.note
|
||||||
|
|
||||||
return QVariant()
|
return ""
|
||||||
|
|
||||||
def foreground_role(self, row: int, column: int, rat: RowAndTrack) -> QBrush:
|
def foreground_role(self, row: int, column: int, rat: RowAndTrack) -> QBrush:
|
||||||
"""Return header foreground colour or QBrush() if none"""
|
"""Return header foreground colour or QBrush() if none"""
|
||||||
|
|
||||||
if self.is_header_row(row):
|
if self.is_header_row(row):
|
||||||
|
if rat.row_fg is None:
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
note_foreground = NoteColours.get_colour(
|
rat.row_fg = NoteColours.get_colour(
|
||||||
session, rat.note, foreground=True
|
session, rat.note, foreground=True
|
||||||
)
|
)
|
||||||
if note_foreground:
|
if rat.row_fg:
|
||||||
return QBrush(QColor(note_foreground))
|
return QBrush(QColor(rat.row_fg))
|
||||||
|
|
||||||
return QBrush()
|
return QBrush()
|
||||||
|
|
||||||
@ -519,19 +536,19 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def font_role(self, row: int, column: int, rat: RowAndTrack) -> QVariant:
|
def font_role(self, row: int, column: int, rat: RowAndTrack) -> QFont:
|
||||||
"""
|
"""
|
||||||
Return font
|
Return font
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Notes column is never bold
|
# Notes column is never bold
|
||||||
if column == Col.NOTE.value:
|
if column == Col.NOTE.value:
|
||||||
return QVariant()
|
return QFont()
|
||||||
|
|
||||||
boldfont = QFont()
|
boldfont = QFont()
|
||||||
boldfont.setBold(not self.playlist_rows[row].played)
|
boldfont.setBold(not self.playlist_rows[row].played)
|
||||||
|
|
||||||
return QVariant(boldfont)
|
return boldfont
|
||||||
|
|
||||||
def get_duplicate_rows(self) -> list[int]:
|
def get_duplicate_rows(self) -> list[int]:
|
||||||
"""
|
"""
|
||||||
@ -620,7 +637,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
for a in self.playlist_rows.values()
|
for a in self.playlist_rows.values()
|
||||||
if not a.played and a.track_id is not None
|
if not a.played and a.track_id is not None
|
||||||
]
|
]
|
||||||
# log.debug(f"{self}: get_unplayed_rows() returned: {result=}")
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def headerData(
|
def headerData(
|
||||||
@ -696,7 +712,11 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.played_tracks_hidden = hide
|
self.played_tracks_hidden = hide
|
||||||
for row_number in range(len(self.playlist_rows)):
|
for row_number in range(len(self.playlist_rows)):
|
||||||
if self.is_played_row(row_number):
|
if self.is_played_row(row_number):
|
||||||
self.invalidate_row(row_number)
|
# only invalidate required roles
|
||||||
|
roles = [
|
||||||
|
Qt.ItemDataRole.DisplayRole,
|
||||||
|
]
|
||||||
|
self.invalidate_row(row_number, roles)
|
||||||
|
|
||||||
def insert_row(
|
def insert_row(
|
||||||
self,
|
self,
|
||||||
@ -728,10 +748,16 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
self.signals.resize_rows_signal.emit(self.playlist_id)
|
self.signals.resize_rows_signal.emit(self.playlist_id)
|
||||||
self.reset_track_sequence_row_numbers()
|
self.reset_track_sequence_row_numbers()
|
||||||
self.invalidate_rows(list(range(new_row_number, len(self.playlist_rows))))
|
# only invalidate required roles
|
||||||
|
roles = [
|
||||||
|
Qt.ItemDataRole.BackgroundRole,
|
||||||
|
Qt.ItemDataRole.DisplayRole,
|
||||||
|
Qt.ItemDataRole.FontRole,
|
||||||
|
Qt.ItemDataRole.ForegroundRole,
|
||||||
|
]
|
||||||
|
self.invalidate_rows(list(range(new_row_number, len(self.playlist_rows))), roles)
|
||||||
|
|
||||||
@line_profiler.profile
|
def invalidate_row(self, modified_row: int, roles: list[Qt.ItemDataRole]) -> None:
|
||||||
def invalidate_row(self, modified_row: int) -> None:
|
|
||||||
"""
|
"""
|
||||||
Signal to view to refresh invalidated row
|
Signal to view to refresh invalidated row
|
||||||
"""
|
"""
|
||||||
@ -741,10 +767,10 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.dataChanged.emit(
|
self.dataChanged.emit(
|
||||||
self.index(modified_row, 0),
|
self.index(modified_row, 0),
|
||||||
self.index(modified_row, self.columnCount() - 1),
|
self.index(modified_row, self.columnCount() - 1),
|
||||||
|
roles
|
||||||
)
|
)
|
||||||
|
|
||||||
@line_profiler.profile
|
def invalidate_rows(self, modified_rows: list[int], roles: list[Qt.ItemDataRole]) -> None:
|
||||||
def invalidate_rows(self, modified_rows: list[int]) -> None:
|
|
||||||
"""
|
"""
|
||||||
Signal to view to refresh invlidated rows
|
Signal to view to refresh invlidated rows
|
||||||
"""
|
"""
|
||||||
@ -752,7 +778,8 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
log.debug(f"issue285: {self}: invalidate_rows({modified_rows=})")
|
log.debug(f"issue285: {self}: invalidate_rows({modified_rows=})")
|
||||||
|
|
||||||
for modified_row in modified_rows:
|
for modified_row in modified_rows:
|
||||||
self.invalidate_row(modified_row)
|
# only invalidate required roles
|
||||||
|
self.invalidate_row(modified_row, roles)
|
||||||
|
|
||||||
def is_header_row(self, row_number: int) -> bool:
|
def is_header_row(self, row_number: int) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -827,7 +854,11 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.refresh_row(session, row_number)
|
self.refresh_row(session, row_number)
|
||||||
|
|
||||||
self.update_track_times()
|
self.update_track_times()
|
||||||
self.invalidate_rows(row_numbers)
|
# only invalidate required roles
|
||||||
|
roles = [
|
||||||
|
Qt.ItemDataRole.FontRole,
|
||||||
|
]
|
||||||
|
self.invalidate_rows(row_numbers, roles)
|
||||||
|
|
||||||
def move_rows(self, from_rows: list[int], to_row_number: int) -> None:
|
def move_rows(self, from_rows: list[int], to_row_number: int) -> None:
|
||||||
"""
|
"""
|
||||||
@ -892,7 +923,11 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# Update display
|
# Update display
|
||||||
self.reset_track_sequence_row_numbers()
|
self.reset_track_sequence_row_numbers()
|
||||||
self.update_track_times()
|
self.update_track_times()
|
||||||
self.invalidate_rows(list(row_map.keys()))
|
# only invalidate required roles
|
||||||
|
roles = [
|
||||||
|
Qt.ItemDataRole.DisplayRole,
|
||||||
|
]
|
||||||
|
self.invalidate_rows(list(row_map.keys()), roles)
|
||||||
|
|
||||||
def move_rows_between_playlists(
|
def move_rows_between_playlists(
|
||||||
self,
|
self,
|
||||||
@ -1069,7 +1104,11 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Update display
|
# Update display
|
||||||
self.invalidate_row(track_sequence.previous.row_number)
|
# only invalidate required roles
|
||||||
|
roles = [
|
||||||
|
Qt.ItemDataRole.BackgroundRole,
|
||||||
|
]
|
||||||
|
self.invalidate_row(track_sequence.previous.row_number, roles)
|
||||||
|
|
||||||
def refresh_data(self, session: Session) -> None:
|
def refresh_data(self, session: Session) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1122,7 +1161,11 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
playlist_row.track_id = None
|
playlist_row.track_id = None
|
||||||
session.commit()
|
session.commit()
|
||||||
self.refresh_row(session, row_number)
|
self.refresh_row(session, row_number)
|
||||||
self.invalidate_row(row_number)
|
# only invalidate required roles
|
||||||
|
roles = [
|
||||||
|
Qt.ItemDataRole.DisplayRole,
|
||||||
|
]
|
||||||
|
self.invalidate_row(row_number, roles)
|
||||||
|
|
||||||
def rescan_track(self, row_number: int) -> None:
|
def rescan_track(self, row_number: int) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1136,11 +1179,15 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
set_track_metadata(track)
|
set_track_metadata(track)
|
||||||
self.refresh_row(session, row_number)
|
self.refresh_row(session, row_number)
|
||||||
self.update_track_times()
|
self.update_track_times()
|
||||||
self.invalidate_row(row_number)
|
roles = [
|
||||||
|
Qt.ItemDataRole.BackgroundRole,
|
||||||
|
Qt.ItemDataRole.DisplayRole,
|
||||||
|
]
|
||||||
|
# only invalidate required roles
|
||||||
|
self.invalidate_row(row_number, roles)
|
||||||
self.signals.resize_rows_signal.emit(self.playlist_id)
|
self.signals.resize_rows_signal.emit(self.playlist_id)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@line_profiler.profile
|
|
||||||
def reset_track_sequence_row_numbers(self) -> None:
|
def reset_track_sequence_row_numbers(self) -> None:
|
||||||
"""
|
"""
|
||||||
Signal handler for when row ordering has changed.
|
Signal handler for when row ordering has changed.
|
||||||
@ -1197,7 +1244,13 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
# self.playlist_rows directly.
|
# self.playlist_rows directly.
|
||||||
self.playlist_rows[row_number].note = ""
|
self.playlist_rows[row_number].note = ""
|
||||||
session.commit()
|
session.commit()
|
||||||
self.invalidate_rows(row_numbers)
|
# only invalidate required roles
|
||||||
|
roles = [
|
||||||
|
Qt.ItemDataRole.BackgroundRole,
|
||||||
|
Qt.ItemDataRole.DisplayRole,
|
||||||
|
Qt.ItemDataRole.ForegroundRole,
|
||||||
|
]
|
||||||
|
self.invalidate_rows(row_numbers, roles)
|
||||||
|
|
||||||
def _reversed_contiguous_row_groups(
|
def _reversed_contiguous_row_groups(
|
||||||
self, row_numbers: list[int]
|
self, row_numbers: list[int]
|
||||||
@ -1275,7 +1328,6 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return header_text
|
return header_text
|
||||||
|
|
||||||
@line_profiler.profile
|
|
||||||
def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
|
def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
|
||||||
"""Standard function for view"""
|
"""Standard function for view"""
|
||||||
|
|
||||||
@ -1407,9 +1459,14 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.signals.search_songfacts_signal.emit(
|
self.signals.search_songfacts_signal.emit(
|
||||||
self.playlist_rows[row_number].title
|
self.playlist_rows[row_number].title
|
||||||
)
|
)
|
||||||
|
roles = [
|
||||||
|
Qt.ItemDataRole.BackgroundRole,
|
||||||
|
]
|
||||||
if old_next_row is not None:
|
if old_next_row is not None:
|
||||||
self.invalidate_row(old_next_row)
|
# only invalidate required roles
|
||||||
self.invalidate_row(row_number)
|
self.invalidate_row(old_next_row, roles)
|
||||||
|
# only invalidate required roles
|
||||||
|
self.invalidate_row(row_number, roles)
|
||||||
|
|
||||||
self.signals.next_track_changed_signal.emit()
|
self.signals.next_track_changed_signal.emit()
|
||||||
self.update_track_times()
|
self.update_track_times()
|
||||||
@ -1559,26 +1616,24 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
def supportedDropActions(self) -> Qt.DropAction:
|
def supportedDropActions(self) -> Qt.DropAction:
|
||||||
return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction
|
return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction
|
||||||
|
|
||||||
def tooltip_role(self, row: int, column: int, rat: RowAndTrack) -> QVariant:
|
def tooltip_role(self, row: int, column: int, rat: RowAndTrack) -> str:
|
||||||
"""
|
"""
|
||||||
Return tooltip. Currently only used for last_played column.
|
Return tooltip. Currently only used for last_played column.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if column != Col.LAST_PLAYED.value:
|
if column != Col.LAST_PLAYED.value:
|
||||||
return QVariant()
|
return ""
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
track_id = self.playlist_rows[row].track_id
|
track_id = self.playlist_rows[row].track_id
|
||||||
if not track_id:
|
if not track_id:
|
||||||
return QVariant()
|
return ""
|
||||||
playdates = Playdates.last_playdates(session, track_id)
|
playdates = Playdates.last_playdates(session, track_id)
|
||||||
return QVariant(
|
return "<br>".join(
|
||||||
"<br>".join(
|
|
||||||
[
|
[
|
||||||
a.lastplayed.strftime(Config.LAST_PLAYED_TOOLTIP_DATE_FORMAT)
|
a.lastplayed.strftime(Config.LAST_PLAYED_TOOLTIP_DATE_FORMAT)
|
||||||
for a in playdates
|
for a in playdates
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
def update_or_insert(self, track_id: int, row_number: int) -> None:
|
def update_or_insert(self, track_id: int, row_number: int) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1593,11 +1648,17 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
for row in track_rows:
|
for row in track_rows:
|
||||||
self.refresh_row(session, row)
|
self.refresh_row(session, row)
|
||||||
self.invalidate_rows(track_rows)
|
# only invalidate required roles
|
||||||
|
roles = [
|
||||||
|
Qt.ItemDataRole.BackgroundRole,
|
||||||
|
Qt.ItemDataRole.DisplayRole,
|
||||||
|
Qt.ItemDataRole.FontRole,
|
||||||
|
Qt.ItemDataRole.ForegroundRole,
|
||||||
|
]
|
||||||
|
self.invalidate_rows(track_rows, roles)
|
||||||
else:
|
else:
|
||||||
self.insert_row(proposed_row_number=row_number, track_id=track_id)
|
self.insert_row(proposed_row_number=row_number, track_id=track_id)
|
||||||
|
|
||||||
@line_profiler.profile
|
|
||||||
def update_track_times(self) -> None:
|
def update_track_times(self) -> None:
|
||||||
"""
|
"""
|
||||||
Update track start/end times in self.playlist_rows
|
Update track start/end times in self.playlist_rows
|
||||||
@ -1743,9 +1804,13 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
|||||||
# milliseconds so that it hides then. We add
|
# milliseconds so that it hides then. We add
|
||||||
# 100mS on so that the if clause above is
|
# 100mS on so that the if clause above is
|
||||||
# true next time through.
|
# true next time through.
|
||||||
|
# only invalidate required roles
|
||||||
|
roles = [
|
||||||
|
Qt.ItemDataRole.DisplayRole,
|
||||||
|
]
|
||||||
QTimer.singleShot(
|
QTimer.singleShot(
|
||||||
Config.HIDE_AFTER_PLAYING_OFFSET + 100,
|
Config.HIDE_AFTER_PLAYING_OFFSET + 100,
|
||||||
lambda: self.sourceModel().invalidate_row(source_row),
|
lambda: self.sourceModel().invalidate_row(source_row, roles),
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
# Next track not playing yet so don't hide previous
|
# Next track not playing yet so don't hide previous
|
||||||
|
|||||||
@ -31,6 +31,7 @@ dependencies = [
|
|||||||
"pyyaml (>=6.0.2,<7.0.0)",
|
"pyyaml (>=6.0.2,<7.0.0)",
|
||||||
"audioop-lts>=0.2.1",
|
"audioop-lts>=0.2.1",
|
||||||
"types-pyyaml>=6.0.12.20241230",
|
"types-pyyaml>=6.0.12.20241230",
|
||||||
|
"dogpile-cache>=1.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
48
uv.lock
48
uv.lock
@ -168,6 +168,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 },
|
{ url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dogpile-cache"
|
||||||
|
version = "1.3.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "decorator" },
|
||||||
|
{ name = "stevedore" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cb/b7/2fa37f52b4f38bc8eb6d4923163dd822ca6f9e2f817378478a5de73b239e/dogpile_cache-1.3.4.tar.gz", hash = "sha256:4f0295575f5fdd3f7e13c84ba8e36656971d1869a2081b4737ec99ede378a8c0", size = 933234 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/82/d118accb66f9048acbc4ff91592755c24d52f54cce40d6b0b2a0ce351cf5/dogpile.cache-1.3.4-py3-none-any.whl", hash = "sha256:a393412f93d24a8942fdf9248dc80678127d54c5e60a7be404027aa193cafe12", size = 62859 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "entrypoints"
|
name = "entrypoints"
|
||||||
version = "0.4"
|
version = "0.4"
|
||||||
@ -449,6 +462,7 @@ dependencies = [
|
|||||||
{ name = "alembic" },
|
{ name = "alembic" },
|
||||||
{ name = "audioop-lts" },
|
{ name = "audioop-lts" },
|
||||||
{ name = "colorlog" },
|
{ name = "colorlog" },
|
||||||
|
{ name = "dogpile-cache" },
|
||||||
{ name = "fuzzywuzzy" },
|
{ name = "fuzzywuzzy" },
|
||||||
{ name = "mutagen" },
|
{ name = "mutagen" },
|
||||||
{ name = "mysqlclient" },
|
{ name = "mysqlclient" },
|
||||||
@ -492,6 +506,7 @@ requires-dist = [
|
|||||||
{ name = "alembic", specifier = ">=1.14.0" },
|
{ name = "alembic", specifier = ">=1.14.0" },
|
||||||
{ name = "audioop-lts", specifier = ">=0.2.1" },
|
{ name = "audioop-lts", specifier = ">=0.2.1" },
|
||||||
{ name = "colorlog", specifier = ">=6.9.0" },
|
{ name = "colorlog", specifier = ">=6.9.0" },
|
||||||
|
{ name = "dogpile-cache", specifier = ">=1.3.4" },
|
||||||
{ name = "fuzzywuzzy", specifier = ">=0.18.0" },
|
{ name = "fuzzywuzzy", specifier = ">=0.18.0" },
|
||||||
{ name = "mutagen", specifier = ">=1.47.0" },
|
{ name = "mutagen", specifier = ">=1.47.0" },
|
||||||
{ name = "mysqlclient", specifier = ">=2.2.5" },
|
{ name = "mysqlclient", specifier = ">=2.2.5" },
|
||||||
@ -641,6 +656,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
|
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pbr"
|
||||||
|
version = "6.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "setuptools" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/d2/510cc0d218e753ba62a1bc1434651db3cd797a9716a0a66cc714cb4f0935/pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b", size = 125702 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/ac/684d71315abc7b1214d59304e23a982472967f6bf4bde5a98f1503f648dc/pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76", size = 108997 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pexpect"
|
name = "pexpect"
|
||||||
version = "4.9.0"
|
version = "4.9.0"
|
||||||
@ -1028,6 +1055,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
|
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "75.8.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d1/53/43d99d7687e8cdef5ab5f9ec5eaf2c0423c2b35133a2b7e7bc276fc32b21/setuptools-75.8.2.tar.gz", hash = "sha256:4880473a969e5f23f2a2be3646b2dfd84af9028716d398e46192f84bc36900d2", size = 1344083 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/38/7d7362e031bd6dc121e5081d8cb6aa6f6fedf2b67bf889962134c6da4705/setuptools-75.8.2-py3-none-any.whl", hash = "sha256:558e47c15f1811c1fa7adbd0096669bf76c1d3f433f58324df69f3f5ecac4e8f", size = 1229385 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlalchemy"
|
name = "sqlalchemy"
|
||||||
version = "2.0.38"
|
version = "2.0.38"
|
||||||
@ -1072,6 +1108,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/7c/15/485186e37a06d28b7fc9020ad57ba1e3778ee9e8930ff6c9ea350946ffe1/stackprinter-0.2.12-py3-none-any.whl", hash = "sha256:0a0623d46a5babd7a8a9787f605f4dd4a42d6ff7aee140541d5e9291a506e8d9", size = 29282 },
|
{ url = "https://files.pythonhosted.org/packages/7c/15/485186e37a06d28b7fc9020ad57ba1e3778ee9e8930ff6c9ea350946ffe1/stackprinter-0.2.12-py3-none-any.whl", hash = "sha256:0a0623d46a5babd7a8a9787f605f4dd4a42d6ff7aee140541d5e9291a506e8d9", size = 29282 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stevedore"
|
||||||
|
version = "5.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pbr" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/28/3f/13cacea96900bbd31bb05c6b74135f85d15564fc583802be56976c940470/stevedore-5.4.1.tar.gz", hash = "sha256:3135b5ae50fe12816ef291baff420acb727fcd356106e3e9cbfa9e5985cd6f4b", size = 513858 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/45/8c4ebc0c460e6ec38e62ab245ad3c7fc10b210116cea7c16d61602aa9558/stevedore-5.4.1-py3-none-any.whl", hash = "sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe", size = 49533 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "text-unidecode"
|
name = "text-unidecode"
|
||||||
version = "1.3"
|
version = "1.3"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user