299 lines
6.4 KiB
Python
299 lines
6.4 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 next-cued track has changed. Used to update
|
|
# playlist headers.
|
|
next_track_changed_signal = pyqtSignal()
|
|
|
|
# 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
|
|
|
|
# TBD
|
|
signal_set_next_track = pyqtSignal(object)
|
|
|
|
# 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__()
|