musicmuster/app/classes.py
2025-08-18 20:02:52 +01:00

293 lines
6.3 KiB
Python

# Standard library imports
from __future__ import annotations
from dataclasses import dataclass
import datetime as dt
from enum import auto, Enum
import functools
import threading
from typing import NamedTuple
# Third party imports
# PyQt imports
from PyQt6.QtCore import (
pyqtSignal,
QObject,
)
from PyQt6.QtWidgets import (
QProxyStyle,
QStyle,
QStyleOption,
)
# App imports
# Define singleton first as it's needed below
def singleton(cls):
"""
Make a class a Singleton class (see
https://realpython.com/primer-on-python-decorators/#creating-singletons)
Added locking.
"""
lock = threading.Lock()
@functools.wraps(cls)
def wrapper_singleton(*args, **kwargs):
if wrapper_singleton.instance is None:
with lock:
if wrapper_singleton.instance is None: # Check still None
wrapper_singleton.instance = cls(*args, **kwargs)
return wrapper_singleton.instance
wrapper_singleton.instance = None
return wrapper_singleton
# DTOs
@dataclass
class PlaylistDTO:
playlist_id: int
name: str
open: bool = False
favourite: bool = False
is_template: bool = False
@dataclass
class QueryDTO:
query_id: int
name: str
favourite: bool
filter: Filter
@dataclass
class TrackDTO:
track_id: int
artist: str
bitrate: int
duration: int
fade_at: int
intro: int | None
path: str
silence_at: int
start_gap: int
title: str
lastplayed: dt.datetime | None
@dataclass
class PlaylistRowDTO:
note: str
played: bool
playlist_id: int
playlistrow_id: int
row_number: int
track: TrackDTO | None
@dataclass
class PlaydatesDTO(TrackDTO):
playdate_id: int
lastplayed: dt.datetime
@dataclass
class NoteColoursDTO:
notecolour_id: int
substring: str
colour: str
enabled: bool = True
foreground: str | None = None
is_regex: bool = False
is_casesensitive: bool = False
order: int | None = None
strip_substring: bool = True
class ApplicationError(Exception):
"""
Custom exception
"""
pass
class AudioMetadata(NamedTuple):
start_gap: int = 0
silence_at: int = 0
fade_at: int = 0
class Col(Enum):
"""
Columns in playlist
"""
START_GAP = 0
TITLE = auto()
ARTIST = auto()
INTRO = auto()
DURATION = auto()
START_TIME = auto()
END_TIME = auto()
LAST_PLAYED = auto()
BITRATE = auto()
NOTE = auto()
class FileErrors(NamedTuple):
path: str
error: str
@dataclass
class Filter:
"""
Filter used in queries to select tracks
"""
version: int = 1
path_type: str = "contains"
path: str = ""
last_played_number: int = 0
last_played_comparator: str = "before"
last_played_unit: str = "years"
duration_type: str = "longer than"
duration_number: int = 0
duration_unit: str = "minutes"
class PlaylistStyle(QProxyStyle):
def drawPrimitive(self, element, option, painter, widget=None):
"""
Draw a line across the entire row rather than just the column
we're hovering over.
"""
if (
element == QStyle.PrimitiveElement.PE_IndicatorItemViewItemDrop
and not option.rect.isNull()
):
option_new = QStyleOption(option)
option_new.rect.setLeft(0)
if widget:
option_new.rect.setRight(widget.width())
option = option_new
super().drawPrimitive(element, option, painter, widget)
class QueryCol(Enum):
"""
Columns in querylist
"""
TITLE = 0
ARTIST = auto()
DURATION = auto()
LAST_PLAYED = auto()
BITRATE = auto()
class Tags(NamedTuple):
artist: str = ""
title: str = ""
bitrate: int = 0
duration: int = 0
class TrackInfo(NamedTuple):
track_id: int
row_number: int
# Classes for signals
@dataclass
class InsertRows:
playlist_id: int
from_row: int
to_row: int
@dataclass
class InsertTrack:
playlist_id: int
track_id: int | None
note: str
@dataclass
class SelectedRows:
playlist_id: int
rows: list[int]
@dataclass
class TrackAndPlaylist:
playlist_id: int
track_id: int
@singleton
@dataclass
class MusicMusterSignals(QObject):
"""
Class for all MusicMuster signals. See:
- https://zetcode.com/gui/pyqt5/eventssignals/
- https://stackoverflow.com/questions/62654525/emit-a-signal-from-another-class-to-main-class
"""
# Used to en/disable escape as a shortcut key to "clear selection".
# We disable it when editing a field in the playlist because we use
# escape there to abandon an edit.
enable_escape_signal = pyqtSignal(bool)
# Signals that the playlist_id passed should resize all rows.
resize_rows_signal = pyqtSignal(int)
# Displays a warning dialog
show_warning_signal = pyqtSignal(str, str)
# Signal to add a track to a header row
signal_add_track_to_header = pyqtSignal(TrackAndPlaylist)
# Signal to receving model that rows will be / have been inserter
signal_begin_insert_rows = pyqtSignal(InsertRows)
signal_end_insert_rows = pyqtSignal(int)
# TBD
signal_insert_track = pyqtSignal(InsertTrack)
# Keep track of which rows are selected (between playlist and model)
signal_playlist_selected_rows = pyqtSignal(SelectedRows)
# Signal to model that selected row is to be next row
signal_set_next_row = pyqtSignal(int)
# signal_set_next_track takes a PlaylistRow as an argument. We can't
# specify that here as it requires us to import PlaylistRow from
# playlistrow.py, which itself imports MusicMusterSignals. It tells
# musicmuster to set the passed track as the next one.
signal_set_next_track = pyqtSignal(object)
# Signals that the next-cued track has changed. Used to update
# playlist headers and track timings.
signal_next_track_changed = pyqtSignal()
# Emited when a track starts playing
signal_track_started = pyqtSignal()
# Emitted when track ends or is manually faded
signal_track_ended = pyqtSignal(int)
# Used by model to signal spanning of cells to playlist for headers
span_cells_signal = pyqtSignal(int, int, int, int, int)
# Dispay status message to user
status_message_signal = pyqtSignal(str, int)
def __post_init__(self):
super().__init__()