# 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) # Signal to open browser at songfacts or wikipedia page matching # passed string. search_songfacts_signal = pyqtSignal(str) search_wikipedia_signal = pyqtSignal(str) # 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(TrackAndPlaylist) # 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) # Emitted when track ends or is manually faded track_ended_signal = pyqtSignal(int) def __post_init__(self): super().__init__()