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 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"

View File

@ -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)
@ -130,7 +130,6 @@ class PlaylistRowsTable(Model):
) )
playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows") 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( track_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("tracks.id", ondelete="CASCADE") ForeignKey("tracks.id", ondelete="CASCADE")
) )

View File

@ -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,34 @@ 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 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) 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 # 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 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 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,16 @@ 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 ""
@staticmethod
def invalidate_cache() -> None:
"""Invalidate dogpile cache"""
cache_region.delete("note_colours_all")
class Playdates(dbtables.PlaydatesTable): class Playdates(dbtables.PlaydatesTable):

View File

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

View File

@ -767,22 +767,14 @@ class PreviewManager:
self.intro = ms self.intro = ms
def set_track_info(self, info: TrackInfo) -> None: def set_track_info(self, track_id: int, track_intro: int, track_path: str) -> None:
self.track_id = info.track_id self.track_id = track_id
self.row_number = info.row_number self.intro = track_intro
self.path = track_path
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
# Check file readable # Check file readable
if file_is_unreadable(self.path): 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) mixer.music.load(self.path)
@ -790,7 +782,6 @@ class PreviewManager:
mixer.music.stop() mixer.music.stop()
mixer.music.unload() mixer.music.unload()
self.path = "" self.path = ""
self.row_number = None
self.track_id = 0 self.track_id = 0
self.start_time = None self.start_time = None
@ -2123,7 +2114,7 @@ class Window(QMainWindow):
webbrowser.get("browser").open_new_tab(url) 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. Paste earlier cut rows.
""" """
@ -2263,15 +2254,33 @@ class Window(QMainWindow):
return return
if not track_info: if not track_info:
return return
self.preview_manager.set_track_info(track_info) 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.preview_manager.play()
self.show_status_message(
f"Preview: {track.title} / {track.artist} (row {track_info.row_number})",
0
)
else: else:
self.preview_manager.stop() self.preview_manager.stop()
self.show_status_message("", 0)
def preview_arm(self): def preview_arm(self):
"""Manager arm button for setting intro length""" """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: def preview_back(self) -> None:
"""Wind back preview file""" """Wind back preview file"""
@ -2308,7 +2317,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"""
@ -2528,10 +2540,14 @@ class Window(QMainWindow):
def show_status_message(self, message: str, timing: int) -> None: def show_status_message(self, message: str, timing: int) -> None:
""" """
Show status message in status bar for timing milliseconds Show status message in status bar for timing milliseconds
Clear message if message is null string
""" """
if self.statusbar: if self.statusbar:
if message:
self.statusbar.showMessage(message, timing) self.statusbar.showMessage(message, timing)
else:
self.statusbar.clearMessage()
def show_track(self, playlist_track: RowAndTrack) -> None: def show_track(self, playlist_track: RowAndTrack) -> None:
"""Scroll to show track in plt""" """Scroll to show track in plt"""
@ -2811,8 +2827,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(
@ -2830,6 +2846,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
@ -2843,10 +2860,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")

View File

@ -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,
@ -76,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()
@ -101,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
@ -119,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
@ -152,42 +150,52 @@ 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)
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""" """Return background setting"""
# Handle entire row colouring # Handle entire row colouring
# 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
@ -221,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()
@ -257,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")
@ -290,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()
@ -315,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 | int:
"""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,
@ -352,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:
@ -385,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
""" """
@ -406,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:
""" """
@ -461,37 +478,38 @@ 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 | int:
""" """
Return text for editing Return value for editing
""" """
# 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 rat.intro or 0
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()
@ -518,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]:
""" """
@ -619,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(
@ -627,22 +644,22 @@ class PlaylistModel(QAbstractTableModel):
section: int, section: int,
orientation: Qt.Orientation, orientation: Qt.Orientation,
role: int = Qt.ItemDataRole.DisplayRole, role: int = Qt.ItemDataRole.DisplayRole,
) -> QVariant: ) -> str | int | QFont | QVariant:
""" """
Return text for headers Return text for headers
""" """
display_dispatch_table = { display_dispatch_table = {
Col.START_GAP.value: QVariant(Config.HEADER_START_GAP), Col.START_GAP.value: Config.HEADER_START_GAP,
Col.INTRO.value: QVariant(Config.HEADER_INTRO), Col.INTRO.value: Config.HEADER_INTRO,
Col.TITLE.value: QVariant(Config.HEADER_TITLE), Col.TITLE.value: Config.HEADER_TITLE,
Col.ARTIST.value: QVariant(Config.HEADER_ARTIST), Col.ARTIST.value: Config.HEADER_ARTIST,
Col.DURATION.value: QVariant(Config.HEADER_DURATION), Col.DURATION.value: Config.HEADER_DURATION,
Col.START_TIME.value: QVariant(Config.HEADER_START_TIME), Col.START_TIME.value: Config.HEADER_START_TIME,
Col.END_TIME.value: QVariant(Config.HEADER_END_TIME), Col.END_TIME.value: Config.HEADER_END_TIME,
Col.LAST_PLAYED.value: QVariant(Config.HEADER_LAST_PLAYED), Col.LAST_PLAYED.value: Config.HEADER_LAST_PLAYED,
Col.BITRATE.value: QVariant(Config.HEADER_BITRATE), Col.BITRATE.value: Config.HEADER_BITRATE,
Col.NOTE.value: QVariant(Config.HEADER_NOTE), Col.NOTE.value: Config.HEADER_NOTE,
} }
if role == Qt.ItemDataRole.DisplayRole: if role == Qt.ItemDataRole.DisplayRole:
@ -650,14 +667,14 @@ class PlaylistModel(QAbstractTableModel):
return display_dispatch_table[section] return display_dispatch_table[section]
else: else:
if Config.ROWS_FROM_ZERO: if Config.ROWS_FROM_ZERO:
return QVariant(str(section)) return section
else: else:
return QVariant(str(section + 1)) return section + 1
elif role == Qt.ItemDataRole.FontRole: elif role == Qt.ItemDataRole.FontRole:
boldfont = QFont() boldfont = QFont()
boldfont.setBold(True) boldfont.setBold(True)
return QVariant(boldfont) return boldfont
return QVariant() return QVariant()
@ -695,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,
@ -727,25 +748,38 @@ 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)
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 Signal to view to refresh invalidated row
""" """
log.debug(f"issue285: {self}: invalidate_row({modified_row=})")
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
) )
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 Signal to view to refresh invlidated 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:
""" """
@ -820,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:
""" """
@ -885,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,
@ -1062,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:
""" """
@ -1115,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:
""" """
@ -1129,7 +1179,12 @@ 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()
@ -1143,7 +1198,7 @@ class PlaylistModel(QAbstractTableModel):
looking up the playlistrow_id and retrieving the row number from the database. 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 # Check the track_sequence.next, current and previous plrs and
# update the row number # update the row number
@ -1189,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]
@ -1398,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()
@ -1550,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:
""" """
@ -1584,7 +1648,14 @@ 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)
@ -1593,7 +1664,7 @@ class PlaylistModel(QAbstractTableModel):
Update track start/end times in self.playlist_rows 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 next_start_time: Optional[dt.datetime] = None
update_rows: list[int] = [] update_rows: list[int] = []
@ -1733,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

View File

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

View File

@ -15,6 +15,7 @@ from PyQt6.QtCore import (
QVariant, QVariant,
) )
from PyQt6.QtGui import ( from PyQt6.QtGui import (
QBrush,
QColor, QColor,
QFont, QFont,
) )
@ -82,27 +83,27 @@ class QuerylistModel(QAbstractTableModel):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<QuerylistModel: filter={self.filter}, {self.rowCount()} rows>" 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""" """Return background setting"""
# Unreadable track file # Unreadable track file
if file_is_unreadable(qrow.path): if file_is_unreadable(qrow.path):
return QVariant(QColor(Config.COLOUR_UNREADABLE)) return QBrush(QColor(Config.COLOUR_UNREADABLE))
# Selected row # Selected row
if row in self._selected_rows: if row in self._selected_rows:
return QVariant(QColor(Config.COLOUR_QUERYLIST_SELECTED)) return QBrush(QColor(Config.COLOUR_QUERYLIST_SELECTED))
# Individual cell colouring # Individual cell colouring
if column == QueryCol.BITRATE.value: if column == QueryCol.BITRATE.value:
if not qrow.bitrate or qrow.bitrate < Config.BITRATE_LOW_THRESHOLD: 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: elif qrow.bitrate < Config.BITRATE_OK_THRESHOLD:
return QVariant(QColor(Config.COLOUR_BITRATE_MEDIUM)) return QBrush(QColor(Config.COLOUR_BITRATE_MEDIUM))
else: 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: def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
"""Standard function for view""" """Standard function for view"""
@ -114,7 +115,23 @@ class QuerylistModel(QAbstractTableModel):
) -> QVariant: ) -> QVariant:
"""Return data to view""" """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() return QVariant()
row = index.row() row = index.row()
@ -124,48 +141,33 @@ class QuerylistModel(QAbstractTableModel):
# Dispatch to role-specific functions # Dispatch to role-specific functions
dispatch_table: dict[int, Callable] = { dispatch_table: dict[int, Callable] = {
int(Qt.ItemDataRole.BackgroundRole): self.background_role, int(Qt.ItemDataRole.BackgroundRole): self._background_role,
int(Qt.ItemDataRole.DisplayRole): self.display_role, int(Qt.ItemDataRole.DisplayRole): self._display_role,
int(Qt.ItemDataRole.ToolTipRole): self.tooltip_role, int(Qt.ItemDataRole.ToolTipRole): self._tooltip_role,
} }
if role in dispatch_table: if role in dispatch_table:
return QVariant(dispatch_table[role](row, column, qrow)) 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 # Fall through to no-op
return QVariant() 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 Return text for display
""" """
dispatch_table = { dispatch_table = {
QueryCol.ARTIST.value: QVariant(qrow.artist), QueryCol.ARTIST.value: qrow.artist,
QueryCol.BITRATE.value: QVariant(qrow.bitrate), QueryCol.BITRATE.value: str(qrow.bitrate),
QueryCol.DURATION.value: QVariant(ms_to_mmss(qrow.duration)), QueryCol.DURATION.value: ms_to_mmss(qrow.duration),
QueryCol.LAST_PLAYED.value: QVariant(get_relative_date(qrow.lastplayed)), QueryCol.LAST_PLAYED.value: get_relative_date(qrow.lastplayed),
QueryCol.TITLE.value: QVariant(qrow.title), QueryCol.TITLE.value: qrow.title,
} }
if column in dispatch_table: if column in dispatch_table:
return dispatch_table[column] return dispatch_table[column]
return QVariant() return ""
def flags(self, index: QModelIndex) -> Qt.ItemFlag: def flags(self, index: QModelIndex) -> Qt.ItemFlag:
""" """
@ -266,7 +268,7 @@ class QuerylistModel(QAbstractTableModel):
bottom_right = self.index(row, self.columnCount() - 1) bottom_right = self.index(row, self.columnCount() - 1)
self.dataChanged.emit(top_left, bottom_right, [Qt.ItemDataRole.BackgroundRole]) 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. Return tooltip. Currently only used for last_played column.
""" """
@ -278,7 +280,7 @@ class QuerylistModel(QAbstractTableModel):
if not track_id: if not track_id:
return QVariant() return QVariant()
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)

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", "types-psutil>=6.0.0.20240621",
"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",
"dogpile-cache>=1.3.4",
] ]
[dependency-groups] [dependency-groups]

View File

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

View File

@ -66,10 +66,10 @@ class TestMMMiscTracks(unittest.TestCase):
self.model.insert_row(proposed_row_number=END_ROW, note="-") self.model.insert_row(proposed_row_number=END_ROW, note="-")
prd = self.model.playlist_rows[START_ROW] 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 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): class TestMMMiscNoPlaylist(unittest.TestCase):
@ -109,7 +109,7 @@ class TestMMMiscNoPlaylist(unittest.TestCase):
_ = str(prd) _ = str(prd)
assert ( assert (
model.edit_role( model._edit_role(
model.rowCount() - 1, playlistmodel.Col.TITLE.value, prd model.rowCount() - 1, playlistmodel.Col.TITLE.value, prd
) )
== metadata["title"] == metadata["title"]
@ -262,7 +262,7 @@ class TestMMMiscRowMove(unittest.TestCase):
# Test against edit_role because display_role for headers is # Test against edit_role because display_role for headers is
# handled differently (sets up row span) # handled differently (sets up row span)
assert ( assert (
self.model.edit_role( self.model._edit_role(
self.model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd self.model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd
) )
== note_text == note_text
@ -280,7 +280,7 @@ class TestMMMiscRowMove(unittest.TestCase):
# Test against edit_role because display_role for headers is # Test against edit_role because display_role for headers is
# handled differently (sets up row span) # handled differently (sets up row span)
assert ( assert (
self.model.edit_role( self.model._edit_role(
self.model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd self.model.rowCount() - 1, playlistmodel.Col.NOTE.value, prd
) )
== note_text == note_text
@ -353,7 +353,7 @@ class TestMMMiscRowMove(unittest.TestCase):
index = model_dst.index( index = model_dst.index(
row_number, playlistmodel.Col.TITLE.value, QModelIndex() 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_src.playlist_rows) == self.ROWS_TO_CREATE - len(from_rows)
assert len(model_dst.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( index = model_dst.index(
row_number, playlistmodel.Col.TITLE.value, QModelIndex() 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_src.playlist_rows) == self.ROWS_TO_CREATE - len(from_rows)
assert len(model_dst.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 }, { 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" },
@ -469,6 +483,7 @@ dependencies = [
{ name = "stackprinter" }, { name = "stackprinter" },
{ name = "tinytag" }, { name = "tinytag" },
{ name = "types-psutil" }, { name = "types-psutil" },
{ name = "types-pyyaml" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
@ -491,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" },
@ -511,6 +527,7 @@ requires-dist = [
{ name = "stackprinter", specifier = ">=0.2.10" }, { name = "stackprinter", specifier = ">=0.2.10" },
{ name = "tinytag", specifier = ">=1.10.1" }, { name = "tinytag", specifier = ">=1.10.1" },
{ name = "types-psutil", specifier = ">=6.0.0.20240621" }, { name = "types-psutil", specifier = ">=6.0.0.20240621" },
{ name = "types-pyyaml", specifier = ">=6.0.12.20241230" },
] ]
[package.metadata.requires-dev] [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 }, { 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"
@ -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 }, { 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"
@ -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 }, { 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"
@ -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 }, { 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]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.12.2" version = "4.12.2"