Compare commits

..

No commits in common. "955433686041fa30e1efd43bd6346caa967d9648" and "3557d22c546635ee1acafd331930edc5ec6c98aa" have entirely different histories.

7 changed files with 175 additions and 164 deletions

View File

@ -1,20 +0,0 @@
from PyQt6.QtCore import pyqtSignal, QObject
from helpers import singleton
@singleton
class MusicMusterSignals(QObject):
"""
Class for all MusicMuster signals. See:
- https://zetcode.com/gui/pyqt5/eventssignals/
- https://stackoverflow.com/questions/62654525/
emit-a-signal-from-another-class-to-main-class
and Singleton class at
https://refactoring.guru/design-patterns/singleton/python/example#example-0
"""
enable_escape_signal = pyqtSignal(bool)
set_next_track_signal = pyqtSignal(int, int)
span_cells_signal = pyqtSignal(int, int, int, int)
add_track_to_playlist_signal = pyqtSignal(int, int, int, str)

View File

@ -1,4 +1,3 @@
import functools
import os
import psutil
import shutil
@ -155,21 +154,23 @@ def get_file_metadata(filepath: str) -> dict:
# Get title, artist, bitrate, duration, path
metadata: Dict[str, str | int | float] = get_tags(filepath)
metadata["mtime"] = os.path.getmtime(filepath)
metadata['mtime'] = os.path.getmtime(filepath)
# Set start_gap, fade_at and silence_at
audio = get_audio_segment(filepath)
if not audio:
audio_values = dict(start_gap=0, fade_at=0, silence_at=0)
audio_values = dict(
start_gap=0,
fade_at=0,
silence_at=0
)
else:
audio_values = dict(
start_gap=leading_silence(audio),
fade_at=int(
round(fade_point(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000
),
fade_at=int(round(fade_point(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000),
silence_at=int(
round(trailing_silence(audio) / 1000, Config.MILLISECOND_SIGFIGS) * 1000
),
)
)
metadata |= audio_values
@ -380,22 +381,6 @@ def show_warning(parent: QMainWindow, title: str, msg: str) -> None:
QMessageBox.warning(parent, title, msg, buttons=QMessageBox.StandardButton.Cancel)
def singleton(cls):
"""
Make a class a Singleton class (see
https://realpython.com/primer-on-python-decorators/#creating-singletons)
"""
@functools.wraps(cls)
def wrapper_singleton(*args, **kwargs):
if not wrapper_singleton.instance:
wrapper_singleton.instance = cls(*args, **kwargs)
return wrapper_singleton.instance
wrapper_singleton.instance = None
return wrapper_singleton
def trailing_silence(
audio_segment: AudioSegment,
silence_threshold: int = -50,

View File

@ -11,7 +11,6 @@ from typing import List, Optional, Sequence
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy import (
bindparam,
Boolean,
DateTime,
delete,
@ -50,9 +49,9 @@ class Carts(Base):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
cart_number: Mapped[int] = mapped_column(unique=True)
name: Mapped[str] = mapped_column(String(256), index=True)
duration: Mapped[Optional[int]] = mapped_column(index=True)
path: Mapped[Optional[str]] = mapped_column(String(2048), index=False)
enabled: Mapped[Optional[bool]] = mapped_column(default=False)
duration: Mapped[int] = mapped_column(index=True)
path: Mapped[str] = mapped_column(String(2048), index=False)
enabled: Mapped[bool] = mapped_column(default=False)
def __repr__(self) -> str:
return (
@ -64,7 +63,7 @@ class Carts(Base):
self,
session: scoped_session,
cart_number: int,
name: str,
name: Optional[str] = None,
duration: Optional[int] = None,
path: Optional[str] = None,
enabled: bool = True,
@ -135,11 +134,14 @@ class NoteColours(Base):
if not text:
return None
for rec in session.scalars(
select(NoteColours)
.filter(NoteColours.enabled.is_(True))
.order_by(NoteColours.order)
).all():
for rec in (
session.scalars(
select(NoteColours)
.filter(NoteColours.enabled.is_(True))
.order_by(NoteColours.order)
)
.all()
):
if rec.is_regex:
flags = re.UNICODE
if not rec.is_casesensitive:
@ -200,11 +202,14 @@ class Playdates(Base):
def played_after(session: scoped_session, since: datetime) -> Sequence["Playdates"]:
"""Return a list of Playdates objects since passed time"""
return session.scalars(
select(Playdates)
.where(Playdates.lastplayed >= since)
.order_by(Playdates.lastplayed)
).all()
return (
session.scalars(
select(Playdates)
.where(Playdates.lastplayed >= since)
.order_by(Playdates.lastplayed)
)
.all()
)
class Playlists(Base):
@ -280,33 +285,42 @@ class Playlists(Base):
def get_all(cls, session: scoped_session) -> Sequence["Playlists"]:
"""Returns a list of all playlists ordered by last use"""
return session.scalars(
select(cls)
.filter(cls.is_template.is_(False))
.order_by(cls.tab.desc(), cls.last_used.desc())
).all()
return (
session.scalars(
select(cls)
.filter(cls.is_template.is_(False))
.order_by(cls.tab.desc(), cls.last_used.desc())
)
.all()
)
@classmethod
def get_all_templates(cls, session: scoped_session) -> Sequence["Playlists"]:
"""Returns a list of all templates ordered by name"""
return session.scalars(
select(cls).filter(cls.is_template.is_(True)).order_by(cls.name)
).all()
return (
session.scalars(
select(cls).filter(cls.is_template.is_(True)).order_by(cls.name)
)
.all()
)
@classmethod
def get_closed(cls, session: scoped_session) -> Sequence["Playlists"]:
"""Returns a list of all closed playlists ordered by last use"""
return session.scalars(
select(cls)
.filter(
cls.tab.is_(None),
cls.is_template.is_(False),
cls.deleted.is_(False),
return (
session.scalars(
select(cls)
.filter(
cls.tab.is_(None),
cls.is_template.is_(False),
cls.deleted.is_(False),
)
.order_by(cls.last_used.desc())
)
.order_by(cls.last_used.desc())
).all()
.all()
)
@classmethod
def get_open(cls, session: scoped_session) -> Sequence[Optional["Playlists"]]:
@ -314,9 +328,10 @@ class Playlists(Base):
Return a list of loaded playlists ordered by tab order.
"""
return session.scalars(
select(cls).where(cls.tab.is_not(None)).order_by(cls.tab)
).all()
return (
session.scalars(select(cls).where(cls.tab.is_not(None)).order_by(cls.tab))
.all()
)
def mark_open(self, session: scoped_session, tab_index: int) -> None:
"""Mark playlist as loaded and used now"""
@ -418,9 +433,12 @@ class PlaylistRows(Base):
def copy_playlist(session: scoped_session, src_id: int, dst_id: int) -> None:
"""Copy playlist entries"""
src_rows = session.scalars(
select(PlaylistRows).filter(PlaylistRows.playlist_id == src_id)
).all()
src_rows = (
session.scalars(
select(PlaylistRows).filter(PlaylistRows.playlist_id == src_id)
)
.all()
)
for plr in src_rows:
PlaylistRows(
@ -494,11 +512,14 @@ class PlaylistRows(Base):
Ensure the row numbers for passed playlist have no gaps
"""
plrs = session.scalars(
select(PlaylistRows)
.where(PlaylistRows.playlist_id == playlist_id)
.order_by(PlaylistRows.plr_rownum)
).all()
plrs = (
session.scalars(
select(PlaylistRows)
.where(PlaylistRows.playlist_id == playlist_id)
.order_by(PlaylistRows.plr_rownum)
)
.all()
)
for i, plr in enumerate(plrs):
plr.plr_rownum = i
@ -515,11 +536,14 @@ class PlaylistRows(Base):
PlaylistRows objects
"""
plrs = session.scalars(
select(cls)
.where(cls.playlist_id == playlist_id, cls.id.in_(plr_ids))
.order_by(cls.plr_rownum)
).all()
plrs = (
session.scalars(
select(cls)
.where(cls.playlist_id == playlist_id, cls.id.in_(plr_ids))
.order_by(cls.plr_rownum)
)
.all()
)
return plrs
@ -557,11 +581,14 @@ class PlaylistRows(Base):
have been played.
"""
plrs = session.scalars(
select(cls)
.where(cls.playlist_id == playlist_id, cls.played.is_(True))
.order_by(cls.plr_rownum)
).all()
plrs = (
session.scalars(
select(cls)
.where(cls.playlist_id == playlist_id, cls.played.is_(True))
.order_by(cls.plr_rownum)
)
.all()
)
return plrs
@ -599,25 +626,21 @@ class PlaylistRows(Base):
have not been played.
"""
plrs = session.scalars(
select(cls)
.where(
cls.playlist_id == playlist_id,
cls.track_id.is_not(None),
cls.played.is_(False),
plrs = (
session.scalars(
select(cls)
.where(
cls.playlist_id == playlist_id,
cls.track_id.is_not(None),
cls.played.is_(False),
)
.order_by(cls.plr_rownum)
)
.order_by(cls.plr_rownum)
).all()
.all()
)
return plrs
@classmethod
def insert_row(
cls, session: scoped_session, playlist_id: int, new_row_number: int
) -> "PlaylistRows":
cls.move_rows_down(session, playlist_id, new_row_number, 1)
return cls(session, playlist_id, new_row_number)
@staticmethod
def move_rows_down(
session: scoped_session, playlist_id: int, starting_row: int, move_by: int
@ -636,26 +659,6 @@ class PlaylistRows(Base):
.values(plr_rownum=PlaylistRows.plr_rownum + move_by)
)
@staticmethod
def update_plr_rownumbers(
session: scoped_session, playlist_id: int, sqla_map: List[dict[str, int]]
) -> None:
"""
Take a {plrid: plr_rownum} dictionary and update the row numbers accordingly
"""
# Update database. Ref:
# https://docs.sqlalchemy.org/en/20/tutorial/data_update.html#the-update-sql-expression-construct
stmt = (
update(PlaylistRows)
.where(
PlaylistRows.playlist_id == playlist_id,
PlaylistRows.id == bindparam("plrid"),
)
.values(plr_rownum=bindparam("plr_rownum"))
)
session.connection().execute(stmt, sqla_map)
class Settings(Base):
"""Manage settings"""

View File

@ -15,6 +15,7 @@ from datetime import datetime, timedelta
from pygame import mixer
from time import sleep
from typing import (
Callable,
cast,
List,
Optional,
@ -68,7 +69,6 @@ import icons_rc # noqa F401
import music
from models import Base, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks
from config import Config
from datastructures import MusicMusterSignals
from playlists import PlaylistTab
from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore
from ui.dlg_TrackSelect_ui import Ui_Dialog # type: ignore
@ -238,6 +238,20 @@ class ImportTrack(QObject):
self.finished.emit(self.playlist)
class MusicMusterSignals(QObject):
"""
Class for all MusicMuster signals. See:
- https://zetcode.com/gui/pyqt5/eventssignals/
- https://stackoverflow.com/questions/62654525/
emit-a-signal-from-another-class-to-main-class
"""
enable_escape_signal = pyqtSignal(bool)
set_next_track_signal = pyqtSignal(int, int)
span_cells_signal = pyqtSignal(int, int, int, int)
add_track_to_playlist_signal = pyqtSignal(int, int, str)
class PlaylistTrack:
"""
Used to provide a single reference point for specific playlist tracks,
@ -335,6 +349,8 @@ class Window(QMainWindow, Ui_MainWindow):
self.previous_track_position: Optional[float] = None
self.selected_plrs: Optional[List[PlaylistRows]] = None
self.signals = MusicMusterSignals()
self.set_main_window_size()
self.lblSumPlaytime = QLabel("")
self.statusbar.addPermanentWidget(self.lblSumPlaytime)
@ -363,7 +379,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer10.start(10)
self.timer500.start(500)
self.timer1000.start(1000)
self.signals = MusicMusterSignals()
self.connect_signals_slots()
def about(self) -> None:
@ -716,6 +731,7 @@ class Window(QMainWindow, Ui_MainWindow):
playlist_tab = PlaylistTab(
musicmuster=self,
playlist_id=playlist.id,
signals=self.signals,
)
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
self.tabPlaylist.setCurrentIndex(idx)
@ -1049,7 +1065,7 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session:
dlg = TrackSelectDialog(
session=session,
new_row_number=self.active_tab().get_selected_row_number(),
signals=self.signals,
playlist_id=self.active_tab().playlist_id,
)
dlg.exec()
@ -1907,7 +1923,7 @@ class TrackSelectDialog(QDialog):
def __init__(
self,
session: scoped_session,
new_row_number: int,
signals: MusicMusterSignals,
playlist_id: int,
*args,
**kwargs,
@ -1918,7 +1934,7 @@ class TrackSelectDialog(QDialog):
super().__init__(*args, **kwargs)
self.session = session
self.new_row_number = new_row_number
self.signals = signals
self.playlist_id = playlist_id
self.ui = Ui_Dialog()
self.ui.setupUi(self)
@ -1930,7 +1946,6 @@ class TrackSelectDialog(QDialog):
self.ui.radioTitle.toggled.connect(self.title_artist_toggle)
self.ui.searchString.textEdited.connect(self.chars_typed)
self.track: Optional[Tracks] = None
self.signals = MusicMusterSignals()
record = Settings.get_int_settings(self.session, "dbdialog_width")
width = record.f_int or 800
@ -1959,9 +1974,7 @@ class TrackSelectDialog(QDialog):
track_id = None
if track:
track_id = track.id
self.signals.add_track_to_playlist_signal.emit(
self.playlist_id, self.new_row_number, track_id, note
)
self.signals.add_track_to_playlist_signal.emit(self.playlist_id, track_id, note)
def add_selected_and_close(self) -> None:
"""Handle Add and Close button"""

View File

@ -3,12 +3,15 @@ from enum import auto, Enum
from sqlalchemy import bindparam, update
from typing import List, Optional, TYPE_CHECKING
from dbconfig import scoped_session, Session
from PyQt6.QtCore import (
QAbstractTableModel,
QModelIndex,
Qt,
QVariant,
)
from PyQt6.QtGui import (
QBrush,
QColor,
@ -16,15 +19,14 @@ from PyQt6.QtGui import (
)
from config import Config
from datastructures import MusicMusterSignals
from dbconfig import scoped_session, Session
from playlists import PlaylistTab
from helpers import (
file_is_unreadable,
)
from models import PlaylistRows, Tracks
HEADER_NOTES_COLUMN = 1
if TYPE_CHECKING:
from musicmuster import MusicMusterSignals
class Col(Enum):
@ -39,6 +41,9 @@ class Col(Enum):
NOTE = auto()
HEADER_NOTES_COLUMN = 1
class PlaylistRowData:
def __init__(self, plr: PlaylistRows) -> None:
"""
@ -93,15 +98,18 @@ class PlaylistModel(QAbstractTableModel):
def __init__(
self,
playlist: PlaylistTab,
playlist_id: int,
signals: "MusicMusterSignals",
*args,
**kwargs,
):
self.playlist = playlist
self.playlist_id = playlist_id
self.signals = signals
super().__init__(*args, **kwargs)
self.playlist_rows: dict[int, PlaylistRowData] = {}
self.signals = MusicMusterSignals()
self.signals.add_track_to_playlist_signal.connect(self.add_track)
@ -114,11 +122,7 @@ class PlaylistModel(QAbstractTableModel):
)
def add_track(
self,
playlist_id: int,
new_row_number: int,
track_id: Optional[int],
note: Optional[str],
self, playlist_id: int, track: Optional[Tracks], note: Optional[str]
) -> None:
"""
Add track if it's for our playlist
@ -128,12 +132,14 @@ class PlaylistModel(QAbstractTableModel):
if playlist_id != self.playlist_id:
return
row_number = self.playlist.get_selected_row_number()
# Insert track if we have one
if track_id:
self.insert_track_row(new_row_number, track_id, note)
if track:
self.insert_track_row(row_number, track, note)
# If we only have a note, add as a header row
elif note:
self.insert_header_row(new_row_number, note)
self.insert_header_row(row_number, note)
else:
# No track, no note, no point
return
@ -360,8 +366,16 @@ class PlaylistModel(QAbstractTableModel):
else:
new_row_number = row_number
# Move rows below new row down
stmt = (
update(PlaylistRows)
.where(PlaylistRows.plr_rownum >= new_row_number)
.values({PlaylistRows.plr_rownum: PlaylistRows.plr_rownum + 1})
)
session.execute(stmt)
# Insert the new row and return it
return PlaylistRows.insert_row(session, self.playlist_id, new_row_number)
return PlaylistRows(session, self.playlist_id, new_row_number)
def insert_track_row(
self, row_number: Optional[int], track_id: int, text: Optional[str]
@ -438,8 +452,20 @@ class PlaylistModel(QAbstractTableModel):
plrid = self.playlist_rows[oldrow].plrid
sqla_map.append({"plrid": plrid, "plr_rownum": newrow})
# Update database. Ref:
# https://docs.sqlalchemy.org/en/20/tutorial/data_update.html#the-update-sql-expression-construct
stmt = (
update(PlaylistRows)
.where(
PlaylistRows.playlist_id == self.playlist_id,
PlaylistRows.id == bindparam("plrid"),
)
.values(plr_rownum=bindparam("plr_rownum"))
)
with Session() as session:
PlaylistRows.update_plr_rownumbers(session, self.playlist_id, sqla_map)
session.connection().execute(stmt, sqla_map)
# Update playlist_rows
self.refresh_data(session)

View File

@ -6,6 +6,7 @@ import threading
import obsws_python as obs # type: ignore
# from collections import namedtuple
from datetime import datetime, timedelta
from typing import Any, cast, List, Optional, Tuple, TYPE_CHECKING
@ -35,9 +36,8 @@ from PyQt6.QtWidgets import (
QStyleOption,
)
from datastructures import MusicMusterSignals
from dbconfig import Session, scoped_session
from config import Config
from dbconfig import Session, scoped_session
from helpers import (
ask_yes_no,
file_is_unreadable,
@ -48,8 +48,10 @@ from helpers import (
set_track_metadata,
)
from log import log
from models import Playlists, PlaylistRows, Settings, Tracks, NoteColours
from playlistmodel import PlaylistModel
import playlistmodel
if TYPE_CHECKING:
from musicmuster import Window, MusicMusterSignals
@ -64,9 +66,9 @@ class EscapeDelegate(QStyledItemDelegate):
- checks with user before abandoning edit on Escape
"""
def __init__(self, parent) -> None:
def __init__(self, parent, signals: "MusicMusterSignals") -> None:
super().__init__(parent)
self.signals = MusicMusterSignals()
self.signals = signals
def createEditor(
self,
@ -78,7 +80,7 @@ class EscapeDelegate(QStyledItemDelegate):
Intercept createEditor call and make row just a little bit taller
"""
signals.enable_escape_signal.emit(False)
self.signals.enable_escape_signal.emit(False)
if isinstance(self.parent(), PlaylistTab):
p = cast(PlaylistTab, self.parent())
if isinstance(index.data(), str):
@ -154,15 +156,17 @@ class PlaylistTab(QTableView):
self,
musicmuster: "Window",
playlist_id: int,
signals: "MusicMusterSignals",
) -> None:
super().__init__()
# Save passed settings
self.musicmuster = musicmuster
self.playlist_id = playlist_id
self.signals = signals
# Set up widget
self.setItemDelegate(EscapeDelegate(self))
self.setItemDelegate(EscapeDelegate(self, self.signals))
self.setAlternatingRowColors(True)
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
@ -190,7 +194,6 @@ class PlaylistTab(QTableView):
h_header.setStretchLastSection(True)
# self.itemSelectionChanged.connect(self._select_event)
# self.signals.set_next_track_signal.connect(self._reset_next)
self.signals = MusicMusterSignals()
self.signals.span_cells_signal.connect(self._span_cells)
# Call self.eventFilter() for events
@ -202,7 +205,7 @@ class PlaylistTab(QTableView):
# self.edit_cell_type: Optional[int]
# Load playlist rows
self.setModel(PlaylistModel(playlist_id))
self.setModel(playlistmodel.PlaylistModel(self, playlist_id, signals))
self._set_column_widths()
# kae def __repr__(self) -> str:

View File

@ -4,12 +4,13 @@ from app.models import (
from app import playlistmodel
from dbconfig import scoped_session
def create_model_with_playlist_rows(
session: scoped_session, rows: int
) -> "playlistmodel.PlaylistModel":
playlist = Playlists(session, "test playlist")
# Create a model
model = playlistmodel.PlaylistModel(playlist.id)
model = playlistmodel.PlaylistModel(playlist.id, None)
for row in range(rows):
plr = model._insert_row(session, row)
newrow = plr.plr_rownum