Compare commits

...

18 Commits

Author SHA1 Message Date
Keith Edmunds
9bf1ab29a8 Fixup tests after data() return type fixups 2025-03-09 16:41:28 +00:00
Keith Edmunds
4e51b44b44 More work on data() return types 2025-03-09 16:40:19 +00:00
Keith Edmunds
582803dccc Put more info in ApplicationError dialog
Show it after dumping error to stderr
2025-03-09 16:34:53 +00:00
Keith Edmunds
5f9fd31dfd Merge branch 'issue285' into dev 2025-03-08 21:38:11 +00:00
Keith Edmunds
74402f640f Only invalidate required roles 2025-03-08 21:36:09 +00:00
Keith Edmunds
963da0b5d0 No db calls when servicing data() except for caching 2025-03-08 21:30:37 +00:00
Keith Edmunds
85493de179 Remove profiling decorators 2025-03-08 12:03:47 +00:00
Keith Edmunds
2f8afeb814 WIP Issue 285 2025-03-08 12:02:07 +00:00
Keith Edmunds
3b004567df Implement dogpile cache for Notecolours 2025-03-08 11:45:38 +00:00
Keith Edmunds
76039aa5e6 Only try to show ApplicationError dialog when we have a QApplication 2025-03-08 11:42:59 +00:00
Keith Edmunds
1f10692c15 Make notes substring unique 2025-03-08 09:57:04 +00:00
Keith Edmunds
6dd34b292f Improve ApplicationError reporting 2025-03-07 15:44:21 +00:00
Keith Edmunds
6e2ad86fb2 Merge branch 'mark_preview' into dev 2025-03-07 09:59:32 +00:00
Keith Edmunds
ccc1737f2d Issue 285: additional logging and profiling 2025-03-07 09:30:23 +00:00
Keith Edmunds
58e244af21 Add profiling information for moving rows 2025-03-06 14:30:03 +00:00
Keith Edmunds
93839c69e2 Remove main_window_ui.py 2025-03-06 14:27:42 +00:00
Keith Edmunds
61b00d8531 Put preview track details in status bar 2025-03-06 14:26:47 +00:00
Keith Edmunds
63b1d0dff4 mypy fixups 2025-03-06 11:33:53 +00:00
14 changed files with 490 additions and 1127 deletions

View File

@ -123,11 +123,11 @@ class Config(object):
ROWS_FROM_ZERO = True
SCROLL_TOP_MARGIN = 3
SECTION_ENDINGS = ("-", "+-", "-+")
SECTION_HEADER = "[Section header]"
SECTION_STARTS = ("+", "+-", "-+")
SONGFACTS_ON_NEXT = False
START_GAP_WARNING_THRESHOLD = 300
SUBTOTAL_ON_ROW_ZERO = "[No subtotal on first row]"
TEXT_NO_TRACK_NO_NOTE = "[Section header]"
TOD_TIME_FORMAT = "%H:%M:%S"
TRACK_TIME_FORMAT = "%H:%M:%S"
VLC_MAIN_PLAYER_NAME = "MusicMuster Main Player"

View File

@ -53,7 +53,7 @@ class NoteColoursTable(Model):
__tablename__ = "notecolours"
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)
enabled: Mapped[bool] = mapped_column(default=True, index=True)
foreground: Mapped[Optional[str]] = mapped_column(String(21), index=False)
@ -130,7 +130,6 @@ class PlaylistRowsTable(Model):
)
playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows")
track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id", ondelete="CASCADE"))
track_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("tracks.id", ondelete="CASCADE")
)

View File

@ -6,16 +6,18 @@ import logging.config
import logging.handlers
import os
import sys
from traceback import print_exception
import traceback
import yaml
# PyQt imports
from PyQt6.QtWidgets import QApplication, QMessageBox
# Third party imports
import stackprinter # type: ignore
# App imports
from config import Config
from classes import ApplicationError
class FunctionFilter(logging.Filter):
@ -76,26 +78,34 @@ with open("app/logging.yaml", "r") as f:
log = logging.getLogger(Config.LOG_NAME)
def log_uncaught_exceptions(type_, value, traceback):
from helpers import send_mail
def handle_exception(exc_type, exc_value, exc_traceback):
error = str(exc_value)
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("\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(
Config.ERRORS_TO,
Config.ERRORS_FROM,
"Exception (log_uncaught_exceptions) from musicmuster",
msg,
)
log.debug(msg)
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
send_mail(
Config.ERRORS_TO,
Config.ERRORS_FROM,
"Exception (log_uncaught_exceptions) from musicmuster",
msg,
)
if QApplication.instance() is not None:
fname = os.path.split(exc_traceback.tb_frame.f_code.co_filename)[1]
msg = f"ApplicationError: {error}\nat {fname}:{exc_traceback.tb_lineno}"
QMessageBox.critical(None, "Application Error", msg)
sys.excepthook = log_uncaught_exceptions
sys.excepthook = handle_exception

View File

@ -10,6 +10,8 @@ import sys
# PyQt imports
# Third party imports
from dogpile.cache import make_region
from dogpile.cache.api import NO_VALUE
from sqlalchemy import (
bindparam,
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")
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]:
"""
@ -54,6 +61,7 @@ def run_sql(session: Session, sql: str) -> Sequence[RowMapping]:
# Database classes
class NoteColours(dbtables.NoteColoursTable):
def __init__(
self,
session: Session,
@ -80,13 +88,28 @@ class NoteColours(dbtables.NoteColoursTable):
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 not NO_VALUE:
return cached_result
# Query the database
result = session.scalars(
select(cls)
.where(
cls.enabled.is_(True),
)
.order_by(cls.order)
).all()
cache_region.set(cache_key, result)
return result
@staticmethod
def get_colour(
session: Session, text: str, foreground: bool = False
) -> Optional[str]:
) -> str:
"""
Parse text and return background (foreground if foreground==True) colour
string if matched, else None
@ -94,16 +117,10 @@ class NoteColours(dbtables.NoteColoursTable):
"""
if not text:
return None
return ""
match = False
for rec in session.scalars(
select(NoteColours)
.where(
NoteColours.enabled.is_(True),
)
.order_by(NoteColours.order)
).all():
for rec in NoteColours.get_all(session):
if rec.is_regex:
flags = re.UNICODE
if not rec.is_casesensitive:
@ -121,10 +138,16 @@ class NoteColours(dbtables.NoteColoursTable):
if match:
if foreground:
return rec.foreground
return rec.foreground or ""
else:
return rec.colour
return None
return ""
@staticmethod
def invalidate_cache() -> None:
"""Invalidate dogpile cache"""
cache_region.delete("note_colours_all")
class Playdates(dbtables.PlaydatesTable):

View File

@ -439,6 +439,12 @@ class RowAndTrack:
self.row_number = playlist_row.row_number
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
if playlist_row.track_id:
self.artist = playlist_row.track.artist
@ -458,7 +464,7 @@ class RowAndTrack:
self.title = playlist_row.track.title
else:
self.artist = ""
self.bitrate = None
self.bitrate = 0
self.duration = 0
self.fade_at = 0
self.intro = None

View File

@ -767,22 +767,14 @@ class PreviewManager:
self.intro = ms
def set_track_info(self, info: TrackInfo) -> None:
self.track_id = info.track_id
self.row_number = info.row_number
with db.Session() as session:
track = session.get(Tracks, self.track_id)
if not track:
raise ValueError(
f"PreviewManager: unable to retreive track {self.track_id=}"
)
self.intro = track.intro
self.path = track.path
def set_track_info(self, track_id: int, track_intro: int, track_path: str) -> None:
self.track_id = track_id
self.intro = track_intro
self.path = track_path
# Check file readable
if file_is_unreadable(self.path):
raise ValueError(f"PreviewManager.__init__: {track.path=} unreadable")
raise ValueError(f"PreviewManager.__init__: {track_path=} unreadable")
mixer.music.load(self.path)
@ -790,7 +782,6 @@ class PreviewManager:
mixer.music.stop()
mixer.music.unload()
self.path = ""
self.row_number = None
self.track_id = 0
self.start_time = None
@ -2123,7 +2114,7 @@ class Window(QMainWindow):
webbrowser.get("browser").open_new_tab(url)
def paste_rows(self) -> None:
def paste_rows(self, dummy_for_profiling: int | None = None) -> None:
"""
Paste earlier cut rows.
"""
@ -2263,15 +2254,33 @@ class Window(QMainWindow):
return
if not track_info:
return
self.preview_manager.set_track_info(track_info)
self.preview_manager.play()
self.preview_manager.row_number = track_info.row_number
with db.Session() as session:
track = session.get(Tracks, track_info.track_id)
if not track:
raise ApplicationError(
f"musicmuster.preview: unable to retreive track {track_info.track_id=}"
)
self.preview_manager.set_track_info(
track_id=track.id,
track_path=track.path,
track_intro=track.intro
)
self.preview_manager.play()
self.show_status_message(
f"Preview: {track.title} / {track.artist} (row {track_info.row_number})",
0
)
else:
self.preview_manager.stop()
self.show_status_message("", 0)
def preview_arm(self):
"""Manager arm button for setting intro length"""
self.footer_section.btnPreviewMark.setEnabled(self.btnPreviewArm.isChecked())
self.footer_section.btnPreviewMark.setEnabled(
self.footer_section.btnPreviewArm.isChecked()
)
def preview_back(self) -> None:
"""Wind back preview file"""
@ -2308,7 +2317,10 @@ class Window(QMainWindow):
session.commit()
self.preview_manager.set_intro(intro)
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:
"""Restart preview"""
@ -2528,10 +2540,14 @@ class Window(QMainWindow):
def show_status_message(self, message: str, timing: int) -> None:
"""
Show status message in status bar for timing milliseconds
Clear message if message is null string
"""
if self.statusbar:
self.statusbar.showMessage(message, timing)
if message:
self.statusbar.showMessage(message, timing)
else:
self.statusbar.clearMessage()
def show_track(self, playlist_track: RowAndTrack) -> None:
"""Scroll to show track in plt"""
@ -2811,8 +2827,8 @@ if __name__ == "__main__":
with db.Session() as session:
update_bitrates(session)
else:
app = QApplication(sys.argv)
try:
app = QApplication(sys.argv)
# PyQt6 defaults to a grey for labels
palette = app.palette()
palette.setColor(
@ -2830,6 +2846,7 @@ if __name__ == "__main__":
win.show()
status = app.exec()
sys.exit(status)
except Exception as exc:
if os.environ["MM_ENV"] == "PRODUCTION":
from helpers import send_mail
@ -2843,10 +2860,8 @@ if __name__ == "__main__":
)
log.debug(msg)
else:
print("\033[1;31;47mUnhandled exception starts")
print(
stackprinter.format(
exc, suppressed_paths=["/pypoetry/virtualenvs/"], style="darkbg"
)
)
print("Unhandled exception ends\033[1;37;40m")

View File

@ -1,5 +1,4 @@
# Standard library imports
# Allow forward reference to PlaylistModel
from __future__ import annotations
from operator import attrgetter
@ -12,7 +11,6 @@ import re
from PyQt6.QtCore import (
QAbstractTableModel,
QModelIndex,
QObject,
QRegularExpression,
QSortFilterProxyModel,
Qt,
@ -76,14 +74,13 @@ class PlaylistModel(QAbstractTableModel):
self,
playlist_id: int,
is_template: bool,
*args: Optional[QObject],
**kwargs: Optional[QObject],
) -> 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 +98,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 +120,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,44 +150,54 @@ 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:
# 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
# Update local copy
self.refresh_row(session, row_number)
# Repaint row
self.invalidate_row(row_number)
session.commit()
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 += " " + note
session.commit()
# Update local copy
self.refresh_row(session, row_number)
# Repaint row
roles = [
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)
def background_role(self, row: int, column: int, rat: RowAndTrack) -> QBrush:
def _background_role(self, row: int, column: int, rat: RowAndTrack) -> QBrush:
"""Return background setting"""
# Handle entire row colouring
# Header row
if self.is_header_row(row):
# Check for specific header colouring
with db.Session() as session:
note_background = NoteColours.get_colour(session, rat.note)
if note_background:
return QBrush(QColor(note_background))
else:
return QBrush(QColor(Config.COLOUR_NOTES_PLAYLIST))
if rat.row_bg is None:
with db.Session() as session:
rat.row_bg = NoteColours.get_colour(session, rat.note)
if rat.row_bg:
return QBrush(QColor(rat.row_bg))
else:
return QBrush(QColor(Config.COLOUR_NOTES_PLAYLIST))
# Unreadable track file
if file_is_unreadable(rat.path):
return QBrush(QColor(Config.COLOUR_UNREADABLE))
@ -221,10 +229,11 @@ class PlaylistModel(QAbstractTableModel):
return QBrush(QColor(Config.COLOUR_BITRATE_OK))
if column == Col.NOTE.value:
if rat.note:
with db.Session() as session:
note_background = NoteColours.get_colour(session, rat.note)
if note_background:
return QBrush(QColor(note_background))
if rat.note_bg is None:
with db.Session() as session:
rat.note_bg = NoteColours.get_colour(session, rat.note)
if rat.note_bg:
return QBrush(QColor(rat.note_bg))
return QBrush()
@ -257,26 +266,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")
@ -290,11 +301,16 @@ class PlaylistModel(QAbstractTableModel):
)
# 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
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
self.update_track_times()
@ -315,14 +331,25 @@ class PlaylistModel(QAbstractTableModel):
if next_row is not None:
self.set_next_row(next_row)
session.commit()
def data(
self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole
) -> QVariant:
) -> QVariant | QFont | QBrush | str | int:
"""Return data to view"""
if not index.isValid() or not (0 <= index.row() < len(self.playlist_rows)):
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,
Qt.ItemDataRole.SizeHintRole,
Qt.ItemDataRole.TextAlignmentRole,
Qt.ItemDataRole.CheckStateRole,
Qt.ItemDataRole.InitialSortOrderRole,
]
):
return QVariant()
row = index.row()
@ -330,32 +357,21 @@ class PlaylistModel(QAbstractTableModel):
# 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,
}
# 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)
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.StatusTipRole,
Qt.ItemDataRole.WhatsThisRole,
Qt.ItemDataRole.SizeHintRole,
Qt.ItemDataRole.TextAlignmentRole,
Qt.ItemDataRole.CheckStateRole,
Qt.ItemDataRole.InitialSortOrderRole,
]:
return QVariant()
# Fall through to no-op
return QVariant()
def delete_rows(self, row_numbers: list[int]) -> None:
@ -385,8 +401,9 @@ 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:
def _display_role(self, row: int, column: int, rat: RowAndTrack) -> str:
"""
Return text for display
"""
@ -406,45 +423,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,37 +478,38 @@ class PlaylistModel(QAbstractTableModel):
super().endResetModel()
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 | int:
"""
Return text for editing
Return value for editing
"""
# 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 rat.intro or 0
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 ""
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"""
if self.is_header_row(row):
with db.Session() as session:
note_foreground = NoteColours.get_colour(
session, rat.note, foreground=True
)
if note_foreground:
return QBrush(QColor(note_foreground))
if rat.row_fg is None:
with db.Session() as session:
rat.row_fg = NoteColours.get_colour(
session, rat.note, foreground=True
)
if rat.row_fg:
return QBrush(QColor(rat.row_fg))
return QBrush()
@ -518,19 +536,19 @@ class PlaylistModel(QAbstractTableModel):
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
"""
# 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]:
"""
@ -619,7 +637,6 @@ class PlaylistModel(QAbstractTableModel):
for a in self.playlist_rows.values()
if not a.played and a.track_id is not None
]
# log.debug(f"{self}: get_unplayed_rows() returned: {result=}")
return result
def headerData(
@ -627,22 +644,22 @@ class PlaylistModel(QAbstractTableModel):
section: int,
orientation: Qt.Orientation,
role: int = Qt.ItemDataRole.DisplayRole,
) -> QVariant:
) -> str | int | QFont | QVariant:
"""
Return text for headers
"""
display_dispatch_table = {
Col.START_GAP.value: QVariant(Config.HEADER_START_GAP),
Col.INTRO.value: QVariant(Config.HEADER_INTRO),
Col.TITLE.value: QVariant(Config.HEADER_TITLE),
Col.ARTIST.value: QVariant(Config.HEADER_ARTIST),
Col.DURATION.value: QVariant(Config.HEADER_DURATION),
Col.START_TIME.value: QVariant(Config.HEADER_START_TIME),
Col.END_TIME.value: QVariant(Config.HEADER_END_TIME),
Col.LAST_PLAYED.value: QVariant(Config.HEADER_LAST_PLAYED),
Col.BITRATE.value: QVariant(Config.HEADER_BITRATE),
Col.NOTE.value: QVariant(Config.HEADER_NOTE),
Col.START_GAP.value: Config.HEADER_START_GAP,
Col.INTRO.value: Config.HEADER_INTRO,
Col.TITLE.value: Config.HEADER_TITLE,
Col.ARTIST.value: Config.HEADER_ARTIST,
Col.DURATION.value: Config.HEADER_DURATION,
Col.START_TIME.value: Config.HEADER_START_TIME,
Col.END_TIME.value: Config.HEADER_END_TIME,
Col.LAST_PLAYED.value: Config.HEADER_LAST_PLAYED,
Col.BITRATE.value: Config.HEADER_BITRATE,
Col.NOTE.value: Config.HEADER_NOTE,
}
if role == Qt.ItemDataRole.DisplayRole:
@ -650,14 +667,14 @@ class PlaylistModel(QAbstractTableModel):
return display_dispatch_table[section]
else:
if Config.ROWS_FROM_ZERO:
return QVariant(str(section))
return section
else:
return QVariant(str(section + 1))
return section + 1
elif role == Qt.ItemDataRole.FontRole:
boldfont = QFont()
boldfont.setBold(True)
return QVariant(boldfont)
return boldfont
return QVariant()
@ -695,7 +712,11 @@ class PlaylistModel(QAbstractTableModel):
self.played_tracks_hidden = hide
for row_number in range(len(self.playlist_rows)):
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(
self,
@ -727,25 +748,38 @@ class PlaylistModel(QAbstractTableModel):
self.signals.resize_rows_signal.emit(self.playlist_id)
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)
def invalidate_row(self, modified_row: int) -> None:
def invalidate_row(self, modified_row: int, roles: list[Qt.ItemDataRole]) -> None:
"""
Signal to view to refresh invalidated row
"""
log.debug(f"issue285: {self}: invalidate_row({modified_row=})")
self.dataChanged.emit(
self.index(modified_row, 0),
self.index(modified_row, self.columnCount() - 1),
roles
)
def invalidate_rows(self, modified_rows: list[int]) -> None:
def invalidate_rows(self, modified_rows: list[int], roles: list[Qt.ItemDataRole]) -> None:
"""
Signal to view to refresh invlidated rows
"""
log.debug(f"issue285: {self}: invalidate_rows({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:
"""
@ -820,7 +854,11 @@ class PlaylistModel(QAbstractTableModel):
self.refresh_row(session, row_number)
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:
"""
@ -885,7 +923,11 @@ class PlaylistModel(QAbstractTableModel):
# Update display
self.reset_track_sequence_row_numbers()
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(
self,
@ -1062,7 +1104,11 @@ class PlaylistModel(QAbstractTableModel):
return
# 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:
"""
@ -1115,7 +1161,11 @@ class PlaylistModel(QAbstractTableModel):
playlist_row.track_id = None
session.commit()
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:
"""
@ -1129,7 +1179,12 @@ class PlaylistModel(QAbstractTableModel):
set_track_metadata(track)
self.refresh_row(session, row_number)
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)
session.commit()
@ -1143,7 +1198,7 @@ class PlaylistModel(QAbstractTableModel):
looking up the playlistrow_id and retrieving the row number from the database.
"""
log.debug(f"{self}: reset_track_sequence_row_numbers()")
log.debug(f"issue285: {self}: reset_track_sequence_row_numbers()")
# Check the track_sequence.next, current and previous plrs and
# update the row number
@ -1189,7 +1244,13 @@ class PlaylistModel(QAbstractTableModel):
# self.playlist_rows directly.
self.playlist_rows[row_number].note = ""
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(
self, row_numbers: list[int]
@ -1398,9 +1459,14 @@ class PlaylistModel(QAbstractTableModel):
self.signals.search_songfacts_signal.emit(
self.playlist_rows[row_number].title
)
roles = [
Qt.ItemDataRole.BackgroundRole,
]
if old_next_row is not None:
self.invalidate_row(old_next_row)
self.invalidate_row(row_number)
# only invalidate required roles
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.update_track_times()
@ -1550,25 +1616,23 @@ 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:
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(
"<br>".join(
[
a.lastplayed.strftime(Config.LAST_PLAYED_TOOLTIP_DATE_FORMAT)
for a in playdates
]
)
return "<br>".join(
[
a.lastplayed.strftime(Config.LAST_PLAYED_TOOLTIP_DATE_FORMAT)
for a in playdates
]
)
def update_or_insert(self, track_id: int, row_number: int) -> None:
@ -1584,7 +1648,14 @@ class PlaylistModel(QAbstractTableModel):
with db.Session() as session:
for row in track_rows:
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:
self.insert_row(proposed_row_number=row_number, track_id=track_id)
@ -1593,7 +1664,7 @@ class PlaylistModel(QAbstractTableModel):
Update track start/end times in self.playlist_rows
"""
log.debug(f"{self}: update_track_times()")
log.debug(f"issue285: {self}: update_track_times()")
next_start_time: Optional[dt.datetime] = None
update_rows: list[int] = []
@ -1733,9 +1804,13 @@ class PlaylistProxyModel(QSortFilterProxyModel):
# milliseconds so that it hides then. We add
# 100mS on so that the if clause above is
# true next time through.
# only invalidate required roles
roles = [
Qt.ItemDataRole.DisplayRole,
]
QTimer.singleShot(
Config.HIDE_AFTER_PLAYING_OFFSET + 100,
lambda: self.sourceModel().invalidate_row(source_row),
lambda: self.sourceModel().invalidate_row(source_row, roles),
)
return True
# Next track not playing yet so don't hide previous

View File

@ -31,7 +31,6 @@ from PyQt6.QtWidgets import (
)
# Third party imports
import line_profiler
# App imports
from audacity_controller import AudacityController
@ -184,11 +183,11 @@ class PlaylistDelegate(QStyledItemDelegate):
data_modified = False
if isinstance(editor, QTextEdit):
data_modified = (
self.original_model_data.value() != editor.toPlainText()
self.original_model_data != editor.toPlainText()
)
elif isinstance(editor, QDoubleSpinBox):
data_modified = (
self.original_model_data.value() != int(editor.value()) * 1000
self.original_model_data != int(editor.value()) * 1000
)
if not data_modified:
self.closeEditor.emit(editor)
@ -247,10 +246,10 @@ class PlaylistDelegate(QStyledItemDelegate):
edit_index, Qt.ItemDataRole.EditRole
)
if index.column() == Col.INTRO.value:
if self.original_model_data.value():
editor.setValue(self.original_model_data.value() / 1000)
if self.original_model_data:
editor.setValue(self.original_model_data / 1000)
else:
editor.setPlainText(self.original_model_data.value())
editor.setPlainText(self.original_model_data)
def setModelData(self, editor, model, index):
proxy_model = index.model()
@ -358,7 +357,7 @@ class PlaylistTab(QTableView):
# Deselect edited line
self.clear_selection()
def dropEvent(self, event: Optional[QDropEvent]) -> None:
def dropEvent(self, event: Optional[QDropEvent], dummy: int | None = None) -> None:
"""
Move dropped rows
"""
@ -1112,7 +1111,6 @@ class PlaylistTab(QTableView):
self.setSpan(row, column, rowSpan, columnSpan)
@line_profiler.profile
def tab_live(self) -> None:
"""
Called when tab gets focus

View File

@ -15,6 +15,7 @@ from PyQt6.QtCore import (
QVariant,
)
from PyQt6.QtGui import (
QBrush,
QColor,
QFont,
)
@ -82,27 +83,27 @@ class QuerylistModel(QAbstractTableModel):
def __repr__(self) -> str:
return f"<QuerylistModel: filter={self.filter}, {self.rowCount()} rows>"
def background_role(self, row: int, column: int, qrow: QueryRow) -> QVariant:
def _background_role(self, row: int, column: int, qrow: QueryRow) -> QBrush:
"""Return background setting"""
# Unreadable track file
if file_is_unreadable(qrow.path):
return QVariant(QColor(Config.COLOUR_UNREADABLE))
return QBrush(QColor(Config.COLOUR_UNREADABLE))
# Selected row
if row in self._selected_rows:
return QVariant(QColor(Config.COLOUR_QUERYLIST_SELECTED))
return QBrush(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))
return QBrush(QColor(Config.COLOUR_BITRATE_LOW))
elif qrow.bitrate < Config.BITRATE_OK_THRESHOLD:
return QVariant(QColor(Config.COLOUR_BITRATE_MEDIUM))
return QBrush(QColor(Config.COLOUR_BITRATE_MEDIUM))
else:
return QVariant(QColor(Config.COLOUR_BITRATE_OK))
return QBrush(QColor(Config.COLOUR_BITRATE_OK))
return QVariant()
return QBrush()
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
"""Standard function for view"""
@ -114,7 +115,23 @@ class QuerylistModel(QAbstractTableModel):
) -> QVariant:
"""Return data to view"""
if not index.isValid() or not (0 <= index.row() < len(self.querylist_rows)):
if (
not index.isValid()
or not (0 <= index.row() < len(self.querylist_rows))
or role
in [
Qt.ItemDataRole.CheckStateRole,
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.WhatsThisRole,
]
):
return QVariant()
row = index.row()
@ -124,48 +141,33 @@ class QuerylistModel(QAbstractTableModel):
# 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,
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:
def _display_role(self, row: int, column: int, qrow: QueryRow) -> str:
"""
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),
QueryCol.ARTIST.value: qrow.artist,
QueryCol.BITRATE.value: str(qrow.bitrate),
QueryCol.DURATION.value: ms_to_mmss(qrow.duration),
QueryCol.LAST_PLAYED.value: get_relative_date(qrow.lastplayed),
QueryCol.TITLE.value: qrow.title,
}
if column in dispatch_table:
return dispatch_table[column]
return QVariant()
return ""
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
"""
@ -266,7 +268,7 @@ class QuerylistModel(QAbstractTableModel):
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:
def _tooltip_role(self, row: int, column: int, rat: RowAndTrack) -> str | QVariant:
"""
Return tooltip. Currently only used for last_played column.
"""
@ -278,7 +280,7 @@ class QuerylistModel(QAbstractTableModel):
if not track_id:
return QVariant()
playdates = Playdates.last_playdates(session, track_id)
return QVariant(
return (
"<br>".join(
[
a.lastplayed.strftime(Config.LAST_PLAYED_TOOLTIP_DATE_FORMAT)

View File

@ -1,850 +0,0 @@
# Form implementation generated from reading ui file 'app/ui/main_window.ui'
#
# Created by: PyQt6 UI code generator 6.8.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(1280, 857)
MainWindow.setMinimumSize(QtCore.QSize(1280, 0))
icon = QtGui.QIcon()
icon.addPixmap(
QtGui.QPixmap(":/icons/musicmuster"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
MainWindow.setWindowIcon(icon)
MainWindow.setStyleSheet("")
self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout_4 = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout_4.setObjectName("gridLayout_4")
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.previous_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.previous_track_2.sizePolicy().hasHeightForWidth()
)
self.previous_track_2.setSizePolicy(sizePolicy)
self.previous_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
self.previous_track_2.setFont(font)
self.previous_track_2.setStyleSheet(
"background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.previous_track_2.setAlignment(
QtCore.Qt.AlignmentFlag.AlignRight
| QtCore.Qt.AlignmentFlag.AlignTrailing
| QtCore.Qt.AlignmentFlag.AlignVCenter
)
self.previous_track_2.setObjectName("previous_track_2")
self.verticalLayout_3.addWidget(self.previous_track_2)
self.current_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.current_track_2.sizePolicy().hasHeightForWidth()
)
self.current_track_2.setSizePolicy(sizePolicy)
self.current_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
self.current_track_2.setFont(font)
self.current_track_2.setStyleSheet(
"background-color: #d4edda;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.current_track_2.setAlignment(
QtCore.Qt.AlignmentFlag.AlignRight
| QtCore.Qt.AlignmentFlag.AlignTrailing
| QtCore.Qt.AlignmentFlag.AlignVCenter
)
self.current_track_2.setObjectName("current_track_2")
self.verticalLayout_3.addWidget(self.current_track_2)
self.next_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.next_track_2.sizePolicy().hasHeightForWidth())
self.next_track_2.setSizePolicy(sizePolicy)
self.next_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
self.next_track_2.setFont(font)
self.next_track_2.setStyleSheet(
"background-color: #fff3cd;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.next_track_2.setAlignment(
QtCore.Qt.AlignmentFlag.AlignRight
| QtCore.Qt.AlignmentFlag.AlignTrailing
| QtCore.Qt.AlignmentFlag.AlignVCenter
)
self.next_track_2.setObjectName("next_track_2")
self.verticalLayout_3.addWidget(self.next_track_2)
self.horizontalLayout_3.addLayout(self.verticalLayout_3)
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.hdrPreviousTrack = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.hdrPreviousTrack.sizePolicy().hasHeightForWidth()
)
self.hdrPreviousTrack.setSizePolicy(sizePolicy)
self.hdrPreviousTrack.setMinimumSize(QtCore.QSize(0, 0))
self.hdrPreviousTrack.setMaximumSize(QtCore.QSize(16777215, 16777215))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
self.hdrPreviousTrack.setFont(font)
self.hdrPreviousTrack.setStyleSheet(
"background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);"
)
self.hdrPreviousTrack.setText("")
self.hdrPreviousTrack.setWordWrap(False)
self.hdrPreviousTrack.setObjectName("hdrPreviousTrack")
self.verticalLayout.addWidget(self.hdrPreviousTrack)
self.hdrCurrentTrack = QtWidgets.QPushButton(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.hdrCurrentTrack.sizePolicy().hasHeightForWidth()
)
self.hdrCurrentTrack.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(20)
self.hdrCurrentTrack.setFont(font)
self.hdrCurrentTrack.setStyleSheet(
"background-color: #d4edda;\n"
"border: 1px solid rgb(85, 87, 83);\n"
"text-align: left;\n"
"padding-left: 8px;\n"
""
)
self.hdrCurrentTrack.setText("")
self.hdrCurrentTrack.setFlat(True)
self.hdrCurrentTrack.setObjectName("hdrCurrentTrack")
self.verticalLayout.addWidget(self.hdrCurrentTrack)
self.hdrNextTrack = QtWidgets.QPushButton(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hdrNextTrack.sizePolicy().hasHeightForWidth())
self.hdrNextTrack.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(20)
self.hdrNextTrack.setFont(font)
self.hdrNextTrack.setStyleSheet(
"background-color: #fff3cd;\n"
"border: 1px solid rgb(85, 87, 83);\n"
"text-align: left;\n"
"padding-left: 8px;"
)
self.hdrNextTrack.setText("")
self.hdrNextTrack.setFlat(True)
self.hdrNextTrack.setObjectName("hdrNextTrack")
self.verticalLayout.addWidget(self.hdrNextTrack)
self.horizontalLayout_3.addLayout(self.verticalLayout)
self.frame_2 = QtWidgets.QFrame(parent=self.centralwidget)
self.frame_2.setMinimumSize(QtCore.QSize(0, 131))
self.frame_2.setMaximumSize(QtCore.QSize(230, 131))
self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_2.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_2.setObjectName("frame_2")
self.verticalLayout_10 = QtWidgets.QVBoxLayout(self.frame_2)
self.verticalLayout_10.setObjectName("verticalLayout_10")
self.lblTOD = QtWidgets.QLabel(parent=self.frame_2)
self.lblTOD.setMinimumSize(QtCore.QSize(208, 0))
font = QtGui.QFont()
font.setPointSize(35)
self.lblTOD.setFont(font)
self.lblTOD.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.lblTOD.setObjectName("lblTOD")
self.verticalLayout_10.addWidget(self.lblTOD)
self.label_elapsed_timer = QtWidgets.QLabel(parent=self.frame_2)
font = QtGui.QFont()
font.setFamily("FreeSans")
font.setPointSize(18)
font.setBold(False)
font.setWeight(50)
self.label_elapsed_timer.setFont(font)
self.label_elapsed_timer.setStyleSheet("color: black;")
self.label_elapsed_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_elapsed_timer.setObjectName("label_elapsed_timer")
self.verticalLayout_10.addWidget(self.label_elapsed_timer)
self.horizontalLayout_3.addWidget(self.frame_2)
self.gridLayout_4.addLayout(self.horizontalLayout_3, 0, 0, 1, 1)
self.frame_4 = QtWidgets.QFrame(parent=self.centralwidget)
self.frame_4.setMinimumSize(QtCore.QSize(0, 16))
self.frame_4.setAutoFillBackground(False)
self.frame_4.setStyleSheet("background-color: rgb(154, 153, 150)")
self.frame_4.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_4.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_4.setObjectName("frame_4")
self.gridLayout_4.addWidget(self.frame_4, 1, 0, 1, 1)
self.cartsWidget = QtWidgets.QWidget(parent=self.centralwidget)
self.cartsWidget.setObjectName("cartsWidget")
self.horizontalLayout_Carts = QtWidgets.QHBoxLayout(self.cartsWidget)
self.horizontalLayout_Carts.setObjectName("horizontalLayout_Carts")
spacerItem = QtWidgets.QSpacerItem(
40,
20,
QtWidgets.QSizePolicy.Policy.Expanding,
QtWidgets.QSizePolicy.Policy.Minimum,
)
self.horizontalLayout_Carts.addItem(spacerItem)
self.gridLayout_4.addWidget(self.cartsWidget, 2, 0, 1, 1)
self.frame_6 = QtWidgets.QFrame(parent=self.centralwidget)
self.frame_6.setMinimumSize(QtCore.QSize(0, 16))
self.frame_6.setAutoFillBackground(False)
self.frame_6.setStyleSheet("background-color: rgb(154, 153, 150)")
self.frame_6.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_6.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_6.setObjectName("frame_6")
self.gridLayout_4.addWidget(self.frame_6, 3, 0, 1, 1)
self.splitter = QtWidgets.QSplitter(parent=self.centralwidget)
self.splitter.setOrientation(QtCore.Qt.Orientation.Vertical)
self.splitter.setObjectName("splitter")
self.tabPlaylist = QtWidgets.QTabWidget(parent=self.splitter)
self.tabPlaylist.setDocumentMode(False)
self.tabPlaylist.setTabsClosable(True)
self.tabPlaylist.setMovable(True)
self.tabPlaylist.setObjectName("tabPlaylist")
self.tabInfolist = InfoTabs(parent=self.splitter)
self.tabInfolist.setDocumentMode(False)
self.tabInfolist.setTabsClosable(True)
self.tabInfolist.setMovable(True)
self.tabInfolist.setTabBarAutoHide(False)
self.tabInfolist.setObjectName("tabInfolist")
self.gridLayout_4.addWidget(self.splitter, 4, 0, 1, 1)
self.InfoFooterFrame = QtWidgets.QFrame(parent=self.centralwidget)
self.InfoFooterFrame.setMaximumSize(QtCore.QSize(16777215, 16777215))
self.InfoFooterFrame.setStyleSheet("background-color: rgb(192, 191, 188)")
self.InfoFooterFrame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.InfoFooterFrame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.InfoFooterFrame.setObjectName("InfoFooterFrame")
self.horizontalLayout = QtWidgets.QHBoxLayout(self.InfoFooterFrame)
self.horizontalLayout.setObjectName("horizontalLayout")
self.FadeStopInfoFrame = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.FadeStopInfoFrame.setMinimumSize(QtCore.QSize(152, 112))
self.FadeStopInfoFrame.setMaximumSize(QtCore.QSize(184, 16777215))
self.FadeStopInfoFrame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.FadeStopInfoFrame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.FadeStopInfoFrame.setObjectName("FadeStopInfoFrame")
self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.FadeStopInfoFrame)
self.verticalLayout_4.setObjectName("verticalLayout_4")
self.btnPreview = QtWidgets.QPushButton(parent=self.FadeStopInfoFrame)
self.btnPreview.setMinimumSize(QtCore.QSize(132, 41))
icon1 = QtGui.QIcon()
icon1.addPixmap(
QtGui.QPixmap(":/icons/headphones"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.btnPreview.setIcon(icon1)
self.btnPreview.setIconSize(QtCore.QSize(30, 30))
self.btnPreview.setCheckable(True)
self.btnPreview.setObjectName("btnPreview")
self.verticalLayout_4.addWidget(self.btnPreview)
self.groupBoxIntroControls = QtWidgets.QGroupBox(parent=self.FadeStopInfoFrame)
self.groupBoxIntroControls.setMinimumSize(QtCore.QSize(132, 46))
self.groupBoxIntroControls.setMaximumSize(QtCore.QSize(132, 46))
self.groupBoxIntroControls.setTitle("")
self.groupBoxIntroControls.setObjectName("groupBoxIntroControls")
self.btnPreviewStart = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewStart.setGeometry(QtCore.QRect(0, 0, 44, 23))
self.btnPreviewStart.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewStart.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewStart.setObjectName("btnPreviewStart")
self.btnPreviewArm = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewArm.setGeometry(QtCore.QRect(44, 0, 44, 23))
self.btnPreviewArm.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewArm.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewArm.setText("")
icon2 = QtGui.QIcon()
icon2.addPixmap(
QtGui.QPixmap(":/icons/record-button.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon2.addPixmap(
QtGui.QPixmap(":/icons/record-red-button.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.On,
)
self.btnPreviewArm.setIcon(icon2)
self.btnPreviewArm.setCheckable(True)
self.btnPreviewArm.setObjectName("btnPreviewArm")
self.btnPreviewEnd = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewEnd.setGeometry(QtCore.QRect(88, 0, 44, 23))
self.btnPreviewEnd.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewEnd.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewEnd.setObjectName("btnPreviewEnd")
self.btnPreviewBack = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewBack.setGeometry(QtCore.QRect(0, 23, 44, 23))
self.btnPreviewBack.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewBack.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewBack.setObjectName("btnPreviewBack")
self.btnPreviewMark = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewMark.setEnabled(False)
self.btnPreviewMark.setGeometry(QtCore.QRect(44, 23, 44, 23))
self.btnPreviewMark.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewMark.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewMark.setText("")
icon3 = QtGui.QIcon()
icon3.addPixmap(
QtGui.QPixmap(":/icons/star.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.On,
)
icon3.addPixmap(
QtGui.QPixmap(":/icons/star_empty.png"),
QtGui.QIcon.Mode.Disabled,
QtGui.QIcon.State.Off,
)
self.btnPreviewMark.setIcon(icon3)
self.btnPreviewMark.setObjectName("btnPreviewMark")
self.btnPreviewFwd = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewFwd.setGeometry(QtCore.QRect(88, 23, 44, 23))
self.btnPreviewFwd.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewFwd.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewFwd.setObjectName("btnPreviewFwd")
self.verticalLayout_4.addWidget(self.groupBoxIntroControls)
self.horizontalLayout.addWidget(self.FadeStopInfoFrame)
self.frame_intro = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_intro.setMinimumSize(QtCore.QSize(152, 112))
self.frame_intro.setStyleSheet("")
self.frame_intro.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_intro.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_intro.setObjectName("frame_intro")
self.verticalLayout_9 = QtWidgets.QVBoxLayout(self.frame_intro)
self.verticalLayout_9.setObjectName("verticalLayout_9")
self.label_7 = QtWidgets.QLabel(parent=self.frame_intro)
self.label_7.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_7.setObjectName("label_7")
self.verticalLayout_9.addWidget(self.label_7)
self.label_intro_timer = QtWidgets.QLabel(parent=self.frame_intro)
font = QtGui.QFont()
font.setFamily("FreeSans")
font.setPointSize(40)
font.setBold(False)
font.setWeight(50)
self.label_intro_timer.setFont(font)
self.label_intro_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_intro_timer.setObjectName("label_intro_timer")
self.verticalLayout_9.addWidget(self.label_intro_timer)
self.horizontalLayout.addWidget(self.frame_intro)
self.frame_toggleplayed_3db = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_toggleplayed_3db.setMinimumSize(QtCore.QSize(152, 112))
self.frame_toggleplayed_3db.setMaximumSize(QtCore.QSize(184, 16777215))
self.frame_toggleplayed_3db.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_toggleplayed_3db.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_toggleplayed_3db.setObjectName("frame_toggleplayed_3db")
self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.frame_toggleplayed_3db)
self.verticalLayout_6.setObjectName("verticalLayout_6")
self.btnDrop3db = QtWidgets.QPushButton(parent=self.frame_toggleplayed_3db)
self.btnDrop3db.setMinimumSize(QtCore.QSize(132, 41))
self.btnDrop3db.setMaximumSize(QtCore.QSize(164, 16777215))
self.btnDrop3db.setCheckable(True)
self.btnDrop3db.setObjectName("btnDrop3db")
self.verticalLayout_6.addWidget(self.btnDrop3db)
self.btnHidePlayed = QtWidgets.QPushButton(parent=self.frame_toggleplayed_3db)
self.btnHidePlayed.setMinimumSize(QtCore.QSize(132, 41))
self.btnHidePlayed.setMaximumSize(QtCore.QSize(164, 16777215))
self.btnHidePlayed.setCheckable(True)
self.btnHidePlayed.setObjectName("btnHidePlayed")
self.verticalLayout_6.addWidget(self.btnHidePlayed)
self.horizontalLayout.addWidget(self.frame_toggleplayed_3db)
self.frame_fade = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_fade.setMinimumSize(QtCore.QSize(152, 112))
self.frame_fade.setStyleSheet("")
self.frame_fade.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_fade.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_fade.setObjectName("frame_fade")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame_fade)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.label_4 = QtWidgets.QLabel(parent=self.frame_fade)
self.label_4.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_4.setObjectName("label_4")
self.verticalLayout_2.addWidget(self.label_4)
self.label_fade_timer = QtWidgets.QLabel(parent=self.frame_fade)
font = QtGui.QFont()
font.setFamily("FreeSans")
font.setPointSize(40)
font.setBold(False)
font.setWeight(50)
self.label_fade_timer.setFont(font)
self.label_fade_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_fade_timer.setObjectName("label_fade_timer")
self.verticalLayout_2.addWidget(self.label_fade_timer)
self.horizontalLayout.addWidget(self.frame_fade)
self.frame_silent = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_silent.setMinimumSize(QtCore.QSize(152, 112))
self.frame_silent.setStyleSheet("")
self.frame_silent.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_silent.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_silent.setObjectName("frame_silent")
self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.frame_silent)
self.verticalLayout_7.setObjectName("verticalLayout_7")
self.label_5 = QtWidgets.QLabel(parent=self.frame_silent)
self.label_5.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_5.setObjectName("label_5")
self.verticalLayout_7.addWidget(self.label_5)
self.label_silent_timer = QtWidgets.QLabel(parent=self.frame_silent)
font = QtGui.QFont()
font.setFamily("FreeSans")
font.setPointSize(40)
font.setBold(False)
font.setWeight(50)
self.label_silent_timer.setFont(font)
self.label_silent_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_silent_timer.setObjectName("label_silent_timer")
self.verticalLayout_7.addWidget(self.label_silent_timer)
self.horizontalLayout.addWidget(self.frame_silent)
self.widgetFadeVolume = PlotWidget(parent=self.InfoFooterFrame)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Policy.Preferred,
QtWidgets.QSizePolicy.Policy.Preferred,
)
sizePolicy.setHorizontalStretch(1)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.widgetFadeVolume.sizePolicy().hasHeightForWidth()
)
self.widgetFadeVolume.setSizePolicy(sizePolicy)
self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0))
self.widgetFadeVolume.setObjectName("widgetFadeVolume")
self.horizontalLayout.addWidget(self.widgetFadeVolume)
self.frame = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame.setMinimumSize(QtCore.QSize(151, 0))
self.frame.setMaximumSize(QtCore.QSize(151, 112))
self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame.setObjectName("frame")
self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.frame)
self.verticalLayout_5.setObjectName("verticalLayout_5")
self.btnFade = QtWidgets.QPushButton(parent=self.frame)
self.btnFade.setMinimumSize(QtCore.QSize(132, 32))
self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215))
icon4 = QtGui.QIcon()
icon4.addPixmap(
QtGui.QPixmap(":/icons/fade"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.btnFade.setIcon(icon4)
self.btnFade.setIconSize(QtCore.QSize(30, 30))
self.btnFade.setObjectName("btnFade")
self.verticalLayout_5.addWidget(self.btnFade)
self.btnStop = QtWidgets.QPushButton(parent=self.frame)
self.btnStop.setMinimumSize(QtCore.QSize(0, 36))
icon5 = QtGui.QIcon()
icon5.addPixmap(
QtGui.QPixmap(":/icons/stopsign"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.btnStop.setIcon(icon5)
self.btnStop.setObjectName("btnStop")
self.verticalLayout_5.addWidget(self.btnStop)
self.horizontalLayout.addWidget(self.frame)
self.gridLayout_4.addWidget(self.InfoFooterFrame, 5, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1280, 29))
self.menubar.setObjectName("menubar")
self.menuFile = QtWidgets.QMenu(parent=self.menubar)
self.menuFile.setObjectName("menuFile")
self.menuPlaylist = QtWidgets.QMenu(parent=self.menubar)
self.menuPlaylist.setObjectName("menuPlaylist")
self.menuSearc_h = QtWidgets.QMenu(parent=self.menubar)
self.menuSearc_h.setObjectName("menuSearc_h")
self.menuHelp = QtWidgets.QMenu(parent=self.menubar)
self.menuHelp.setObjectName("menuHelp")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
self.statusbar.setEnabled(True)
self.statusbar.setStyleSheet("background-color: rgb(211, 215, 207);")
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.actionPlay_next = QtGui.QAction(parent=MainWindow)
icon6 = QtGui.QIcon()
icon6.addPixmap(
QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-play.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionPlay_next.setIcon(icon6)
self.actionPlay_next.setObjectName("actionPlay_next")
self.actionSkipToNext = QtGui.QAction(parent=MainWindow)
icon7 = QtGui.QIcon()
icon7.addPixmap(
QtGui.QPixmap(":/icons/next"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionSkipToNext.setIcon(icon7)
self.actionSkipToNext.setObjectName("actionSkipToNext")
self.actionInsertTrack = QtGui.QAction(parent=MainWindow)
icon8 = QtGui.QIcon()
icon8.addPixmap(
QtGui.QPixmap(
"app/ui/../../../../../../.designer/backup/icon_search_database.png"
),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionInsertTrack.setIcon(icon8)
self.actionInsertTrack.setObjectName("actionInsertTrack")
self.actionAdd_file = QtGui.QAction(parent=MainWindow)
icon9 = QtGui.QIcon()
icon9.addPixmap(
QtGui.QPixmap(
"app/ui/../../../../../../.designer/backup/icon_open_file.png"
),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionAdd_file.setIcon(icon9)
self.actionAdd_file.setObjectName("actionAdd_file")
self.actionFade = QtGui.QAction(parent=MainWindow)
icon10 = QtGui.QIcon()
icon10.addPixmap(
QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-fade.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionFade.setIcon(icon10)
self.actionFade.setObjectName("actionFade")
self.actionStop = QtGui.QAction(parent=MainWindow)
icon11 = QtGui.QIcon()
icon11.addPixmap(
QtGui.QPixmap(":/icons/stop"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.actionStop.setIcon(icon11)
self.actionStop.setObjectName("actionStop")
self.action_Clear_selection = QtGui.QAction(parent=MainWindow)
self.action_Clear_selection.setObjectName("action_Clear_selection")
self.action_Resume_previous = QtGui.QAction(parent=MainWindow)
icon12 = QtGui.QIcon()
icon12.addPixmap(
QtGui.QPixmap(":/icons/previous"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.action_Resume_previous.setIcon(icon12)
self.action_Resume_previous.setObjectName("action_Resume_previous")
self.actionE_xit = QtGui.QAction(parent=MainWindow)
self.actionE_xit.setObjectName("actionE_xit")
self.actionTest = QtGui.QAction(parent=MainWindow)
self.actionTest.setObjectName("actionTest")
self.actionOpenPlaylist = QtGui.QAction(parent=MainWindow)
self.actionOpenPlaylist.setObjectName("actionOpenPlaylist")
self.actionNewPlaylist = QtGui.QAction(parent=MainWindow)
self.actionNewPlaylist.setObjectName("actionNewPlaylist")
self.actionTestFunction = QtGui.QAction(parent=MainWindow)
self.actionTestFunction.setObjectName("actionTestFunction")
self.actionSkipToFade = QtGui.QAction(parent=MainWindow)
self.actionSkipToFade.setObjectName("actionSkipToFade")
self.actionSkipToEnd = QtGui.QAction(parent=MainWindow)
self.actionSkipToEnd.setObjectName("actionSkipToEnd")
self.actionClosePlaylist = QtGui.QAction(parent=MainWindow)
self.actionClosePlaylist.setEnabled(True)
self.actionClosePlaylist.setObjectName("actionClosePlaylist")
self.actionRenamePlaylist = QtGui.QAction(parent=MainWindow)
self.actionRenamePlaylist.setEnabled(True)
self.actionRenamePlaylist.setObjectName("actionRenamePlaylist")
self.actionDeletePlaylist = QtGui.QAction(parent=MainWindow)
self.actionDeletePlaylist.setEnabled(True)
self.actionDeletePlaylist.setObjectName("actionDeletePlaylist")
self.actionMoveSelected = QtGui.QAction(parent=MainWindow)
self.actionMoveSelected.setObjectName("actionMoveSelected")
self.actionExport_playlist = QtGui.QAction(parent=MainWindow)
self.actionExport_playlist.setObjectName("actionExport_playlist")
self.actionSetNext = QtGui.QAction(parent=MainWindow)
self.actionSetNext.setObjectName("actionSetNext")
self.actionSelect_next_track = QtGui.QAction(parent=MainWindow)
self.actionSelect_next_track.setObjectName("actionSelect_next_track")
self.actionSelect_previous_track = QtGui.QAction(parent=MainWindow)
self.actionSelect_previous_track.setObjectName("actionSelect_previous_track")
self.actionSelect_played_tracks = QtGui.QAction(parent=MainWindow)
self.actionSelect_played_tracks.setObjectName("actionSelect_played_tracks")
self.actionMoveUnplayed = QtGui.QAction(parent=MainWindow)
self.actionMoveUnplayed.setObjectName("actionMoveUnplayed")
self.actionAdd_note = QtGui.QAction(parent=MainWindow)
self.actionAdd_note.setObjectName("actionAdd_note")
self.actionEnable_controls = QtGui.QAction(parent=MainWindow)
self.actionEnable_controls.setObjectName("actionEnable_controls")
self.actionImport = QtGui.QAction(parent=MainWindow)
self.actionImport.setObjectName("actionImport")
self.actionDownload_CSV_of_played_tracks = QtGui.QAction(parent=MainWindow)
self.actionDownload_CSV_of_played_tracks.setObjectName(
"actionDownload_CSV_of_played_tracks"
)
self.actionSearch = QtGui.QAction(parent=MainWindow)
self.actionSearch.setObjectName("actionSearch")
self.actionInsertSectionHeader = QtGui.QAction(parent=MainWindow)
self.actionInsertSectionHeader.setObjectName("actionInsertSectionHeader")
self.actionRemove = QtGui.QAction(parent=MainWindow)
self.actionRemove.setObjectName("actionRemove")
self.actionFind_next = QtGui.QAction(parent=MainWindow)
self.actionFind_next.setObjectName("actionFind_next")
self.actionFind_previous = QtGui.QAction(parent=MainWindow)
self.actionFind_previous.setObjectName("actionFind_previous")
self.action_About = QtGui.QAction(parent=MainWindow)
self.action_About.setObjectName("action_About")
self.actionSave_as_template = QtGui.QAction(parent=MainWindow)
self.actionSave_as_template.setObjectName("actionSave_as_template")
self.actionManage_templates = QtGui.QAction(parent=MainWindow)
self.actionManage_templates.setObjectName("actionManage_templates")
self.actionDebug = QtGui.QAction(parent=MainWindow)
self.actionDebug.setObjectName("actionDebug")
self.actionAdd_cart = QtGui.QAction(parent=MainWindow)
self.actionAdd_cart.setObjectName("actionAdd_cart")
self.actionMark_for_moving = QtGui.QAction(parent=MainWindow)
self.actionMark_for_moving.setObjectName("actionMark_for_moving")
self.actionPaste = QtGui.QAction(parent=MainWindow)
self.actionPaste.setObjectName("actionPaste")
self.actionResume = QtGui.QAction(parent=MainWindow)
self.actionResume.setObjectName("actionResume")
self.actionSearch_title_in_Wikipedia = QtGui.QAction(parent=MainWindow)
self.actionSearch_title_in_Wikipedia.setObjectName(
"actionSearch_title_in_Wikipedia"
)
self.actionSearch_title_in_Songfacts = QtGui.QAction(parent=MainWindow)
self.actionSearch_title_in_Songfacts.setObjectName(
"actionSearch_title_in_Songfacts"
)
self.actionSelect_duplicate_rows = QtGui.QAction(parent=MainWindow)
self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows")
self.actionImport_files = QtGui.QAction(parent=MainWindow)
self.actionImport_files.setObjectName("actionImport_files")
self.actionOpenQuerylist = QtGui.QAction(parent=MainWindow)
self.actionOpenQuerylist.setObjectName("actionOpenQuerylist")
self.actionManage_querylists = QtGui.QAction(parent=MainWindow)
self.actionManage_querylists.setObjectName("actionManage_querylists")
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionInsertTrack)
self.menuFile.addAction(self.actionRemove)
self.menuFile.addAction(self.actionInsertSectionHeader)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionMark_for_moving)
self.menuFile.addAction(self.actionPaste)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionExport_playlist)
self.menuFile.addAction(self.actionDownload_CSV_of_played_tracks)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionSelect_duplicate_rows)
self.menuFile.addAction(self.actionMoveSelected)
self.menuFile.addAction(self.actionMoveUnplayed)
self.menuFile.addAction(self.action_Clear_selection)
self.menuPlaylist.addSeparator()
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionOpenPlaylist)
self.menuPlaylist.addAction(self.actionNewPlaylist)
self.menuPlaylist.addAction(self.actionClosePlaylist)
self.menuPlaylist.addAction(self.actionRenamePlaylist)
self.menuPlaylist.addAction(self.actionDeletePlaylist)
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionOpenQuerylist)
self.menuPlaylist.addAction(self.actionManage_querylists)
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionSave_as_template)
self.menuPlaylist.addAction(self.actionManage_templates)
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionImport_files)
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionE_xit)
self.menuSearc_h.addAction(self.actionSetNext)
self.menuSearc_h.addAction(self.actionPlay_next)
self.menuSearc_h.addAction(self.actionFade)
self.menuSearc_h.addAction(self.actionStop)
self.menuSearc_h.addAction(self.actionResume)
self.menuSearc_h.addAction(self.actionSkipToNext)
self.menuSearc_h.addSeparator()
self.menuSearc_h.addAction(self.actionSearch)
self.menuSearc_h.addAction(self.actionSearch_title_in_Wikipedia)
self.menuSearc_h.addAction(self.actionSearch_title_in_Songfacts)
self.menuHelp.addAction(self.action_About)
self.menuHelp.addAction(self.actionDebug)
self.menubar.addAction(self.menuPlaylist.menuAction())
self.menubar.addAction(self.menuFile.menuAction())
self.menubar.addAction(self.menuSearc_h.menuAction())
self.menubar.addAction(self.menuHelp.menuAction())
self.retranslateUi(MainWindow)
self.tabPlaylist.setCurrentIndex(-1)
self.tabInfolist.setCurrentIndex(-1)
self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Music Muster"))
self.previous_track_2.setText(_translate("MainWindow", "Last track:"))
self.current_track_2.setText(_translate("MainWindow", "Current track:"))
self.next_track_2.setText(_translate("MainWindow", "Next track:"))
self.lblTOD.setText(_translate("MainWindow", "00:00:00"))
self.label_elapsed_timer.setText(_translate("MainWindow", "00:00 / 00:00"))
self.btnPreview.setText(_translate("MainWindow", " Preview"))
self.btnPreviewStart.setText(_translate("MainWindow", "<<"))
self.btnPreviewEnd.setText(_translate("MainWindow", ">>"))
self.btnPreviewBack.setText(_translate("MainWindow", "<"))
self.btnPreviewFwd.setText(_translate("MainWindow", ">"))
self.label_7.setText(_translate("MainWindow", "Intro"))
self.label_intro_timer.setText(_translate("MainWindow", "0:0"))
self.btnDrop3db.setText(_translate("MainWindow", "-3dB to talk"))
self.btnHidePlayed.setText(_translate("MainWindow", "Hide played"))
self.label_4.setText(_translate("MainWindow", "Fade"))
self.label_fade_timer.setText(_translate("MainWindow", "00:00"))
self.label_5.setText(_translate("MainWindow", "Silent"))
self.label_silent_timer.setText(_translate("MainWindow", "00:00"))
self.btnFade.setText(_translate("MainWindow", " Fade"))
self.btnStop.setText(_translate("MainWindow", " Stop"))
self.menuFile.setTitle(_translate("MainWindow", "&Playlist"))
self.menuPlaylist.setTitle(_translate("MainWindow", "&File"))
self.menuSearc_h.setTitle(_translate("MainWindow", "&Music"))
self.menuHelp.setTitle(_translate("MainWindow", "Help"))
self.actionPlay_next.setText(_translate("MainWindow", "&Play next"))
self.actionPlay_next.setShortcut(_translate("MainWindow", "Return"))
self.actionSkipToNext.setText(_translate("MainWindow", "Skip to &next"))
self.actionSkipToNext.setShortcut(_translate("MainWindow", "Ctrl+Alt+Return"))
self.actionInsertTrack.setText(_translate("MainWindow", "Insert &track..."))
self.actionInsertTrack.setShortcut(_translate("MainWindow", "Ctrl+T"))
self.actionAdd_file.setText(_translate("MainWindow", "Add &file"))
self.actionAdd_file.setShortcut(_translate("MainWindow", "Ctrl+F"))
self.actionFade.setText(_translate("MainWindow", "F&ade"))
self.actionFade.setShortcut(_translate("MainWindow", "Ctrl+Z"))
self.actionStop.setText(_translate("MainWindow", "S&top"))
self.actionStop.setShortcut(_translate("MainWindow", "Ctrl+Alt+S"))
self.action_Clear_selection.setText(
_translate("MainWindow", "Clear &selection")
)
self.action_Clear_selection.setShortcut(_translate("MainWindow", "Esc"))
self.action_Resume_previous.setText(
_translate("MainWindow", "&Resume previous")
)
self.actionE_xit.setText(_translate("MainWindow", "E&xit"))
self.actionTest.setText(_translate("MainWindow", "&Test"))
self.actionOpenPlaylist.setText(_translate("MainWindow", "O&pen..."))
self.actionNewPlaylist.setText(_translate("MainWindow", "&New..."))
self.actionTestFunction.setText(_translate("MainWindow", "&Test function"))
self.actionSkipToFade.setText(
_translate("MainWindow", "&Skip to start of fade")
)
self.actionSkipToEnd.setText(_translate("MainWindow", "Skip to &end of track"))
self.actionClosePlaylist.setText(_translate("MainWindow", "&Close"))
self.actionRenamePlaylist.setText(_translate("MainWindow", "&Rename..."))
self.actionDeletePlaylist.setText(_translate("MainWindow", "Dele&te..."))
self.actionMoveSelected.setText(
_translate("MainWindow", "Mo&ve selected tracks to...")
)
self.actionExport_playlist.setText(_translate("MainWindow", "E&xport..."))
self.actionSetNext.setText(_translate("MainWindow", "Set &next"))
self.actionSetNext.setShortcut(_translate("MainWindow", "Ctrl+N"))
self.actionSelect_next_track.setText(
_translate("MainWindow", "Select next track")
)
self.actionSelect_next_track.setShortcut(_translate("MainWindow", "J"))
self.actionSelect_previous_track.setText(
_translate("MainWindow", "Select previous track")
)
self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K"))
self.actionSelect_played_tracks.setText(
_translate("MainWindow", "Select played tracks")
)
self.actionMoveUnplayed.setText(
_translate("MainWindow", "Move &unplayed tracks to...")
)
self.actionAdd_note.setText(_translate("MainWindow", "Add note..."))
self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T"))
self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls"))
self.actionImport.setText(_translate("MainWindow", "Import track..."))
self.actionImport.setShortcut(_translate("MainWindow", "Ctrl+Shift+I"))
self.actionDownload_CSV_of_played_tracks.setText(
_translate("MainWindow", "Download CSV of played tracks...")
)
self.actionSearch.setText(_translate("MainWindow", "Search..."))
self.actionSearch.setShortcut(_translate("MainWindow", "/"))
self.actionInsertSectionHeader.setText(
_translate("MainWindow", "Insert &section header...")
)
self.actionInsertSectionHeader.setShortcut(_translate("MainWindow", "Ctrl+H"))
self.actionRemove.setText(_translate("MainWindow", "&Remove track"))
self.actionFind_next.setText(_translate("MainWindow", "Find next"))
self.actionFind_next.setShortcut(_translate("MainWindow", "N"))
self.actionFind_previous.setText(_translate("MainWindow", "Find previous"))
self.actionFind_previous.setShortcut(_translate("MainWindow", "P"))
self.action_About.setText(_translate("MainWindow", "&About"))
self.actionSave_as_template.setText(
_translate("MainWindow", "Save as template...")
)
self.actionManage_templates.setText(
_translate("MainWindow", "Manage templates...")
)
self.actionDebug.setText(_translate("MainWindow", "Debug"))
self.actionAdd_cart.setText(_translate("MainWindow", "Edit cart &1..."))
self.actionMark_for_moving.setText(_translate("MainWindow", "Mark for moving"))
self.actionMark_for_moving.setShortcut(_translate("MainWindow", "Ctrl+C"))
self.actionPaste.setText(_translate("MainWindow", "Paste"))
self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V"))
self.actionResume.setText(_translate("MainWindow", "Resume"))
self.actionResume.setShortcut(_translate("MainWindow", "Ctrl+R"))
self.actionSearch_title_in_Wikipedia.setText(
_translate("MainWindow", "Search title in Wikipedia")
)
self.actionSearch_title_in_Wikipedia.setShortcut(
_translate("MainWindow", "Ctrl+W")
)
self.actionSearch_title_in_Songfacts.setText(
_translate("MainWindow", "Search title in Songfacts")
)
self.actionSearch_title_in_Songfacts.setShortcut(
_translate("MainWindow", "Ctrl+S")
)
self.actionSelect_duplicate_rows.setText(
_translate("MainWindow", "Select duplicate rows...")
)
self.actionImport_files.setText(_translate("MainWindow", "Import files..."))
from infotabs import InfoTabs
from pyqtgraph import PlotWidget # type: ignore

View File

@ -30,6 +30,8 @@ dependencies = [
"types-psutil>=6.0.0.20240621",
"pyyaml (>=6.0.2,<7.0.0)",
"audioop-lts>=0.2.1",
"types-pyyaml>=6.0.12.20241230",
"dogpile-cache>=1.3.4",
]
[dependency-groups]

View File

@ -21,7 +21,9 @@ from app.models import (
class TestMMModels(unittest.TestCase):
def setUp(self):
"""Runs before each test"""
db.create_all()
NoteColours.invalidate_cache()
with db.Session() as session:
track1_path = "testdata/isa.mp3"
@ -31,6 +33,7 @@ class TestMMModels(unittest.TestCase):
self.track2 = Tracks(session, **helpers.get_all_track_metadata(track2_path))
def tearDown(self):
"""Runs after each test"""
db.drop_all()
def test_track_repr(self):
@ -70,7 +73,7 @@ class TestMMModels(unittest.TestCase):
NoteColours(session, substring="substring", colour=note_colour)
result = NoteColours.get_colour(session, "xyz")
assert result is None
assert result == ""
def test_notecolours_get_colour_match(self):
note_colour = "#4bcdef"
@ -200,7 +203,7 @@ class TestMMModels(unittest.TestCase):
nc = NoteColours(session, substring="x", colour="x")
_ = str(nc)
def test_get_colour(self):
def test_get_colour_1(self):
"""Test for errors in execution"""
GOOD_STRING = "cantelope"
@ -213,22 +216,42 @@ class TestMMModels(unittest.TestCase):
session, substring=SUBSTR, colour=COLOUR, is_casesensitive=True
)
session.commit()
_ = nc1.get_colour(session, "")
colour = nc1.get_colour(session, GOOD_STRING)
assert colour == COLOUR
colour = nc1.get_colour(session, BAD_STRING)
assert colour is None
assert colour == ""
def test_get_colour_2(self):
"""Test for errors in execution"""
GOOD_STRING = "cantelope"
BAD_STRING = "ericTheBee"
SUBSTR = "ant"
COLOUR = "blue"
with db.Session() as session:
nc2 = NoteColours(
session, substring=".*" + SUBSTR, colour=COLOUR, is_regex=True
)
session.commit()
colour = nc2.get_colour(session, GOOD_STRING)
assert colour == COLOUR
colour = nc2.get_colour(session, BAD_STRING)
assert colour is None
assert colour == ""
def test_get_colour_3(self):
"""Test for errors in execution"""
GOOD_STRING = "cantelope"
BAD_STRING = "ericTheBee"
SUBSTR = "ant"
COLOUR = "blue"
with db.Session() as session:
nc3 = NoteColours(
session,
substring=".*" + SUBSTR,
@ -236,12 +259,13 @@ class TestMMModels(unittest.TestCase):
is_regex=True,
is_casesensitive=True,
)
session.commit()
colour = nc3.get_colour(session, GOOD_STRING)
assert colour == COLOUR
colour = nc3.get_colour(session, BAD_STRING)
assert colour is None
assert colour == ""
def test_name_available(self):
PLAYLIST_NAME = "a name"

View File

@ -66,10 +66,10 @@ class TestMMMiscTracks(unittest.TestCase):
self.model.insert_row(proposed_row_number=END_ROW, note="-")
prd = self.model.playlist_rows[START_ROW]
qv_value = self.model.display_role(
qv_value = self.model._display_role(
START_ROW, playlistmodel.HEADER_NOTES_COLUMN, prd
)
assert qv_value.value() == "start [1 tracks, 4:23 unplayed]"
assert qv_value == "start [1 tracks, 4:23 unplayed]"
class TestMMMiscNoPlaylist(unittest.TestCase):
@ -109,7 +109,7 @@ class TestMMMiscNoPlaylist(unittest.TestCase):
_ = str(prd)
assert (
model.edit_role(
model._edit_role(
model.rowCount() - 1, playlistmodel.Col.TITLE.value, prd
)
== metadata["title"]
@ -262,7 +262,7 @@ class TestMMMiscRowMove(unittest.TestCase):
# Test against edit_role because display_role for headers is
# handled differently (sets up row span)
assert (
self.model.edit_role(
self.model._edit_role(
self.model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd
)
== note_text
@ -280,7 +280,7 @@ class TestMMMiscRowMove(unittest.TestCase):
# Test against edit_role because display_role for headers is
# handled differently (sets up row span)
assert (
self.model.edit_role(
self.model._edit_role(
self.model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd
)
== note_text
@ -353,7 +353,7 @@ class TestMMMiscRowMove(unittest.TestCase):
index = model_dst.index(
row_number, playlistmodel.Col.TITLE.value, QModelIndex()
)
row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value())
row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole))
assert len(model_src.playlist_rows) == self.ROWS_TO_CREATE - len(from_rows)
assert len(model_dst.playlist_rows) == self.ROWS_TO_CREATE + len(from_rows)
@ -380,7 +380,7 @@ class TestMMMiscRowMove(unittest.TestCase):
index = model_dst.index(
row_number, playlistmodel.Col.TITLE.value, QModelIndex()
)
row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value())
row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole))
assert len(model_src.playlist_rows) == self.ROWS_TO_CREATE - len(from_rows)
assert len(model_dst.playlist_rows) == self.ROWS_TO_CREATE + len(from_rows)

59
uv.lock
View File

@ -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 },
]
[[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]]
name = "entrypoints"
version = "0.4"
@ -449,6 +462,7 @@ dependencies = [
{ name = "alembic" },
{ name = "audioop-lts" },
{ name = "colorlog" },
{ name = "dogpile-cache" },
{ name = "fuzzywuzzy" },
{ name = "mutagen" },
{ name = "mysqlclient" },
@ -469,6 +483,7 @@ dependencies = [
{ name = "stackprinter" },
{ name = "tinytag" },
{ name = "types-psutil" },
{ name = "types-pyyaml" },
]
[package.dev-dependencies]
@ -491,6 +506,7 @@ requires-dist = [
{ name = "alembic", specifier = ">=1.14.0" },
{ name = "audioop-lts", specifier = ">=0.2.1" },
{ name = "colorlog", specifier = ">=6.9.0" },
{ name = "dogpile-cache", specifier = ">=1.3.4" },
{ name = "fuzzywuzzy", specifier = ">=0.18.0" },
{ name = "mutagen", specifier = ">=1.47.0" },
{ name = "mysqlclient", specifier = ">=2.2.5" },
@ -511,6 +527,7 @@ requires-dist = [
{ name = "stackprinter", specifier = ">=0.2.10" },
{ name = "tinytag", specifier = ">=1.10.1" },
{ name = "types-psutil", specifier = ">=6.0.0.20240621" },
{ name = "types-pyyaml", specifier = ">=6.0.12.20241230" },
]
[package.metadata.requires-dev]
@ -639,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 },
]
[[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]]
name = "pexpect"
version = "4.9.0"
@ -1026,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 },
]
[[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]]
name = "sqlalchemy"
version = "2.0.38"
@ -1070,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 },
]
[[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]]
name = "text-unidecode"
version = "1.3"
@ -1115,6 +1165,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/50/c8/f4365293408da4a9bcb1849d3efd8c60427cffff68cbb98ab1b81851d8bb/types_psutil-7.0.0.20250218-py3-none-any.whl", hash = "sha256:1447a30c282aafefcf8941ece854e1100eee7b0296a9d9be9977292f0269b121", size = 22763 },
]
[[package]]
name = "types-pyyaml"
version = "6.0.12.20241230"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/f9/4d566925bcf9396136c0a2e5dc7e230ff08d86fa011a69888dd184469d80/types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c", size = 17078 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/c1/48474fbead512b70ccdb4f81ba5eb4a58f69d100ba19f17c92c0c4f50ae6/types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6", size = 20029 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"