Compare commits

..

No commits in common. "master" and "v4.2.0" have entirely different histories.

53 changed files with 4462 additions and 8705 deletions

2
.envrc
View File

@ -1,4 +1,4 @@
layout uv layout poetry
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
View File

@ -1 +0,0 @@
*.py diff=python

2
.gitignore vendored
View File

@ -2,7 +2,6 @@
*.pyc *.pyc
*.swp *.swp
tags tags
.venv/
venv/ venv/
Session.vim Session.vim
*.flac *.flac
@ -14,4 +13,3 @@ StudioPlaylist.png
tmp/ tmp/
.coverage .coverage
profile_output* profile_output*
kae.py

View File

@ -1 +1 @@
3.13 musicmuster

View File

@ -0,0 +1,78 @@
#!/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()

View File

@ -0,0 +1,94 @@
#!/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()

View File

@ -69,8 +69,7 @@ class AudacityController:
select_status = self._send_command("SelectAll") select_status = self._send_command("SelectAll")
log.debug(f"{select_status=}") log.debug(f"{select_status=}")
# Escape any double quotes in filename export_cmd = f'Export2: Filename="{self.path}" NumChannels=2'
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 = ""

View File

@ -1,10 +1,9 @@
# Standard library imports # Standard library imports
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass, field
from enum import auto, Enum from enum import auto, Enum
import functools import functools
import threading
from typing import NamedTuple from typing import NamedTuple
# Third party imports # Third party imports
@ -14,31 +13,32 @@ from PyQt6.QtCore import (
pyqtSignal, pyqtSignal,
QObject, QObject,
) )
from PyQt6.QtWidgets import (
QProxyStyle,
QStyle,
QStyleOption,
)
# App imports # App imports
# Define singleton first as it's needed below 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()
def singleton(cls): def singleton(cls):
""" """
Make a class a Singleton class (see Make a class a Singleton class (see
https://realpython.com/primer-on-python-decorators/#creating-singletons) https://realpython.com/primer-on-python-decorators/#creating-singletons)
Added locking.
""" """
lock = threading.Lock()
@functools.wraps(cls) @functools.wraps(cls)
def wrapper_singleton(*args, **kwargs): def wrapper_singleton(*args, **kwargs):
if wrapper_singleton.instance is None: if not wrapper_singleton.instance:
with lock:
if wrapper_singleton.instance is None: # Check still None
wrapper_singleton.instance = cls(*args, **kwargs) wrapper_singleton.instance = cls(*args, **kwargs)
return wrapper_singleton.instance return wrapper_singleton.instance
@ -46,6 +46,11 @@ 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
@ -60,44 +65,16 @@ 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/emit-a-signal-from-another-class-to-main-class - https://stackoverflow.com/questions/62654525/
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)
@ -116,37 +93,11 @@ 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
bitrate: int = 0 bitrate: int
duration: int = 0 duration: int
class TrackInfo(NamedTuple): class TrackInfo(NamedTuple):

View File

@ -2,6 +2,7 @@
import datetime as dt import datetime as dt
import logging import logging
import os import os
from typing import List, Optional
# PyQt imports # PyQt imports
@ -31,15 +32,15 @@ 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
DEBUG_FUNCTIONS: List[Optional[str]] = []
DEBUG_MODULES: List[Optional[str]] = []
DEFAULT_COLUMN_WIDTH = 200 DEFAULT_COLUMN_WIDTH = 200
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"]
@ -50,23 +51,6 @@ 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_SELECT_ARTIST = 80.0
FUZZYMATCH_MINIMUM_SELECT_TITLE = 80.0
FUZZYMATCH_SHOW_SCORES = True
HEADER_ARTIST = "Artist" HEADER_ARTIST = "Artist"
HEADER_BITRATE = "bps" HEADER_BITRATE = "bps"
HEADER_DURATION = "Length" HEADER_DURATION = "Length"
@ -78,8 +62,8 @@ class Config(object):
HEADER_START_TIME = "Start" HEADER_START_TIME = "Start"
HEADER_TITLE = "Title" HEADER_TITLE = "Title"
HIDE_AFTER_PLAYING_OFFSET = 5000 HIDE_AFTER_PLAYING_OFFSET = 5000
HIDE_PLAYED_MODE_SECTIONS = "SECTIONS"
HIDE_PLAYED_MODE_TRACKS = "TRACKS" HIDE_PLAYED_MODE_TRACKS = "TRACKS"
HIDE_PLAYED_MODE_SECTIONS = "SECTIONS"
IMPORT_AS_NEW = "Import as new track" IMPORT_AS_NEW = "Import as new track"
INFO_TAB_TITLE_LENGTH = 15 INFO_TAB_TITLE_LENGTH = 15
INTRO_SECONDS_FORMAT = ".1f" INTRO_SECONDS_FORMAT = ".1f"
@ -94,14 +78,12 @@ class Config(object):
MAIL_SERVER = os.environ.get("MAIL_SERVER") or "woodlands.midnighthax.com" MAIL_SERVER = os.environ.get("MAIL_SERVER") or "woodlands.midnighthax.com"
MAIL_USERNAME = os.environ.get("MAIL_USERNAME") MAIL_USERNAME = os.environ.get("MAIL_USERNAME")
MAIL_USE_TLS = os.environ.get("MAIL_USE_TLS") is not None MAIL_USE_TLS = os.environ.get("MAIL_USE_TLS") is not None
MAIN_WINDOW_TITLE = "MusicMuster"
MAX_IMPORT_MATCHES = 5 MAX_IMPORT_MATCHES = 5
MAX_IMPORT_THREADS = 3
MAX_INFO_TABS = 5 MAX_INFO_TABS = 5
MAX_MISSING_FILES_TO_REPORT = 10 MAX_MISSING_FILES_TO_REPORT = 10
MILLISECOND_SIGFIGS = 0 MILLISECOND_SIGFIGS = 0
MINIMUM_FUZZYMATCH = 60.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"
@ -111,7 +93,6 @@ 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
@ -123,23 +104,24 @@ 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"
USE_INTERNAL_BROWSER = False
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 = 100 VLC_VOLUME_DEFAULT = 75
VLC_VOLUME_DROP3db = 70 VLC_VOLUME_DROP3db = 65
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_TRACKS HIDE_PLAYED_MODE = HIDE_PLAYED_MODE_SECTIONS
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)

View File

@ -18,8 +18,7 @@ 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)
# Database managed by Alembic so no create_all() required self.db.create_all()
# 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")

View File

@ -1,8 +1,6 @@
# Standard library imports # Standard library imports
from typing import Optional from typing import List, Optional
from dataclasses import asdict
import datetime as dt import datetime as dt
import json
# PyQt imports # PyQt imports
@ -15,37 +13,13 @@ 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
@ -53,7 +27,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, unique=True) substring: Mapped[str] = mapped_column(String(256), index=False)
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)
@ -74,10 +48,9 @@ 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", ondelete="CASCADE")) track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id"))
track: Mapped["TracksTable"] = relationship( track: Mapped["TracksTable"] = relationship(
"TracksTable", "TracksTable", back_populates="playdates"
back_populates="playdates",
) )
def __repr__(self) -> str: def __repr__(self) -> str:
@ -100,15 +73,12 @@ class PlaylistsTable(Model):
tab: Mapped[Optional[int]] = mapped_column(default=None) tab: Mapped[Optional[int]] = mapped_column(default=None)
open: Mapped[bool] = mapped_column(default=False) open: Mapped[bool] = mapped_column(default=False)
is_template: Mapped[bool] = mapped_column(default=False) is_template: Mapped[bool] = mapped_column(default=False)
rows: Mapped[list["PlaylistRowsTable"]] = relationship( rows: Mapped[List["PlaylistRowsTable"]] = relationship(
"PlaylistRowsTable", "PlaylistRowsTable",
back_populates="playlist", back_populates="playlist",
cascade="all, delete-orphan", cascade="all, delete-orphan",
order_by="PlaylistRowsTable.row_number", order_by="PlaylistRowsTable.row_number",
) )
favourite: Mapped[bool] = mapped_column(
Boolean, nullable=False, index=False, default=False
)
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -125,14 +95,9 @@ class PlaylistRowsTable(Model):
note: Mapped[str] = mapped_column( note: Mapped[str] = mapped_column(
String(2048), index=False, default="", nullable=False String(2048), index=False, default="", nullable=False
) )
playlist_id: Mapped[int] = mapped_column( playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id"), index=True)
ForeignKey("playlists.id", ondelete="CASCADE"), index=True
)
playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows") playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows")
track_id: Mapped[Optional[int]] = mapped_column( track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id"))
ForeignKey("tracks.id", ondelete="CASCADE")
)
track: Mapped["TracksTable"] = relationship( track: Mapped["TracksTable"] = relationship(
"TracksTable", "TracksTable",
back_populates="playlistrows", back_populates="playlistrows",
@ -149,31 +114,6 @@ class PlaylistRowsTable(Model):
) )
class QueriesTable(Model):
__tablename__ = "queries"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(128), 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)
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:
return f"<QueriesTable(id={self.id}, name={self.name}, filter={self.filter})>"
class SettingsTable(Model): class SettingsTable(Model):
"""Manage settings""" """Manage settings"""
@ -197,7 +137,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[int] = mapped_column(default=None) bitrate: Mapped[Optional[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)
@ -206,16 +146,13 @@ class TracksTable(Model):
start_gap: Mapped[int] = mapped_column(index=False) start_gap: Mapped[int] = mapped_column(index=False)
title: Mapped[str] = mapped_column(String(256), index=True) title: Mapped[str] = mapped_column(String(256), index=True)
playlistrows: Mapped[list[PlaylistRowsTable]] = relationship( playlistrows: Mapped[List[PlaylistRowsTable]] = relationship(
"PlaylistRowsTable", "PlaylistRowsTable", back_populates="track"
back_populates="track",
cascade="all, delete-orphan",
) )
playlists = association_proxy("playlistrows", "playlist") playlists = association_proxy("playlistrows", "playlist")
playdates: Mapped[list[PlaydatesTable]] = relationship( playdates: Mapped[List[PlaydatesTable]] = relationship(
"PlaydatesTable", "PlaydatesTable",
back_populates="track", back_populates="track",
cascade="all, delete-orphan",
lazy="joined", lazy="joined",
) )

View File

@ -1,5 +1,6 @@
# Standard library imports # Standard library imports
from typing import Optional from typing import Optional
import os
# PyQt imports # PyQt imports
from PyQt6.QtCore import QEvent, Qt from PyQt6.QtCore import QEvent, Qt
@ -8,22 +9,27 @@ from PyQt6.QtWidgets import (
QDialog, QDialog,
QListWidgetItem, QListWidgetItem,
QMainWindow, QMainWindow,
QTableWidgetItem,
) )
# Third party imports # Third party imports
import pydymenu # type: ignore
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
# App imports # App imports
from classes import MusicMusterSignals from classes import MusicMusterSignals
from config import Config
from helpers import ( from helpers import (
ask_yes_no, ask_yes_no,
get_relative_date, get_relative_date,
get_tags,
ms_to_mmss, ms_to_mmss,
show_warning,
) )
from log import log from log import log
from models import Settings, Tracks from models import db, Settings, Tracks
from playlistmodel import PlaylistModel from playlistmodel import PlaylistModel
from ui import dlg_TrackSelect_ui from ui import dlg_TrackSelect_ui, dlg_replace_files_ui
class TrackSelectDialog(QDialog): class TrackSelectDialog(QDialog):

File diff suppressed because it is too large Load Diff

View File

@ -10,10 +10,9 @@ import ssl
import tempfile import tempfile
# PyQt imports # PyQt imports
from PyQt6.QtWidgets import QInputDialog, QMainWindow, QMessageBox, QWidget from PyQt6.QtWidgets import QMainWindow, QMessageBox
# 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
@ -51,14 +50,6 @@ 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,
@ -81,7 +72,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
@ -103,9 +94,6 @@ 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)
@ -162,23 +150,6 @@ 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:
@ -229,17 +200,9 @@ def get_tags(path: str) -> Tags:
try: try:
tag = TinyTag.get(path) tag = TinyTag.get(path)
except FileNotFoundError: except FileNotFoundError:
raise ApplicationError(f"File not found: {path}") raise ApplicationError(f"File not found: get_tags({path=})")
except TinyTagException: except TinyTagException:
raise ApplicationError(f"Can't read tags in {path}") raise ApplicationError(f"Can't read tags: get_tags({path=})")
if (
tag.title is None
or tag.artist is None
or tag.bitrate is None
or tag.duration is None
):
raise ApplicationError(f"Missing tags in {path}")
return Tags( return Tags(
title=tag.title, title=tag.title,
@ -429,16 +392,10 @@ def set_track_metadata(track: Tracks) -> None:
setattr(track, tag_key, getattr(tags, tag_key)) setattr(track, tag_key, getattr(tags, tag_key))
def show_OK(title: str, msg: str, parent: Optional[QWidget] = None) -> None: def show_OK(parent: QMainWindow, title: str, msg: str) -> None:
"""Display a message to user""" """Display a message to user"""
dlg = QMessageBox(parent) QMessageBox.information(parent, title, msg, buttons=QMessageBox.StandardButton.Ok)
dlg.setIcon(QMessageBox.Icon.Information)
dlg.setWindowTitle(title)
dlg.setText(msg)
dlg.setStandardButtons(QMessageBox.StandardButton.Ok)
_ = dlg.exec()
def show_warning(parent: Optional[QMainWindow], title: str, msg: str) -> None: def show_warning(parent: Optional[QMainWindow], title: str, msg: str) -> None:

68
app/infotabs.py Normal file
View File

@ -0,0 +1,68 @@
# Standard library imports
import datetime as dt
from typing import Dict, Optional
# PyQt imports
from PyQt6.QtCore import QUrl
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QTabWidget, QWidget
# Third party imports
# App imports
from config import Config
class InfoTabs(QTabWidget):
"""
Class to manage info tabs
"""
def __init__(self, parent: Optional[QWidget] = None) -> None:
super().__init__(parent)
if Config.USE_INTERNAL_BROWSER:
# re-use the oldest one later)
self.last_update: Dict[QWebEngineView, dt.datetime] = {}
self.tabtitles: Dict[int, str] = {}
# Create one tab which (for some reason) creates flickering if
# done later
widget = QWebEngineView()
widget.setZoomFactor(Config.WEB_ZOOM_FACTOR)
self.last_update[widget] = dt.datetime.now()
_ = self.addTab(widget, "")
def open_tab(self, url: str, title: str) -> None:
"""
Open passed URL. If URL currently displayed, switch to that tab.
Create new tab if we're below the maximum
number otherwise reuse oldest content tab.
"""
if url in self.tabtitles.values():
self.setCurrentIndex(
list(self.tabtitles.keys())[list(self.tabtitles.values()).index(url)]
)
return
short_title = title[: Config.INFO_TAB_TITLE_LENGTH]
if self.count() < Config.MAX_INFO_TABS:
# Create a new tab
widget = QWebEngineView()
widget.setZoomFactor(Config.WEB_ZOOM_FACTOR)
tab_index = self.addTab(widget, short_title)
else:
# Reuse oldest widget
widget = min(self.last_update, key=self.last_update.get) # type: ignore
tab_index = self.indexOf(widget)
self.setTabText(tab_index, short_title)
widget.setUrl(QUrl(url))
self.last_update[widget] = dt.datetime.now()
self.tabtitles[tab_index] = url
# Show newly updated tab
self.setCurrentIndex(tab_index)

View File

@ -1,56 +0,0 @@
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,
)

View File

@ -1,62 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/python3
# Standard library imports # Standard library imports
from collections import defaultdict
from functools import wraps
import logging import logging
import logging.config
import logging.handlers import logging.handlers
import os import os
import sys import sys
import traceback from traceback import print_exception
import yaml
# PyQt imports # PyQt imports
from PyQt6.QtWidgets import QApplication, QMessageBox
# Third party imports # Third party imports
import colorlog
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):
"""Filter to allow category-based logging to stderr."""
def __init__(self, module_functions: dict[str, list[str]]):
super().__init__()
self.modules: list[str] = []
self.functions: defaultdict[str, list[str]] = defaultdict(list)
if module_functions:
for module in module_functions.keys():
if module_functions[module]:
for function in module_functions[module]:
self.functions[module].append(function)
else:
self.modules.append(module)
def filter(self, record: logging.LogRecord) -> bool:
if not getattr(record, "levelname", None) == "DEBUG":
# Only prcess DEBUG messages
return False
module = getattr(record, "module", None)
if not module:
# No module in record
return False
# Process if this is a module we're tracking
if module in self.modules:
return True
# Process if this is a function we're tracking
if getattr(record, "funcName", None) in self.functions[module]:
return True
return False
class LevelTagFilter(logging.Filter): class LevelTagFilter(logging.Filter):
@ -65,74 +22,57 @@ class LevelTagFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool: def filter(self, record: logging.LogRecord) -> bool:
# Extract the first character of the level name # Extract the first character of the level name
record.leveltag = record.levelname[0] record.leveltag = record.levelname[0]
# We never actually filter messages out, just add an extra field
# to the LogRecord # We never actually filter messages out, just abuse filtering to add an
# extra field to the LogRecord
return True return True
# Load YAML logging configuration
with open("app/logging.yaml", "r") as f:
config = yaml.safe_load(f)
logging.config.dictConfig(config)
# Get logger
log = logging.getLogger(Config.LOG_NAME) log = logging.getLogger(Config.LOG_NAME)
log.setLevel(logging.DEBUG)
local_filter = LevelTagFilter()
# stderr
stderr = colorlog.StreamHandler()
stderr.setLevel(Config.LOG_LEVEL_STDERR)
stderr.addFilter(local_filter)
stderr_fmt = colorlog.ColoredFormatter(
"%(log_color)s[%(asctime)s] %(filename)s:%(lineno)s %(message)s", datefmt="%H:%M:%S"
)
stderr.setFormatter(stderr_fmt)
log.addHandler(stderr)
# syslog
syslog = logging.handlers.SysLogHandler(address="/dev/log")
syslog.setLevel(Config.LOG_LEVEL_SYSLOG)
syslog.addFilter(local_filter)
syslog_fmt = logging.Formatter(
"[%(name)s] %(filename)s:%(lineno)s %(leveltag)s: %(message)s"
)
syslog.setFormatter(syslog_fmt)
log.addHandler(syslog)
def handle_exception(exc_type, exc_value, exc_traceback): def log_uncaught_exceptions(type_, value, 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,
) )
if QApplication.instance() is not None: log.debug(msg)
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)
def truncate_large(obj, limit=5): sys.excepthook = log_uncaught_exceptions
"""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

View File

@ -1,55 +0,0 @@
version: 1
disable_existing_loggers: True
formatters:
colored:
(): colorlog.ColoredFormatter
format: "%(log_color)s[%(asctime)s] %(filename)s.%(funcName)s:%(lineno)s %(blue)s%(message)s"
datefmt: "%H:%M:%S"
syslog:
format: "[%(name)s] %(filename)s:%(lineno)s %(leveltag)s: %(message)s"
filters:
leveltag:
(): log.LevelTagFilter
category_filter:
(): log.FunctionFilter
module_functions:
# Optionally additionally log some debug calls to stderr
# log all debug calls in a module:
# module-name: []
# log debug calls for some functions in a module:
# module-name:
# - function-name-1
# - function-name-2
musicmuster:
- play_next
jittermonitor: []
handlers:
stderr:
class: colorlog.StreamHandler
level: INFO
formatter: colored
filters: [leveltag]
stream: ext://sys.stderr
syslog:
class: logging.handlers.SysLogHandler
level: DEBUG
formatter: syslog
filters: [leveltag]
address: "/dev/log"
debug_stderr:
class: colorlog.StreamHandler
level: DEBUG
formatter: colored
filters: [leveltag, category_filter]
stream: ext://sys.stderr
loggers:
musicmuster:
level: DEBUG
handlers: [stderr, syslog, debug_stderr]
propagate: false

View File

@ -1,29 +0,0 @@
#!/usr/bin/env python3
from log import log
# Testing
def fa():
log.debug("fa Debug message")
log.info("fa Info message")
log.warning("fa Warning message")
log.error("fa Error message")
log.critical("fa Critical message")
print()
def fb():
log.debug("fb Debug message")
log.info("fb Info message")
log.warning("fb Warning message")
log.error("fb Error message")
log.critical("fb Critical message")
print()
def testing():
fa()
fb()
testing()

View File

@ -1,104 +0,0 @@
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"

View File

@ -1,7 +1,7 @@
# Standard library imports # Standard library imports
from __future__ import annotations from __future__ import annotations
from typing import Optional, Sequence from typing import List, Optional, Sequence
import datetime as dt import datetime as dt
import os import os
import re import re
@ -10,24 +10,20 @@ import sys
# PyQt imports # PyQt imports
# Third party imports # Third party imports
from dogpile.cache import make_region import line_profiler
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, ProgrammingError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.orm import joinedload, selectinload from sqlalchemy.orm import joinedload
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
@ -41,28 +37,11 @@ if DATABASE_URL is None:
if "unittest" in sys.modules and "sqlite" not in DATABASE_URL: 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
db.create_all()
# 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,
@ -89,28 +68,13 @@ class NoteColours(dbtables.NoteColoursTable):
Return all records Return all records
""" """
cache_key = "note_colours_all" result = session.scalars(select(cls)).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
) -> str: ) -> Optional[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
@ -118,10 +82,16 @@ class NoteColours(dbtables.NoteColoursTable):
""" """
if not text: if not text:
return "" return None
match = False match = False
for rec in NoteColours.get_all(session): for rec in session.scalars(
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:
@ -139,28 +109,17 @@ class NoteColours(dbtables.NoteColoursTable):
if match: if match:
if foreground: if foreground:
return rec.foreground or "" return rec.foreground
else: else:
return rec.colour return rec.colour
return "" return None
@staticmethod
def invalidate_cache() -> None:
"""Invalidate dogpile cache"""
cache_region.delete("note_colours_all")
class Playdates(dbtables.PlaydatesTable): class Playdates(dbtables.PlaydatesTable):
def __init__( def __init__(self, session: Session, track_id: int) -> None:
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()
@ -171,13 +130,13 @@ class Playdates(dbtables.PlaydatesTable):
) -> Sequence["Playdates"]: ) -> Sequence["Playdates"]:
""" """
Return a list of the last limit playdates for this track, sorted Return a list of the last limit playdates for this track, sorted
latest to earliest. earliest to latest.
""" """
return session.scalars( return session.scalars(
Playdates.select() Playdates.select()
.where(Playdates.track_id == track_id) .where(Playdates.track_id == track_id)
.order_by(Playdates.lastplayed.desc()) .order_by(Playdates.lastplayed.asc())
.limit(limit) .limit(limit)
).all() ).all()
@ -222,20 +181,14 @@ class Playdates(dbtables.PlaydatesTable):
class Playlists(dbtables.PlaylistsTable): class Playlists(dbtables.PlaylistsTable):
def __init__(self, session: Session, name: str, template_id: int) -> None: def __init__(self, session: Session, name: str):
"""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:
""" """
Make all tab records NULL Make all tab records NULL
""" """
@ -250,6 +203,34 @@ 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"""
@ -264,17 +245,9 @@ class Playlists(dbtables.PlaylistsTable):
def get_all_templates(cls, session: Session) -> Sequence["Playlists"]: def get_all_templates(cls, session: Session) -> Sequence["Playlists"]:
"""Returns a list of all templates ordered by name""" """Returns a list of all templates ordered by name"""
return session.scalars(
select(cls).where(cls.is_template.is_(True)).order_by(cls.name)
).all()
@classmethod
def get_favourite_templates(cls, session: Session) -> Sequence["Playlists"]:
"""Returns a list of favourite templates ordered by name"""
return session.scalars( return session.scalars(
select(cls) select(cls)
.where(cls.is_template.is_(True), cls.favourite.is_(True)) .where(cls.is_template.is_(True))
.order_by(cls.name) .order_by(cls.name)
).all() ).all()
@ -332,7 +305,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_id=0) template = Playlists(session, template_name)
if not template or not template.id: if not template or not template.id:
return return
@ -455,7 +428,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
@classmethod @classmethod
def plrids_to_plrs( def plrids_to_plrs(
cls, session: Session, playlist_id: int, plr_ids: list[int] cls, session: Session, playlist_id: int, plr_ids: List[int]
) -> Sequence["PlaylistRows"]: ) -> Sequence["PlaylistRows"]:
""" """
Take a list of PlaylistRows ids and return a list of corresponding Take a list of PlaylistRows ids and return a list of corresponding
@ -520,13 +493,9 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
For passed playlist, return a list of rows. For passed playlist, return a list of rows.
""" """
stmt = ( plrs = session.scalars(
select(cls) select(cls).where(cls.playlist_id == playlist_id).order_by(cls.row_number)
.where(cls.playlist_id == playlist_id) ).all()
.options(selectinload(cls.track))
.order_by(cls.row_number)
)
plrs = session.execute(stmt).scalars().all()
return plrs return plrs
@ -608,10 +577,12 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
) )
@staticmethod @staticmethod
@line_profiler.profile
def update_plr_row_numbers( def update_plr_row_numbers(
session: Session, session: Session,
playlist_id: int, playlist_id: int,
sqla_map: list[dict[str, int]], sqla_map: List[dict[str, int]],
dummy_for_profiling: Optional[int] = None,
) -> None: ) -> None:
""" """
Take a {plrid: row_number} dictionary and update the row numbers accordingly Take a {plrid: row_number} dictionary and update the row numbers accordingly
@ -630,39 +601,8 @@ 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) -> None: def __init__(self, session: Session, name: str):
self.name = name self.name = name
session.add(self) session.add(self)
session.commit() session.commit()
@ -690,7 +630,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
@ -745,77 +685,6 @@ 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"]:
""" """

View File

@ -6,7 +6,6 @@ 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
@ -30,7 +29,6 @@ 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
@ -156,11 +154,14 @@ class _FadeCurve:
if self.region is None: if self.region is None:
# Create the region now that we're into fade # Create the region now that we're into fade
log.debug("issue223: _FadeCurve: create region")
self.region = pg.LinearRegionItem([0, 0], bounds=[0, len(self.graph_array)]) self.region = pg.LinearRegionItem([0, 0], bounds=[0, len(self.graph_array)])
self.GraphWidget.addItem(self.region) self.GraphWidget.addItem(self.region)
# Update region position # Update region position
if self.region: if self.region:
# Next line is very noisy
# log.debug("issue223: _FadeCurve: update region")
self.region.setRegion([0, ms_of_graph * self.ms_to_array_factor]) self.region.setRegion([0, ms_of_graph * self.ms_to_array_factor])
@ -354,6 +355,21 @@ 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
@ -377,6 +393,17 @@ 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"""
@ -415,12 +442,6 @@ 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
@ -440,7 +461,7 @@ class RowAndTrack:
self.title = playlist_row.track.title self.title = playlist_row.track.title
else: else:
self.artist = "" self.artist = ""
self.bitrate = 0 self.bitrate = None
self.duration = 0 self.duration = 0
self.fade_at = 0 self.fade_at = 0
self.intro = None self.intro = None
@ -557,6 +578,7 @@ class RowAndTrack:
def play(self, position: Optional[float] = None) -> None: def play(self, position: Optional[float] = None) -> None:
"""Play track""" """Play track"""
log.debug(f"issue223: RowAndTrack: play {self.track_id=}")
now = dt.datetime.now() now = dt.datetime.now()
self.start_time = now self.start_time = now

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
# 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
@ -11,6 +12,7 @@ import re
from PyQt6.QtCore import ( from PyQt6.QtCore import (
QAbstractTableModel, QAbstractTableModel,
QModelIndex, QModelIndex,
QObject,
QRegularExpression, QRegularExpression,
QSortFilterProxyModel, QSortFilterProxyModel,
Qt, Qt,
@ -24,15 +26,13 @@ from PyQt6.QtGui import (
) )
# Third party imports # Third party imports
# import line_profiler 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
# App imports # App imports
from classes import ( from classes import (
ApplicationError,
Col, Col,
MusicMusterSignals, MusicMusterSignals,
) )
@ -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, log_call from log import log
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 = 0 HEADER_NOTES_COLUMN = 1
scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]") scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]")
@ -74,14 +74,13 @@ class PlaylistModel(QAbstractTableModel):
def __init__( def __init__(
self, self,
playlist_id: int, playlist_id: int,
is_template: bool, *args: Optional[QObject],
**kwargs: Optional[QObject],
) -> None: ) -> None:
super().__init__()
log.debug("PlaylistModel.__init__()") log.debug("PlaylistModel.__init__()")
self.playlist_id = playlist_id self.playlist_id = playlist_id
self.is_template = is_template super().__init__(*args, **kwargs)
self.playlist_rows: dict[int, RowAndTrack] = {} self.playlist_rows: dict[int, RowAndTrack] = {}
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
@ -99,17 +98,13 @@ class PlaylistModel(QAbstractTableModel):
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
f"<PlaylistModel: playlist_id={self.playlist_id}, " f"<PlaylistModel: playlist_id={self.playlist_id}, {self.rowCount()} rows>"
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 any of the following below it: Return the row number of the first header that has either unplayed tracks
- unplayed tracks or currently being played track below it.
- the currently being played track
- the track marked as next to play
""" """
header_row = 0 header_row = 0
@ -121,20 +116,23 @@ class PlaylistModel(QAbstractTableModel):
if not self.is_played_row(row_number): if not self.is_played_row(row_number):
break break
# Here means that row_number points to a played track. The # If track is played, we need to check it's not the current
# current track will be marked as played when we start # next or previous track because we don't want to scroll them
# playing it. It's also possible that the track marked as # out of view
# next has already been played. Check for either of those.
for ts in [track_sequence.next, track_sequence.current]: for ts in [
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
): ):
# We've found the current or next track, so return break
# the last-found header row else:
return header_row continue # continue iterating over playlist_rows
break # current row is in one of the track sequences
return header_row return header_row
@ -151,52 +149,42 @@ class PlaylistModel(QAbstractTableModel):
try: try:
rat = self.playlist_rows[row_number] rat = self.playlist_rows[row_number]
except KeyError: except KeyError:
raise ApplicationError( log.error(
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:
raise ApplicationError( log.error(
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 not playlistrow: if 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 += " " + note playlistrow.note += "\n" + 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
roles = [ self.invalidate_row(row_number)
Qt.ItemDataRole.BackgroundRole, session.commit()
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:
rat.row_bg = NoteColours.get_colour(session, rat.note) note_background = NoteColours.get_colour(session, rat.note)
if rat.row_bg: if note_background:
return QBrush(QColor(rat.row_bg)) return QBrush(QColor(note_background))
else: else:
return QBrush(QColor(Config.COLOUR_NOTES_PLAYLIST)) return QBrush(QColor(Config.COLOUR_NOTES_PLAYLIST))
# Unreadable track file # Unreadable track file
@ -230,11 +218,10 @@ 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:
rat.note_bg = NoteColours.get_colour(session, rat.note) note_background = NoteColours.get_colour(session, rat.note)
if rat.note_bg: if note_background:
return QBrush(QColor(rat.note_bg)) return QBrush(QColor(note_background))
return QBrush() return QBrush()
@ -267,27 +254,26 @@ 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
track_id = track_sequence.current.track_id if not track_sequence.current.track_id:
if not track_id: log.error(
raise ApplicationError( f"{self}: current_track_started() called with {track_sequence.current.track_id=}"
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 {track_id=}") log.debug(f"{self}: update playdates")
Playdates(session, track_id) Playdates(session, track_sequence.current.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")
@ -297,62 +283,48 @@ 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, " f"{self}: Can't retrieve plr, {track_sequence.current.playlistrow_id=}"
f"{track_sequence.current.playlistrow_id=}"
) )
session.commit()
# Update colour and times for current row # Update colour and times for current row
# only invalidate required roles self.invalidate_row(row_number)
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:
# only invalidate required roles self.invalidate_row(track_sequence.previous.row_number)
self.invalidate_row(track_sequence.previous.row_number, roles)
# Update all other track times
self.update_track_times()
# Find next track # Find next track
# Get all unplayed track rows
log.debug(f"{self}: Find next track")
next_row = None next_row = None
unplayed_rows = [ unplayed_rows = self.get_unplayed_rows()
a
for a in self.get_unplayed_rows()
if not self.is_header_row(a)
and not file_is_unreadable(self.playlist_rows[a].path)
]
if unplayed_rows: if unplayed_rows:
try: try:
next_row = min([a for a in unplayed_rows if a > row_number]) # Find next row after current track
next_row = min(
[
a
for a in unplayed_rows
if a > row_number and not self.is_header_row(a)
]
)
except ValueError: except ValueError:
# Find first unplayed track
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 | QFont | QBrush | str | int: ) -> QVariant:
"""Return data to view""" """Return data to view"""
if ( if not index.isValid() or not (0 <= index.row() < len(self.playlist_rows)):
not index.isValid()
or not (0 <= index.row() < len(self.playlist_rows))
or role
in [
Qt.ItemDataRole.DecorationRole,
Qt.ItemDataRole.StatusTipRole,
Qt.ItemDataRole.WhatsThisRole,
Qt.ItemDataRole.SizeHintRole,
Qt.ItemDataRole.TextAlignmentRole,
Qt.ItemDataRole.CheckStateRole,
Qt.ItemDataRole.InitialSortOrderRole,
]
):
return QVariant() return QVariant()
row = index.row() row = index.row()
@ -360,21 +332,32 @@ class PlaylistModel(QAbstractTableModel):
# rat for playlist row data as it's used a lot # rat for playlist row data as it's used a lot
rat = self.playlist_rows[row] rat = self.playlist_rows[row]
# These are ordered in approximately the frequency with which # Dispatch to role-specific functions
# they are called dispatch_table = {
if role == Qt.ItemDataRole.BackgroundRole: int(Qt.ItemDataRole.BackgroundRole): self.background_role,
return self._background_role(row, column, rat) int(Qt.ItemDataRole.DisplayRole): self.display_role,
elif role == Qt.ItemDataRole.DisplayRole: int(Qt.ItemDataRole.EditRole): self.edit_role,
return self._display_role(row, column, rat) int(Qt.ItemDataRole.FontRole): self.font_role,
elif role == Qt.ItemDataRole.EditRole: int(Qt.ItemDataRole.ForegroundRole): self.foreground_role,
return self._edit_role(row, column, rat) int(Qt.ItemDataRole.ToolTipRole): self.tooltip_role,
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)
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.StatusTipRole,
Qt.ItemDataRole.WhatsThisRole,
Qt.ItemDataRole.SizeHintRole,
Qt.ItemDataRole.TextAlignmentRole,
Qt.ItemDataRole.CheckStateRole,
Qt.ItemDataRole.InitialSortOrderRole,
]:
return QVariant()
# Fall through to no-op
return QVariant() return QVariant()
def delete_rows(self, row_numbers: list[int]) -> None: def delete_rows(self, row_numbers: list[int]) -> None:
@ -404,9 +387,8 @@ 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) -> str: def display_role(self, row: int, column: int, rat: RowAndTrack) -> QVariant:
""" """
Return text for display Return text for display
""" """
@ -417,7 +399,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() - HEADER_NOTES_COLUMN column_span = self.columnCount() - 1
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
) )
@ -426,45 +408,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 Config.SECTION_HEADER return QVariant(Config.TEXT_NO_TRACK_NO_NOTE)
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 trimmed_header return QVariant(trimmed_header)
else: else:
return "" return QVariant("")
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 start_time.strftime(Config.TRACK_TIME_FORMAT) return QVariant(start_time.strftime(Config.TRACK_TIME_FORMAT))
return "" return QVariant()
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 end_time.strftime(Config.TRACK_TIME_FORMAT) return QVariant(end_time.strftime(Config.TRACK_TIME_FORMAT))
return "" return QVariant()
if column == Col.INTRO.value: if column == Col.INTRO.value:
if rat.intro: if rat.intro:
return f"{rat.intro / 1000:{Config.INTRO_SECONDS_FORMAT}}" return QVariant(f"{rat.intro / 1000:{Config.INTRO_SECONDS_FORMAT}}")
else: else:
return "" return QVariant("")
dispatch_table: dict[int, str] = { dispatch_table = {
Col.ARTIST.value: rat.artist, Col.ARTIST.value: QVariant(rat.artist),
Col.BITRATE.value: str(rat.bitrate), Col.BITRATE.value: QVariant(rat.bitrate),
Col.DURATION.value: ms_to_mmss(rat.duration), Col.DURATION.value: QVariant(ms_to_mmss(rat.duration)),
Col.LAST_PLAYED.value: get_relative_date(rat.lastplayed), Col.LAST_PLAYED.value: QVariant(get_relative_date(rat.lastplayed)),
Col.NOTE.value: rat.note, Col.NOTE.value: QVariant(rat.note),
Col.START_GAP.value: str(rat.start_gap), Col.START_GAP.value: QVariant(rat.start_gap),
Col.TITLE.value: rat.title, Col.TITLE.value: QVariant(rat.title),
} }
if column in dispatch_table: if column in dispatch_table:
return dispatch_table[column] return dispatch_table[column]
return "" return QVariant()
def end_reset_model(self, playlist_id: int) -> None: def end_reset_model(self, playlist_id: int) -> None:
""" """
@ -481,38 +463,37 @@ 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) -> str | int: def edit_role(self, row: int, column: int, rat: RowAndTrack) -> QVariant:
""" """
Return value for editing Return text 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 rat.note return QVariant(rat.note)
if column == Col.INTRO.value: if column == Col.INTRO.value:
return rat.intro or 0 return QVariant(rat.intro)
if column == Col.TITLE.value: if column == Col.TITLE.value:
return rat.title return QVariant(rat.title)
if column == Col.ARTIST.value: if column == Col.ARTIST.value:
return rat.artist return QVariant(rat.artist)
if column == Col.NOTE.value: if column == Col.NOTE.value:
return rat.note return QVariant(rat.note)
return "" return QVariant()
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:
rat.row_fg = NoteColours.get_colour( note_foreground = NoteColours.get_colour(
session, rat.note, foreground=True session, rat.note, foreground=True
) )
if rat.row_fg: if note_foreground:
return QBrush(QColor(rat.row_fg)) return QBrush(QColor(note_foreground))
return QBrush() return QBrush()
@ -534,24 +515,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) -> QFont: def font_role(self, row: int, column: int, rat: RowAndTrack) -> QVariant:
""" """
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 QFont() return QVariant()
boldfont = QFont() boldfont = QFont()
boldfont.setBold(not self.playlist_rows[row].played) boldfont.setBold(not self.playlist_rows[row].played)
return boldfont return QVariant(boldfont)
def get_duplicate_rows(self) -> list[int]: def get_duplicate_rows(self) -> list[int]:
""" """
@ -640,6 +621,7 @@ 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(
@ -647,22 +629,22 @@ class PlaylistModel(QAbstractTableModel):
section: int, section: int,
orientation: Qt.Orientation, orientation: Qt.Orientation,
role: int = Qt.ItemDataRole.DisplayRole, role: int = Qt.ItemDataRole.DisplayRole,
) -> str | int | QFont | QVariant: ) -> QVariant:
""" """
Return text for headers Return text for headers
""" """
display_dispatch_table = { display_dispatch_table = {
Col.START_GAP.value: Config.HEADER_START_GAP, Col.START_GAP.value: QVariant(Config.HEADER_START_GAP),
Col.INTRO.value: Config.HEADER_INTRO, Col.INTRO.value: QVariant(Config.HEADER_INTRO),
Col.TITLE.value: Config.HEADER_TITLE, Col.TITLE.value: QVariant(Config.HEADER_TITLE),
Col.ARTIST.value: Config.HEADER_ARTIST, Col.ARTIST.value: QVariant(Config.HEADER_ARTIST),
Col.DURATION.value: Config.HEADER_DURATION, Col.DURATION.value: QVariant(Config.HEADER_DURATION),
Col.START_TIME.value: Config.HEADER_START_TIME, Col.START_TIME.value: QVariant(Config.HEADER_START_TIME),
Col.END_TIME.value: Config.HEADER_END_TIME, Col.END_TIME.value: QVariant(Config.HEADER_END_TIME),
Col.LAST_PLAYED.value: Config.HEADER_LAST_PLAYED, Col.LAST_PLAYED.value: QVariant(Config.HEADER_LAST_PLAYED),
Col.BITRATE.value: Config.HEADER_BITRATE, Col.BITRATE.value: QVariant(Config.HEADER_BITRATE),
Col.NOTE.value: Config.HEADER_NOTE, Col.NOTE.value: QVariant(Config.HEADER_NOTE),
} }
if role == Qt.ItemDataRole.DisplayRole: if role == Qt.ItemDataRole.DisplayRole:
@ -670,14 +652,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 section return QVariant(str(section))
else: else:
return section + 1 return QVariant(str(section + 1))
elif role == Qt.ItemDataRole.FontRole: elif role == Qt.ItemDataRole.FontRole:
boldfont = QFont() boldfont = QFont()
boldfont.setBold(True) boldfont.setBold(True)
return boldfont return QVariant(boldfont)
return QVariant() return QVariant()
@ -715,11 +697,7 @@ 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):
# only invalidate required roles self.invalidate_row(row_number)
roles = [
Qt.ItemDataRole.DisplayRole,
]
self.invalidate_row(row_number, roles)
def insert_row( def insert_row(
self, self,
@ -751,16 +729,9 @@ 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()
# only invalidate required roles self.invalidate_rows(list(range(new_row_number, len(self.playlist_rows))))
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, roles: list[Qt.ItemDataRole]) -> None: def invalidate_row(self, modified_row: int) -> None:
""" """
Signal to view to refresh invalidated row Signal to view to refresh invalidated row
""" """
@ -768,17 +739,15 @@ 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], roles: list[Qt.ItemDataRole]) -> None: def invalidate_rows(self, modified_rows: list[int]) -> 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:
# only invalidate required roles self.invalidate_row(modified_row)
self.invalidate_row(modified_row, roles)
def is_header_row(self, row_number: int) -> bool: def is_header_row(self, row_number: int) -> bool:
""" """
@ -808,7 +777,9 @@ class PlaylistModel(QAbstractTableModel):
return None return None
def load_data(self, session: Session) -> None: def load_data(
self, session: db.session, dummy_for_profiling: Optional[int] = None
) -> 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
@ -853,13 +824,15 @@ class PlaylistModel(QAbstractTableModel):
self.refresh_row(session, row_number) self.refresh_row(session, row_number)
self.update_track_times() self.update_track_times()
# only invalidate required roles self.invalidate_rows(row_numbers)
roles = [
Qt.ItemDataRole.FontRole,
]
self.invalidate_rows(row_numbers, roles)
def move_rows(self, from_rows: list[int], to_row_number: int) -> None: @line_profiler.profile
def move_rows(
self,
from_rows: list[int],
to_row_number: int,
dummy_for_profiling: Optional[int] = None,
) -> None:
""" """
Move the playlist rows given to to_row and below. Move the playlist rows given to to_row and below.
""" """
@ -922,17 +895,15 @@ 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()
# only invalidate required roles self.invalidate_rows(list(row_map.keys()))
roles = [
Qt.ItemDataRole.DisplayRole,
]
self.invalidate_rows(list(row_map.keys()), roles)
@line_profiler.profile
def move_rows_between_playlists( def move_rows_between_playlists(
self, self,
from_rows: list[int], from_rows: list[int],
to_row_number: int, to_row_number: int,
to_playlist_id: int, to_playlist_id: int,
dummy_for_profiling: Optional[int] = None,
) -> None: ) -> None:
""" """
Move the playlist rows given to to_row and below of to_playlist. Move the playlist rows given to to_row and below of to_playlist.
@ -1075,7 +1046,7 @@ class PlaylistModel(QAbstractTableModel):
log.debug(f"{self}: OBS scene changed to '{scene_name}'") log.debug(f"{self}: OBS scene changed to '{scene_name}'")
continue continue
except obswebsocket.exceptions.ConnectionFailure: except obswebsocket.exceptions.ConnectionFailure:
log.warning(f"{self}: OBS connection refused") log.error(f"{self}: OBS connection refused")
return return
def previous_track_ended(self) -> None: def previous_track_ended(self) -> None:
@ -1103,13 +1074,12 @@ class PlaylistModel(QAbstractTableModel):
return return
# Update display # Update display
# only invalidate required roles self.invalidate_row(track_sequence.previous.row_number)
roles = [
Qt.ItemDataRole.BackgroundRole,
]
self.invalidate_row(track_sequence.previous.row_number, roles)
def refresh_data(self, session: Session) -> None: @line_profiler.profile
def refresh_data(
self, session: db.session, dummy_for_profiling: Optional[int] = None
) -> None:
""" """
Populate self.playlist_rows with playlist data Populate self.playlist_rows with playlist data
@ -1160,11 +1130,7 @@ 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)
# only invalidate required roles self.invalidate_row(row_number)
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:
""" """
@ -1178,12 +1144,7 @@ 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()
roles = [ self.invalidate_row(row_number)
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()
@ -1197,6 +1158,8 @@ 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:
@ -1207,7 +1170,6 @@ class PlaylistModel(QAbstractTableModel):
]: ]:
if ts: if ts:
ts.update_playlist_and_row(session) ts.update_playlist_and_row(session)
session.commit()
self.update_track_times() self.update_track_times()
@ -1241,13 +1203,7 @@ 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()
# only invalidate required roles self.invalidate_rows(row_numbers)
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]
@ -1340,17 +1296,13 @@ class PlaylistModel(QAbstractTableModel):
unplayed_count: int = 0 unplayed_count: int = 0
duration: int = 0 duration: int = 0
if rat.row_number == 0:
# Meaningless to have a subtotal on row 0
return Config.SUBTOTAL_ON_ROW_ZERO
# Show subtotal # Show subtotal
for row_number in range(rat.row_number - 1, -1, -1): for row_number in range(rat.row_number - 1, -1, -1):
row_rat = self.playlist_rows[row_number] row_rat = self.playlist_rows[row_number]
if self.is_header_row(row_number) or row_number == 0: if self.is_header_row(row_number):
if row_rat.note.endswith(Config.SECTION_STARTS) or row_number == 0: if row_rat.note.endswith(Config.SECTION_STARTS):
# If we are playing this section, also # If we are playing this section, also
# calculate end time when all tracks are played. # calculate end time if all tracks are played.
end_time_str = "" end_time_str = ""
if ( if (
track_sequence.current track_sequence.current
@ -1389,8 +1341,9 @@ class PlaylistModel(QAbstractTableModel):
unplayed_count += 1 unplayed_count += 1
duration += row_rat.duration duration += row_rat.duration
# We should never get here # We should only get here if there were no rows in section (ie,
raise ApplicationError("Error in section_subtotal_header()") # this was row zero)
return Config.SUBTOTAL_ON_ROW_ZERO
def selection_is_sortable(self, row_numbers: list[int]) -> bool: def selection_is_sortable(self, row_numbers: list[int]) -> bool:
""" """
@ -1456,14 +1409,9 @@ 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:
# only invalidate required roles self.invalidate_row(old_next_row)
self.invalidate_row(old_next_row, roles) self.invalidate_row(row_number)
# 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()
@ -1613,54 +1561,34 @@ 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) -> str: def tooltip_role(self, row: int, column: int, rat: RowAndTrack) -> QVariant:
""" """
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 "" return QVariant()
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 "" return QVariant()
playdates = Playdates.last_playdates(session, track_id) playdates = Playdates.last_playdates(session, track_id)
return "<br>".join( return QVariant(
"<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 reversed(playdates)
] ]
) )
)
def update_or_insert(self, track_id: int, row_number: int) -> None:
"""
If the passed track_id exists in this playlist, update the
row(s), otherwise insert this track at row_number.
"""
track_rows = [
a.row_number for a in self.playlist_rows.values() if a.track_id == track_id
]
if track_rows:
with db.Session() as session:
for row in track_rows:
self.refresh_row(session, row)
# only invalidate required roles
roles = [
Qt.ItemDataRole.BackgroundRole,
Qt.ItemDataRole.DisplayRole,
Qt.ItemDataRole.FontRole,
Qt.ItemDataRole.ForegroundRole,
]
self.invalidate_rows(track_rows, roles)
else:
self.insert_row(proposed_row_number=row_number, track_id=track_id)
def update_track_times(self) -> None: def update_track_times(self) -> None:
""" """
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)
@ -1799,13 +1727,9 @@ 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, roles), lambda: self.sourceModel().invalidate_row(source_row),
) )
return True return True
# Next track not playing yet so don't hide previous # Next track not playing yet so don't hide previous

View File

@ -1,14 +1,13 @@
# Standard library imports # Standard library imports
from typing import Any, Callable, cast, Optional, TYPE_CHECKING from typing import Any, Callable, cast, List, Optional, TYPE_CHECKING
# PyQt imports # PyQt imports
from PyQt6.QtCore import ( from PyQt6.QtCore import (
QAbstractItemModel, QAbstractItemModel,
QEvent, QEvent,
QItemSelection,
QModelIndex, QModelIndex,
QObject, QObject,
QPoint, QItemSelection,
QSize, QSize,
Qt, Qt,
QTimer, QTimer,
@ -22,7 +21,10 @@ from PyQt6.QtWidgets import (
QFrame, QFrame,
QMenu, QMenu,
QMessageBox, QMessageBox,
QProxyStyle,
QStyle,
QStyledItemDelegate, QStyledItemDelegate,
QStyleOption,
QStyleOptionViewItem, QStyleOptionViewItem,
QTableView, QTableView,
QTableWidgetItem, QTableWidgetItem,
@ -31,10 +33,11 @@ from PyQt6.QtWidgets import (
) )
# Third party imports # Third party imports
# import line_profiler import line_profiler
# App imports # App imports
from classes import ApplicationError, Col, MusicMusterSignals, PlaylistStyle, TrackInfo from audacity_controller import AudacityController
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 (
@ -43,7 +46,7 @@ from helpers import (
show_OK, show_OK,
show_warning, show_warning,
) )
from log import log, log_call from log import log
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
@ -183,11 +186,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 != editor.toPlainText() self.original_model_data.value() != editor.toPlainText()
) )
elif isinstance(editor, QDoubleSpinBox): elif isinstance(editor, QDoubleSpinBox):
data_modified = ( data_modified = (
self.original_model_data != int(editor.value()) * 1000 self.original_model_data.value() != int(editor.value()) * 1000
) )
if not data_modified: if not data_modified:
self.closeEditor.emit(editor) self.closeEditor.emit(editor)
@ -211,10 +214,10 @@ class PlaylistDelegate(QStyledItemDelegate):
doc.setTextWidth(option.rect.width()) doc.setTextWidth(option.rect.width())
doc.setDefaultFont(option.font) doc.setDefaultFont(option.font)
doc.setDocumentMargin(Config.ROW_PADDING) doc.setDocumentMargin(Config.ROW_PADDING)
if "\n" in option.text: if '\n' in option.text:
txt = option.text.replace("\n", "<br>") txt = option.text.replace('\n', '<br>')
elif "\u2028" in option.text: elif '\u2028' in option.text:
txt = option.text.replace("\u2028", "<br>") txt = option.text.replace('\u2028', '<br>')
else: else:
txt = option.text txt = option.text
doc.setHtml(txt) doc.setHtml(txt)
@ -246,10 +249,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: if self.original_model_data.value():
editor.setValue(self.original_model_data / 1000) editor.setValue(self.original_model_data.value() / 1000)
else: else:
editor.setPlainText(self.original_model_data) editor.setPlainText(self.original_model_data.value())
def setModelData(self, editor, model, index): def setModelData(self, editor, model, index):
proxy_model = index.model() proxy_model = index.model()
@ -265,6 +268,24 @@ 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
@ -307,6 +328,13 @@ 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()
@ -336,7 +364,7 @@ class PlaylistTab(QTableView):
Override closeEditor to enable play controls and update display. Override closeEditor to enable play controls and update display.
""" """
self.musicmuster.enable_escape(True) self.musicmuster.action_Clear_selection.setEnabled(True)
super(PlaylistTab, self).closeEditor(editor, hint) super(PlaylistTab, self).closeEditor(editor, hint)
@ -350,7 +378,10 @@ class PlaylistTab(QTableView):
# Deselect edited line # Deselect edited line
self.clear_selection() self.clear_selection()
def dropEvent(self, event: Optional[QDropEvent], dummy: int | None = None) -> None: @line_profiler.profile
def dropEvent(
self, event: Optional[QDropEvent], dummy_for_profiling: Optional[int] = None
) -> None:
""" """
Move dropped rows Move dropped rows
""" """
@ -538,8 +569,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 and self.musicmuster.ac: if track_row and not this_is_current_row:
if track_path == self.musicmuster.ac.path: if self.ac and track_path == self.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",
@ -649,8 +680,8 @@ class PlaylistTab(QTableView):
that we have an edit open. that we have an edit open.
""" """
if self.musicmuster.ac: if self.ac:
self.musicmuster.ac.path = None self.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"""
@ -746,29 +777,14 @@ 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:
@ -812,50 +828,36 @@ class PlaylistTab(QTableView):
else: else:
return None return None
def get_selected_rows(self) -> list[int]: def get_selected_rows(self) -> List[int]:
"""Return a list of model-selected row numbers sorted by row""" """Return a list of model-selected row numbers sorted by row"""
# 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)
selected_indexes = self.selectedIndexes() result = sorted(
list(
set([self.model().mapToSource(a).row() for a in self.selectedIndexes()])
)
)
if not selected_indexes: log.debug(f"get_selected_rows() returned: {result=}")
return [] return result
return sorted(list(set([self.model().mapToSource(a).row() for a in selected_indexes])))
def get_top_visible_row(self) -> int:
"""
Get the viewport of the table view
"""
index = self.indexAt(QPoint(0, 0))
if index.isValid():
return index.row()
else:
# If no index is found, it means the table might be empty or scrolled beyond content
return -1
def hide_played_sections(self) -> None: def hide_played_sections(self) -> None:
""" """
Scroll played sections off screen, but only if current top row is above Scroll played sections off screen
the active header
""" """
active_header_row = self.get_base_model().active_section_header() self.scroll_to_top(self.get_base_model().active_section_header())
if self.get_top_visible_row() < active_header_row:
self.scroll_to_top(active_header_row)
def _import_from_audacity(self, row_number: int) -> None: def _import_from_audacity(self, row_number: int) -> None:
""" """
Import current Audacity track to passed row Import current Audacity track to passed row
""" """
if not self.musicmuster.ac: if not self.ac:
return return
try: try:
self.musicmuster.ac.export() self.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))
@ -878,9 +880,9 @@ class PlaylistTab(QTableView):
else: else:
txt = f"Can't find info about row{row_number}" txt = f"Can't find info about row{row_number}"
show_OK("Track info", txt, self.musicmuster) show_OK(self.musicmuster, "Track info", txt)
def _mark_as_unplayed(self, row_numbers: list[int]) -> None: def _mark_as_unplayed(self, row_numbers: List[int]) -> None:
"""Mark row as unplayed""" """Mark row as unplayed"""
self.get_base_model().mark_unplayed(row_numbers) self.get_base_model().mark_unplayed(row_numbers)
@ -912,16 +914,15 @@ 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:
self.musicmuster.ac.open(path) if not self.ac:
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))
@ -1005,7 +1006,7 @@ class PlaylistTab(QTableView):
return None return None
return self.model().mapToSource(selected_index).row() return self.model().mapToSource(selected_index).row()
def selected_model_row_numbers(self) -> list[int]: def selected_model_row_numbers(self) -> List[int]:
""" """
Return a list of model row numbers corresponding to the selected rows or Return a list of model row numbers corresponding to the selected rows or
an empty list. an empty list.
@ -1034,7 +1035,7 @@ class PlaylistTab(QTableView):
return row_indexes[0] return row_indexes[0]
def _selected_row_indexes(self) -> list[QModelIndex]: def _selected_row_indexes(self) -> List[QModelIndex]:
""" """
Return a list of indexes of column 0 of selected rows Return a list of indexes of column 0 of selected rows
""" """

View File

@ -1,290 +0,0 @@
# 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)
]
)
)

View File

@ -1,94 +0,0 @@
<?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 &amp;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>

View File

@ -1,45 +0,0 @@
# 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"))

View File

@ -1,7 +1,6 @@
<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>

File diff suppressed because it is too large Load Diff

View File

@ -997,13 +997,10 @@ 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"/>
<addaction name="actionImport_files"/> <addaction name="actionReplace_files"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionE_xit"/> <addaction name="actionE_xit"/>
</widget> </widget>
@ -1367,21 +1364,11 @@ padding-left: 8px;</string>
<string>Select duplicate rows...</string> <string>Select duplicate rows...</string>
</property> </property>
</action> </action>
<action name="actionImport_files"> <action name="actionReplace_files">
<property name="text"> <property name="text">
<string>Import files...</string> <string>Import files...</string>
</property> </property>
</action> </action>
<action name="actionOpenQuerylist">
<property name="text">
<string>Open &amp;querylist...</string>
</property>
</action>
<action name="actionManage_querylists">
<property name="text">
<string>Manage querylists...</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>

View File

@ -1,589 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FooterSection</class>
<widget class="QWidget" name="FooterSection">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1237</width>
<height>154</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QFrame" name="InfoFooterFrame">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">background-color: rgb(192, 191, 188)</string>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QFrame" name="FadeStopInfoFrame">
<property name="minimumSize">
<size>
<width>152</width>
<height>112</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>184</width>
<height>16777215</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QPushButton" name="btnPreview">
<property name="minimumSize">
<size>
<width>132</width>
<height>41</height>
</size>
</property>
<property name="text">
<string> Preview</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/headphones</normaloff>:/icons/headphones</iconset>
</property>
<property name="iconSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxIntroControls">
<property name="minimumSize">
<size>
<width>132</width>
<height>46</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>132</width>
<height>46</height>
</size>
</property>
<property name="title">
<string/>
</property>
<widget class="QPushButton" name="btnPreviewStart">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>44</width>
<height>23</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="text">
<string>&lt;&lt;</string>
</property>
</widget>
<widget class="QPushButton" name="btnPreviewArm">
<property name="geometry">
<rect>
<x>44</x>
<y>0</y>
<width>44</width>
<height>23</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/record-button.png</normaloff>
<normalon>:/icons/record-red-button.png</normalon>:/icons/record-button.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
<widget class="QPushButton" name="btnPreviewEnd">
<property name="geometry">
<rect>
<x>88</x>
<y>0</y>
<width>44</width>
<height>23</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="text">
<string>&gt;&gt;</string>
</property>
</widget>
<widget class="QPushButton" name="btnPreviewBack">
<property name="geometry">
<rect>
<x>0</x>
<y>23</y>
<width>44</width>
<height>23</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="text">
<string>&lt;</string>
</property>
</widget>
<widget class="QPushButton" name="btnPreviewMark">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>44</x>
<y>23</y>
<width>44</width>
<height>23</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normalon>:/icons/star.png</normalon>
<disabledoff>:/icons/star_empty.png</disabledoff>
</iconset>
</property>
</widget>
<widget class="QPushButton" name="btnPreviewFwd">
<property name="geometry">
<rect>
<x>88</x>
<y>23</y>
<width>44</width>
<height>23</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>44</width>
<height>23</height>
</size>
</property>
<property name="text">
<string>&gt;</string>
</property>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_intro">
<property name="minimumSize">
<size>
<width>152</width>
<height>112</height>
</size>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Intro</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_intro_timer">
<property name="font">
<font>
<family>FreeSans</family>
<pointsize>40</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>0:0</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_toggleplayed_3db">
<property name="minimumSize">
<size>
<width>152</width>
<height>112</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>184</width>
<height>16777215</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QPushButton" name="btnDrop3db">
<property name="minimumSize">
<size>
<width>132</width>
<height>41</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>164</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>-3dB to talk</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnHidePlayed">
<property name="minimumSize">
<size>
<width>132</width>
<height>41</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>164</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Hide played</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_fade">
<property name="minimumSize">
<size>
<width>152</width>
<height>112</height>
</size>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Fade</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_fade_timer">
<property name="font">
<font>
<family>FreeSans</family>
<pointsize>40</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>00:00</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_silent">
<property name="minimumSize">
<size>
<width>152</width>
<height>112</height>
</size>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Silent</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_silent_timer">
<property name="font">
<font>
<family>FreeSans</family>
<pointsize>40</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>00:00</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="PlotWidget" name="widgetFadeVolume" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="minimumSize">
<size>
<width>151</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>151</width>
<height>112</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QPushButton" name="btnFade">
<property name="minimumSize">
<size>
<width>132</width>
<height>32</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>164</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string> Fade</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/fade</normaloff>:/icons/fade</iconset>
</property>
<property name="iconSize">
<size>
<width>30</width>
<height>30</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnStop">
<property name="minimumSize">
<size>
<width>0</width>
<height>36</height>
</size>
</property>
<property name="text">
<string> Stop</string>
</property>
<property name="icon">
<iconset resource="icons.qrc">
<normaloff>:/icons/stopsign</normaloff>:/icons/stopsign</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>PlotWidget</class>
<extends>QWidget</extends>
<header>pyqtgraph</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="icons.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -1,274 +0,0 @@
# Form implementation generated from reading ui file 'app/ui/main_window_footer.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_FooterSection(object):
def setupUi(self, FooterSection):
FooterSection.setObjectName("FooterSection")
FooterSection.resize(1237, 154)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(FooterSection)
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.InfoFooterFrame = QtWidgets.QFrame(parent=FooterSection)
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))
icon = QtGui.QIcon()
icon.addPixmap(
QtGui.QPixmap(":/icons/headphones"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.btnPreview.setIcon(icon)
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("")
icon1 = QtGui.QIcon()
icon1.addPixmap(
QtGui.QPixmap(":/icons/record-button.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
icon1.addPixmap(
QtGui.QPixmap(":/icons/record-red-button.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.On,
)
self.btnPreviewArm.setIcon(icon1)
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("")
icon2 = QtGui.QIcon()
icon2.addPixmap(
QtGui.QPixmap(":/icons/star.png"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.On,
)
icon2.addPixmap(
QtGui.QPixmap(":/icons/star_empty.png"),
QtGui.QIcon.Mode.Disabled,
QtGui.QIcon.State.Off,
)
self.btnPreviewMark.setIcon(icon2)
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))
icon3 = QtGui.QIcon()
icon3.addPixmap(
QtGui.QPixmap(":/icons/fade"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.btnFade.setIcon(icon3)
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))
icon4 = QtGui.QIcon()
icon4.addPixmap(
QtGui.QPixmap(":/icons/stopsign"),
QtGui.QIcon.Mode.Normal,
QtGui.QIcon.State.Off,
)
self.btnStop.setIcon(icon4)
self.btnStop.setObjectName("btnStop")
self.verticalLayout_5.addWidget(self.btnStop)
self.horizontalLayout.addWidget(self.frame)
self.horizontalLayout_2.addWidget(self.InfoFooterFrame)
self.retranslateUi(FooterSection)
QtCore.QMetaObject.connectSlotsByName(FooterSection)
def retranslateUi(self, FooterSection):
_translate = QtCore.QCoreApplication.translate
FooterSection.setWindowTitle(_translate("FooterSection", "Form"))
self.btnPreview.setText(_translate("FooterSection", " Preview"))
self.btnPreviewStart.setText(_translate("FooterSection", "<<"))
self.btnPreviewEnd.setText(_translate("FooterSection", ">>"))
self.btnPreviewBack.setText(_translate("FooterSection", "<"))
self.btnPreviewFwd.setText(_translate("FooterSection", ">"))
self.label_7.setText(_translate("FooterSection", "Intro"))
self.label_intro_timer.setText(_translate("FooterSection", "0:0"))
self.btnDrop3db.setText(_translate("FooterSection", "-3dB to talk"))
self.btnHidePlayed.setText(_translate("FooterSection", "Hide played"))
self.label_4.setText(_translate("FooterSection", "Fade"))
self.label_fade_timer.setText(_translate("FooterSection", "00:00"))
self.label_5.setText(_translate("FooterSection", "Silent"))
self.label_silent_timer.setText(_translate("FooterSection", "00:00"))
self.btnFade.setText(_translate("FooterSection", " Fade"))
self.btnStop.setText(_translate("FooterSection", " Stop"))
from pyqtgraph import PlotWidget # type: ignore

View File

@ -1,314 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>HeaderSection</class>
<widget class="QWidget" name="HeaderSection">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1273</width>
<height>179</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="previous_track_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>230</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<family>Sans</family>
<pointsize>20</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">background-color: #f8d7da;
border: 1px solid rgb(85, 87, 83);</string>
</property>
<property name="text">
<string>Last track:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="current_track_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>230</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<family>Sans</family>
<pointsize>20</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">background-color: #d4edda;
border: 1px solid rgb(85, 87, 83);</string>
</property>
<property name="text">
<string>Current track:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="next_track_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>230</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<family>Sans</family>
<pointsize>20</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">background-color: #fff3cd;
border: 1px solid rgb(85, 87, 83);</string>
</property>
<property name="text">
<string>Next track:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="hdrPreviousTrack">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<family>Sans</family>
<pointsize>20</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">background-color: #f8d7da;
border: 1px solid rgb(85, 87, 83);</string>
</property>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="hdrCurrentTrack">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>20</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">background-color: #d4edda;
border: 1px solid rgb(85, 87, 83);
text-align: left;
padding-left: 8px;
</string>
</property>
<property name="text">
<string/>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="hdrNextTrack">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>20</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">background-color: #fff3cd;
border: 1px solid rgb(85, 87, 83);
text-align: left;
padding-left: 8px;</string>
</property>
<property name="text">
<string/>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QFrame" name="frame_2">
<property name="minimumSize">
<size>
<width>0</width>
<height>131</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>230</width>
<height>131</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_10">
<item>
<widget class="QLabel" name="lblTOD">
<property name="minimumSize">
<size>
<width>208</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<pointsize>35</pointsize>
</font>
</property>
<property name="text">
<string>00:00:00</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_elapsed_timer">
<property name="font">
<font>
<family>FreeSans</family>
<pointsize>18</pointsize>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">color: black;</string>
</property>
<property name="text">
<string>00:00 / 00:00</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QFrame" name="frame_4">
<property name="minimumSize">
<size>
<width>0</width>
<height>16</height>
</size>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true">background-color: rgb(154, 153, 150)</string>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,178 +0,0 @@
# Form implementation generated from reading ui file 'app/ui/main_window_header.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_HeaderSection(object):
def setupUi(self, HeaderSection):
HeaderSection.setObjectName("HeaderSection")
HeaderSection.resize(1273, 179)
self.horizontalLayout = QtWidgets.QHBoxLayout(HeaderSection)
self.horizontalLayout.setObjectName("horizontalLayout")
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setObjectName("gridLayout")
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=HeaderSection)
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=HeaderSection)
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=HeaderSection)
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=HeaderSection)
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=HeaderSection)
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=HeaderSection)
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=HeaderSection)
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.addLayout(self.horizontalLayout_3, 0, 0, 1, 1)
self.frame_4 = QtWidgets.QFrame(parent=HeaderSection)
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.addWidget(self.frame_4, 1, 0, 1, 1)
self.horizontalLayout.addLayout(self.gridLayout)
self.retranslateUi(HeaderSection)
QtCore.QMetaObject.connectSlotsByName(HeaderSection)
def retranslateUi(self, HeaderSection):
_translate = QtCore.QCoreApplication.translate
HeaderSection.setWindowTitle(_translate("HeaderSection", "Form"))
self.previous_track_2.setText(_translate("HeaderSection", "Last track:"))
self.current_track_2.setText(_translate("HeaderSection", "Current track:"))
self.next_track_2.setText(_translate("HeaderSection", "Next track:"))
self.lblTOD.setText(_translate("HeaderSection", "00:00:00"))
self.label_elapsed_timer.setText(_translate("HeaderSection", "00:00 / 00:00"))

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PlaylistSection</class>
<widget class="QWidget" name="PlaylistSection">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1249</width>
<height>538</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QTabWidget" name="tabPlaylist">
<property name="currentIndex">
<number>-1</number>
</property>
<property name="documentMode">
<bool>false</bool>
</property>
<property name="tabsClosable">
<bool>true</bool>
</property>
<property name="movable">
<bool>true</bool>
</property>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,34 +0,0 @@
# Form implementation generated from reading ui file 'app/ui/main_window_playlist.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_PlaylistSection(object):
def setupUi(self, PlaylistSection):
PlaylistSection.setObjectName("PlaylistSection")
PlaylistSection.resize(1249, 499)
self.horizontalLayout = QtWidgets.QHBoxLayout(PlaylistSection)
self.horizontalLayout.setObjectName("horizontalLayout")
self.splitter = QtWidgets.QSplitter(parent=PlaylistSection)
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.horizontalLayout.addWidget(self.splitter)
self.retranslateUi(PlaylistSection)
self.tabPlaylist.setCurrentIndex(-1)
QtCore.QMetaObject.connectSlotsByName(PlaylistSection)
def retranslateUi(self, PlaylistSection):
_translate = QtCore.QCoreApplication.translate
PlaylistSection.setWindowTitle(_translate("PlaylistSection", "Form"))

681
app/ui/main_window_ui.py Normal file
View File

@ -0,0 +1,681 @@
# Form implementation generated from reading ui file 'app/ui/main_window.ui'
#
# Created by: PyQt6 UI code generator 6.7.1
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(1280, 857)
MainWindow.setMinimumSize(QtCore.QSize(1280, 0))
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(":/icons/musicmuster"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
MainWindow.setWindowIcon(icon)
MainWindow.setStyleSheet("")
self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout_4 = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout_4.setObjectName("gridLayout_4")
self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.previous_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.previous_track_2.sizePolicy().hasHeightForWidth())
self.previous_track_2.setSizePolicy(sizePolicy)
self.previous_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
self.previous_track_2.setFont(font)
self.previous_track_2.setStyleSheet("background-color: #f8d7da;\n"
"border: 1px solid rgb(85, 87, 83);")
self.previous_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.previous_track_2.setObjectName("previous_track_2")
self.verticalLayout_3.addWidget(self.previous_track_2)
self.current_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.current_track_2.sizePolicy().hasHeightForWidth())
self.current_track_2.setSizePolicy(sizePolicy)
self.current_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
self.current_track_2.setFont(font)
self.current_track_2.setStyleSheet("background-color: #d4edda;\n"
"border: 1px solid rgb(85, 87, 83);")
self.current_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.current_track_2.setObjectName("current_track_2")
self.verticalLayout_3.addWidget(self.current_track_2)
self.next_track_2 = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.next_track_2.sizePolicy().hasHeightForWidth())
self.next_track_2.setSizePolicy(sizePolicy)
self.next_track_2.setMaximumSize(QtCore.QSize(230, 16777215))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
self.next_track_2.setFont(font)
self.next_track_2.setStyleSheet("background-color: #fff3cd;\n"
"border: 1px solid rgb(85, 87, 83);")
self.next_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
self.next_track_2.setObjectName("next_track_2")
self.verticalLayout_3.addWidget(self.next_track_2)
self.horizontalLayout_3.addLayout(self.verticalLayout_3)
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.hdrPreviousTrack = QtWidgets.QLabel(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hdrPreviousTrack.sizePolicy().hasHeightForWidth())
self.hdrPreviousTrack.setSizePolicy(sizePolicy)
self.hdrPreviousTrack.setMinimumSize(QtCore.QSize(0, 0))
self.hdrPreviousTrack.setMaximumSize(QtCore.QSize(16777215, 16777215))
font = QtGui.QFont()
font.setFamily("Sans")
font.setPointSize(20)
self.hdrPreviousTrack.setFont(font)
self.hdrPreviousTrack.setStyleSheet("background-color: #f8d7da;\n"
"border: 1px solid rgb(85, 87, 83);")
self.hdrPreviousTrack.setText("")
self.hdrPreviousTrack.setWordWrap(False)
self.hdrPreviousTrack.setObjectName("hdrPreviousTrack")
self.verticalLayout.addWidget(self.hdrPreviousTrack)
self.hdrCurrentTrack = QtWidgets.QPushButton(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hdrCurrentTrack.sizePolicy().hasHeightForWidth())
self.hdrCurrentTrack.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(20)
self.hdrCurrentTrack.setFont(font)
self.hdrCurrentTrack.setStyleSheet("background-color: #d4edda;\n"
"border: 1px solid rgb(85, 87, 83);\n"
"text-align: left;\n"
"padding-left: 8px;\n"
"")
self.hdrCurrentTrack.setText("")
self.hdrCurrentTrack.setFlat(True)
self.hdrCurrentTrack.setObjectName("hdrCurrentTrack")
self.verticalLayout.addWidget(self.hdrCurrentTrack)
self.hdrNextTrack = QtWidgets.QPushButton(parent=self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.hdrNextTrack.sizePolicy().hasHeightForWidth())
self.hdrNextTrack.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setPointSize(20)
self.hdrNextTrack.setFont(font)
self.hdrNextTrack.setStyleSheet("background-color: #fff3cd;\n"
"border: 1px solid rgb(85, 87, 83);\n"
"text-align: left;\n"
"padding-left: 8px;")
self.hdrNextTrack.setText("")
self.hdrNextTrack.setFlat(True)
self.hdrNextTrack.setObjectName("hdrNextTrack")
self.verticalLayout.addWidget(self.hdrNextTrack)
self.horizontalLayout_3.addLayout(self.verticalLayout)
self.frame_2 = QtWidgets.QFrame(parent=self.centralwidget)
self.frame_2.setMinimumSize(QtCore.QSize(0, 131))
self.frame_2.setMaximumSize(QtCore.QSize(230, 131))
self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_2.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_2.setObjectName("frame_2")
self.verticalLayout_10 = QtWidgets.QVBoxLayout(self.frame_2)
self.verticalLayout_10.setObjectName("verticalLayout_10")
self.lblTOD = QtWidgets.QLabel(parent=self.frame_2)
self.lblTOD.setMinimumSize(QtCore.QSize(208, 0))
font = QtGui.QFont()
font.setPointSize(35)
self.lblTOD.setFont(font)
self.lblTOD.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.lblTOD.setObjectName("lblTOD")
self.verticalLayout_10.addWidget(self.lblTOD)
self.label_elapsed_timer = QtWidgets.QLabel(parent=self.frame_2)
font = QtGui.QFont()
font.setFamily("FreeSans")
font.setPointSize(18)
font.setBold(False)
font.setWeight(50)
self.label_elapsed_timer.setFont(font)
self.label_elapsed_timer.setStyleSheet("color: black;")
self.label_elapsed_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_elapsed_timer.setObjectName("label_elapsed_timer")
self.verticalLayout_10.addWidget(self.label_elapsed_timer)
self.horizontalLayout_3.addWidget(self.frame_2)
self.gridLayout_4.addLayout(self.horizontalLayout_3, 0, 0, 1, 1)
self.frame_4 = QtWidgets.QFrame(parent=self.centralwidget)
self.frame_4.setMinimumSize(QtCore.QSize(0, 16))
self.frame_4.setAutoFillBackground(False)
self.frame_4.setStyleSheet("background-color: rgb(154, 153, 150)")
self.frame_4.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_4.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_4.setObjectName("frame_4")
self.gridLayout_4.addWidget(self.frame_4, 1, 0, 1, 1)
self.cartsWidget = QtWidgets.QWidget(parent=self.centralwidget)
self.cartsWidget.setObjectName("cartsWidget")
self.horizontalLayout_Carts = QtWidgets.QHBoxLayout(self.cartsWidget)
self.horizontalLayout_Carts.setObjectName("horizontalLayout_Carts")
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_Carts.addItem(spacerItem)
self.gridLayout_4.addWidget(self.cartsWidget, 2, 0, 1, 1)
self.frame_6 = QtWidgets.QFrame(parent=self.centralwidget)
self.frame_6.setMinimumSize(QtCore.QSize(0, 16))
self.frame_6.setAutoFillBackground(False)
self.frame_6.setStyleSheet("background-color: rgb(154, 153, 150)")
self.frame_6.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_6.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_6.setObjectName("frame_6")
self.gridLayout_4.addWidget(self.frame_6, 3, 0, 1, 1)
self.splitter = QtWidgets.QSplitter(parent=self.centralwidget)
self.splitter.setOrientation(QtCore.Qt.Orientation.Vertical)
self.splitter.setObjectName("splitter")
self.tabPlaylist = QtWidgets.QTabWidget(parent=self.splitter)
self.tabPlaylist.setDocumentMode(False)
self.tabPlaylist.setTabsClosable(True)
self.tabPlaylist.setMovable(True)
self.tabPlaylist.setObjectName("tabPlaylist")
self.tabInfolist = InfoTabs(parent=self.splitter)
self.tabInfolist.setDocumentMode(False)
self.tabInfolist.setTabsClosable(True)
self.tabInfolist.setMovable(True)
self.tabInfolist.setTabBarAutoHide(False)
self.tabInfolist.setObjectName("tabInfolist")
self.gridLayout_4.addWidget(self.splitter, 4, 0, 1, 1)
self.InfoFooterFrame = QtWidgets.QFrame(parent=self.centralwidget)
self.InfoFooterFrame.setMaximumSize(QtCore.QSize(16777215, 16777215))
self.InfoFooterFrame.setStyleSheet("background-color: rgb(192, 191, 188)")
self.InfoFooterFrame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.InfoFooterFrame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.InfoFooterFrame.setObjectName("InfoFooterFrame")
self.horizontalLayout = QtWidgets.QHBoxLayout(self.InfoFooterFrame)
self.horizontalLayout.setObjectName("horizontalLayout")
self.FadeStopInfoFrame = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.FadeStopInfoFrame.setMinimumSize(QtCore.QSize(152, 112))
self.FadeStopInfoFrame.setMaximumSize(QtCore.QSize(184, 16777215))
self.FadeStopInfoFrame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.FadeStopInfoFrame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.FadeStopInfoFrame.setObjectName("FadeStopInfoFrame")
self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.FadeStopInfoFrame)
self.verticalLayout_4.setObjectName("verticalLayout_4")
self.btnPreview = QtWidgets.QPushButton(parent=self.FadeStopInfoFrame)
self.btnPreview.setMinimumSize(QtCore.QSize(132, 41))
icon1 = QtGui.QIcon()
icon1.addPixmap(QtGui.QPixmap(":/icons/headphones"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnPreview.setIcon(icon1)
self.btnPreview.setIconSize(QtCore.QSize(30, 30))
self.btnPreview.setCheckable(True)
self.btnPreview.setObjectName("btnPreview")
self.verticalLayout_4.addWidget(self.btnPreview)
self.groupBoxIntroControls = QtWidgets.QGroupBox(parent=self.FadeStopInfoFrame)
self.groupBoxIntroControls.setMinimumSize(QtCore.QSize(132, 46))
self.groupBoxIntroControls.setMaximumSize(QtCore.QSize(132, 46))
self.groupBoxIntroControls.setTitle("")
self.groupBoxIntroControls.setObjectName("groupBoxIntroControls")
self.btnPreviewStart = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewStart.setGeometry(QtCore.QRect(0, 0, 44, 23))
self.btnPreviewStart.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewStart.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewStart.setObjectName("btnPreviewStart")
self.btnPreviewArm = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewArm.setGeometry(QtCore.QRect(44, 0, 44, 23))
self.btnPreviewArm.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewArm.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewArm.setText("")
icon2 = QtGui.QIcon()
icon2.addPixmap(QtGui.QPixmap(":/icons/record-button.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon2.addPixmap(QtGui.QPixmap(":/icons/record-red-button.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
self.btnPreviewArm.setIcon(icon2)
self.btnPreviewArm.setCheckable(True)
self.btnPreviewArm.setObjectName("btnPreviewArm")
self.btnPreviewEnd = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewEnd.setGeometry(QtCore.QRect(88, 0, 44, 23))
self.btnPreviewEnd.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewEnd.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewEnd.setObjectName("btnPreviewEnd")
self.btnPreviewBack = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewBack.setGeometry(QtCore.QRect(0, 23, 44, 23))
self.btnPreviewBack.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewBack.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewBack.setObjectName("btnPreviewBack")
self.btnPreviewMark = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewMark.setEnabled(False)
self.btnPreviewMark.setGeometry(QtCore.QRect(44, 23, 44, 23))
self.btnPreviewMark.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewMark.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewMark.setText("")
icon3 = QtGui.QIcon()
icon3.addPixmap(QtGui.QPixmap(":/icons/star.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
icon3.addPixmap(QtGui.QPixmap(":/icons/star_empty.png"), QtGui.QIcon.Mode.Disabled, QtGui.QIcon.State.Off)
self.btnPreviewMark.setIcon(icon3)
self.btnPreviewMark.setObjectName("btnPreviewMark")
self.btnPreviewFwd = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewFwd.setGeometry(QtCore.QRect(88, 23, 44, 23))
self.btnPreviewFwd.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewFwd.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewFwd.setObjectName("btnPreviewFwd")
self.verticalLayout_4.addWidget(self.groupBoxIntroControls)
self.horizontalLayout.addWidget(self.FadeStopInfoFrame)
self.frame_intro = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_intro.setMinimumSize(QtCore.QSize(152, 112))
self.frame_intro.setStyleSheet("")
self.frame_intro.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_intro.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_intro.setObjectName("frame_intro")
self.verticalLayout_9 = QtWidgets.QVBoxLayout(self.frame_intro)
self.verticalLayout_9.setObjectName("verticalLayout_9")
self.label_7 = QtWidgets.QLabel(parent=self.frame_intro)
self.label_7.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_7.setObjectName("label_7")
self.verticalLayout_9.addWidget(self.label_7)
self.label_intro_timer = QtWidgets.QLabel(parent=self.frame_intro)
font = QtGui.QFont()
font.setFamily("FreeSans")
font.setPointSize(40)
font.setBold(False)
font.setWeight(50)
self.label_intro_timer.setFont(font)
self.label_intro_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_intro_timer.setObjectName("label_intro_timer")
self.verticalLayout_9.addWidget(self.label_intro_timer)
self.horizontalLayout.addWidget(self.frame_intro)
self.frame_toggleplayed_3db = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_toggleplayed_3db.setMinimumSize(QtCore.QSize(152, 112))
self.frame_toggleplayed_3db.setMaximumSize(QtCore.QSize(184, 16777215))
self.frame_toggleplayed_3db.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_toggleplayed_3db.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_toggleplayed_3db.setObjectName("frame_toggleplayed_3db")
self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.frame_toggleplayed_3db)
self.verticalLayout_6.setObjectName("verticalLayout_6")
self.btnDrop3db = QtWidgets.QPushButton(parent=self.frame_toggleplayed_3db)
self.btnDrop3db.setMinimumSize(QtCore.QSize(132, 41))
self.btnDrop3db.setMaximumSize(QtCore.QSize(164, 16777215))
self.btnDrop3db.setCheckable(True)
self.btnDrop3db.setObjectName("btnDrop3db")
self.verticalLayout_6.addWidget(self.btnDrop3db)
self.btnHidePlayed = QtWidgets.QPushButton(parent=self.frame_toggleplayed_3db)
self.btnHidePlayed.setMinimumSize(QtCore.QSize(132, 41))
self.btnHidePlayed.setMaximumSize(QtCore.QSize(164, 16777215))
self.btnHidePlayed.setCheckable(True)
self.btnHidePlayed.setObjectName("btnHidePlayed")
self.verticalLayout_6.addWidget(self.btnHidePlayed)
self.horizontalLayout.addWidget(self.frame_toggleplayed_3db)
self.frame_fade = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_fade.setMinimumSize(QtCore.QSize(152, 112))
self.frame_fade.setStyleSheet("")
self.frame_fade.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_fade.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_fade.setObjectName("frame_fade")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame_fade)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.label_4 = QtWidgets.QLabel(parent=self.frame_fade)
self.label_4.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_4.setObjectName("label_4")
self.verticalLayout_2.addWidget(self.label_4)
self.label_fade_timer = QtWidgets.QLabel(parent=self.frame_fade)
font = QtGui.QFont()
font.setFamily("FreeSans")
font.setPointSize(40)
font.setBold(False)
font.setWeight(50)
self.label_fade_timer.setFont(font)
self.label_fade_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_fade_timer.setObjectName("label_fade_timer")
self.verticalLayout_2.addWidget(self.label_fade_timer)
self.horizontalLayout.addWidget(self.frame_fade)
self.frame_silent = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_silent.setMinimumSize(QtCore.QSize(152, 112))
self.frame_silent.setStyleSheet("")
self.frame_silent.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_silent.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame_silent.setObjectName("frame_silent")
self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.frame_silent)
self.verticalLayout_7.setObjectName("verticalLayout_7")
self.label_5 = QtWidgets.QLabel(parent=self.frame_silent)
self.label_5.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_5.setObjectName("label_5")
self.verticalLayout_7.addWidget(self.label_5)
self.label_silent_timer = QtWidgets.QLabel(parent=self.frame_silent)
font = QtGui.QFont()
font.setFamily("FreeSans")
font.setPointSize(40)
font.setBold(False)
font.setWeight(50)
self.label_silent_timer.setFont(font)
self.label_silent_timer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.label_silent_timer.setObjectName("label_silent_timer")
self.verticalLayout_7.addWidget(self.label_silent_timer)
self.horizontalLayout.addWidget(self.frame_silent)
self.widgetFadeVolume = PlotWidget(parent=self.InfoFooterFrame)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(1)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.widgetFadeVolume.sizePolicy().hasHeightForWidth())
self.widgetFadeVolume.setSizePolicy(sizePolicy)
self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0))
self.widgetFadeVolume.setObjectName("widgetFadeVolume")
self.horizontalLayout.addWidget(self.widgetFadeVolume)
self.frame = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame.setMinimumSize(QtCore.QSize(151, 0))
self.frame.setMaximumSize(QtCore.QSize(151, 112))
self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
self.frame.setObjectName("frame")
self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.frame)
self.verticalLayout_5.setObjectName("verticalLayout_5")
self.btnFade = QtWidgets.QPushButton(parent=self.frame)
self.btnFade.setMinimumSize(QtCore.QSize(132, 32))
self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215))
icon4 = QtGui.QIcon()
icon4.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnFade.setIcon(icon4)
self.btnFade.setIconSize(QtCore.QSize(30, 30))
self.btnFade.setObjectName("btnFade")
self.verticalLayout_5.addWidget(self.btnFade)
self.btnStop = QtWidgets.QPushButton(parent=self.frame)
self.btnStop.setMinimumSize(QtCore.QSize(0, 36))
icon5 = QtGui.QIcon()
icon5.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnStop.setIcon(icon5)
self.btnStop.setObjectName("btnStop")
self.verticalLayout_5.addWidget(self.btnStop)
self.horizontalLayout.addWidget(self.frame)
self.gridLayout_4.addWidget(self.InfoFooterFrame, 5, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(parent=MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1280, 29))
self.menubar.setObjectName("menubar")
self.menuFile = QtWidgets.QMenu(parent=self.menubar)
self.menuFile.setObjectName("menuFile")
self.menuPlaylist = QtWidgets.QMenu(parent=self.menubar)
self.menuPlaylist.setObjectName("menuPlaylist")
self.menuSearc_h = QtWidgets.QMenu(parent=self.menubar)
self.menuSearc_h.setObjectName("menuSearc_h")
self.menuHelp = QtWidgets.QMenu(parent=self.menubar)
self.menuHelp.setObjectName("menuHelp")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
self.statusbar.setEnabled(True)
self.statusbar.setStyleSheet("background-color: rgb(211, 215, 207);")
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.actionPlay_next = QtGui.QAction(parent=MainWindow)
icon6 = QtGui.QIcon()
icon6.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-play.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionPlay_next.setIcon(icon6)
self.actionPlay_next.setObjectName("actionPlay_next")
self.actionSkipToNext = QtGui.QAction(parent=MainWindow)
icon7 = QtGui.QIcon()
icon7.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionSkipToNext.setIcon(icon7)
self.actionSkipToNext.setObjectName("actionSkipToNext")
self.actionInsertTrack = QtGui.QAction(parent=MainWindow)
icon8 = QtGui.QIcon()
icon8.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionInsertTrack.setIcon(icon8)
self.actionInsertTrack.setObjectName("actionInsertTrack")
self.actionAdd_file = QtGui.QAction(parent=MainWindow)
icon9 = QtGui.QIcon()
icon9.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionAdd_file.setIcon(icon9)
self.actionAdd_file.setObjectName("actionAdd_file")
self.actionFade = QtGui.QAction(parent=MainWindow)
icon10 = QtGui.QIcon()
icon10.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-fade.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionFade.setIcon(icon10)
self.actionFade.setObjectName("actionFade")
self.actionStop = QtGui.QAction(parent=MainWindow)
icon11 = QtGui.QIcon()
icon11.addPixmap(QtGui.QPixmap(":/icons/stop"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionStop.setIcon(icon11)
self.actionStop.setObjectName("actionStop")
self.action_Clear_selection = QtGui.QAction(parent=MainWindow)
self.action_Clear_selection.setObjectName("action_Clear_selection")
self.action_Resume_previous = QtGui.QAction(parent=MainWindow)
icon12 = QtGui.QIcon()
icon12.addPixmap(QtGui.QPixmap(":/icons/previous"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.action_Resume_previous.setIcon(icon12)
self.action_Resume_previous.setObjectName("action_Resume_previous")
self.actionE_xit = QtGui.QAction(parent=MainWindow)
self.actionE_xit.setObjectName("actionE_xit")
self.actionTest = QtGui.QAction(parent=MainWindow)
self.actionTest.setObjectName("actionTest")
self.actionOpenPlaylist = QtGui.QAction(parent=MainWindow)
self.actionOpenPlaylist.setObjectName("actionOpenPlaylist")
self.actionNewPlaylist = QtGui.QAction(parent=MainWindow)
self.actionNewPlaylist.setObjectName("actionNewPlaylist")
self.actionTestFunction = QtGui.QAction(parent=MainWindow)
self.actionTestFunction.setObjectName("actionTestFunction")
self.actionSkipToFade = QtGui.QAction(parent=MainWindow)
self.actionSkipToFade.setObjectName("actionSkipToFade")
self.actionSkipToEnd = QtGui.QAction(parent=MainWindow)
self.actionSkipToEnd.setObjectName("actionSkipToEnd")
self.actionClosePlaylist = QtGui.QAction(parent=MainWindow)
self.actionClosePlaylist.setEnabled(True)
self.actionClosePlaylist.setObjectName("actionClosePlaylist")
self.actionRenamePlaylist = QtGui.QAction(parent=MainWindow)
self.actionRenamePlaylist.setEnabled(True)
self.actionRenamePlaylist.setObjectName("actionRenamePlaylist")
self.actionDeletePlaylist = QtGui.QAction(parent=MainWindow)
self.actionDeletePlaylist.setEnabled(True)
self.actionDeletePlaylist.setObjectName("actionDeletePlaylist")
self.actionMoveSelected = QtGui.QAction(parent=MainWindow)
self.actionMoveSelected.setObjectName("actionMoveSelected")
self.actionExport_playlist = QtGui.QAction(parent=MainWindow)
self.actionExport_playlist.setObjectName("actionExport_playlist")
self.actionSetNext = QtGui.QAction(parent=MainWindow)
self.actionSetNext.setObjectName("actionSetNext")
self.actionSelect_next_track = QtGui.QAction(parent=MainWindow)
self.actionSelect_next_track.setObjectName("actionSelect_next_track")
self.actionSelect_previous_track = QtGui.QAction(parent=MainWindow)
self.actionSelect_previous_track.setObjectName("actionSelect_previous_track")
self.actionSelect_played_tracks = QtGui.QAction(parent=MainWindow)
self.actionSelect_played_tracks.setObjectName("actionSelect_played_tracks")
self.actionMoveUnplayed = QtGui.QAction(parent=MainWindow)
self.actionMoveUnplayed.setObjectName("actionMoveUnplayed")
self.actionAdd_note = QtGui.QAction(parent=MainWindow)
self.actionAdd_note.setObjectName("actionAdd_note")
self.actionEnable_controls = QtGui.QAction(parent=MainWindow)
self.actionEnable_controls.setObjectName("actionEnable_controls")
self.actionImport = QtGui.QAction(parent=MainWindow)
self.actionImport.setObjectName("actionImport")
self.actionDownload_CSV_of_played_tracks = QtGui.QAction(parent=MainWindow)
self.actionDownload_CSV_of_played_tracks.setObjectName("actionDownload_CSV_of_played_tracks")
self.actionSearch = QtGui.QAction(parent=MainWindow)
self.actionSearch.setObjectName("actionSearch")
self.actionInsertSectionHeader = QtGui.QAction(parent=MainWindow)
self.actionInsertSectionHeader.setObjectName("actionInsertSectionHeader")
self.actionRemove = QtGui.QAction(parent=MainWindow)
self.actionRemove.setObjectName("actionRemove")
self.actionFind_next = QtGui.QAction(parent=MainWindow)
self.actionFind_next.setObjectName("actionFind_next")
self.actionFind_previous = QtGui.QAction(parent=MainWindow)
self.actionFind_previous.setObjectName("actionFind_previous")
self.action_About = QtGui.QAction(parent=MainWindow)
self.action_About.setObjectName("action_About")
self.actionSave_as_template = QtGui.QAction(parent=MainWindow)
self.actionSave_as_template.setObjectName("actionSave_as_template")
self.actionManage_templates = QtGui.QAction(parent=MainWindow)
self.actionManage_templates.setObjectName("actionManage_templates")
self.actionDebug = QtGui.QAction(parent=MainWindow)
self.actionDebug.setObjectName("actionDebug")
self.actionAdd_cart = QtGui.QAction(parent=MainWindow)
self.actionAdd_cart.setObjectName("actionAdd_cart")
self.actionMark_for_moving = QtGui.QAction(parent=MainWindow)
self.actionMark_for_moving.setObjectName("actionMark_for_moving")
self.actionPaste = QtGui.QAction(parent=MainWindow)
self.actionPaste.setObjectName("actionPaste")
self.actionResume = QtGui.QAction(parent=MainWindow)
self.actionResume.setObjectName("actionResume")
self.actionSearch_title_in_Wikipedia = QtGui.QAction(parent=MainWindow)
self.actionSearch_title_in_Wikipedia.setObjectName("actionSearch_title_in_Wikipedia")
self.actionSearch_title_in_Songfacts = QtGui.QAction(parent=MainWindow)
self.actionSearch_title_in_Songfacts.setObjectName("actionSearch_title_in_Songfacts")
self.actionSelect_duplicate_rows = QtGui.QAction(parent=MainWindow)
self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows")
self.actionReplace_files = QtGui.QAction(parent=MainWindow)
self.actionReplace_files.setObjectName("actionReplace_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.actionReplace_files)
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionE_xit)
self.menuSearc_h.addAction(self.actionSetNext)
self.menuSearc_h.addAction(self.actionPlay_next)
self.menuSearc_h.addAction(self.actionFade)
self.menuSearc_h.addAction(self.actionStop)
self.menuSearc_h.addAction(self.actionResume)
self.menuSearc_h.addAction(self.actionSkipToNext)
self.menuSearc_h.addSeparator()
self.menuSearc_h.addAction(self.actionSearch)
self.menuSearc_h.addAction(self.actionSearch_title_in_Wikipedia)
self.menuSearc_h.addAction(self.actionSearch_title_in_Songfacts)
self.menuHelp.addAction(self.action_About)
self.menuHelp.addAction(self.actionDebug)
self.menubar.addAction(self.menuPlaylist.menuAction())
self.menubar.addAction(self.menuFile.menuAction())
self.menubar.addAction(self.menuSearc_h.menuAction())
self.menubar.addAction(self.menuHelp.menuAction())
self.retranslateUi(MainWindow)
self.tabPlaylist.setCurrentIndex(-1)
self.tabInfolist.setCurrentIndex(-1)
self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "Music Muster"))
self.previous_track_2.setText(_translate("MainWindow", "Last track:"))
self.current_track_2.setText(_translate("MainWindow", "Current track:"))
self.next_track_2.setText(_translate("MainWindow", "Next track:"))
self.lblTOD.setText(_translate("MainWindow", "00:00:00"))
self.label_elapsed_timer.setText(_translate("MainWindow", "00:00 / 00:00"))
self.btnPreview.setText(_translate("MainWindow", " Preview"))
self.btnPreviewStart.setText(_translate("MainWindow", "<<"))
self.btnPreviewEnd.setText(_translate("MainWindow", ">>"))
self.btnPreviewBack.setText(_translate("MainWindow", "<"))
self.btnPreviewFwd.setText(_translate("MainWindow", ">"))
self.label_7.setText(_translate("MainWindow", "Intro"))
self.label_intro_timer.setText(_translate("MainWindow", "0:0"))
self.btnDrop3db.setText(_translate("MainWindow", "-3dB to talk"))
self.btnHidePlayed.setText(_translate("MainWindow", "Hide played"))
self.label_4.setText(_translate("MainWindow", "Fade"))
self.label_fade_timer.setText(_translate("MainWindow", "00:00"))
self.label_5.setText(_translate("MainWindow", "Silent"))
self.label_silent_timer.setText(_translate("MainWindow", "00:00"))
self.btnFade.setText(_translate("MainWindow", " Fade"))
self.btnStop.setText(_translate("MainWindow", " Stop"))
self.menuFile.setTitle(_translate("MainWindow", "&Playlist"))
self.menuPlaylist.setTitle(_translate("MainWindow", "&File"))
self.menuSearc_h.setTitle(_translate("MainWindow", "&Music"))
self.menuHelp.setTitle(_translate("MainWindow", "Help"))
self.actionPlay_next.setText(_translate("MainWindow", "&Play next"))
self.actionPlay_next.setShortcut(_translate("MainWindow", "Return"))
self.actionSkipToNext.setText(_translate("MainWindow", "Skip to &next"))
self.actionSkipToNext.setShortcut(_translate("MainWindow", "Ctrl+Alt+Return"))
self.actionInsertTrack.setText(_translate("MainWindow", "Insert &track..."))
self.actionInsertTrack.setShortcut(_translate("MainWindow", "Ctrl+T"))
self.actionAdd_file.setText(_translate("MainWindow", "Add &file"))
self.actionAdd_file.setShortcut(_translate("MainWindow", "Ctrl+F"))
self.actionFade.setText(_translate("MainWindow", "F&ade"))
self.actionFade.setShortcut(_translate("MainWindow", "Ctrl+Z"))
self.actionStop.setText(_translate("MainWindow", "S&top"))
self.actionStop.setShortcut(_translate("MainWindow", "Ctrl+Alt+S"))
self.action_Clear_selection.setText(_translate("MainWindow", "Clear &selection"))
self.action_Clear_selection.setShortcut(_translate("MainWindow", "Esc"))
self.action_Resume_previous.setText(_translate("MainWindow", "&Resume previous"))
self.actionE_xit.setText(_translate("MainWindow", "E&xit"))
self.actionTest.setText(_translate("MainWindow", "&Test"))
self.actionOpenPlaylist.setText(_translate("MainWindow", "O&pen..."))
self.actionNewPlaylist.setText(_translate("MainWindow", "&New..."))
self.actionTestFunction.setText(_translate("MainWindow", "&Test function"))
self.actionSkipToFade.setText(_translate("MainWindow", "&Skip to start of fade"))
self.actionSkipToEnd.setText(_translate("MainWindow", "Skip to &end of track"))
self.actionClosePlaylist.setText(_translate("MainWindow", "&Close"))
self.actionRenamePlaylist.setText(_translate("MainWindow", "&Rename..."))
self.actionDeletePlaylist.setText(_translate("MainWindow", "Dele&te..."))
self.actionMoveSelected.setText(_translate("MainWindow", "Mo&ve selected tracks to..."))
self.actionExport_playlist.setText(_translate("MainWindow", "E&xport..."))
self.actionSetNext.setText(_translate("MainWindow", "Set &next"))
self.actionSetNext.setShortcut(_translate("MainWindow", "Ctrl+N"))
self.actionSelect_next_track.setText(_translate("MainWindow", "Select next track"))
self.actionSelect_next_track.setShortcut(_translate("MainWindow", "J"))
self.actionSelect_previous_track.setText(_translate("MainWindow", "Select previous track"))
self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K"))
self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks"))
self.actionMoveUnplayed.setText(_translate("MainWindow", "Move &unplayed tracks to..."))
self.actionAdd_note.setText(_translate("MainWindow", "Add note..."))
self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T"))
self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls"))
self.actionImport.setText(_translate("MainWindow", "Import track..."))
self.actionImport.setShortcut(_translate("MainWindow", "Ctrl+Shift+I"))
self.actionDownload_CSV_of_played_tracks.setText(_translate("MainWindow", "Download CSV of played tracks..."))
self.actionSearch.setText(_translate("MainWindow", "Search..."))
self.actionSearch.setShortcut(_translate("MainWindow", "/"))
self.actionInsertSectionHeader.setText(_translate("MainWindow", "Insert &section header..."))
self.actionInsertSectionHeader.setShortcut(_translate("MainWindow", "Ctrl+H"))
self.actionRemove.setText(_translate("MainWindow", "&Remove track"))
self.actionFind_next.setText(_translate("MainWindow", "Find next"))
self.actionFind_next.setShortcut(_translate("MainWindow", "N"))
self.actionFind_previous.setText(_translate("MainWindow", "Find previous"))
self.actionFind_previous.setShortcut(_translate("MainWindow", "P"))
self.action_About.setText(_translate("MainWindow", "&About"))
self.actionSave_as_template.setText(_translate("MainWindow", "Save as template..."))
self.actionManage_templates.setText(_translate("MainWindow", "Manage templates..."))
self.actionDebug.setText(_translate("MainWindow", "Debug"))
self.actionAdd_cart.setText(_translate("MainWindow", "Edit cart &1..."))
self.actionMark_for_moving.setText(_translate("MainWindow", "Mark for moving"))
self.actionMark_for_moving.setShortcut(_translate("MainWindow", "Ctrl+C"))
self.actionPaste.setText(_translate("MainWindow", "Paste"))
self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V"))
self.actionResume.setText(_translate("MainWindow", "Resume"))
self.actionResume.setShortcut(_translate("MainWindow", "Ctrl+R"))
self.actionSearch_title_in_Wikipedia.setText(_translate("MainWindow", "Search title in Wikipedia"))
self.actionSearch_title_in_Wikipedia.setShortcut(_translate("MainWindow", "Ctrl+W"))
self.actionSearch_title_in_Songfacts.setText(_translate("MainWindow", "Search title in Songfacts"))
self.actionSearch_title_in_Songfacts.setShortcut(_translate("MainWindow", "Ctrl+S"))
self.actionSelect_duplicate_rows.setText(_translate("MainWindow", "Select duplicate rows..."))
self.actionReplace_files.setText(_translate("MainWindow", "Import files..."))
from infotabs import InfoTabs # type: ignore
from pyqtgraph import PlotWidget # type: ignore

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

73
audacity_tester.py Executable file
View File

@ -0,0 +1,73 @@
#!/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
devnotes.txt Normal file
View File

@ -0,0 +1 @@
Run Flake8 and Black

View File

@ -1,58 +0,0 @@
"""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 ###

View File

@ -1,47 +0,0 @@
"""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 ###

View File

@ -1,46 +0,0 @@
"""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 Executable file
View File

@ -0,0 +1,6 @@
#!/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

1976
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,63 +1,53 @@
[project] [tool.poetry]
name = "musicmuster" name = "musicmuster"
version = "4.1.10" version = "1.7.5"
description = "Music player for internet radio" description = "Music player for internet radio"
authors = [{ name = "Keith Edmunds", email = "kae@midnighthax.com" }] authors = ["Keith Edmunds <kae@midnighthax.com>"]
requires-python = ">=3.13,<4"
readme = "README.md"
dependencies = [
"alchemical>=1.0.2",
"alembic>=1.14.0",
"colorlog>=6.9.0",
"fuzzywuzzy>=0.18.0",
"mutagen>=1.47.0",
"mysqlclient>=2.2.5",
"obs-websocket-py>=1.0",
"psutil>=6.1.0",
"pydub>=0.25.1",
"pydymenu>=0.5.2",
"pyfzf>=0.3.1",
"pygame>=2.6.1",
"pyqt6>=6.7.1",
"pyqt6-webengine>=6.7.0",
"pyqtgraph>=0.13.3",
"python-levenshtein>=0.26.1",
"python-slugify>=8.0.4",
"python-vlc>=3.0.21203",
"SQLAlchemy>=2.0.36",
"stackprinter>=0.2.10",
"tinytag>=1.10.1",
"types-psutil>=6.0.0.20240621",
"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] [tool.poetry.dependencies]
dev = [ python = "^3.11"
"flakehell>=0.9.0,<0.10", tinytag = "^1.10.1"
"ipdb>=0.13.9,<0.14", SQLAlchemy = "^2.0.36"
"line-profiler>=4.2.0,<5", python-vlc = "^3.0.21203"
"mypy>=1.15.0,<2", mysqlclient = "^2.2.5"
"pudb", mutagen = "^1.47.0"
"pydub-stubs>=0.25.1,<0.26", alembic = "^1.14.0"
"pytest>=8.3.4,<9", pydub = "^0.25.1"
"pytest-qt>=4.4.0,<5", python-slugify = "^8.0.4"
"black>=25.1.0,<26", pyfzf = "^0.3.1"
"pytest-cov>=6.0.0,<7", pydymenu = "^0.5.2"
] stackprinter = "^0.2.10"
pyqt6 = "^6.7.1"
pyqtgraph = "^0.13.3"
colorlog = "^6.9.0"
alchemical = "^1.0.2"
obs-websocket-py = "^1.0"
pygame = "^2.6.1"
psutil = "^6.1.0"
pyqt6-webengine = "^6.7.0"
fuzzywuzzy = "^0.18.0"
python-levenshtein = "^0.26.1"
[tool.uv] [tool.poetry.dev-dependencies]
package = false ipdb = "^0.13.9"
pytest-qt = "^4.4.0"
pydub-stubs = "^0.25.1"
line-profiler = "^4.1.3"
flakehell = "^0.9.0"
[tool.poetry.group.dev.dependencies]
pudb = "*"
flakehell = "^0.9.0"
mypy = "^1.7.0"
pytest-cov = "^5.0.0"
pytest = "^8.1.1"
black = "^24.3.0"
types-psutil = "^6.0.0.20240621"
pdbp = "^1.5.3"
[build-system] [build-system]
requires = ["hatchling"] requires = ["poetry-core>=1.0.0"]
build-backend = "hatchling.build" build-backend = "poetry.core.masonry.api"
[tool.mypy] [tool.mypy]
mypy_path = "/home/kae/git/musicmuster/app" mypy_path = "/home/kae/git/musicmuster/app"
@ -66,9 +56,6 @@ 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"]
@ -78,4 +65,3 @@ filterwarnings = ["ignore:'audioop' is deprecated", "ignore:pkg_resources"]
exclude = ["migrations", "app/ui", "archive"] exclude = ["migrations", "app/ui", "archive"]
paths = ["app"] paths = ["app"]
make_whitelist = true make_whitelist = true

View File

@ -1,489 +0,0 @@
"""
Tests are named 'test_nnn_xxxx' where 'nn n' is a number. This is used to ensure that
the tests run in order as we rely (in some cases) upon the results of an earlier test.
Yes, we shouldn't do that.
"""
# Standard library imports
import os
import shutil
import tempfile
import unittest
from unittest.mock import MagicMock, patch
# PyQt imports
from PyQt6.QtWidgets import QDialog, QFileDialog
# Third party imports
from mutagen.mp3 import MP3 # type: ignore
import pytest
from pytestqt.plugin import QtBot # type: ignore
# App imports
from app import musicmuster
from app.models import (
db,
Playlists,
Tracks,
)
from config import Config
from file_importer import FileImporter
# Custom fixture to adapt qtbot for use with unittest.TestCase
@pytest.fixture(scope="class")
def qtbot_adapter(qapp, request):
"""Adapt qtbot fixture for usefixtures and unittest.TestCase"""
request.cls.qtbot = QtBot(request)
# Fixture for tmp_path to be available in the class
@pytest.fixture(scope="class")
def class_tmp_path(request, tmp_path_factory):
"""Provide a class-wide tmp_path"""
request.cls.tmp_path = tmp_path_factory.mktemp("pytest_tmp")
@pytest.mark.usefixtures("qtbot_adapter", "class_tmp_path")
class MyTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Runs once before any test in this class"""
db.create_all()
cls.widget = musicmuster.Window()
# Create a playlist for all tests
playlist_name = "file importer playlist"
with db.Session() as session:
playlist = Playlists(session=session, name=playlist_name, template_id=0)
cls.widget._open_playlist(playlist)
# Create our musicstore
cls.import_source = tempfile.mkdtemp(suffix="_MMsource_pytest", dir="/tmp")
Config.REPLACE_FILES_DEFAULT_SOURCE = cls.import_source
cls.musicstore = tempfile.mkdtemp(suffix="_MMstore_pytest", dir="/tmp")
Config.IMPORT_DESTINATION = cls.musicstore
@classmethod
def tearDownClass(cls):
"""Runs once after all tests"""
db.drop_all()
shutil.rmtree(cls.musicstore)
shutil.rmtree(cls.import_source)
def setUp(self):
"""Runs before each test"""
with self.qtbot.waitExposed(self.widget):
self.widget.show()
def tearDown(self):
"""Runs after each test"""
self.widget.close() # Close UI to prevent side effects
def wait_for_workers(self, timeout: int = 10000):
"""
Let import threads workers run to completion
"""
def workers_empty():
assert FileImporter.workers == {}
self.qtbot.waitUntil(workers_empty, timeout=timeout)
def test_001_import_no_files(self):
"""Try importing with no files to import"""
with patch("file_importer.show_OK") as mock_show_ok:
self.widget.import_files_wrapper()
mock_show_ok.assert_called_once_with(
"File import",
f"No files in {Config.REPLACE_FILES_DEFAULT_SOURCE} to import",
None,
)
def test_002_import_file_and_cancel(self):
"""Cancel file import"""
test_track_path = "testdata/isa.mp3"
shutil.copy(test_track_path, self.import_source)
with (
patch("file_importer.PickMatch") as MockPickMatch,
patch("file_importer.show_OK") as mock_show_ok,
):
# Create a mock instance of PickMatch
mock_dialog_instance = MagicMock()
MockPickMatch.return_value = mock_dialog_instance
# Simulate the user clicking OK in the dialog
mock_dialog_instance.exec.return_value = QDialog.DialogCode.Rejected
mock_dialog_instance.selected_track_id = -1 # Simulated return value
self.widget.import_files_wrapper()
# Ensure PickMatch was instantiated correctly
MockPickMatch.assert_called_once_with(
new_track_description="I'm So Afraid (Fleetwood Mac)",
choices=[("Do not import", -1, ""), ("Import as new track", 0, "")],
default=1,
)
# Verify exec() was called
mock_dialog_instance.exec.assert_called_once()
# Ensure selected_track_id was accessed after dialog.exec()
assert mock_dialog_instance.selected_track_id < 0
mock_show_ok.assert_called_once_with(
"File not imported",
"isa.mp3 will not be imported because you asked not to import this file",
)
def test_003_import_first_file(self):
"""Import file into empty directory"""
test_track_path = "testdata/isa.mp3"
shutil.copy(test_track_path, self.import_source)
with patch("file_importer.PickMatch") as MockPickMatch:
# Create a mock instance of PickMatch
mock_dialog_instance = MagicMock()
MockPickMatch.return_value = mock_dialog_instance
# Simulate the user clicking OK in the dialog
mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted
mock_dialog_instance.selected_track_id = 0 # Simulated return value
self.widget.import_files_wrapper()
# Ensure PickMatch was instantiated correctly
MockPickMatch.assert_called_once_with(
new_track_description="I'm So Afraid (Fleetwood Mac)",
choices=[("Do not import", -1, ""), ("Import as new track", 0, "")],
default=1,
)
# Verify exec() was called
mock_dialog_instance.exec.assert_called_once()
# Ensure selected_track_id was accessed after dialog.exec()
assert mock_dialog_instance.selected_track_id == 0
self.wait_for_workers()
# Check track was imported
with db.Session() as session:
tracks = Tracks.get_all(session)
assert len(tracks) == 1
track = tracks[0]
assert track.title == "I'm So Afraid"
assert track.artist == "Fleetwood Mac"
track_file = os.path.join(
self.musicstore, os.path.basename(test_track_path)
)
assert track.path == track_file
assert os.path.exists(track_file)
assert os.listdir(self.import_source) == []
def test_004_import_second_file(self):
"""Import a second file"""
test_track_path = "testdata/lovecats.mp3"
shutil.copy(test_track_path, self.import_source)
with patch("file_importer.PickMatch") as MockPickMatch:
# Create a mock instance of PickMatch
mock_dialog_instance = MagicMock()
MockPickMatch.return_value = mock_dialog_instance
# Simulate the user clicking OK in the dialog
mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted
mock_dialog_instance.selected_track_id = 0 # Simulated return value
self.widget.import_files_wrapper()
# Ensure PickMatch was instantiated correctly
MockPickMatch.assert_called_once_with(
new_track_description="The Lovecats (The Cure)",
choices=[("Do not import", -1, ""), ("Import as new track", 0, "")],
default=1,
)
# Verify exec() was called
mock_dialog_instance.exec.assert_called_once()
# Ensure selected_track_id was accessed after dialog.exec()
assert mock_dialog_instance.selected_track_id == 0
self.wait_for_workers()
# Check track was imported
with db.Session() as session:
tracks = Tracks.get_all(session)
assert len(tracks) == 2
track = tracks[1]
assert track.title == "The Lovecats"
assert track.artist == "The Cure"
track_file = os.path.join(
self.musicstore, os.path.basename(test_track_path)
)
assert track.path == track_file
assert os.path.exists(track_file)
assert os.listdir(self.import_source) == []
def test_005_replace_file(self):
"""Import the same file again and update existing track"""
test_track_path = "testdata/lovecats.mp3"
shutil.copy(test_track_path, self.import_source)
with patch("file_importer.PickMatch") as MockPickMatch:
# Create a mock instance of PickMatch
mock_dialog_instance = MagicMock()
MockPickMatch.return_value = mock_dialog_instance
# Simulate the user clicking OK in the dialog
mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted
mock_dialog_instance.selected_track_id = 2 # Simulated return value
self.widget.import_files_wrapper()
# Ensure PickMatch was instantiated correctly
MockPickMatch.assert_called_once_with(
new_track_description="The Lovecats (The Cure)",
choices=[
("Do not import", -1, ""),
("Import as new track", 0, ""),
(
"The Lovecats (The Cure) (100%)",
2,
os.path.join(
self.musicstore, os.path.basename(test_track_path)
),
),
],
default=2,
)
# Verify exec() was called
mock_dialog_instance.exec.assert_called_once()
self.wait_for_workers()
# Check track was imported
with db.Session() as session:
tracks = Tracks.get_all(session)
assert len(tracks) == 2
track = tracks[1]
assert track.title == "The Lovecats"
assert track.artist == "The Cure"
assert track.id == 2
track_file = os.path.join(
self.musicstore, os.path.basename(test_track_path)
)
assert track.path == track_file
assert os.path.exists(track_file)
assert os.listdir(self.import_source) == []
def test_006_import_file_no_tags(self) -> None:
"""Try to import untagged file"""
test_track_path = "testdata/lovecats.mp3"
test_filename = os.path.basename(test_track_path)
shutil.copy(test_track_path, self.import_source)
import_file = os.path.join(self.import_source, test_filename)
assert os.path.exists(import_file)
# Remove tags
src = MP3(import_file)
src.delete()
src.save()
with patch("file_importer.show_OK") as mock_show_ok:
self.widget.import_files_wrapper()
mock_show_ok.assert_called_once_with(
"File not imported",
f"{test_filename} will not be imported because of tag errors "
f"(Missing tags in {import_file})",
)
def test_007_import_unreadable_file(self) -> None:
"""Import unreadable file"""
test_track_path = "testdata/lovecats.mp3"
test_filename = os.path.basename(test_track_path)
shutil.copy(test_track_path, self.import_source)
import_file = os.path.join(self.import_source, test_filename)
assert os.path.exists(import_file)
# Make undreadable
os.chmod(import_file, 0)
with patch("file_importer.show_OK") as mock_show_ok:
self.widget.import_files_wrapper()
mock_show_ok.assert_called_once_with(
"File not imported",
f"{test_filename} will not be imported because {import_file} is unreadable",
)
# clean up
os.chmod(import_file, 0o777)
os.unlink(import_file)
def test_008_import_new_file_existing_destination(self) -> None:
"""Import duplicate file"""
test_track_path = "testdata/lovecats.mp3"
test_filename = os.path.basename(test_track_path)
new_destination = os.path.join(self.musicstore, "lc2.mp3")
shutil.copy(test_track_path, self.import_source)
import_file = os.path.join(self.import_source, test_filename)
assert os.path.exists(import_file)
with (
patch("file_importer.PickMatch") as MockPickMatch,
patch.object(
QFileDialog, "getSaveFileName", return_value=(new_destination, "")
) as mock_file_dialog,
patch("file_importer.show_OK") as mock_show_ok,
):
mock_file_dialog.return_value = (
new_destination,
"",
) # Ensure mock correctly returns expected value
# Create a mock instance of PickMatch
mock_dialog_instance = MagicMock()
MockPickMatch.return_value = mock_dialog_instance
# Simulate the user clicking OK in the dialog
mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted
mock_dialog_instance.selected_track_id = 0 # Simulated return value
self.widget.import_files_wrapper()
# Ensure PickMatch was instantiated correctly
MockPickMatch.assert_called_once_with(
new_track_description="The Lovecats (The Cure)",
choices=[
("Do not import", -1, ""),
("Import as new track", 0, ""),
(
"The Lovecats (The Cure) (100%)",
2,
os.path.join(
self.musicstore, os.path.basename(test_track_path)
),
),
],
default=2,
)
# Verify exec() was called
mock_dialog_instance.exec.assert_called_once()
destination = os.path.join(self.musicstore, test_filename)
mock_show_ok.assert_called_once_with(
title="Desintation path exists",
msg=f"New import requested but default destination path ({destination}) "
"already exists. Click OK and choose where to save this track",
parent=None,
)
self.wait_for_workers()
# Ensure QFileDialog was called and returned expected value
assert mock_file_dialog.called # Ensure the mock was used
result = mock_file_dialog()
assert result[0] == new_destination # Validate return value
# Check track was imported
with db.Session() as session:
tracks = Tracks.get_all(session)
assert len(tracks) == 3
track = tracks[2]
assert track.title == "The Lovecats"
assert track.artist == "The Cure"
assert track.id == 3
assert track.path == new_destination
assert os.path.exists(new_destination)
assert os.listdir(self.import_source) == []
# Remove file so as not to interfere with later tests
session.delete(track)
tracks = Tracks.get_all(session)
assert len(tracks) == 2
session.commit()
os.unlink(new_destination)
assert not os.path.exists(new_destination)
def test_009_import_similar_file(self) -> None:
"""Import file with similar, but different, title"""
test_track_path = "testdata/lovecats.mp3"
test_filename = os.path.basename(test_track_path)
shutil.copy(test_track_path, self.import_source)
import_file = os.path.join(self.import_source, test_filename)
assert os.path.exists(import_file)
# Change title tag
src = MP3(import_file)
src["TIT2"].text[0] += " xyz"
src.save()
with patch("file_importer.PickMatch") as MockPickMatch:
# Create a mock instance of PickMatch
mock_dialog_instance = MagicMock()
MockPickMatch.return_value = mock_dialog_instance
# Simulate the user clicking OK in the dialog
mock_dialog_instance.exec.return_value = QDialog.DialogCode.Accepted
mock_dialog_instance.selected_track_id = 2 # Simulated return value
self.widget.import_files_wrapper()
# Ensure PickMatch was instantiated correctly
MockPickMatch.assert_called_once_with(
new_track_description="The Lovecats xyz (The Cure)",
choices=[
("Do not import", -1, ""),
("Import as new track", 0, ""),
(
"The Lovecats (The Cure) (93%)",
2,
os.path.join(
self.musicstore, os.path.basename(test_track_path)
),
),
],
default=2,
)
# Verify exec() was called
mock_dialog_instance.exec.assert_called_once()
self.wait_for_workers()
# Check track was imported
with db.Session() as session:
tracks = Tracks.get_all(session)
assert len(tracks) == 2
track = tracks[1]
assert track.title == "The Lovecats xyz"
assert track.artist == "The Cure"
assert track.id == 2
track_file = os.path.join(
self.musicstore, os.path.basename(test_track_path)
)
assert track.path == track_file
assert os.path.exists(track_file)
assert os.listdir(self.import_source) == []

View File

@ -21,9 +21,7 @@ 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"
@ -33,7 +31,6 @@ 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):
@ -73,7 +70,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 == "" assert result is None
def test_notecolours_get_colour_match(self): def test_notecolours_get_colour_match(self):
note_colour = "#4bcdef" note_colour = "#4bcdef"
@ -111,7 +108,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", template_id=0) playlist = Playlists(session, "my playlist")
assert playlist assert playlist
# test repr # test repr
_ = str(playlist) _ = str(playlist)
@ -122,18 +119,23 @@ 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
session.delete(playlist) playlist.delete(session)
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", template_id=0) playlist = Playlists(session, "my playlist")
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
@ -153,8 +155,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, template_id=0) playlist1 = Playlists(session, p1_name)
_ = Playlists(session, p2_name, template_id=0) _ = Playlists(session, p2_name)
all_playlists = Playlists.get_all(session) all_playlists = Playlists.get_all(session)
assert len(all_playlists) == 2 assert len(all_playlists) == 2
@ -203,7 +205,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_1(self): def test_get_colour(self):
"""Test for errors in execution""" """Test for errors in execution"""
GOOD_STRING = "cantelope" GOOD_STRING = "cantelope"
@ -216,42 +218,22 @@ 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 == "" assert colour is None
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 == "" assert colour is None
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,
@ -259,13 +241,12 @@ 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 == "" assert colour is None
def test_name_available(self): def test_name_available(self):
PLAYLIST_NAME = "a name" PLAYLIST_NAME = "a name"
@ -273,7 +254,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, template_id=0) playlist = Playlists(session, PLAYLIST_NAME)
assert playlist assert playlist
assert Playlists.name_is_available(session, PLAYLIST_NAME) is False assert Playlists.name_is_available(session, PLAYLIST_NAME) is False
@ -285,7 +266,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=session, name=PLAYLIST_NAME, template_id=0) playlist = Playlists(session, PLAYLIST_NAME)
plr = PlaylistRows(session, playlist.id, 1) plr = PlaylistRows(session, playlist.id, 1)
assert plr assert plr
@ -298,7 +279,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=session, name=PLAYLIST_NAME, template_id=0) playlist = Playlists(session, PLAYLIST_NAME)
plr = PlaylistRows(session, playlist.id, 1) plr = PlaylistRows(session, playlist.id, 1)
assert plr assert plr

View File

@ -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, template_id=0) self.playlist = Playlists(session, PLAYLIST_NAME)
self.model = playlistmodel.PlaylistModel(self.playlist.id, is_template=False) self.model = playlistmodel.PlaylistModel(self.playlist.id)
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 == "start [1 tracks, 4:23 unplayed]" assert qv_value.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, template_id=0) playlist = Playlists(session, self.PLAYLIST_NAME)
# Create a model # Create a model
model = playlistmodel.PlaylistModel(playlist.id, is_template=False) model = playlistmodel.PlaylistModel(playlist.id)
# 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, template_id=0) self.playlist = Playlists(session, self.PLAYLIST_NAME)
self.model = playlistmodel.PlaylistModel(self.playlist.id, is_template=False) self.model = playlistmodel.PlaylistModel(self.playlist.id)
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, template_id=0) playlist_dst = Playlists(session, destination_playlist)
model_dst = playlistmodel.PlaylistModel(playlist_dst.id, is_template=False) model_dst = playlistmodel.PlaylistModel(playlist_dst.id)
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, template_id=0) playlist_dst = Playlists(session, destination_playlist)
model_dst = playlistmodel.PlaylistModel(playlist_dst.id, is_template=False) model_dst = playlistmodel.PlaylistModel(playlist_dst.id)
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)) row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value())
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, template_id=0) playlist_dst = Playlists(session, destination_playlist)
model_dst = playlistmodel.PlaylistModel(playlist_dst.id, is_template=False) model_dst = playlistmodel.PlaylistModel(playlist_dst.id)
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)) row_notes.append(model_dst.data(index, Qt.ItemDataRole.EditRole).value())
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)

View File

@ -1,130 +0,0 @@
# 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

View File

@ -3,15 +3,19 @@ import os
import unittest import unittest
# PyQt imports # PyQt imports
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QColor
# Third party imports # Third party imports
import pytest import pytest
from pytestqt.plugin import QtBot # type: ignore from pytestqt.plugin import QtBot # type: ignore
# App imports # App imports
from config import Config
from app import playlistmodel, utilities from app import playlistmodel, utilities
from app.models import ( from app.models import (
db, db,
NoteColours,
Playlists, Playlists,
Tracks, Tracks,
) )
@ -90,8 +94,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, template_id=0) playlist = Playlists(session, playlist_name)
self.widget._open_playlist(playlist, is_template=False) self.widget.create_playlist_tab(playlist)
with self.qtbot.waitExposed(self.widget): with self.qtbot.waitExposed(self.widget):
self.widget.show() self.widget.show()
@ -103,8 +107,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, template_id=0) playlist = Playlists(session, playlist_name)
model = playlistmodel.PlaylistModel(playlist.id, is_template=False) model = playlistmodel.PlaylistModel(playlist.id)
# Add a track with a note # Add a track with a note
model.insert_row( model.insert_row(
@ -139,7 +143,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", template_id=0) # playlist = models.Playlists(session, "my playlist")
# playlist_tab = playlists.PlaylistTab(None, session, playlist.id) # playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
# # Add some tracks # # Add some tracks
@ -167,8 +171,7 @@ 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
@ -249,7 +252,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", template_id=0) # playlist = models.Playlists(session, "my playlist")
# playlist_tab = playlists.PlaylistTab(None, session, playlist.id) # playlist_tab = playlists.PlaylistTab(None, session, playlist.id)
# # Add some tracks # # Add some tracks
@ -275,7 +278,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", template_id=0) # playlist = models.Playlists(session, "test playlist")
# playlist_tab = playlists.PlaylistTab(window, session, playlist.id) # playlist_tab = playlists.PlaylistTab(window, session, playlist.id)
# # Add some tracks # # Add some tracks
@ -307,7 +310,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, template_id=0) # playlist = models.Playlists(session, playlist_name)
# 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)

1316
uv.lock

File diff suppressed because it is too large Load Diff