Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8956642e05 | ||
|
|
791fad680a | ||
|
|
8c60d6a03d | ||
|
|
7391b4e61c | ||
|
|
266be281d0 | ||
|
|
ac487a5fa5 | ||
|
|
7d1bb0d3f7 | ||
|
|
e8d9cf8f00 | ||
|
|
7e7ae7dddf | ||
|
|
25cb444335 | ||
|
|
fa14fc7c52 | ||
|
|
6e51e65ba8 | ||
|
|
19b1bf3fde | ||
|
|
316b4708c6 | ||
|
|
4fd9a0381f | ||
|
|
88cce738d7 | ||
|
|
9720c11ecc | ||
|
|
ca4c490091 | ||
|
|
1749f0a0b8 | ||
|
|
c9ff1aa668 | ||
|
|
49776731bf | ||
|
|
9bf1ab29a8 | ||
|
|
4e51b44b44 | ||
|
|
582803dccc | ||
|
|
5f9fd31dfd | ||
|
|
74402f640f | ||
|
|
963da0b5d0 | ||
|
|
85493de179 | ||
|
|
2f8afeb814 | ||
|
|
3b004567df | ||
|
|
76039aa5e6 | ||
|
|
1f10692c15 | ||
|
|
6dd34b292f | ||
|
|
77a9baa34f | ||
|
|
6e2ad86fb2 | ||
|
|
be54187b48 | ||
|
|
6d56a94bca | ||
|
|
ccc1737f2d | ||
|
|
58e244af21 | ||
|
|
93839c69e2 | ||
|
|
61b00d8531 | ||
|
|
63b1d0dff4 | ||
|
|
2293c663b9 | ||
|
|
f5c77ddffd | ||
|
|
1cf75a5d42 | ||
|
|
7fd655f96f | ||
|
|
096889d6cb | ||
|
|
67c48f5022 | ||
|
|
8e48d63ebb | ||
|
|
aa6ab03555 | ||
|
|
fc02a4aa7e | ||
|
|
6223ef0ef0 | ||
|
|
76e6084419 | ||
|
|
90d72464cb | ||
|
|
82e707a6f6 | ||
|
|
b4f5d92f5d | ||
|
|
985629446a | ||
|
|
64ccb485b5 | ||
|
|
3f248d363f | ||
|
|
40756469ec | ||
|
|
306ab103b6 | ||
|
|
994d510ed9 | ||
|
|
8b8edba64d | ||
|
|
678515403c | ||
|
|
e6404d075e | ||
|
|
7c0db00b75 | ||
|
|
e4e061cf1c | ||
|
|
61021b33b8 | ||
|
|
a33589a9a1 | ||
|
|
3547046cc1 | ||
|
|
95983c73b1 | ||
|
|
499c0c6b70 | ||
|
|
33e2c4bf31 | ||
|
|
589a664971 | ||
|
|
67bf926ed8 | ||
|
|
040020e7ed | ||
|
|
911859ef49 | ||
|
|
68bdff53cf | ||
|
|
632937101a | ||
|
|
639f006a10 | ||
|
|
9e27418f80 | ||
|
|
c1448dfdd5 | ||
|
|
5f396a0993 | ||
|
|
e10c2adafe | ||
|
|
b0f6e4e819 | ||
|
|
afd3be608c | ||
|
|
955bea2037 |
2
.envrc
2
.envrc
@ -1,4 +1,4 @@
|
|||||||
layout poetry
|
layout uv
|
||||||
export LINE_PROFILE=1
|
export LINE_PROFILE=1
|
||||||
export MAIL_PASSWORD="ewacyay5seu2qske"
|
export MAIL_PASSWORD="ewacyay5seu2qske"
|
||||||
export MAIL_PORT=587
|
export MAIL_PORT=587
|
||||||
|
|||||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.py diff=python
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,6 +2,7 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
*.swp
|
*.swp
|
||||||
tags
|
tags
|
||||||
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
Session.vim
|
Session.vim
|
||||||
*.flac
|
*.flac
|
||||||
@ -13,3 +14,4 @@ StudioPlaylist.png
|
|||||||
tmp/
|
tmp/
|
||||||
.coverage
|
.coverage
|
||||||
profile_output*
|
profile_output*
|
||||||
|
kae.py
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
musicmuster
|
3.13
|
||||||
|
|||||||
@ -1,78 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from PyQt6.QtCore import Qt, QEvent, QObject
|
|
||||||
from PyQt6.QtWidgets import (
|
|
||||||
QAbstractItemView,
|
|
||||||
QApplication,
|
|
||||||
QMainWindow,
|
|
||||||
QMessageBox,
|
|
||||||
QPlainTextEdit,
|
|
||||||
QStyledItemDelegate,
|
|
||||||
QTableWidget,
|
|
||||||
QTableWidgetItem,
|
|
||||||
)
|
|
||||||
|
|
||||||
from PyQt6.QtGui import QKeyEvent
|
|
||||||
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
|
|
||||||
class EscapeDelegate(QStyledItemDelegate):
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
|
||||||
return QPlainTextEdit(parent)
|
|
||||||
|
|
||||||
def eventFilter(self, editor: QObject, event: QEvent):
|
|
||||||
"""By default, QPlainTextEdit doesn't handle enter or return"""
|
|
||||||
|
|
||||||
print("EscapeDelegate event handler")
|
|
||||||
if event.type() == QEvent.Type.KeyPress:
|
|
||||||
key_event = cast(QKeyEvent, event)
|
|
||||||
if key_event.key() == Qt.Key.Key_Return:
|
|
||||||
if key_event.modifiers() == (Qt.KeyboardModifier.ControlModifier):
|
|
||||||
print("save data")
|
|
||||||
self.commitData.emit(editor)
|
|
||||||
self.closeEditor.emit(editor)
|
|
||||||
return True
|
|
||||||
elif key_event.key() == Qt.Key.Key_Escape:
|
|
||||||
discard_edits = QMessageBox.question(
|
|
||||||
self.parent(), "Abandon edit", "Discard changes?"
|
|
||||||
)
|
|
||||||
if discard_edits == QMessageBox.StandardButton.Yes:
|
|
||||||
print("abandon edit")
|
|
||||||
self.closeEditor.emit(editor)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class MyTableWidget(QTableWidget):
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setItemDelegate(EscapeDelegate(self))
|
|
||||||
# self.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked)
|
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.table_widget = MyTableWidget(self)
|
|
||||||
self.table_widget.setRowCount(2)
|
|
||||||
self.table_widget.setColumnCount(2)
|
|
||||||
for row in range(2):
|
|
||||||
for col in range(2):
|
|
||||||
item = QTableWidgetItem()
|
|
||||||
item.setText(f"Row {row}, Col {col}")
|
|
||||||
self.table_widget.setItem(row, col, item)
|
|
||||||
self.setCentralWidget(self.table_widget)
|
|
||||||
|
|
||||||
self.table_widget.resizeColumnsToContents()
|
|
||||||
self.table_widget.resizeRowsToContents()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app = QApplication([])
|
|
||||||
window = MainWindow()
|
|
||||||
window.show()
|
|
||||||
app.exec()
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from PyQt6.QtCore import Qt, QEvent, QObject, QVariant, QAbstractTableModel
|
|
||||||
from PyQt6.QtWidgets import (
|
|
||||||
QApplication,
|
|
||||||
QMainWindow,
|
|
||||||
QMessageBox,
|
|
||||||
QPlainTextEdit,
|
|
||||||
QStyledItemDelegate,
|
|
||||||
QTableView,
|
|
||||||
)
|
|
||||||
|
|
||||||
from PyQt6.QtGui import QKeyEvent
|
|
||||||
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
|
|
||||||
class EscapeDelegate(QStyledItemDelegate):
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
|
||||||
return QPlainTextEdit(parent)
|
|
||||||
|
|
||||||
def eventFilter(self, editor: QObject, event: QEvent):
|
|
||||||
"""By default, QPlainTextEdit doesn't handle enter or return"""
|
|
||||||
|
|
||||||
if event.type() == QEvent.Type.KeyPress:
|
|
||||||
key_event = cast(QKeyEvent, event)
|
|
||||||
print(key_event.key())
|
|
||||||
if key_event.key() == Qt.Key.Key_Return:
|
|
||||||
if key_event.modifiers() == (Qt.KeyboardModifier.ControlModifier):
|
|
||||||
print("save data")
|
|
||||||
self.commitData.emit(editor)
|
|
||||||
self.closeEditor.emit(editor)
|
|
||||||
return True
|
|
||||||
elif key_event.key() == Qt.Key.Key_Escape:
|
|
||||||
discard_edits = QMessageBox.question(
|
|
||||||
self.parent(), "Abandon edit", "Discard changes?"
|
|
||||||
)
|
|
||||||
if discard_edits == QMessageBox.StandardButton.Yes:
|
|
||||||
print("abandon edit")
|
|
||||||
self.closeEditor.emit(editor)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class MyTableWidget(QTableView):
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setItemDelegate(EscapeDelegate(self))
|
|
||||||
self.setModel(MyModel())
|
|
||||||
|
|
||||||
|
|
||||||
class MyModel(QAbstractTableModel):
|
|
||||||
def columnCount(self, index):
|
|
||||||
return 2
|
|
||||||
|
|
||||||
def rowCount(self, index):
|
|
||||||
return 2
|
|
||||||
|
|
||||||
def data(self, index, role):
|
|
||||||
if not index.isValid() or not (0 <= index.row() < 2):
|
|
||||||
return QVariant()
|
|
||||||
|
|
||||||
row = index.row()
|
|
||||||
column = index.column()
|
|
||||||
if role == Qt.ItemDataRole.DisplayRole:
|
|
||||||
return QVariant(f"Row {row}, Col {column}")
|
|
||||||
return QVariant()
|
|
||||||
|
|
||||||
def flags(self, index):
|
|
||||||
return (
|
|
||||||
Qt.ItemFlag.ItemIsEnabled
|
|
||||||
| Qt.ItemFlag.ItemIsSelectable
|
|
||||||
| Qt.ItemFlag.ItemIsEditable
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.table_widget = MyTableWidget(self)
|
|
||||||
self.setCentralWidget(self.table_widget)
|
|
||||||
|
|
||||||
self.table_widget.resizeColumnsToContents()
|
|
||||||
self.table_widget.resizeRowsToContents()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app = QApplication([])
|
|
||||||
window = MainWindow()
|
|
||||||
window.show()
|
|
||||||
app.exec()
|
|
||||||
@ -69,7 +69,8 @@ class AudacityController:
|
|||||||
select_status = self._send_command("SelectAll")
|
select_status = self._send_command("SelectAll")
|
||||||
log.debug(f"{select_status=}")
|
log.debug(f"{select_status=}")
|
||||||
|
|
||||||
export_cmd = f'Export2: Filename="{self.path}" NumChannels=2'
|
# Escape any double quotes in filename
|
||||||
|
export_cmd = f'Export2: Filename="{self.path.replace('"', '\\"')}" NumChannels=2'
|
||||||
export_status = self._send_command(export_cmd)
|
export_status = self._send_command(export_cmd)
|
||||||
log.debug(f"{export_status=}")
|
log.debug(f"{export_status=}")
|
||||||
self.path = ""
|
self.path = ""
|
||||||
|
|||||||
@ -14,23 +14,16 @@ from PyQt6.QtCore import (
|
|||||||
pyqtSignal,
|
pyqtSignal,
|
||||||
QObject,
|
QObject,
|
||||||
)
|
)
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QProxyStyle,
|
||||||
|
QStyle,
|
||||||
|
QStyleOption,
|
||||||
|
)
|
||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
|
|
||||||
|
|
||||||
class Col(Enum):
|
# Define singleton first as it's needed below
|
||||||
START_GAP = 0
|
|
||||||
TITLE = auto()
|
|
||||||
ARTIST = auto()
|
|
||||||
INTRO = auto()
|
|
||||||
DURATION = auto()
|
|
||||||
START_TIME = auto()
|
|
||||||
END_TIME = auto()
|
|
||||||
LAST_PLAYED = auto()
|
|
||||||
BITRATE = auto()
|
|
||||||
NOTE = auto()
|
|
||||||
|
|
||||||
|
|
||||||
def singleton(cls):
|
def singleton(cls):
|
||||||
"""
|
"""
|
||||||
Make a class a Singleton class (see
|
Make a class a Singleton class (see
|
||||||
@ -53,11 +46,6 @@ def singleton(cls):
|
|||||||
return wrapper_singleton
|
return wrapper_singleton
|
||||||
|
|
||||||
|
|
||||||
class FileErrors(NamedTuple):
|
|
||||||
path: str
|
|
||||||
error: str
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationError(Exception):
|
class ApplicationError(Exception):
|
||||||
"""
|
"""
|
||||||
Custom exception
|
Custom exception
|
||||||
@ -72,16 +60,44 @@ class AudioMetadata(NamedTuple):
|
|||||||
fade_at: int = 0
|
fade_at: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class Col(Enum):
|
||||||
|
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:
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
@dataclass
|
@dataclass
|
||||||
class MusicMusterSignals(QObject):
|
class MusicMusterSignals(QObject):
|
||||||
"""
|
"""
|
||||||
Class for all MusicMuster signals. See:
|
Class for all MusicMuster signals. See:
|
||||||
- https://zetcode.com/gui/pyqt5/eventssignals/
|
- https://zetcode.com/gui/pyqt5/eventssignals/
|
||||||
- https://stackoverflow.com/questions/62654525/
|
- https://stackoverflow.com/questions/62654525/emit-a-signal-from-another-class-to-main-class
|
||||||
emit-a-signal-from-another-class-to-main-class
|
|
||||||
and Singleton class at
|
|
||||||
https://refactoring.guru/design-patterns/singleton/python/example#example-0
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
begin_reset_model_signal = pyqtSignal(int)
|
begin_reset_model_signal = pyqtSignal(int)
|
||||||
@ -100,6 +116,32 @@ class MusicMusterSignals(QObject):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
TITLE = 0
|
||||||
|
ARTIST = auto()
|
||||||
|
DURATION = auto()
|
||||||
|
LAST_PLAYED = auto()
|
||||||
|
BITRATE = auto()
|
||||||
|
|
||||||
|
|
||||||
class Tags(NamedTuple):
|
class Tags(NamedTuple):
|
||||||
artist: str = ""
|
artist: str = ""
|
||||||
title: str = ""
|
title: str = ""
|
||||||
|
|||||||
@ -31,6 +31,7 @@ class Config(object):
|
|||||||
COLOUR_NORMAL_TAB = "#000000"
|
COLOUR_NORMAL_TAB = "#000000"
|
||||||
COLOUR_NOTES_PLAYLIST = "#b8daff"
|
COLOUR_NOTES_PLAYLIST = "#b8daff"
|
||||||
COLOUR_ODD_PLAYLIST = "#f2f2f2"
|
COLOUR_ODD_PLAYLIST = "#f2f2f2"
|
||||||
|
COLOUR_QUERYLIST_SELECTED = "#d3ffd3"
|
||||||
COLOUR_UNREADABLE = "#dc3545"
|
COLOUR_UNREADABLE = "#dc3545"
|
||||||
COLOUR_WARNING_TIMER = "#ffc107"
|
COLOUR_WARNING_TIMER = "#ffc107"
|
||||||
DBFS_SILENCE = -50
|
DBFS_SILENCE = -50
|
||||||
@ -38,6 +39,7 @@ class Config(object):
|
|||||||
DISPLAY_SQL = False
|
DISPLAY_SQL = False
|
||||||
DO_NOT_IMPORT = "Do not import"
|
DO_NOT_IMPORT = "Do not import"
|
||||||
ENGINE_OPTIONS = dict(pool_pre_ping=True)
|
ENGINE_OPTIONS = dict(pool_pre_ping=True)
|
||||||
|
# ENGINE_OPTIONS = dict(pool_pre_ping=True, echo=True)
|
||||||
EPOCH = dt.datetime(1970, 1, 1)
|
EPOCH = dt.datetime(1970, 1, 1)
|
||||||
ERRORS_FROM = ["noreply@midnighthax.com"]
|
ERRORS_FROM = ["noreply@midnighthax.com"]
|
||||||
ERRORS_TO = ["kae@midnighthax.com"]
|
ERRORS_TO = ["kae@midnighthax.com"]
|
||||||
@ -48,6 +50,19 @@ class Config(object):
|
|||||||
FADEOUT_DB = -10
|
FADEOUT_DB = -10
|
||||||
FADEOUT_SECONDS = 5
|
FADEOUT_SECONDS = 5
|
||||||
FADEOUT_STEPS_PER_SECOND = 5
|
FADEOUT_STEPS_PER_SECOND = 5
|
||||||
|
FILTER_DURATION_LONGER = "longer than"
|
||||||
|
FILTER_DURATION_MINUTES = "minutes"
|
||||||
|
FILTER_DURATION_SECONDS = "seconds"
|
||||||
|
FILTER_DURATION_SHORTER = "shorter than"
|
||||||
|
FILTER_PATH_CONTAINS = "contains"
|
||||||
|
FILTER_PATH_EXCLUDING = "excluding"
|
||||||
|
FILTER_PLAYED_COMPARATOR_ANYTIME = "Any time"
|
||||||
|
FILTER_PLAYED_COMPARATOR_BEFORE = "before"
|
||||||
|
FILTER_PLAYED_COMPARATOR_NEVER = "never"
|
||||||
|
FILTER_PLAYED_DAYS = "days"
|
||||||
|
FILTER_PLAYED_MONTHS = "months"
|
||||||
|
FILTER_PLAYED_WEEKS = "weeks"
|
||||||
|
FILTER_PLAYED_YEARS = "years"
|
||||||
FUZZYMATCH_MINIMUM_LIST = 60.0
|
FUZZYMATCH_MINIMUM_LIST = 60.0
|
||||||
FUZZYMATCH_MINIMUM_SELECT_ARTIST = 80.0
|
FUZZYMATCH_MINIMUM_SELECT_ARTIST = 80.0
|
||||||
FUZZYMATCH_MINIMUM_SELECT_TITLE = 80.0
|
FUZZYMATCH_MINIMUM_SELECT_TITLE = 80.0
|
||||||
@ -86,6 +101,7 @@ class Config(object):
|
|||||||
MAX_MISSING_FILES_TO_REPORT = 10
|
MAX_MISSING_FILES_TO_REPORT = 10
|
||||||
MILLISECOND_SIGFIGS = 0
|
MILLISECOND_SIGFIGS = 0
|
||||||
MINIMUM_ROW_HEIGHT = 30
|
MINIMUM_ROW_HEIGHT = 30
|
||||||
|
NO_QUERY_NAME = "Select query"
|
||||||
NO_TEMPLATE_NAME = "None"
|
NO_TEMPLATE_NAME = "None"
|
||||||
NOTE_TIME_FORMAT = "%H:%M"
|
NOTE_TIME_FORMAT = "%H:%M"
|
||||||
OBS_HOST = "localhost"
|
OBS_HOST = "localhost"
|
||||||
@ -95,6 +111,7 @@ class Config(object):
|
|||||||
PLAY_SETTLE = 500000
|
PLAY_SETTLE = 500000
|
||||||
PLAYLIST_ICON_CURRENT = ":/icons/green-circle.png"
|
PLAYLIST_ICON_CURRENT = ":/icons/green-circle.png"
|
||||||
PLAYLIST_ICON_NEXT = ":/icons/yellow-circle.png"
|
PLAYLIST_ICON_NEXT = ":/icons/yellow-circle.png"
|
||||||
|
PLAYLIST_ICON_TEMPLATE = ":/icons/redstar.png"
|
||||||
PREVIEW_ADVANCE_MS = 5000
|
PREVIEW_ADVANCE_MS = 5000
|
||||||
PREVIEW_BACK_MS = 5000
|
PREVIEW_BACK_MS = 5000
|
||||||
PREVIEW_END_BUFFER_MS = 1000
|
PREVIEW_END_BUFFER_MS = 1000
|
||||||
@ -106,23 +123,23 @@ 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"
|
||||||
VLC_PREVIEW_PLAYER_NAME = "MusicMuster Preview Player"
|
VLC_PREVIEW_PLAYER_NAME = "MusicMuster Preview Player"
|
||||||
VLC_VOLUME_DEFAULT = 75
|
VLC_VOLUME_DEFAULT = 100
|
||||||
VLC_VOLUME_DROP3db = 65
|
VLC_VOLUME_DROP3db = 70
|
||||||
WARNING_MS_BEFORE_FADE = 5500
|
WARNING_MS_BEFORE_FADE = 5500
|
||||||
WARNING_MS_BEFORE_SILENCE = 5500
|
WARNING_MS_BEFORE_SILENCE = 5500
|
||||||
WEB_ZOOM_FACTOR = 1.2
|
WEB_ZOOM_FACTOR = 1.2
|
||||||
WIKIPEDIA_ON_NEXT = False
|
WIKIPEDIA_ON_NEXT = False
|
||||||
|
|
||||||
# These rely on earlier definitions
|
# These rely on earlier definitions
|
||||||
HIDE_PLAYED_MODE = HIDE_PLAYED_MODE_SECTIONS
|
HIDE_PLAYED_MODE = HIDE_PLAYED_MODE_TRACKS
|
||||||
IMPORT_DESTINATION = os.path.join(ROOT, "Singles")
|
IMPORT_DESTINATION = os.path.join(ROOT, "Singles")
|
||||||
REPLACE_FILES_DEFAULT_DESTINATION = os.path.dirname(REPLACE_FILES_DEFAULT_SOURCE)
|
REPLACE_FILES_DEFAULT_DESTINATION = os.path.dirname(REPLACE_FILES_DEFAULT_SOURCE)
|
||||||
|
|||||||
@ -18,7 +18,8 @@ class DatabaseManager:
|
|||||||
def __init__(self, database_url: str, **kwargs: dict) -> None:
|
def __init__(self, database_url: str, **kwargs: dict) -> None:
|
||||||
if DatabaseManager.__instance is None:
|
if DatabaseManager.__instance is None:
|
||||||
self.db = Alchemical(database_url, **kwargs)
|
self.db = Alchemical(database_url, **kwargs)
|
||||||
self.db.create_all()
|
# Database managed by Alembic so no create_all() required
|
||||||
|
# self.db.create_all()
|
||||||
DatabaseManager.__instance = self
|
DatabaseManager.__instance = self
|
||||||
else:
|
else:
|
||||||
raise Exception("Attempted to create a second DatabaseManager instance")
|
raise Exception("Attempted to create a second DatabaseManager instance")
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
# Standard library imports
|
# Standard library imports
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from dataclasses import asdict
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
import json
|
||||||
|
|
||||||
# PyQt imports
|
# PyQt imports
|
||||||
|
|
||||||
@ -13,13 +15,37 @@ from sqlalchemy import (
|
|||||||
String,
|
String,
|
||||||
)
|
)
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
|
from sqlalchemy.engine.interfaces import Dialect
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import (
|
||||||
Mapped,
|
Mapped,
|
||||||
mapped_column,
|
mapped_column,
|
||||||
relationship,
|
relationship,
|
||||||
)
|
)
|
||||||
|
from sqlalchemy.types import TypeDecorator, TEXT
|
||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
|
from classes import Filter
|
||||||
|
|
||||||
|
|
||||||
|
class JSONEncodedDict(TypeDecorator):
|
||||||
|
"""
|
||||||
|
Custom JSON Type for MariaDB (since native JSON type is just LONGTEXT)
|
||||||
|
"""
|
||||||
|
|
||||||
|
impl = TEXT
|
||||||
|
|
||||||
|
def process_bind_param(self, value: dict | None, dialect: Dialect) -> str | None:
|
||||||
|
"""Convert Python dictionary to JSON string before saving."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return json.dumps(value, default=lambda o: o.__dict__)
|
||||||
|
|
||||||
|
def process_result_value(self, value: str | None, dialect: Dialect) -> dict | None:
|
||||||
|
"""Convert JSON string back to Python dictionary after retrieval."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return json.loads(value)
|
||||||
|
|
||||||
|
|
||||||
# Database classes
|
# Database classes
|
||||||
@ -27,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)
|
||||||
@ -48,7 +74,7 @@ class PlaydatesTable(Model):
|
|||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
lastplayed: Mapped[dt.datetime] = mapped_column(index=True)
|
lastplayed: Mapped[dt.datetime] = mapped_column(index=True)
|
||||||
track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id"))
|
track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id", ondelete="CASCADE"))
|
||||||
track: Mapped["TracksTable"] = relationship(
|
track: Mapped["TracksTable"] = relationship(
|
||||||
"TracksTable",
|
"TracksTable",
|
||||||
back_populates="playdates",
|
back_populates="playdates",
|
||||||
@ -80,8 +106,8 @@ class PlaylistsTable(Model):
|
|||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
order_by="PlaylistRowsTable.row_number",
|
order_by="PlaylistRowsTable.row_number",
|
||||||
)
|
)
|
||||||
query: Mapped["QueriesTable"] = relationship(
|
favourite: Mapped[bool] = mapped_column(
|
||||||
back_populates="playlist", cascade="all, delete-orphan"
|
Boolean, nullable=False, index=False, default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
@ -104,7 +130,9 @@ 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(
|
||||||
|
ForeignKey("tracks.id", ondelete="CASCADE")
|
||||||
|
)
|
||||||
track: Mapped["TracksTable"] = relationship(
|
track: Mapped["TracksTable"] = relationship(
|
||||||
"TracksTable",
|
"TracksTable",
|
||||||
back_populates="playlistrows",
|
back_populates="playlistrows",
|
||||||
@ -125,14 +153,25 @@ class QueriesTable(Model):
|
|||||||
__tablename__ = "queries"
|
__tablename__ = "queries"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
query: Mapped[str] = mapped_column(
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
String(2048), index=False, default="", nullable=False
|
_filter_data: Mapped[dict | None] = mapped_column("filter_data", JSONEncodedDict, nullable=False)
|
||||||
)
|
favourite: Mapped[bool] = mapped_column(Boolean, nullable=False, index=False, default=False)
|
||||||
playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id"), index=True)
|
|
||||||
playlist: Mapped[PlaylistsTable] = relationship(back_populates="query")
|
def _get_filter(self) -> Filter:
|
||||||
|
"""Convert stored JSON dictionary to a Filter object."""
|
||||||
|
if isinstance(self._filter_data, dict):
|
||||||
|
return Filter(**self._filter_data)
|
||||||
|
return Filter() # Default object if None or invalid data
|
||||||
|
|
||||||
|
def _set_filter(self, value: Filter | None) -> None:
|
||||||
|
"""Convert a Filter object to JSON before storing."""
|
||||||
|
self._filter_data = asdict(value) if isinstance(value, Filter) else None
|
||||||
|
|
||||||
|
# Single definition of `filter`
|
||||||
|
filter = property(_get_filter, _set_filter)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Queries(id={self.id}, playlist={self.playlist}, query={self.query}>"
|
return f"<QueriesTable(id={self.id}, name={self.name}, filter={self.filter})>"
|
||||||
|
|
||||||
|
|
||||||
class SettingsTable(Model):
|
class SettingsTable(Model):
|
||||||
@ -158,7 +197,7 @@ class TracksTable(Model):
|
|||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
artist: Mapped[str] = mapped_column(String(256), index=True)
|
artist: Mapped[str] = mapped_column(String(256), index=True)
|
||||||
bitrate: Mapped[Optional[int]] = mapped_column(default=None)
|
bitrate: Mapped[int] = mapped_column(default=None)
|
||||||
duration: Mapped[int] = mapped_column(index=True)
|
duration: Mapped[int] = mapped_column(index=True)
|
||||||
fade_at: Mapped[int] = mapped_column(index=False)
|
fade_at: Mapped[int] = mapped_column(index=False)
|
||||||
intro: Mapped[Optional[int]] = mapped_column(default=None)
|
intro: Mapped[Optional[int]] = mapped_column(default=None)
|
||||||
|
|||||||
@ -35,6 +35,7 @@ from classes import (
|
|||||||
)
|
)
|
||||||
from config import Config
|
from config import Config
|
||||||
from helpers import (
|
from helpers import (
|
||||||
|
audio_file_extension,
|
||||||
file_is_unreadable,
|
file_is_unreadable,
|
||||||
get_tags,
|
get_tags,
|
||||||
show_OK,
|
show_OK,
|
||||||
@ -104,16 +105,14 @@ class FileImporter:
|
|||||||
# variable or an instance variable are effectively the same thing.
|
# variable or an instance variable are effectively the same thing.
|
||||||
workers: dict[str, DoTrackImport] = {}
|
workers: dict[str, DoTrackImport] = {}
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, base_model: PlaylistModel, row_number: int) -> None:
|
||||||
self, base_model: PlaylistModel, row_number: Optional[int] = None
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Initialise the FileImporter singleton instance.
|
Initialise the FileImporter singleton instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.debug(f"FileImporter.__init__({base_model=}, {row_number=})")
|
||||||
|
|
||||||
# Create ModelData
|
# Create ModelData
|
||||||
if not row_number:
|
|
||||||
row_number = base_model.rowCount()
|
|
||||||
self.model_data = ThreadData(base_model=base_model, row_number=row_number)
|
self.model_data = ThreadData(base_model=base_model, row_number=row_number)
|
||||||
|
|
||||||
# Data structure to track files to import
|
# Data structure to track files to import
|
||||||
@ -202,6 +201,7 @@ class FileImporter:
|
|||||||
self.sort_track_match_data(tfd)
|
self.sort_track_match_data(tfd)
|
||||||
selection = self.get_user_choices(tfd)
|
selection = self.get_user_choices(tfd)
|
||||||
if self.process_selection(tfd, selection):
|
if self.process_selection(tfd, selection):
|
||||||
|
if self.extension_check(tfd):
|
||||||
if self.validate_file_data(tfd):
|
if self.validate_file_data(tfd):
|
||||||
tfd.import_this_file = True
|
tfd.import_this_file = True
|
||||||
|
|
||||||
@ -237,6 +237,26 @@ class FileImporter:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def extension_check(self, tfd: TrackFileData) -> bool:
|
||||||
|
"""
|
||||||
|
If we are replacing an existing file, check that the correct file
|
||||||
|
extension of the replacement file matches the existing file
|
||||||
|
extension and return True if it does (or if there is no exsting
|
||||||
|
file), else False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not tfd.file_path_to_remove:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if tfd.file_path_to_remove.endswith(audio_file_extension(tfd.source_path)):
|
||||||
|
return True
|
||||||
|
|
||||||
|
tfd.error = (
|
||||||
|
f"Existing file ({tfd.file_path_to_remove}) has a different "
|
||||||
|
f"extension to replacement file ({tfd.source_path})"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
def find_similar(self, tfd: TrackFileData) -> None:
|
def find_similar(self, tfd: TrackFileData) -> None:
|
||||||
"""
|
"""
|
||||||
- Search title in existing tracks
|
- Search title in existing tracks
|
||||||
@ -445,7 +465,8 @@ class FileImporter:
|
|||||||
if tfd.track_id == 0 and tfd.destination_path != tfd.file_path_to_remove:
|
if tfd.track_id == 0 and tfd.destination_path != tfd.file_path_to_remove:
|
||||||
while os.path.exists(tfd.destination_path):
|
while os.path.exists(tfd.destination_path):
|
||||||
msg = (
|
msg = (
|
||||||
f"New import requested but default destination path ({tfd.destination_path})"
|
"New import requested but default destination path"
|
||||||
|
f" ({tfd.destination_path})"
|
||||||
" already exists. Click OK and choose where to save this track"
|
" already exists. Click OK and choose where to save this track"
|
||||||
)
|
)
|
||||||
show_OK(title="Desintation path exists", msg=msg, parent=None)
|
show_OK(title="Desintation path exists", msg=msg, parent=None)
|
||||||
@ -627,7 +648,8 @@ class DoTrackImport(QThread):
|
|||||||
f"Importing {os.path.basename(self.import_file_path)}", 5000
|
f"Importing {os.path.basename(self.import_file_path)}", 5000
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get audio metadata in this thread rather than calling function to save interactive time
|
# Get audio metadata in this thread rather than calling
|
||||||
|
# function to save interactive time
|
||||||
self.audio_metadata = helpers.get_audio_metadata(self.import_file_path)
|
self.audio_metadata = helpers.get_audio_metadata(self.import_file_path)
|
||||||
|
|
||||||
# Remove old file if so requested
|
# Remove old file if so requested
|
||||||
|
|||||||
@ -10,9 +10,10 @@ import ssl
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
# PyQt imports
|
# PyQt imports
|
||||||
from PyQt6.QtWidgets import QMainWindow, QMessageBox, QWidget
|
from PyQt6.QtWidgets import QInputDialog, QMainWindow, QMessageBox, QWidget
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
|
import filetype
|
||||||
from mutagen.flac import FLAC # type: ignore
|
from mutagen.flac import FLAC # type: ignore
|
||||||
from mutagen.mp3 import MP3 # type: ignore
|
from mutagen.mp3 import MP3 # type: ignore
|
||||||
from pydub import AudioSegment, effects
|
from pydub import AudioSegment, effects
|
||||||
@ -50,6 +51,14 @@ def ask_yes_no(
|
|||||||
return button == QMessageBox.StandardButton.Yes
|
return button == QMessageBox.StandardButton.Yes
|
||||||
|
|
||||||
|
|
||||||
|
def audio_file_extension(fpath: str) -> str | None:
|
||||||
|
"""
|
||||||
|
Return the correct extension for this type of file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return filetype.guess(fpath).extension
|
||||||
|
|
||||||
|
|
||||||
def fade_point(
|
def fade_point(
|
||||||
audio_segment: AudioSegment,
|
audio_segment: AudioSegment,
|
||||||
fade_threshold: float = 0.0,
|
fade_threshold: float = 0.0,
|
||||||
@ -72,7 +81,7 @@ def fade_point(
|
|||||||
fade_threshold = max_vol
|
fade_threshold = max_vol
|
||||||
|
|
||||||
while (
|
while (
|
||||||
audio_segment[trim_ms : trim_ms + chunk_size].dBFS < fade_threshold
|
audio_segment[trim_ms: trim_ms + chunk_size].dBFS < fade_threshold
|
||||||
and trim_ms > 0
|
and trim_ms > 0
|
||||||
): # noqa W503
|
): # noqa W503
|
||||||
trim_ms -= chunk_size
|
trim_ms -= chunk_size
|
||||||
@ -94,6 +103,9 @@ def file_is_unreadable(path: Optional[str]) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def get_audio_segment(path: str) -> Optional[AudioSegment]:
|
def get_audio_segment(path: str) -> Optional[AudioSegment]:
|
||||||
|
if not path.endswith(audio_file_extension(path)):
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if path.endswith(".mp3"):
|
if path.endswith(".mp3"):
|
||||||
return AudioSegment.from_mp3(path)
|
return AudioSegment.from_mp3(path)
|
||||||
@ -150,6 +162,23 @@ def get_audio_metadata(filepath: str) -> AudioMetadata:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_name(prompt: str, default: str = "") -> str | None:
|
||||||
|
"""Get a name from the user"""
|
||||||
|
|
||||||
|
dlg = QInputDialog()
|
||||||
|
dlg.setInputMode(QInputDialog.InputMode.TextInput)
|
||||||
|
dlg.setLabelText(prompt)
|
||||||
|
while True:
|
||||||
|
if default:
|
||||||
|
dlg.setTextValue(default)
|
||||||
|
dlg.resize(500, 100)
|
||||||
|
ok = dlg.exec()
|
||||||
|
if ok:
|
||||||
|
return dlg.textValue()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_relative_date(
|
def get_relative_date(
|
||||||
past_date: Optional[dt.datetime], reference_date: Optional[dt.datetime] = None
|
past_date: Optional[dt.datetime], reference_date: Optional[dt.datetime] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
|
|||||||
56
app/jittermonitor.py
Normal file
56
app/jittermonitor.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
from PyQt6.QtCore import QObject, QTimer, QElapsedTimer
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
class EventLoopJitterMonitor(QObject):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
interval_ms: int = 20,
|
||||||
|
jitter_threshold_ms: int = 100,
|
||||||
|
log_cooldown_s: float = 1.0,
|
||||||
|
):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._interval = interval_ms
|
||||||
|
self._jitter_threshold = jitter_threshold_ms
|
||||||
|
self._log_cooldown_s = log_cooldown_s
|
||||||
|
|
||||||
|
self._timer = QTimer(self)
|
||||||
|
self._timer.setInterval(self._interval)
|
||||||
|
self._timer.timeout.connect(self._on_timeout)
|
||||||
|
|
||||||
|
self._elapsed = QElapsedTimer()
|
||||||
|
self._elapsed.start()
|
||||||
|
self._last = self._elapsed.elapsed()
|
||||||
|
|
||||||
|
# child logger: e.g. "musicmuster.jitter"
|
||||||
|
self._log = logging.getLogger(f"{Config.LOG_NAME}.jitter")
|
||||||
|
self._last_log_time = 0.0
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
self._timer.start()
|
||||||
|
|
||||||
|
def _on_timeout(self) -> None:
|
||||||
|
now_ms = self._elapsed.elapsed()
|
||||||
|
delta = now_ms - self._last
|
||||||
|
self._last = now_ms
|
||||||
|
|
||||||
|
if delta > (self._interval + self._jitter_threshold):
|
||||||
|
self._log_jitter(now_ms, delta)
|
||||||
|
|
||||||
|
def _log_jitter(self, now_ms: int, gap_ms: int) -> None:
|
||||||
|
now = time.monotonic()
|
||||||
|
|
||||||
|
# simple rate limit: only one log every log_cooldown_s
|
||||||
|
if now - self._last_log_time < self._log_cooldown_s:
|
||||||
|
return
|
||||||
|
self._last_log_time = now
|
||||||
|
|
||||||
|
self._log.warning(
|
||||||
|
"Event loop gap detected: t=%d ms, gap=%d ms (interval=%d ms)",
|
||||||
|
now_ms,
|
||||||
|
gap_ms,
|
||||||
|
self._interval,
|
||||||
|
)
|
||||||
65
app/log.py
65
app/log.py
@ -1,21 +1,24 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# Standard library imports
|
# Standard library imports
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from functools import wraps
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
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 +79,60 @@ 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
|
def truncate_large(obj, limit=5):
|
||||||
|
"""Helper to truncate large lists or other iterables."""
|
||||||
|
if isinstance(obj, (list, tuple, set)):
|
||||||
|
if len(obj) > limit:
|
||||||
|
return f"{type(obj).__name__}(len={len(obj)}, items={list(obj)[:limit]}...)"
|
||||||
|
|
||||||
|
return repr(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def log_call(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
args_repr = [truncate_large(a) for a in args]
|
||||||
|
kwargs_repr = [f"{k}={truncate_large(v)}" for k, v in kwargs.items()]
|
||||||
|
params_repr = ", ".join(args_repr + kwargs_repr)
|
||||||
|
log.debug(f"call {func.__name__}({params_repr})")
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
log.debug(f"return {func.__name__}: {truncate_large(result)}")
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"exception in {func.__name__}: {e}")
|
||||||
|
raise
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
sys.excepthook = handle_exception
|
||||||
|
|||||||
@ -23,8 +23,8 @@ filters:
|
|||||||
# - function-name-1
|
# - function-name-1
|
||||||
# - function-name-2
|
# - function-name-2
|
||||||
musicmuster:
|
musicmuster:
|
||||||
- update_clocks
|
|
||||||
- play_next
|
- play_next
|
||||||
|
jittermonitor: []
|
||||||
|
|
||||||
handlers:
|
handlers:
|
||||||
stderr:
|
stderr:
|
||||||
|
|||||||
104
app/menu.yaml
Normal file
104
app/menu.yaml
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
menus:
|
||||||
|
- title: "&File"
|
||||||
|
actions:
|
||||||
|
- text: "Save as Template"
|
||||||
|
handler: "save_as_template"
|
||||||
|
- text: "Manage Templates"
|
||||||
|
handler: "manage_templates_wrapper"
|
||||||
|
- separator: true
|
||||||
|
- text: "Manage Queries"
|
||||||
|
handler: "manage_queries_wrapper"
|
||||||
|
- separator: true
|
||||||
|
- text: "Exit"
|
||||||
|
handler: "close"
|
||||||
|
|
||||||
|
- title: "&Playlist"
|
||||||
|
actions:
|
||||||
|
- text: "Open Playlist"
|
||||||
|
handler: "open_existing_playlist"
|
||||||
|
shortcut: "Ctrl+O"
|
||||||
|
- text: "New Playlist"
|
||||||
|
handler: "new_playlist_dynamic_submenu"
|
||||||
|
submenu: true
|
||||||
|
- text: "Close Playlist"
|
||||||
|
handler: "close_playlist_tab"
|
||||||
|
- text: "Rename Playlist"
|
||||||
|
handler: "rename_playlist"
|
||||||
|
- text: "Delete Playlist"
|
||||||
|
handler: "delete_playlist"
|
||||||
|
- separator: true
|
||||||
|
- text: "Insert Track"
|
||||||
|
handler: "insert_track"
|
||||||
|
shortcut: "Ctrl+T"
|
||||||
|
- text: "Select Track from Query"
|
||||||
|
handler: "query_dynamic_submenu"
|
||||||
|
submenu: true
|
||||||
|
- text: "Insert Section Header"
|
||||||
|
handler: "insert_header"
|
||||||
|
shortcut: "Ctrl+H"
|
||||||
|
- text: "Import Files"
|
||||||
|
handler: "import_files_wrapper"
|
||||||
|
shortcut: "Ctrl+Shift+I"
|
||||||
|
- separator: true
|
||||||
|
- text: "Mark for Moving"
|
||||||
|
handler: "mark_rows_for_moving"
|
||||||
|
shortcut: "Ctrl+C"
|
||||||
|
- text: "Paste"
|
||||||
|
handler: "paste_rows"
|
||||||
|
shortcut: "Ctrl+V"
|
||||||
|
- separator: true
|
||||||
|
- text: "Export Playlist"
|
||||||
|
handler: "export_playlist_tab"
|
||||||
|
- text: "Download CSV of Played Tracks"
|
||||||
|
handler: "download_played_tracks"
|
||||||
|
- separator: true
|
||||||
|
- text: "Select Duplicate Rows"
|
||||||
|
handler: "select_duplicate_rows"
|
||||||
|
- text: "Move Selected"
|
||||||
|
handler: "move_selected"
|
||||||
|
- text: "Move Unplayed"
|
||||||
|
handler: "move_unplayed"
|
||||||
|
- separator: true
|
||||||
|
- text: "Clear Selection"
|
||||||
|
handler: "clear_selection"
|
||||||
|
shortcut: "Esc"
|
||||||
|
store_reference: true # So we can enable/disable later
|
||||||
|
|
||||||
|
- title: "&Music"
|
||||||
|
actions:
|
||||||
|
- text: "Set Next"
|
||||||
|
handler: "set_selected_track_next"
|
||||||
|
shortcut: "Ctrl+N"
|
||||||
|
- text: "Play Next"
|
||||||
|
handler: "play_next"
|
||||||
|
shortcut: "Return"
|
||||||
|
- text: "Fade"
|
||||||
|
handler: "fade"
|
||||||
|
shortcut: "Ctrl+Z"
|
||||||
|
- text: "Stop"
|
||||||
|
handler: "stop"
|
||||||
|
shortcut: "Ctrl+Alt+S"
|
||||||
|
- text: "Resume"
|
||||||
|
handler: "resume"
|
||||||
|
shortcut: "Ctrl+R"
|
||||||
|
- text: "Skip to Next"
|
||||||
|
handler: "play_next"
|
||||||
|
shortcut: "Ctrl+Alt+Return"
|
||||||
|
- separator: true
|
||||||
|
- text: "Search"
|
||||||
|
handler: "search_playlist"
|
||||||
|
shortcut: "/"
|
||||||
|
- text: "Search Title in Wikipedia"
|
||||||
|
handler: "lookup_row_in_wikipedia"
|
||||||
|
shortcut: "Ctrl+W"
|
||||||
|
- text: "Search Title in Songfacts"
|
||||||
|
handler: "lookup_row_in_songfacts"
|
||||||
|
shortcut: "Ctrl+S"
|
||||||
|
|
||||||
|
- title: "Help"
|
||||||
|
actions:
|
||||||
|
- text: "About"
|
||||||
|
handler: "about"
|
||||||
|
- text: "Debug"
|
||||||
|
handler: "debug"
|
||||||
|
|
||||||
237
app/models.py
237
app/models.py
@ -10,19 +10,24 @@ 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,
|
||||||
func,
|
func,
|
||||||
select,
|
select,
|
||||||
|
text,
|
||||||
update,
|
update,
|
||||||
)
|
)
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError, ProgrammingError
|
||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload, selectinload
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
from sqlalchemy.engine.row import RowMapping
|
||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
|
from classes import ApplicationError, Filter
|
||||||
from config import Config
|
from config import Config
|
||||||
from dbmanager import DatabaseManager
|
from dbmanager import DatabaseManager
|
||||||
import dbtables
|
import dbtables
|
||||||
@ -37,9 +42,27 @@ 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]:
|
||||||
|
"""
|
||||||
|
Run a sql string and return results
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
return session.execute(text(sql)).mappings().all()
|
||||||
|
except ProgrammingError as e:
|
||||||
|
raise ApplicationError(e)
|
||||||
|
|
||||||
|
|
||||||
# Database classes
|
# Database classes
|
||||||
class NoteColours(dbtables.NoteColoursTable):
|
class NoteColours(dbtables.NoteColoursTable):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
session: Session,
|
session: Session,
|
||||||
@ -66,13 +89,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
|
||||||
@ -80,16 +118,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:
|
||||||
@ -107,17 +139,28 @@ 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):
|
||||||
def __init__(self, session: Session, track_id: int) -> None:
|
def __init__(
|
||||||
|
self, session: Session, track_id: int, when: Optional[dt.datetime] = None
|
||||||
|
) -> None:
|
||||||
"""Record that track was played"""
|
"""Record that track was played"""
|
||||||
|
|
||||||
|
if not when:
|
||||||
self.lastplayed = dt.datetime.now()
|
self.lastplayed = dt.datetime.now()
|
||||||
|
else:
|
||||||
|
self.lastplayed = when
|
||||||
self.track_id = track_id
|
self.track_id = track_id
|
||||||
session.add(self)
|
session.add(self)
|
||||||
session.commit()
|
session.commit()
|
||||||
@ -179,12 +222,18 @@ class Playdates(dbtables.PlaydatesTable):
|
|||||||
|
|
||||||
|
|
||||||
class Playlists(dbtables.PlaylistsTable):
|
class Playlists(dbtables.PlaylistsTable):
|
||||||
def __init__(self, session: Session, name: str):
|
def __init__(self, session: Session, name: str, template_id: int) -> None:
|
||||||
|
"""Create playlist with passed name"""
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.last_used = dt.datetime.now()
|
self.last_used = dt.datetime.now()
|
||||||
session.add(self)
|
session.add(self)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
# If a template is specified, copy from it
|
||||||
|
if template_id:
|
||||||
|
PlaylistRows.copy_playlist(session, template_id, self.id)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clear_tabs(session: Session, playlist_ids: list[int]) -> None:
|
def clear_tabs(session: Session, playlist_ids: list[int]) -> None:
|
||||||
"""
|
"""
|
||||||
@ -201,34 +250,6 @@ class Playlists(dbtables.PlaylistsTable):
|
|||||||
self.open = False
|
self.open = False
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_playlist_from_template(
|
|
||||||
cls, session: Session, template: "Playlists", playlist_name: str
|
|
||||||
) -> Optional["Playlists"]:
|
|
||||||
"""Create a new playlist from template"""
|
|
||||||
|
|
||||||
# Sanity check
|
|
||||||
if not template.id:
|
|
||||||
return None
|
|
||||||
|
|
||||||
playlist = cls(session, playlist_name)
|
|
||||||
|
|
||||||
# Sanity / mypy checks
|
|
||||||
if not playlist or not playlist.id:
|
|
||||||
return None
|
|
||||||
|
|
||||||
PlaylistRows.copy_playlist(session, template.id, playlist.id)
|
|
||||||
|
|
||||||
return playlist
|
|
||||||
|
|
||||||
def delete(self, session: Session) -> None:
|
|
||||||
"""
|
|
||||||
Delete playlist
|
|
||||||
"""
|
|
||||||
|
|
||||||
session.execute(delete(Playlists).where(Playlists.id == self.id))
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all(cls, session: Session) -> Sequence["Playlists"]:
|
def get_all(cls, session: Session) -> Sequence["Playlists"]:
|
||||||
"""Returns a list of all playlists ordered by last use"""
|
"""Returns a list of all playlists ordered by last use"""
|
||||||
@ -247,6 +268,16 @@ class Playlists(dbtables.PlaylistsTable):
|
|||||||
select(cls).where(cls.is_template.is_(True)).order_by(cls.name)
|
select(cls).where(cls.is_template.is_(True)).order_by(cls.name)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_favourite_templates(cls, session: Session) -> Sequence["Playlists"]:
|
||||||
|
"""Returns a list of favourite templates ordered by name"""
|
||||||
|
|
||||||
|
return session.scalars(
|
||||||
|
select(cls)
|
||||||
|
.where(cls.is_template.is_(True), cls.favourite.is_(True))
|
||||||
|
.order_by(cls.name)
|
||||||
|
).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_closed(cls, session: Session) -> Sequence["Playlists"]:
|
def get_closed(cls, session: Session) -> Sequence["Playlists"]:
|
||||||
"""Returns a list of all closed playlists ordered by last use"""
|
"""Returns a list of all closed playlists ordered by last use"""
|
||||||
@ -301,7 +332,7 @@ class Playlists(dbtables.PlaylistsTable):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Save passed playlist as new template"""
|
"""Save passed playlist as new template"""
|
||||||
|
|
||||||
template = Playlists(session, template_name)
|
template = Playlists(session, template_name, template_id=0)
|
||||||
if not template or not template.id:
|
if not template or not template.id:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -489,9 +520,13 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
|
|||||||
For passed playlist, return a list of rows.
|
For passed playlist, return a list of rows.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
plrs = session.scalars(
|
stmt = (
|
||||||
select(cls).where(cls.playlist_id == playlist_id).order_by(cls.row_number)
|
select(cls)
|
||||||
).all()
|
.where(cls.playlist_id == playlist_id)
|
||||||
|
.options(selectinload(cls.track))
|
||||||
|
.order_by(cls.row_number)
|
||||||
|
)
|
||||||
|
plrs = session.execute(stmt).scalars().all()
|
||||||
|
|
||||||
return plrs
|
return plrs
|
||||||
|
|
||||||
@ -595,8 +630,39 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
|
|||||||
session.connection().execute(stmt, sqla_map)
|
session.connection().execute(stmt, sqla_map)
|
||||||
|
|
||||||
|
|
||||||
|
class Queries(dbtables.QueriesTable):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
session: Session,
|
||||||
|
name: str,
|
||||||
|
filter: dbtables.Filter,
|
||||||
|
favourite: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Create new query"""
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
self.filter = filter
|
||||||
|
self.favourite = favourite
|
||||||
|
session.add(self)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all(cls, session: Session) -> Sequence["Queries"]:
|
||||||
|
"""Returns a list of all queries ordered by name"""
|
||||||
|
|
||||||
|
return session.scalars(select(cls).order_by(cls.name)).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_favourites(cls, session: Session) -> Sequence["Queries"]:
|
||||||
|
"""Returns a list of favourite queries ordered by name"""
|
||||||
|
|
||||||
|
return session.scalars(
|
||||||
|
select(cls).where(cls.favourite.is_(True)).order_by(cls.name)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
|
||||||
class Settings(dbtables.SettingsTable):
|
class Settings(dbtables.SettingsTable):
|
||||||
def __init__(self, session: Session, name: str):
|
def __init__(self, session: Session, name: str) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
session.add(self)
|
session.add(self)
|
||||||
session.commit()
|
session.commit()
|
||||||
@ -624,7 +690,7 @@ class Tracks(dbtables.TracksTable):
|
|||||||
fade_at: int,
|
fade_at: int,
|
||||||
silence_at: int,
|
silence_at: int,
|
||||||
bitrate: int,
|
bitrate: int,
|
||||||
):
|
) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
self.title = title
|
self.title = title
|
||||||
self.artist = artist
|
self.artist = artist
|
||||||
@ -679,6 +745,77 @@ class Tracks(dbtables.TracksTable):
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_filtered_tracks(
|
||||||
|
cls, session: Session, filter: Filter
|
||||||
|
) -> Sequence["Tracks"]:
|
||||||
|
"""
|
||||||
|
Return tracks matching filter
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = select(cls)
|
||||||
|
|
||||||
|
# Path specification
|
||||||
|
if filter.path:
|
||||||
|
if filter.path_type == "contains":
|
||||||
|
query = query.where(cls.path.ilike(f"%{filter.path}%"))
|
||||||
|
elif filter.path_type == "excluding":
|
||||||
|
query = query.where(cls.path.notilike(f"%{filter.path}%"))
|
||||||
|
else:
|
||||||
|
raise ApplicationError(f"Can't process filter path ({filter=})")
|
||||||
|
|
||||||
|
# Duration specification
|
||||||
|
seconds_duration = filter.duration_number
|
||||||
|
if filter.duration_unit == Config.FILTER_DURATION_MINUTES:
|
||||||
|
seconds_duration *= 60
|
||||||
|
elif filter.duration_unit != Config.FILTER_DURATION_SECONDS:
|
||||||
|
raise ApplicationError(f"Can't process filter duration ({filter=})")
|
||||||
|
|
||||||
|
if filter.duration_type == Config.FILTER_DURATION_LONGER:
|
||||||
|
query = query.where(cls.duration >= seconds_duration)
|
||||||
|
elif filter.duration_unit == Config.FILTER_DURATION_SHORTER:
|
||||||
|
query = query.where(cls.duration <= seconds_duration)
|
||||||
|
else:
|
||||||
|
raise ApplicationError(f"Can't process filter duration type ({filter=})")
|
||||||
|
|
||||||
|
# Process comparator
|
||||||
|
if filter.last_played_comparator == Config.FILTER_PLAYED_COMPARATOR_NEVER:
|
||||||
|
# Select tracks that have never been played
|
||||||
|
query = query.outerjoin(Playdates, cls.id == Playdates.track_id).where(
|
||||||
|
Playdates.id.is_(None)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Last played specification
|
||||||
|
now = dt.datetime.now()
|
||||||
|
# Set sensible default, and correct for Config.FILTER_PLAYED_COMPARATOR_ANYTIME
|
||||||
|
before = now
|
||||||
|
# If not ANYTIME, set 'before' appropriates
|
||||||
|
if filter.last_played_comparator != Config.FILTER_PLAYED_COMPARATOR_ANYTIME:
|
||||||
|
if filter.last_played_unit == Config.FILTER_PLAYED_DAYS:
|
||||||
|
before = now - dt.timedelta(days=filter.last_played_number)
|
||||||
|
elif filter.last_played_unit == Config.FILTER_PLAYED_WEEKS:
|
||||||
|
before = now - dt.timedelta(days=7 * filter.last_played_number)
|
||||||
|
elif filter.last_played_unit == Config.FILTER_PLAYED_MONTHS:
|
||||||
|
before = now - dt.timedelta(days=30 * filter.last_played_number)
|
||||||
|
elif filter.last_played_unit == Config.FILTER_PLAYED_YEARS:
|
||||||
|
before = now - dt.timedelta(days=365 * filter.last_played_number)
|
||||||
|
|
||||||
|
subquery = (
|
||||||
|
select(
|
||||||
|
Playdates.track_id,
|
||||||
|
func.max(Playdates.lastplayed).label("max_last_played"),
|
||||||
|
)
|
||||||
|
.group_by(Playdates.track_id)
|
||||||
|
.subquery()
|
||||||
|
)
|
||||||
|
query = query.join(subquery, Tracks.id == subquery.c.track_id).where(
|
||||||
|
subquery.c.max_last_played < before
|
||||||
|
)
|
||||||
|
|
||||||
|
records = session.scalars(query).unique().all()
|
||||||
|
|
||||||
|
return records
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by_path(cls, session: Session, path: str) -> Optional["Tracks"]:
|
def get_by_path(cls, session: Session, path: str) -> Optional["Tracks"]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from time import sleep
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
|
# import line_profiler
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg # type: ignore
|
import pyqtgraph as pg # type: ignore
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
@ -29,6 +30,7 @@ from log import log
|
|||||||
from models import PlaylistRows
|
from models import PlaylistRows
|
||||||
from vlcmanager import VLCManager
|
from vlcmanager import VLCManager
|
||||||
|
|
||||||
|
|
||||||
# Define the VLC callback function type
|
# Define the VLC callback function type
|
||||||
# import ctypes
|
# import ctypes
|
||||||
# import platform
|
# import platform
|
||||||
@ -352,21 +354,6 @@ class _Music:
|
|||||||
self.player.set_position(position)
|
self.player.set_position(position)
|
||||||
self.start_dt = start_time
|
self.start_dt = start_time
|
||||||
|
|
||||||
# For as-yet unknown reasons. sometimes the volume gets
|
|
||||||
# reset to zero within 200mS or so of starting play. This
|
|
||||||
# only happened since moving to Debian 12, which uses
|
|
||||||
# Pipewire for sound (which may be irrelevant).
|
|
||||||
# It has been known for the volume to need correcting more
|
|
||||||
# than once in the first 200mS.
|
|
||||||
# Update August 2024: This no longer seems to be an issue
|
|
||||||
# for _ in range(3):
|
|
||||||
# if self.player:
|
|
||||||
# volume = self.player.audio_get_volume()
|
|
||||||
# if volume < Config.VLC_VOLUME_DEFAULT:
|
|
||||||
# self.set_volume(Config.VLC_VOLUME_DEFAULT)
|
|
||||||
# log.error(f"Reset from {volume=}")
|
|
||||||
# sleep(0.1)
|
|
||||||
|
|
||||||
def set_position(self, position: float) -> None:
|
def set_position(self, position: float) -> None:
|
||||||
"""
|
"""
|
||||||
Set player position
|
Set player position
|
||||||
@ -390,17 +377,6 @@ class _Music:
|
|||||||
volume = Config.VLC_VOLUME_DEFAULT
|
volume = Config.VLC_VOLUME_DEFAULT
|
||||||
|
|
||||||
self.player.audio_set_volume(volume)
|
self.player.audio_set_volume(volume)
|
||||||
# Ensure volume correct
|
|
||||||
# For as-yet unknown reasons. sometimes the volume gets
|
|
||||||
# reset to zero within 200mS or so of starting play. This
|
|
||||||
# only happened since moving to Debian 12, which uses
|
|
||||||
# Pipewire for sound (which may be irrelevant).
|
|
||||||
for _ in range(3):
|
|
||||||
current_volume = self.player.audio_get_volume()
|
|
||||||
if current_volume < volume:
|
|
||||||
self.player.audio_set_volume(volume)
|
|
||||||
log.debug(f"Reset from {volume=}")
|
|
||||||
sleep(0.1)
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
"""Immediately stop playing"""
|
"""Immediately stop playing"""
|
||||||
@ -439,6 +415,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 +440,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
|
||||||
|
|||||||
1721
app/musicmuster.py
1721
app/musicmuster.py
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||||
@ -26,6 +24,8 @@ from PyQt6.QtGui import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
|
# import line_profiler
|
||||||
|
from sqlalchemy.orm.session import Session
|
||||||
import obswebsocket # type: ignore
|
import obswebsocket # type: ignore
|
||||||
|
|
||||||
# import snoop # type: ignore
|
# import snoop # type: ignore
|
||||||
@ -46,12 +46,12 @@ from helpers import (
|
|||||||
remove_substring_case_insensitive,
|
remove_substring_case_insensitive,
|
||||||
set_track_metadata,
|
set_track_metadata,
|
||||||
)
|
)
|
||||||
from log import log
|
from log import log, log_call
|
||||||
from models import db, NoteColours, Playdates, PlaylistRows, Tracks
|
from models import db, NoteColours, Playdates, PlaylistRows, Tracks
|
||||||
from music_manager import RowAndTrack, track_sequence
|
from music_manager import RowAndTrack, track_sequence
|
||||||
|
|
||||||
|
|
||||||
HEADER_NOTES_COLUMN = 1
|
HEADER_NOTES_COLUMN = 0
|
||||||
scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]")
|
scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]")
|
||||||
|
|
||||||
|
|
||||||
@ -74,13 +74,14 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
playlist_id: int,
|
playlist_id: int,
|
||||||
*args: Optional[QObject],
|
is_template: bool,
|
||||||
**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
|
||||||
super().__init__(*args, **kwargs)
|
self.is_template = is_template
|
||||||
|
|
||||||
self.playlist_rows: dict[int, RowAndTrack] = {}
|
self.playlist_rows: dict[int, RowAndTrack] = {}
|
||||||
self.signals = MusicMusterSignals()
|
self.signals = MusicMusterSignals()
|
||||||
@ -98,13 +99,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
|
||||||
@ -116,23 +121,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
|
||||||
|
|
||||||
@ -149,42 +151,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
|
||||||
@ -218,10 +230,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()
|
||||||
|
|
||||||
@ -254,26 +267,27 @@ 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)
|
||||||
|
|
||||||
# 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")
|
||||||
@ -283,18 +297,22 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
self.refresh_row(session, plr.row_number)
|
self.refresh_row(session, plr.row_number)
|
||||||
else:
|
else:
|
||||||
log.error(
|
log.error(
|
||||||
f"{self}: Can't retrieve plr, {track_sequence.current.playlistrow_id=}"
|
f"{self}: Can't retrieve plr, "
|
||||||
|
f"{track_sequence.current.playlistrow_id=}"
|
||||||
)
|
)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
# 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
|
|
||||||
self.update_track_times()
|
|
||||||
|
|
||||||
# Find next track
|
# Find next track
|
||||||
next_row = None
|
next_row = None
|
||||||
@ -311,37 +329,21 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
next_row = min(unplayed_rows)
|
next_row = min(unplayed_rows)
|
||||||
if next_row is not None:
|
if next_row is not None:
|
||||||
self.set_next_row(next_row)
|
self.set_next_row(next_row)
|
||||||
|
else:
|
||||||
|
# set_next_row() calls update_track_times(); else we call it
|
||||||
|
self.update_track_times()
|
||||||
|
|
||||||
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,
|
||||||
@ -349,10 +351,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:
|
||||||
@ -382,8 +404,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
|
||||||
"""
|
"""
|
||||||
@ -394,7 +417,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
if column == HEADER_NOTES_COLUMN:
|
if column == HEADER_NOTES_COLUMN:
|
||||||
column_span = 1
|
column_span = 1
|
||||||
if header_row:
|
if header_row:
|
||||||
column_span = self.columnCount() - 1
|
column_span = self.columnCount() - HEADER_NOTES_COLUMN
|
||||||
self.signals.span_cells_signal.emit(
|
self.signals.span_cells_signal.emit(
|
||||||
self.playlist_id, row, HEADER_NOTES_COLUMN, 1, column_span
|
self.playlist_id, row, HEADER_NOTES_COLUMN, 1, column_span
|
||||||
)
|
)
|
||||||
@ -403,45 +426,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:
|
||||||
"""
|
"""
|
||||||
@ -458,37 +481,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()
|
||||||
|
|
||||||
@ -510,24 +534,24 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
Col.ARTIST.value,
|
Col.ARTIST.value,
|
||||||
Col.NOTE.value,
|
Col.NOTE.value,
|
||||||
Col.INTRO.value,
|
Col.INTRO.value,
|
||||||
]:
|
] or self.is_header_row(index.row()) and index.column() == HEADER_NOTES_COLUMN:
|
||||||
return default | Qt.ItemFlag.ItemIsEditable
|
return default | Qt.ItemFlag.ItemIsEditable
|
||||||
|
|
||||||
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]:
|
||||||
"""
|
"""
|
||||||
@ -616,7 +640,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(
|
||||||
@ -624,22 +647,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:
|
||||||
@ -647,14 +670,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()
|
||||||
|
|
||||||
@ -692,7 +715,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,
|
||||||
@ -724,9 +751,16 @@ 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
|
||||||
"""
|
"""
|
||||||
@ -734,15 +768,17 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
@ -772,7 +808,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def load_data(self, session: db.session) -> None:
|
def load_data(self, session: Session) -> None:
|
||||||
"""
|
"""
|
||||||
Same as refresh data, but only used when creating playslit.
|
Same as refresh data, but only used when creating playslit.
|
||||||
Distinguishes profile time between initial load and other
|
Distinguishes profile time between initial load and other
|
||||||
@ -817,7 +853,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:
|
||||||
"""
|
"""
|
||||||
@ -882,7 +922,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,
|
||||||
@ -1059,9 +1103,13 @@ 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: db.session) -> None:
|
def refresh_data(self, session: Session) -> None:
|
||||||
"""
|
"""
|
||||||
Populate self.playlist_rows with playlist data
|
Populate self.playlist_rows with playlist data
|
||||||
|
|
||||||
@ -1112,7 +1160,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:
|
||||||
"""
|
"""
|
||||||
@ -1126,7 +1178,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()
|
||||||
|
|
||||||
@ -1140,8 +1197,6 @@ 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()")
|
|
||||||
|
|
||||||
# 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
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
@ -1186,7 +1241,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]
|
||||||
@ -1395,9 +1456,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()
|
||||||
@ -1547,26 +1613,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:
|
||||||
"""
|
"""
|
||||||
@ -1581,7 +1645,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)
|
||||||
|
|
||||||
@ -1590,8 +1661,6 @@ 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()")
|
|
||||||
|
|
||||||
next_start_time: Optional[dt.datetime] = None
|
next_start_time: Optional[dt.datetime] = None
|
||||||
update_rows: list[int] = []
|
update_rows: list[int] = []
|
||||||
row_count = len(self.playlist_rows)
|
row_count = len(self.playlist_rows)
|
||||||
@ -1730,9 +1799,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
|
||||||
|
|||||||
@ -22,10 +22,7 @@ from PyQt6.QtWidgets import (
|
|||||||
QFrame,
|
QFrame,
|
||||||
QMenu,
|
QMenu,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
QProxyStyle,
|
|
||||||
QStyle,
|
|
||||||
QStyledItemDelegate,
|
QStyledItemDelegate,
|
||||||
QStyleOption,
|
|
||||||
QStyleOptionViewItem,
|
QStyleOptionViewItem,
|
||||||
QTableView,
|
QTableView,
|
||||||
QTableWidgetItem,
|
QTableWidgetItem,
|
||||||
@ -34,10 +31,10 @@ from PyQt6.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
|
# import line_profiler
|
||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
from audacity_controller import AudacityController
|
from classes import ApplicationError, Col, MusicMusterSignals, PlaylistStyle, TrackInfo
|
||||||
from classes import ApplicationError, Col, MusicMusterSignals, TrackInfo
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from dialogs import TrackSelectDialog
|
from dialogs import TrackSelectDialog
|
||||||
from helpers import (
|
from helpers import (
|
||||||
@ -46,7 +43,7 @@ from helpers import (
|
|||||||
show_OK,
|
show_OK,
|
||||||
show_warning,
|
show_warning,
|
||||||
)
|
)
|
||||||
from log import log
|
from log import log, log_call
|
||||||
from models import db, Settings
|
from models import db, Settings
|
||||||
from music_manager import track_sequence
|
from music_manager import track_sequence
|
||||||
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
||||||
@ -186,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)
|
||||||
@ -249,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()
|
||||||
@ -268,24 +265,6 @@ class PlaylistDelegate(QStyledItemDelegate):
|
|||||||
editor.setGeometry(option.rect)
|
editor.setGeometry(option.rect)
|
||||||
|
|
||||||
|
|
||||||
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 PlaylistTab(QTableView):
|
class PlaylistTab(QTableView):
|
||||||
"""
|
"""
|
||||||
The playlist view
|
The playlist view
|
||||||
@ -328,13 +307,6 @@ class PlaylistTab(QTableView):
|
|||||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
|
|
||||||
# Set up for Audacity
|
|
||||||
try:
|
|
||||||
self.ac: Optional[AudacityController] = AudacityController()
|
|
||||||
except ApplicationError as e:
|
|
||||||
self.ac = None
|
|
||||||
show_warning(self.musicmuster, "Audacity error", str(e))
|
|
||||||
|
|
||||||
# Load model, set column widths
|
# Load model, set column widths
|
||||||
self.setModel(model)
|
self.setModel(model)
|
||||||
self._set_column_widths()
|
self._set_column_widths()
|
||||||
@ -364,7 +336,7 @@ class PlaylistTab(QTableView):
|
|||||||
Override closeEditor to enable play controls and update display.
|
Override closeEditor to enable play controls and update display.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.musicmuster.action_Clear_selection.setEnabled(True)
|
self.musicmuster.enable_escape(True)
|
||||||
|
|
||||||
super(PlaylistTab, self).closeEditor(editor, hint)
|
super(PlaylistTab, self).closeEditor(editor, hint)
|
||||||
|
|
||||||
@ -378,7 +350,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
|
||||||
"""
|
"""
|
||||||
@ -566,8 +538,8 @@ class PlaylistTab(QTableView):
|
|||||||
track_path = base_model.get_row_info(model_row_number).path
|
track_path = base_model.get_row_info(model_row_number).path
|
||||||
|
|
||||||
# Open/import in/from Audacity
|
# Open/import in/from Audacity
|
||||||
if track_row and not this_is_current_row:
|
if track_row and not this_is_current_row and self.musicmuster.ac:
|
||||||
if self.ac and track_path == self.ac.path:
|
if track_path == self.musicmuster.ac.path:
|
||||||
# This track was opened in Audacity
|
# This track was opened in Audacity
|
||||||
self._add_context_menu(
|
self._add_context_menu(
|
||||||
"Update from Audacity",
|
"Update from Audacity",
|
||||||
@ -677,8 +649,8 @@ class PlaylistTab(QTableView):
|
|||||||
that we have an edit open.
|
that we have an edit open.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.ac:
|
if self.musicmuster.ac:
|
||||||
self.ac.path = None
|
self.musicmuster.ac.path = None
|
||||||
|
|
||||||
def clear_selection(self) -> None:
|
def clear_selection(self) -> None:
|
||||||
"""Unselect all tracks and reset drag mode"""
|
"""Unselect all tracks and reset drag mode"""
|
||||||
@ -774,14 +746,29 @@ class PlaylistTab(QTableView):
|
|||||||
if row_count < 1:
|
if row_count < 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Don't delete current or next tracks
|
||||||
|
selected_row_numbers = self.selected_model_row_numbers()
|
||||||
|
for ts in [
|
||||||
|
track_sequence.next,
|
||||||
|
track_sequence.current,
|
||||||
|
]:
|
||||||
|
if ts:
|
||||||
|
if (
|
||||||
|
ts.playlist_id == self.playlist_id
|
||||||
|
and ts.row_number in selected_row_numbers
|
||||||
|
):
|
||||||
|
self.musicmuster.show_warning(
|
||||||
|
"Delete not allowed", "Can't delete current or next track"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Get confirmation
|
# Get confirmation
|
||||||
plural = "s" if row_count > 1 else ""
|
plural = "s" if row_count > 1 else ""
|
||||||
if not ask_yes_no("Delete rows", f"Really delete {row_count} row{plural}?"):
|
if not ask_yes_no("Delete rows", f"Really delete {row_count} row{plural}?"):
|
||||||
return
|
return
|
||||||
|
|
||||||
base_model = self.get_base_model()
|
base_model = self.get_base_model()
|
||||||
|
base_model.delete_rows(selected_row_numbers)
|
||||||
base_model.delete_rows(self.selected_model_row_numbers())
|
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
def get_base_model(self) -> PlaylistModel:
|
def get_base_model(self) -> PlaylistModel:
|
||||||
@ -830,14 +817,12 @@ class PlaylistTab(QTableView):
|
|||||||
|
|
||||||
# Use a set to deduplicate result (a selected row will have all
|
# Use a set to deduplicate result (a selected row will have all
|
||||||
# items in that row selected)
|
# items in that row selected)
|
||||||
result = sorted(
|
selected_indexes = self.selectedIndexes()
|
||||||
list(
|
|
||||||
set([self.model().mapToSource(a).row() for a in self.selectedIndexes()])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
log.debug(f"get_selected_rows() returned: {result=}")
|
if not selected_indexes:
|
||||||
return result
|
return []
|
||||||
|
|
||||||
|
return sorted(list(set([self.model().mapToSource(a).row() for a in selected_indexes])))
|
||||||
|
|
||||||
def get_top_visible_row(self) -> int:
|
def get_top_visible_row(self) -> int:
|
||||||
"""
|
"""
|
||||||
@ -867,10 +852,10 @@ class PlaylistTab(QTableView):
|
|||||||
Import current Audacity track to passed row
|
Import current Audacity track to passed row
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.ac:
|
if not self.musicmuster.ac:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
self.ac.export()
|
self.musicmuster.ac.export()
|
||||||
self._rescan(row_number)
|
self._rescan(row_number)
|
||||||
except ApplicationError as e:
|
except ApplicationError as e:
|
||||||
show_warning(self.musicmuster, "Audacity error", str(e))
|
show_warning(self.musicmuster, "Audacity error", str(e))
|
||||||
@ -927,15 +912,16 @@ class PlaylistTab(QTableView):
|
|||||||
Open track in passed row in Audacity
|
Open track in passed row in Audacity
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if not self.musicmuster.ac:
|
||||||
|
return
|
||||||
|
|
||||||
path = self.get_base_model().get_row_track_path(row_number)
|
path = self.get_base_model().get_row_track_path(row_number)
|
||||||
if not path:
|
if not path:
|
||||||
log.error(f"_open_in_audacity: can't get path for {row_number=}")
|
log.error(f"_open_in_audacity: can't get path for {row_number=}")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not self.ac:
|
self.musicmuster.ac.open(path)
|
||||||
self.ac = AudacityController()
|
|
||||||
self.ac.open(path)
|
|
||||||
except ApplicationError as e:
|
except ApplicationError as e:
|
||||||
show_warning(self.musicmuster, "Audacity error", str(e))
|
show_warning(self.musicmuster, "Audacity error", str(e))
|
||||||
|
|
||||||
|
|||||||
290
app/querylistmodel.py
Normal file
290
app/querylistmodel.py
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
# Standard library imports
|
||||||
|
# Allow forward reference to PlaylistModel
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
|
from PyQt6.QtCore import (
|
||||||
|
QAbstractTableModel,
|
||||||
|
QModelIndex,
|
||||||
|
Qt,
|
||||||
|
QVariant,
|
||||||
|
)
|
||||||
|
from PyQt6.QtGui import (
|
||||||
|
QBrush,
|
||||||
|
QColor,
|
||||||
|
QFont,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
|
# import snoop # type: ignore
|
||||||
|
|
||||||
|
# App imports
|
||||||
|
from classes import (
|
||||||
|
ApplicationError,
|
||||||
|
Filter,
|
||||||
|
QueryCol,
|
||||||
|
)
|
||||||
|
from config import Config
|
||||||
|
from helpers import (
|
||||||
|
file_is_unreadable,
|
||||||
|
get_relative_date,
|
||||||
|
ms_to_mmss,
|
||||||
|
show_warning,
|
||||||
|
)
|
||||||
|
from log import log
|
||||||
|
from models import db, Playdates, Tracks
|
||||||
|
from music_manager import RowAndTrack
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class QueryRow:
|
||||||
|
artist: str
|
||||||
|
bitrate: int
|
||||||
|
duration: int
|
||||||
|
lastplayed: Optional[dt.datetime]
|
||||||
|
path: str
|
||||||
|
title: str
|
||||||
|
track_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class QuerylistModel(QAbstractTableModel):
|
||||||
|
"""
|
||||||
|
The Querylist Model
|
||||||
|
|
||||||
|
Used to support query lists. The underlying database is never
|
||||||
|
updated. We just present tracks that match a query and allow the user
|
||||||
|
to copy those to a playlist.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, session: Session, filter: Filter) -> None:
|
||||||
|
"""
|
||||||
|
Load query
|
||||||
|
"""
|
||||||
|
|
||||||
|
log.debug(f"QuerylistModel.__init__({filter=})")
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
self.session = session
|
||||||
|
self.filter = filter
|
||||||
|
|
||||||
|
self.querylist_rows: dict[int, QueryRow] = {}
|
||||||
|
self._selected_rows: set[int] = set()
|
||||||
|
|
||||||
|
self.load_data()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<QuerylistModel: filter={self.filter}, {self.rowCount()} rows>"
|
||||||
|
|
||||||
|
def _background_role(self, row: int, column: int, qrow: QueryRow) -> QBrush:
|
||||||
|
"""Return background setting"""
|
||||||
|
|
||||||
|
# Unreadable track file
|
||||||
|
if file_is_unreadable(qrow.path):
|
||||||
|
return QBrush(QColor(Config.COLOUR_UNREADABLE))
|
||||||
|
|
||||||
|
# Selected row
|
||||||
|
if row in self._selected_rows:
|
||||||
|
return QBrush(QColor(Config.COLOUR_QUERYLIST_SELECTED))
|
||||||
|
|
||||||
|
# Individual cell colouring
|
||||||
|
if column == QueryCol.BITRATE.value:
|
||||||
|
if not qrow.bitrate or qrow.bitrate < Config.BITRATE_LOW_THRESHOLD:
|
||||||
|
return QBrush(QColor(Config.COLOUR_BITRATE_LOW))
|
||||||
|
elif qrow.bitrate < Config.BITRATE_OK_THRESHOLD:
|
||||||
|
return QBrush(QColor(Config.COLOUR_BITRATE_MEDIUM))
|
||||||
|
else:
|
||||||
|
return QBrush(QColor(Config.COLOUR_BITRATE_OK))
|
||||||
|
|
||||||
|
return QBrush()
|
||||||
|
|
||||||
|
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||||
|
"""Standard function for view"""
|
||||||
|
|
||||||
|
return len(QueryCol)
|
||||||
|
|
||||||
|
def data(
|
||||||
|
self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole
|
||||||
|
) -> QVariant:
|
||||||
|
"""Return data to view"""
|
||||||
|
|
||||||
|
if (
|
||||||
|
not index.isValid()
|
||||||
|
or not (0 <= index.row() < len(self.querylist_rows))
|
||||||
|
or role
|
||||||
|
in [
|
||||||
|
Qt.ItemDataRole.CheckStateRole,
|
||||||
|
Qt.ItemDataRole.DecorationRole,
|
||||||
|
Qt.ItemDataRole.EditRole,
|
||||||
|
Qt.ItemDataRole.FontRole,
|
||||||
|
Qt.ItemDataRole.ForegroundRole,
|
||||||
|
Qt.ItemDataRole.InitialSortOrderRole,
|
||||||
|
Qt.ItemDataRole.SizeHintRole,
|
||||||
|
Qt.ItemDataRole.StatusTipRole,
|
||||||
|
Qt.ItemDataRole.TextAlignmentRole,
|
||||||
|
Qt.ItemDataRole.WhatsThisRole,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
return QVariant()
|
||||||
|
|
||||||
|
row = index.row()
|
||||||
|
column = index.column()
|
||||||
|
# rat for playlist row data as it's used a lot
|
||||||
|
qrow = self.querylist_rows[row]
|
||||||
|
|
||||||
|
# Dispatch to role-specific functions
|
||||||
|
dispatch_table: dict[int, Callable] = {
|
||||||
|
int(Qt.ItemDataRole.BackgroundRole): self._background_role,
|
||||||
|
int(Qt.ItemDataRole.DisplayRole): self._display_role,
|
||||||
|
int(Qt.ItemDataRole.ToolTipRole): self._tooltip_role,
|
||||||
|
}
|
||||||
|
|
||||||
|
if role in dispatch_table:
|
||||||
|
return QVariant(dispatch_table[role](row, column, qrow))
|
||||||
|
|
||||||
|
# Fall through to no-op
|
||||||
|
return QVariant()
|
||||||
|
|
||||||
|
def _display_role(self, row: int, column: int, qrow: QueryRow) -> str:
|
||||||
|
"""
|
||||||
|
Return text for display
|
||||||
|
"""
|
||||||
|
|
||||||
|
dispatch_table = {
|
||||||
|
QueryCol.ARTIST.value: qrow.artist,
|
||||||
|
QueryCol.BITRATE.value: str(qrow.bitrate),
|
||||||
|
QueryCol.DURATION.value: ms_to_mmss(qrow.duration),
|
||||||
|
QueryCol.LAST_PLAYED.value: get_relative_date(qrow.lastplayed),
|
||||||
|
QueryCol.TITLE.value: qrow.title,
|
||||||
|
}
|
||||||
|
if column in dispatch_table:
|
||||||
|
return dispatch_table[column]
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
|
||||||
|
"""
|
||||||
|
Standard model flags
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not index.isValid():
|
||||||
|
return Qt.ItemFlag.NoItemFlags
|
||||||
|
|
||||||
|
return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
|
||||||
|
|
||||||
|
def get_selected_track_ids(self) -> list[int]:
|
||||||
|
"""
|
||||||
|
Return a list of track_ids from selected tracks
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [self.querylist_rows[row].track_id for row in self._selected_rows]
|
||||||
|
|
||||||
|
def headerData(
|
||||||
|
self,
|
||||||
|
section: int,
|
||||||
|
orientation: Qt.Orientation,
|
||||||
|
role: int = Qt.ItemDataRole.DisplayRole,
|
||||||
|
) -> QVariant:
|
||||||
|
"""
|
||||||
|
Return text for headers
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_dispatch_table = {
|
||||||
|
QueryCol.TITLE.value: QVariant(Config.HEADER_TITLE),
|
||||||
|
QueryCol.ARTIST.value: QVariant(Config.HEADER_ARTIST),
|
||||||
|
QueryCol.DURATION.value: QVariant(Config.HEADER_DURATION),
|
||||||
|
QueryCol.LAST_PLAYED.value: QVariant(Config.HEADER_LAST_PLAYED),
|
||||||
|
QueryCol.BITRATE.value: QVariant(Config.HEADER_BITRATE),
|
||||||
|
}
|
||||||
|
|
||||||
|
if role == Qt.ItemDataRole.DisplayRole:
|
||||||
|
if orientation == Qt.Orientation.Horizontal:
|
||||||
|
return display_dispatch_table[section]
|
||||||
|
else:
|
||||||
|
if Config.ROWS_FROM_ZERO:
|
||||||
|
return QVariant(str(section))
|
||||||
|
else:
|
||||||
|
return QVariant(str(section + 1))
|
||||||
|
|
||||||
|
elif role == Qt.ItemDataRole.FontRole:
|
||||||
|
boldfont = QFont()
|
||||||
|
boldfont.setBold(True)
|
||||||
|
return QVariant(boldfont)
|
||||||
|
|
||||||
|
return QVariant()
|
||||||
|
|
||||||
|
def load_data(self) -> None:
|
||||||
|
"""
|
||||||
|
Populate self.querylist_rows
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Clear any exsiting rows
|
||||||
|
self.querylist_rows = {}
|
||||||
|
row = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
results = Tracks.get_filtered_tracks(self.session, self.filter)
|
||||||
|
for result in results:
|
||||||
|
lastplayed = None
|
||||||
|
if hasattr(result, "playdates"):
|
||||||
|
pds = result.playdates
|
||||||
|
if pds:
|
||||||
|
lastplayed = max([a.lastplayed for a in pds])
|
||||||
|
queryrow = QueryRow(
|
||||||
|
artist=result.artist,
|
||||||
|
bitrate=result.bitrate or 0,
|
||||||
|
duration=result.duration,
|
||||||
|
lastplayed=lastplayed,
|
||||||
|
path=result.path,
|
||||||
|
title=result.title,
|
||||||
|
track_id=result.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.querylist_rows[row] = queryrow
|
||||||
|
row += 1
|
||||||
|
except ApplicationError as e:
|
||||||
|
show_warning(None, "Query error", f"Error loading query data ({e})")
|
||||||
|
|
||||||
|
def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
|
||||||
|
"""Standard function for view"""
|
||||||
|
|
||||||
|
return len(self.querylist_rows)
|
||||||
|
|
||||||
|
def toggle_row_selection(self, row: int) -> None:
|
||||||
|
if row in self._selected_rows:
|
||||||
|
self._selected_rows.discard(row)
|
||||||
|
else:
|
||||||
|
self._selected_rows.add(row)
|
||||||
|
|
||||||
|
# Emit dataChanged for the entire row
|
||||||
|
top_left = self.index(row, 0)
|
||||||
|
bottom_right = self.index(row, self.columnCount() - 1)
|
||||||
|
self.dataChanged.emit(top_left, bottom_right, [Qt.ItemDataRole.BackgroundRole])
|
||||||
|
|
||||||
|
def _tooltip_role(self, row: int, column: int, rat: RowAndTrack) -> str | QVariant:
|
||||||
|
"""
|
||||||
|
Return tooltip. Currently only used for last_played column.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if column != QueryCol.LAST_PLAYED.value:
|
||||||
|
return QVariant()
|
||||||
|
with db.Session() as session:
|
||||||
|
track_id = self.querylist_rows[row].track_id
|
||||||
|
if not track_id:
|
||||||
|
return QVariant()
|
||||||
|
playdates = Playdates.last_playdates(session, track_id)
|
||||||
|
return (
|
||||||
|
"<br>".join(
|
||||||
|
[
|
||||||
|
a.lastplayed.strftime(Config.LAST_PLAYED_TOOLTIP_DATE_FORMAT)
|
||||||
|
for a in reversed(playdates)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
94
app/ui/dlgQuery.ui
Normal file
94
app/ui/dlgQuery.ui
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>queryDialog</class>
|
||||||
|
<widget class="QDialog" name="queryDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>762</width>
|
||||||
|
<height>686</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Query</string>
|
||||||
|
</property>
|
||||||
|
<widget class="QTableView" name="tableView">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>10</x>
|
||||||
|
<y>65</y>
|
||||||
|
<width>741</width>
|
||||||
|
<height>561</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>20</x>
|
||||||
|
<y>10</y>
|
||||||
|
<width>61</width>
|
||||||
|
<height>24</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Query:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QComboBox" name="cboQuery">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>80</x>
|
||||||
|
<y>10</y>
|
||||||
|
<width>221</width>
|
||||||
|
<height>32</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QPushButton" name="btnAddTracks">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>530</x>
|
||||||
|
<y>640</y>
|
||||||
|
<width>102</width>
|
||||||
|
<height>36</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Add &tracks</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QLabel" name="lblDescription">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>330</x>
|
||||||
|
<y>10</y>
|
||||||
|
<width>401</width>
|
||||||
|
<height>46</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>TextLabel</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QPushButton" name="pushButton">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>650</x>
|
||||||
|
<y>640</y>
|
||||||
|
<width>102</width>
|
||||||
|
<height>36</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Close</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
45
app/ui/dlgQuery_ui.py
Normal file
45
app/ui/dlgQuery_ui.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Form implementation generated from reading ui file 'app/ui/dlgQuery.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_queryDialog(object):
|
||||||
|
def setupUi(self, queryDialog):
|
||||||
|
queryDialog.setObjectName("queryDialog")
|
||||||
|
queryDialog.resize(762, 686)
|
||||||
|
self.tableView = QtWidgets.QTableView(parent=queryDialog)
|
||||||
|
self.tableView.setGeometry(QtCore.QRect(10, 65, 741, 561))
|
||||||
|
self.tableView.setObjectName("tableView")
|
||||||
|
self.label = QtWidgets.QLabel(parent=queryDialog)
|
||||||
|
self.label.setGeometry(QtCore.QRect(20, 10, 61, 24))
|
||||||
|
self.label.setObjectName("label")
|
||||||
|
self.cboQuery = QtWidgets.QComboBox(parent=queryDialog)
|
||||||
|
self.cboQuery.setGeometry(QtCore.QRect(80, 10, 221, 32))
|
||||||
|
self.cboQuery.setObjectName("cboQuery")
|
||||||
|
self.btnAddTracks = QtWidgets.QPushButton(parent=queryDialog)
|
||||||
|
self.btnAddTracks.setGeometry(QtCore.QRect(530, 640, 102, 36))
|
||||||
|
self.btnAddTracks.setObjectName("btnAddTracks")
|
||||||
|
self.lblDescription = QtWidgets.QLabel(parent=queryDialog)
|
||||||
|
self.lblDescription.setGeometry(QtCore.QRect(330, 10, 401, 46))
|
||||||
|
self.lblDescription.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop)
|
||||||
|
self.lblDescription.setObjectName("lblDescription")
|
||||||
|
self.pushButton = QtWidgets.QPushButton(parent=queryDialog)
|
||||||
|
self.pushButton.setGeometry(QtCore.QRect(650, 640, 102, 36))
|
||||||
|
self.pushButton.setObjectName("pushButton")
|
||||||
|
|
||||||
|
self.retranslateUi(queryDialog)
|
||||||
|
QtCore.QMetaObject.connectSlotsByName(queryDialog)
|
||||||
|
|
||||||
|
def retranslateUi(self, queryDialog):
|
||||||
|
_translate = QtCore.QCoreApplication.translate
|
||||||
|
queryDialog.setWindowTitle(_translate("queryDialog", "Query"))
|
||||||
|
self.label.setText(_translate("queryDialog", "Query:"))
|
||||||
|
self.btnAddTracks.setText(_translate("queryDialog", "Add &tracks"))
|
||||||
|
self.lblDescription.setText(_translate("queryDialog", "TextLabel"))
|
||||||
|
self.pushButton.setText(_translate("queryDialog", "Close"))
|
||||||
@ -1,6 +1,7 @@
|
|||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="icons">
|
<qresource prefix="icons">
|
||||||
<file>yellow-circle.png</file>
|
<file>yellow-circle.png</file>
|
||||||
|
<file>redstar.png</file>
|
||||||
<file>green-circle.png</file>
|
<file>green-circle.png</file>
|
||||||
<file>star.png</file>
|
<file>star.png</file>
|
||||||
<file>star_empty.png</file>
|
<file>star_empty.png</file>
|
||||||
|
|||||||
1399
app/ui/icons_rc.py
1399
app/ui/icons_rc.py
File diff suppressed because it is too large
Load Diff
@ -997,6 +997,9 @@ padding-left: 8px;</string>
|
|||||||
<addaction name="actionRenamePlaylist"/>
|
<addaction name="actionRenamePlaylist"/>
|
||||||
<addaction name="actionDeletePlaylist"/>
|
<addaction name="actionDeletePlaylist"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
|
<addaction name="actionOpenQuerylist"/>
|
||||||
|
<addaction name="actionManage_querylists"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
<addaction name="actionSave_as_template"/>
|
<addaction name="actionSave_as_template"/>
|
||||||
<addaction name="actionManage_templates"/>
|
<addaction name="actionManage_templates"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
@ -1369,6 +1372,16 @@ padding-left: 8px;</string>
|
|||||||
<string>Import files...</string>
|
<string>Import files...</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionOpenQuerylist">
|
||||||
|
<property name="text">
|
||||||
|
<string>Open &querylist...</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionManage_querylists">
|
||||||
|
<property name="text">
|
||||||
|
<string>Manage querylists...</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>1249</width>
|
<width>1249</width>
|
||||||
<height>499</height>
|
<height>538</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
|
|||||||
@ -1,843 +0,0 @@
|
|||||||
# Form implementation generated from reading ui file 'app/ui/main_window.ui'
|
|
||||||
#
|
|
||||||
# Created by: PyQt6 UI code generator 6.8.0
|
|
||||||
#
|
|
||||||
# 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.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.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 §ion 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
|
|
||||||
BIN
app/ui/redstar.png
Normal file
BIN
app/ui/redstar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@ -1,73 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""Tests the audacity pipe.
|
|
||||||
Keep pipe_test.py short!!
|
|
||||||
You can make more complicated longer tests to test other functionality
|
|
||||||
or to generate screenshots etc in other scripts.
|
|
||||||
Make sure Audacity is running first and that mod-script-pipe is enabled
|
|
||||||
before running this script.
|
|
||||||
Requires Python 2.7 or later. Python 3 is strongly recommended.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
print("pipe-test.py, running on windows")
|
|
||||||
TONAME = '\\\\.\\pipe\\ToSrvPipe'
|
|
||||||
FROMNAME = '\\\\.\\pipe\\FromSrvPipe'
|
|
||||||
EOL = '\r\n\0'
|
|
||||||
else:
|
|
||||||
print("pipe-test.py, running on linux or mac")
|
|
||||||
TONAME = '/tmp/audacity_script_pipe.to.' + str(os.getuid())
|
|
||||||
FROMNAME = '/tmp/audacity_script_pipe.from.' + str(os.getuid())
|
|
||||||
EOL = '\n'
|
|
||||||
|
|
||||||
print("Write to \"" + TONAME + "\"")
|
|
||||||
if not os.path.exists(TONAME):
|
|
||||||
print(" does not exist. Ensure Audacity is running with mod-script-pipe.")
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
print("Read from \"" + FROMNAME + "\"")
|
|
||||||
if not os.path.exists(FROMNAME):
|
|
||||||
print(" does not exist. Ensure Audacity is running with mod-script-pipe.")
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
print("-- Both pipes exist. Good.")
|
|
||||||
|
|
||||||
TOFILE = open(TONAME, 'w')
|
|
||||||
print("-- File to write to has been opened")
|
|
||||||
FROMFILE = open(FROMNAME, 'rt')
|
|
||||||
print("-- File to read from has now been opened too\r\n")
|
|
||||||
|
|
||||||
|
|
||||||
def send_command(command):
|
|
||||||
"""Send a single command."""
|
|
||||||
print("Send: >>> \n"+command)
|
|
||||||
TOFILE.write(command + EOL)
|
|
||||||
TOFILE.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def get_response():
|
|
||||||
"""Return the command response."""
|
|
||||||
result = ''
|
|
||||||
line = ''
|
|
||||||
while True:
|
|
||||||
result += line
|
|
||||||
line = FROMFILE.readline()
|
|
||||||
if line == '\n' and len(result) > 0:
|
|
||||||
break
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def do_command(command):
|
|
||||||
"""Send one command, and return the response."""
|
|
||||||
send_command(command)
|
|
||||||
response = get_response()
|
|
||||||
print("Rcvd: <<< \n" + response)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
do_command('Import2: Filename=/home/kae/git/musicmuster/archive/boot.flac')
|
|
||||||
@ -1 +0,0 @@
|
|||||||
Run Flake8 and Black
|
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
"""add favouirit to playlists
|
||||||
|
|
||||||
|
Revision ID: 04df697e40cd
|
||||||
|
Revises: 33c04e3c12c8
|
||||||
|
Create Date: 2025-02-22 20:20:45.030024
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '04df697e40cd'
|
||||||
|
down_revision = '33c04e3c12c8'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(engine_name: str) -> None:
|
||||||
|
globals()["upgrade_%s" % engine_name]()
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(engine_name: str) -> None:
|
||||||
|
globals()["downgrade_%s" % engine_name]()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade_() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('notecolours', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('strip_substring', sa.Boolean(), nullable=False))
|
||||||
|
batch_op.create_index(batch_op.f('ix_notecolours_substring'), ['substring'], unique=False)
|
||||||
|
|
||||||
|
with op.batch_alter_table('playlist_rows', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint('playlist_rows_ibfk_1', type_='foreignkey')
|
||||||
|
|
||||||
|
with op.batch_alter_table('playlists', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('favourite', sa.Boolean(), nullable=False))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade_() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('playlists', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('favourite')
|
||||||
|
|
||||||
|
with op.batch_alter_table('playlist_rows', schema=None) as batch_op:
|
||||||
|
batch_op.create_foreign_key('playlist_rows_ibfk_1', 'tracks', ['track_id'], ['id'])
|
||||||
|
|
||||||
|
with op.batch_alter_table('notecolours', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_notecolours_substring'))
|
||||||
|
batch_op.drop_column('strip_substring')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
47
migrations/versions/4fc2a9a82ab0_create_queries_table.py
Normal file
47
migrations/versions/4fc2a9a82ab0_create_queries_table.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""create queries table
|
||||||
|
|
||||||
|
Revision ID: 4fc2a9a82ab0
|
||||||
|
Revises: ab475332d873
|
||||||
|
Create Date: 2025-02-26 13:13:25.118489
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import dbtables
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4fc2a9a82ab0'
|
||||||
|
down_revision = 'ab475332d873'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(engine_name: str) -> None:
|
||||||
|
globals()["upgrade_%s" % engine_name]()
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(engine_name: str) -> None:
|
||||||
|
globals()["downgrade_%s" % engine_name]()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade_() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('queries',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('filter_data', dbtables.JSONEncodedDict(), nullable=False),
|
||||||
|
sa.Column('favourite', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade_() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('queries')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
46
migrations/versions/ab475332d873_fix_playdates_cascades.py
Normal file
46
migrations/versions/ab475332d873_fix_playdates_cascades.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""fix playlist cascades
|
||||||
|
|
||||||
|
Revision ID: ab475332d873
|
||||||
|
Revises: 04df697e40cd
|
||||||
|
Create Date: 2025-02-26 13:11:15.417278
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'ab475332d873'
|
||||||
|
down_revision = '04df697e40cd'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(engine_name: str) -> None:
|
||||||
|
globals()["upgrade_%s" % engine_name]()
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(engine_name: str) -> None:
|
||||||
|
globals()["downgrade_%s" % engine_name]()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade_() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('playdates', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint('fk_playdates_track_id_tracks', type_='foreignkey')
|
||||||
|
batch_op.create_foreign_key(None, 'tracks', ['track_id'], ['id'], ondelete='CASCADE')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade_() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('playdates', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint(None, type_='foreignkey')
|
||||||
|
batch_op.create_foreign_key('fk_playdates_track_id_tracks', 'tracks', ['track_id'], ['id'])
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
6
mmimport
6
mmimport
@ -1,6 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# cd /home/kae/mm
|
|
||||||
# MYSQL_CONNECT="mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod" ROOT="/home/kae/music" direnv exec .
|
|
||||||
for file in "$@"; do
|
|
||||||
app/songdb.py -i "$file"
|
|
||||||
done
|
|
||||||
2048
poetry.lock
generated
2048
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -2,11 +2,9 @@
|
|||||||
name = "musicmuster"
|
name = "musicmuster"
|
||||||
version = "4.1.10"
|
version = "4.1.10"
|
||||||
description = "Music player for internet radio"
|
description = "Music player for internet radio"
|
||||||
authors = [
|
authors = [{ name = "Keith Edmunds", email = "kae@midnighthax.com" }]
|
||||||
{ name = "Keith Edmunds", email = "kae@midnighthax.com" }
|
requires-python = ">=3.13,<4"
|
||||||
]
|
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11,<4.0"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alchemical>=1.0.2",
|
"alchemical>=1.0.2",
|
||||||
"alembic>=1.14.0",
|
"alembic>=1.14.0",
|
||||||
@ -31,27 +29,35 @@ dependencies = [
|
|||||||
"tinytag>=1.10.1",
|
"tinytag>=1.10.1",
|
||||||
"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",
|
||||||
|
"types-pyyaml>=6.0.12.20241230",
|
||||||
|
"dogpile-cache>=1.3.4",
|
||||||
|
"pdbpp>=0.10.3",
|
||||||
|
"filetype>=1.2.0",
|
||||||
|
"black>=25.1.0",
|
||||||
|
"slugify>=0.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"flakehell>=0.9.0,<0.10",
|
||||||
|
"ipdb>=0.13.9,<0.14",
|
||||||
|
"line-profiler>=4.2.0,<5",
|
||||||
|
"mypy>=1.15.0,<2",
|
||||||
|
"pudb",
|
||||||
|
"pydub-stubs>=0.25.1,<0.26",
|
||||||
|
"pytest>=8.3.4,<9",
|
||||||
|
"pytest-qt>=4.4.0,<5",
|
||||||
|
"black>=25.1.0,<26",
|
||||||
|
"pytest-cov>=6.0.0,<7",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.uv]
|
||||||
package-mode = false
|
package = false
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
flakehell = "^0.9.0"
|
|
||||||
ipdb = "^0.13.9"
|
|
||||||
line-profiler = "^4.2.0"
|
|
||||||
mypy = "^1.15.0"
|
|
||||||
pudb = "*"
|
|
||||||
pydub-stubs = "^0.25.1"
|
|
||||||
pytest = "^8.3.4"
|
|
||||||
pytest-qt = "^4.4.0"
|
|
||||||
black = "^25.1.0"
|
|
||||||
pytest-cov = "^6.0.0"
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["hatchling"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
mypy_path = "/home/kae/git/musicmuster/app"
|
mypy_path = "/home/kae/git/musicmuster/app"
|
||||||
@ -60,6 +66,9 @@ python_version = 3.11
|
|||||||
warn_unused_configs = true
|
warn_unused_configs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
|
||||||
|
[tool.pylsp.plugins.pycodestyle]
|
||||||
|
maxLineLength = 88
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "--exitfirst --showlocals --capture=no"
|
addopts = "--exitfirst --showlocals --capture=no"
|
||||||
pythonpath = [".", "app"]
|
pythonpath = [".", "app"]
|
||||||
|
|||||||
@ -57,8 +57,8 @@ class MyTestCase(unittest.TestCase):
|
|||||||
# Create a playlist for all tests
|
# Create a playlist for all tests
|
||||||
playlist_name = "file importer playlist"
|
playlist_name = "file importer playlist"
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
playlist = Playlists(session, playlist_name)
|
playlist = Playlists(session=session, name=playlist_name, template_id=0)
|
||||||
cls.widget.create_playlist_tab(playlist)
|
cls.widget._open_playlist(playlist)
|
||||||
|
|
||||||
# Create our musicstore
|
# Create our musicstore
|
||||||
cls.import_source = tempfile.mkdtemp(suffix="_MMsource_pytest", dir="/tmp")
|
cls.import_source = tempfile.mkdtemp(suffix="_MMsource_pytest", dir="/tmp")
|
||||||
|
|||||||
@ -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"
|
||||||
@ -108,7 +111,7 @@ class TestMMModels(unittest.TestCase):
|
|||||||
TEMPLATE_NAME = "my template"
|
TEMPLATE_NAME = "my template"
|
||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
playlist = Playlists(session, "my playlist")
|
playlist = Playlists(session, "my playlist", template_id=0)
|
||||||
assert playlist
|
assert playlist
|
||||||
# test repr
|
# test repr
|
||||||
_ = str(playlist)
|
_ = str(playlist)
|
||||||
@ -119,23 +122,18 @@ class TestMMModels(unittest.TestCase):
|
|||||||
# create template
|
# create template
|
||||||
Playlists.save_as_template(session, playlist.id, TEMPLATE_NAME)
|
Playlists.save_as_template(session, playlist.id, TEMPLATE_NAME)
|
||||||
|
|
||||||
# test create template
|
|
||||||
_ = Playlists.create_playlist_from_template(
|
|
||||||
session, playlist, "my new name"
|
|
||||||
)
|
|
||||||
|
|
||||||
# get all templates
|
# get all templates
|
||||||
all_templates = Playlists.get_all_templates(session)
|
all_templates = Playlists.get_all_templates(session)
|
||||||
assert len(all_templates) == 1
|
assert len(all_templates) == 1
|
||||||
# Save as template creates new playlist
|
# Save as template creates new playlist
|
||||||
assert all_templates[0] != playlist
|
assert all_templates[0] != playlist
|
||||||
# test delete playlist
|
# test delete playlist
|
||||||
playlist.delete(session)
|
session.delete(playlist)
|
||||||
|
|
||||||
def test_playlist_open_and_close(self):
|
def test_playlist_open_and_close(self):
|
||||||
# We need a playlist
|
# We need a playlist
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
playlist = Playlists(session, "my playlist")
|
playlist = Playlists(session, "my playlist", template_id=0)
|
||||||
|
|
||||||
assert len(Playlists.get_open(session)) == 0
|
assert len(Playlists.get_open(session)) == 0
|
||||||
assert len(Playlists.get_closed(session)) == 1
|
assert len(Playlists.get_closed(session)) == 1
|
||||||
@ -155,8 +153,8 @@ class TestMMModels(unittest.TestCase):
|
|||||||
p1_name = "playlist one"
|
p1_name = "playlist one"
|
||||||
p2_name = "playlist two"
|
p2_name = "playlist two"
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
playlist1 = Playlists(session, p1_name)
|
playlist1 = Playlists(session, p1_name, template_id=0)
|
||||||
_ = Playlists(session, p2_name)
|
_ = Playlists(session, p2_name, template_id=0)
|
||||||
|
|
||||||
all_playlists = Playlists.get_all(session)
|
all_playlists = Playlists.get_all(session)
|
||||||
assert len(all_playlists) == 2
|
assert len(all_playlists) == 2
|
||||||
@ -205,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"
|
||||||
@ -218,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,
|
||||||
@ -241,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"
|
||||||
@ -254,7 +273,7 @@ class TestMMModels(unittest.TestCase):
|
|||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
if Playlists.name_is_available(session, PLAYLIST_NAME):
|
if Playlists.name_is_available(session, PLAYLIST_NAME):
|
||||||
playlist = Playlists(session, PLAYLIST_NAME)
|
playlist = Playlists(session, PLAYLIST_NAME, template_id=0)
|
||||||
assert playlist
|
assert playlist
|
||||||
|
|
||||||
assert Playlists.name_is_available(session, PLAYLIST_NAME) is False
|
assert Playlists.name_is_available(session, PLAYLIST_NAME) is False
|
||||||
@ -266,7 +285,7 @@ class TestMMModels(unittest.TestCase):
|
|||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
if Playlists.name_is_available(session, PLAYLIST_NAME):
|
if Playlists.name_is_available(session, PLAYLIST_NAME):
|
||||||
playlist = Playlists(session, PLAYLIST_NAME)
|
playlist = Playlists(session=session, name=PLAYLIST_NAME, template_id=0)
|
||||||
|
|
||||||
plr = PlaylistRows(session, playlist.id, 1)
|
plr = PlaylistRows(session, playlist.id, 1)
|
||||||
assert plr
|
assert plr
|
||||||
@ -279,7 +298,7 @@ class TestMMModels(unittest.TestCase):
|
|||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
if Playlists.name_is_available(session, PLAYLIST_NAME):
|
if Playlists.name_is_available(session, PLAYLIST_NAME):
|
||||||
playlist = Playlists(session, PLAYLIST_NAME)
|
playlist = Playlists(session=session, name=PLAYLIST_NAME, template_id=0)
|
||||||
|
|
||||||
plr = PlaylistRows(session, playlist.id, 1)
|
plr = PlaylistRows(session, playlist.id, 1)
|
||||||
assert plr
|
assert plr
|
||||||
|
|||||||
@ -34,8 +34,8 @@ class TestMMMiscTracks(unittest.TestCase):
|
|||||||
|
|
||||||
# Create a playlist and model
|
# Create a playlist and model
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
self.playlist = Playlists(session, PLAYLIST_NAME)
|
self.playlist = Playlists(session, PLAYLIST_NAME, template_id=0)
|
||||||
self.model = playlistmodel.PlaylistModel(self.playlist.id)
|
self.model = playlistmodel.PlaylistModel(self.playlist.id, is_template=False)
|
||||||
|
|
||||||
for row in range(len(self.test_tracks)):
|
for row in range(len(self.test_tracks)):
|
||||||
track_path = self.test_tracks[row % len(self.test_tracks)]
|
track_path = self.test_tracks[row % len(self.test_tracks)]
|
||||||
@ -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):
|
||||||
@ -93,9 +93,9 @@ class TestMMMiscNoPlaylist(unittest.TestCase):
|
|||||||
def test_insert_track_new_playlist(self):
|
def test_insert_track_new_playlist(self):
|
||||||
# insert a track into a new playlist
|
# insert a track into a new playlist
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
playlist = Playlists(session, self.PLAYLIST_NAME)
|
playlist = Playlists(session, self.PLAYLIST_NAME, template_id=0)
|
||||||
# Create a model
|
# Create a model
|
||||||
model = playlistmodel.PlaylistModel(playlist.id)
|
model = playlistmodel.PlaylistModel(playlist.id, is_template=False)
|
||||||
# test repr
|
# test repr
|
||||||
_ = str(model)
|
_ = str(model)
|
||||||
|
|
||||||
@ -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"]
|
||||||
@ -124,8 +124,8 @@ class TestMMMiscRowMove(unittest.TestCase):
|
|||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
self.playlist = Playlists(session, self.PLAYLIST_NAME)
|
self.playlist = Playlists(session, self.PLAYLIST_NAME, template_id=0)
|
||||||
self.model = playlistmodel.PlaylistModel(self.playlist.id)
|
self.model = playlistmodel.PlaylistModel(self.playlist.id, is_template=False)
|
||||||
for row in range(self.ROWS_TO_CREATE):
|
for row in range(self.ROWS_TO_CREATE):
|
||||||
self.model.insert_row(proposed_row_number=row, note=str(row))
|
self.model.insert_row(proposed_row_number=row, note=str(row))
|
||||||
|
|
||||||
@ -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
|
||||||
@ -318,8 +318,8 @@ class TestMMMiscRowMove(unittest.TestCase):
|
|||||||
|
|
||||||
model_src = self.model
|
model_src = self.model
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
playlist_dst = Playlists(session, destination_playlist)
|
playlist_dst = Playlists(session, destination_playlist, template_id=0)
|
||||||
model_dst = playlistmodel.PlaylistModel(playlist_dst.id)
|
model_dst = playlistmodel.PlaylistModel(playlist_dst.id, is_template=False)
|
||||||
for row in range(self.ROWS_TO_CREATE):
|
for row in range(self.ROWS_TO_CREATE):
|
||||||
model_dst.insert_row(proposed_row_number=row, note=str(row))
|
model_dst.insert_row(proposed_row_number=row, note=str(row))
|
||||||
|
|
||||||
@ -339,8 +339,8 @@ class TestMMMiscRowMove(unittest.TestCase):
|
|||||||
|
|
||||||
model_src = self.model
|
model_src = self.model
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
playlist_dst = Playlists(session, destination_playlist)
|
playlist_dst = Playlists(session, destination_playlist, template_id=0)
|
||||||
model_dst = playlistmodel.PlaylistModel(playlist_dst.id)
|
model_dst = playlistmodel.PlaylistModel(playlist_dst.id, is_template=False)
|
||||||
for row in range(self.ROWS_TO_CREATE):
|
for row in range(self.ROWS_TO_CREATE):
|
||||||
model_dst.insert_row(proposed_row_number=row, note=str(row))
|
model_dst.insert_row(proposed_row_number=row, note=str(row))
|
||||||
|
|
||||||
@ -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)
|
||||||
@ -366,8 +366,8 @@ class TestMMMiscRowMove(unittest.TestCase):
|
|||||||
|
|
||||||
model_src = self.model
|
model_src = self.model
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
playlist_dst = Playlists(session, destination_playlist)
|
playlist_dst = Playlists(session, destination_playlist, template_id=0)
|
||||||
model_dst = playlistmodel.PlaylistModel(playlist_dst.id)
|
model_dst = playlistmodel.PlaylistModel(playlist_dst.id, is_template=False)
|
||||||
for row in range(self.ROWS_TO_CREATE):
|
for row in range(self.ROWS_TO_CREATE):
|
||||||
model_dst.insert_row(proposed_row_number=row, note=str(row))
|
model_dst.insert_row(proposed_row_number=row, note=str(row))
|
||||||
|
|
||||||
@ -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)
|
||||||
|
|||||||
130
tests/test_queries.py
Normal file
130
tests/test_queries.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# Standard library imports
|
||||||
|
import datetime as dt
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
|
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
|
||||||
|
# App imports
|
||||||
|
from app.models import (
|
||||||
|
db,
|
||||||
|
Playdates,
|
||||||
|
Tracks,
|
||||||
|
)
|
||||||
|
from classes import (
|
||||||
|
Filter,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MyTestCase(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
"""Runs once before any test in this class"""
|
||||||
|
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
# Create some track entries
|
||||||
|
_ = Tracks(**dict(
|
||||||
|
session=session,
|
||||||
|
artist="a",
|
||||||
|
bitrate=0,
|
||||||
|
duration=100,
|
||||||
|
fade_at=0,
|
||||||
|
path="/alpha/bravo/charlie",
|
||||||
|
silence_at=0,
|
||||||
|
start_gap=0,
|
||||||
|
title="abc"
|
||||||
|
))
|
||||||
|
track2 = Tracks(**dict(
|
||||||
|
session=session,
|
||||||
|
artist="a",
|
||||||
|
bitrate=0,
|
||||||
|
duration=100,
|
||||||
|
fade_at=0,
|
||||||
|
path="/xray/yankee/zulu",
|
||||||
|
silence_at=0,
|
||||||
|
start_gap=0,
|
||||||
|
title="xyz"
|
||||||
|
))
|
||||||
|
track2_id = track2.id
|
||||||
|
# Add playdates
|
||||||
|
# Track 2 played just over a year ago
|
||||||
|
just_over_a_year_ago = dt.datetime.now() - dt.timedelta(days=367)
|
||||||
|
_ = Playdates(session, track2_id, when=just_over_a_year_ago)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
"""Runs once after all tests"""
|
||||||
|
|
||||||
|
db.drop_all()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Runs before each test"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Runs after each test"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_search_path_1(self):
|
||||||
|
"""Search for unplayed track"""
|
||||||
|
|
||||||
|
filter = Filter(path="alpha", last_played_comparator="never")
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
results = Tracks.get_filtered_tracks(session, filter)
|
||||||
|
assert len(results) == 1
|
||||||
|
assert 'alpha' in results[0].path
|
||||||
|
|
||||||
|
def test_search_path_2(self):
|
||||||
|
"""Search for unplayed track that doesn't exist"""
|
||||||
|
|
||||||
|
filter = Filter(path="xray", last_played_comparator="never")
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
results = Tracks.get_filtered_tracks(session, filter)
|
||||||
|
assert len(results) == 0
|
||||||
|
|
||||||
|
def test_played_over_a_year_ago(self):
|
||||||
|
"""Search for tracks played over a year ago"""
|
||||||
|
|
||||||
|
filter = Filter(last_played_unit="years", last_played_number=1)
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
results = Tracks.get_filtered_tracks(session, filter)
|
||||||
|
assert len(results) == 1
|
||||||
|
assert 'zulu' in results[0].path
|
||||||
|
|
||||||
|
def test_played_over_two_years_ago(self):
|
||||||
|
"""Search for tracks played over 2 years ago"""
|
||||||
|
|
||||||
|
filter = Filter(last_played_unit="years", last_played_number=2)
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
results = Tracks.get_filtered_tracks(session, filter)
|
||||||
|
assert len(results) == 0
|
||||||
|
|
||||||
|
def test_never_played(self):
|
||||||
|
"""Search for tracks never played"""
|
||||||
|
|
||||||
|
filter = Filter(last_played_comparator="never")
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
results = Tracks.get_filtered_tracks(session, filter)
|
||||||
|
assert len(results) == 1
|
||||||
|
assert 'alpha' in results[0].path
|
||||||
|
|
||||||
|
def test_played_anytime(self):
|
||||||
|
"""Search for tracks played over a year ago"""
|
||||||
|
|
||||||
|
filter = Filter(last_played_comparator="Any time")
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
results = Tracks.get_filtered_tracks(session, filter)
|
||||||
|
assert len(results) == 1
|
||||||
|
assert 'zulu' in results[0].path
|
||||||
@ -90,8 +90,8 @@ class MyTestCase(unittest.TestCase):
|
|||||||
playlist_name = "test_init playlist"
|
playlist_name = "test_init playlist"
|
||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
playlist = Playlists(session, playlist_name)
|
playlist = Playlists(session, playlist_name, template_id=0)
|
||||||
self.widget.create_playlist_tab(playlist)
|
self.widget._open_playlist(playlist, is_template=False)
|
||||||
with self.qtbot.waitExposed(self.widget):
|
with self.qtbot.waitExposed(self.widget):
|
||||||
self.widget.show()
|
self.widget.show()
|
||||||
|
|
||||||
@ -103,8 +103,8 @@ class MyTestCase(unittest.TestCase):
|
|||||||
playlist_name = "test_save_and_restore playlist"
|
playlist_name = "test_save_and_restore playlist"
|
||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
playlist = Playlists(session, playlist_name)
|
playlist = Playlists(session, playlist_name, template_id=0)
|
||||||
model = playlistmodel.PlaylistModel(playlist.id)
|
model = playlistmodel.PlaylistModel(playlist.id, is_template=False)
|
||||||
|
|
||||||
# Add a track with a note
|
# Add a track with a note
|
||||||
model.insert_row(
|
model.insert_row(
|
||||||
@ -139,7 +139,7 @@ class MyTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
# def test_meta_all_clear(qtbot, session):
|
# def test_meta_all_clear(qtbot, session):
|
||||||
# # Create playlist
|
# # Create playlist
|
||||||
# playlist = models.Playlists(session, "my playlist")
|
# playlist = models.Playlists(session, "my playlist", template_id=0)
|
||||||
# playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
# playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||||
|
|
||||||
# # Add some tracks
|
# # Add some tracks
|
||||||
@ -167,7 +167,8 @@ class MyTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
# def test_meta(qtbot, session):
|
# def test_meta(qtbot, session):
|
||||||
# # Create playlist
|
# # Create playlist
|
||||||
# playlist = playlists.Playlists(session, "my playlist")
|
# playlist = playlists.Playlists(session, "my playlist",
|
||||||
|
# template_id=0)
|
||||||
# playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
# playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||||
|
|
||||||
# # Add some tracks
|
# # Add some tracks
|
||||||
@ -248,7 +249,7 @@ class MyTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
# def test_clear_next(qtbot, session):
|
# def test_clear_next(qtbot, session):
|
||||||
# # Create playlist
|
# # Create playlist
|
||||||
# playlist = models.Playlists(session, "my playlist")
|
# playlist = models.Playlists(session, "my playlist", template_id=0)
|
||||||
# playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
# playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
|
||||||
|
|
||||||
# # Add some tracks
|
# # Add some tracks
|
||||||
@ -274,7 +275,7 @@ class MyTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
# # Create playlist and playlist_tab
|
# # Create playlist and playlist_tab
|
||||||
# window = musicmuster.Window()
|
# window = musicmuster.Window()
|
||||||
# playlist = models.Playlists(session, "test playlist")
|
# playlist = models.Playlists(session, "test playlist", template_id=0)
|
||||||
# playlist_tab = playlists.PlaylistTab(window, session, playlist.id)
|
# playlist_tab = playlists.PlaylistTab(window, session, playlist.id)
|
||||||
|
|
||||||
# # Add some tracks
|
# # Add some tracks
|
||||||
@ -306,7 +307,7 @@ class MyTestCase(unittest.TestCase):
|
|||||||
# playlist_name = "test playlist"
|
# playlist_name = "test playlist"
|
||||||
# # Create testing playlist
|
# # Create testing playlist
|
||||||
# window = musicmuster.Window()
|
# window = musicmuster.Window()
|
||||||
# playlist = models.Playlists(session, playlist_name)
|
# playlist = models.Playlists(session, playlist_name, template_id=0)
|
||||||
# playlist_tab = playlists.PlaylistTab(window, session, playlist.id)
|
# playlist_tab = playlists.PlaylistTab(window, session, playlist.id)
|
||||||
# idx = window.tabPlaylist.addTab(playlist_tab, playlist_name)
|
# idx = window.tabPlaylist.addTab(playlist_tab, playlist_name)
|
||||||
# window.tabPlaylist.setCurrentIndex(idx)
|
# window.tabPlaylist.setCurrentIndex(idx)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user