Compare commits
9 Commits
8fa98a2207
...
c6be215bd4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6be215bd4 | ||
|
|
22e16144e1 | ||
|
|
1932719fea | ||
|
|
e9f4ecf5ef | ||
|
|
ce224a41d1 | ||
|
|
bf89172f8a | ||
|
|
9f234bb007 | ||
|
|
6fb6339843 | ||
|
|
bf5563f2f1 |
@ -1,4 +1,6 @@
|
||||
# Standard library imports
|
||||
import os
|
||||
import sys
|
||||
|
||||
# PyQt imports
|
||||
|
||||
@ -6,6 +8,7 @@
|
||||
from alchemical import Alchemical # type:ignore
|
||||
|
||||
# App imports
|
||||
from config import Config
|
||||
|
||||
|
||||
class DatabaseManager:
|
||||
@ -29,3 +32,12 @@ class DatabaseManager:
|
||||
if DatabaseManager.__instance is None:
|
||||
DatabaseManager(database_url, **kwargs)
|
||||
return DatabaseManager.__instance
|
||||
|
||||
|
||||
# Establish database connection
|
||||
DATABASE_URL = os.environ.get("DATABASE_URL")
|
||||
if DATABASE_URL is None:
|
||||
raise ValueError("DATABASE_URL is undefined")
|
||||
if "unittest" in sys.modules and "sqlite" not in DATABASE_URL:
|
||||
raise ValueError("Unit tests running on non-Sqlite database")
|
||||
db = DatabaseManager.get_instance(DATABASE_URL, engine_options=Config.ENGINE_OPTIONS).db
|
||||
|
||||
249
app/ds.py
249
app/ds.py
@ -29,9 +29,9 @@ from classes import (
|
||||
TrackDTO,
|
||||
)
|
||||
from config import Config
|
||||
from dbmanager import db
|
||||
from log import log, log_call
|
||||
from models import (
|
||||
db,
|
||||
NoteColours,
|
||||
Playdates,
|
||||
PlaylistRows,
|
||||
@ -44,8 +44,8 @@ from models import (
|
||||
|
||||
# Configure the dogpile 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
|
||||
"dogpile.cache.memory", # Use in-memory caching for now (switch to Redis if needed)
|
||||
expiration_time=600, # Cache expires after 10 minutes
|
||||
)
|
||||
|
||||
|
||||
@ -179,7 +179,9 @@ def notecolours_remove_colour_substring(text: str) -> str:
|
||||
|
||||
# Track functions
|
||||
# @log_call
|
||||
def _tracks_where(query: BinaryExpression | ColumnElement[bool],) -> list[TrackDTO]:
|
||||
def _tracks_where(
|
||||
query: BinaryExpression | ColumnElement[bool],
|
||||
) -> list[TrackDTO]:
|
||||
"""
|
||||
filter_by_last_played: bool = False,
|
||||
last_played_before: dt.datetime | None = None,
|
||||
@ -435,9 +437,7 @@ def tracks_filtered(filter: Filter) -> list[TrackDTO]:
|
||||
|
||||
|
||||
# @log_call
|
||||
def track_update(
|
||||
track_id: int, metadata: dict[str, str | int | float]
|
||||
) -> TrackDTO:
|
||||
def track_update(track_id: int, metadata: dict[str, str | int | float]) -> TrackDTO:
|
||||
"""
|
||||
Update an existing track db entry return the DTO
|
||||
"""
|
||||
@ -472,6 +472,8 @@ def _playlist_check_playlist(
|
||||
else raise ApplicationError.
|
||||
"""
|
||||
|
||||
fixed = False
|
||||
|
||||
playlist_rows = (
|
||||
session.execute(
|
||||
select(PlaylistRows)
|
||||
@ -492,9 +494,13 @@ def _playlist_check_playlist(
|
||||
if fix:
|
||||
log.debug(msg)
|
||||
plr.row_number = idx
|
||||
fixed = True
|
||||
else:
|
||||
raise ApplicationError(msg)
|
||||
|
||||
if fixed:
|
||||
session.commit()
|
||||
|
||||
|
||||
# @log_call
|
||||
def _playlist_shift_rows(
|
||||
@ -523,13 +529,17 @@ def _playlists_where(
|
||||
Return playlists selected by query
|
||||
"""
|
||||
|
||||
stmt = select(
|
||||
Playlists.favourite,
|
||||
Playlists.is_template,
|
||||
Playlists.id.label("playlist_id"),
|
||||
Playlists.name,
|
||||
Playlists.open,
|
||||
).where(query).order_by(Playlists.tab)
|
||||
stmt = (
|
||||
select(
|
||||
Playlists.favourite,
|
||||
Playlists.is_template,
|
||||
Playlists.id.label("playlist_id"),
|
||||
Playlists.name,
|
||||
Playlists.open,
|
||||
)
|
||||
.where(query)
|
||||
.order_by(Playlists.tab)
|
||||
)
|
||||
|
||||
results: list[PlaylistDTO] = []
|
||||
|
||||
@ -597,7 +607,9 @@ def playlists_closed() -> list[PlaylistDTO]:
|
||||
|
||||
|
||||
# @log_call
|
||||
def playlist_create(name: str, template_id: int, as_template: bool = False) -> PlaylistDTO:
|
||||
def playlist_create(
|
||||
name: str, template_id: int, as_template: bool = False
|
||||
) -> PlaylistDTO:
|
||||
"""
|
||||
Create playlist and return DTO.
|
||||
"""
|
||||
@ -676,9 +688,7 @@ def playlist_mark_status(playlist_id: int, open: bool) -> None:
|
||||
|
||||
with db.Session() as session:
|
||||
session.execute(
|
||||
update(Playlists)
|
||||
.where(Playlists.id == playlist_id)
|
||||
.values(open=open)
|
||||
update(Playlists).where(Playlists.id == playlist_id).values(open=open)
|
||||
)
|
||||
|
||||
session.commit()
|
||||
@ -692,88 +702,145 @@ def playlist_move_rows(
|
||||
to_playlist_id: int | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Move rows with or between playlists.
|
||||
Call helper function depending upon whether we are moving rows within
|
||||
a playlist or between playlists.
|
||||
"""
|
||||
|
||||
# If to_playlist_id isn't specified, we're moving within the one
|
||||
# playlist.
|
||||
if to_playlist_id is None or to_playlist_id == from_playlist_id:
|
||||
_playlist_move_rows_within_playlist(from_rows, from_playlist_id, to_row)
|
||||
else:
|
||||
_playlist_move_rows_between_playlists(
|
||||
from_rows, from_playlist_id, to_row, to_playlist_id
|
||||
)
|
||||
|
||||
|
||||
def _playlist_move_rows_between_playlists(
|
||||
from_rows: list[int],
|
||||
from_playlist_id: int,
|
||||
to_row: int,
|
||||
to_playlist_id: int,
|
||||
) -> None:
|
||||
"""
|
||||
Move rows between playlists.
|
||||
|
||||
Algorithm:
|
||||
- Sanity check row numbers
|
||||
- Check there are no playlist rows with playlist_id == PENDING_MOVE
|
||||
- Put rows to be moved into PENDING_MOVE playlist
|
||||
- Resequence remaining row numbers
|
||||
- Make space for moved rows
|
||||
- Move the PENDING_MOVE rows back and fixup row numbers
|
||||
- Sanity check row numbers
|
||||
"""
|
||||
|
||||
# If to_playlist_id isn't specified, we're moving within the one
|
||||
# playlist.
|
||||
if to_playlist_id is None:
|
||||
to_playlist_id = from_playlist_id
|
||||
# Sanity check destination not being moved
|
||||
if to_row in from_rows:
|
||||
log.error(
|
||||
f"ds._playlist_move_rows_within_playlist: {to_row=} in {from_rows=}"
|
||||
)
|
||||
return
|
||||
|
||||
with db.Session() as session:
|
||||
# Sanity check row numbers
|
||||
_playlist_check_playlist(session, from_playlist_id, fix=False)
|
||||
if from_playlist_id != to_playlist_id:
|
||||
_playlist_check_playlist(session, to_playlist_id, fix=False)
|
||||
_playlist_check_playlist(session, to_playlist_id, fix=False)
|
||||
|
||||
# Check there are no playlist rows with playlist_id == PENDING_MOVE
|
||||
pending_move_rows = playlistrows_by_playlist(Config.PLAYLIST_PENDING_MOVE)
|
||||
if pending_move_rows:
|
||||
raise ApplicationError(f"move_rows_to_playlist: {pending_move_rows=}")
|
||||
# Make space in destination playlist
|
||||
_playlist_shift_rows(session, to_playlist_id, to_row, len(from_rows))
|
||||
|
||||
# We need playlist length if we're moving within a playlist. Get
|
||||
# that now before we remove rows.
|
||||
from_playlist_length = len(playlistrows_by_playlist(from_playlist_id))
|
||||
# Put rows to be moved into PENDING_MOVE playlist
|
||||
session.execute(
|
||||
update(PlaylistRows)
|
||||
.where(
|
||||
PlaylistRows.playlist_id == from_playlist_id,
|
||||
PlaylistRows.row_number.in_(from_rows),
|
||||
)
|
||||
.values(playlist_id=Config.PLAYLIST_PENDING_MOVE)
|
||||
)
|
||||
|
||||
# Resequence remaining row numbers
|
||||
_playlist_check_playlist(session, from_playlist_id, fix=True)
|
||||
session.commit()
|
||||
|
||||
# Make space for moved rows. If moving within one playlist,
|
||||
# determning where to make the space is non-trivial. For example,
|
||||
# if the playlist has ten entries and we're moving four of them
|
||||
# to row 8, after we've moved the rows to the
|
||||
# PLAYLIST_PENDING_MOVE there will only be six entries left.
|
||||
# Clearly we can't make space at row 8...
|
||||
space_row = to_row
|
||||
if to_playlist_id == from_playlist_id:
|
||||
overflow = max(to_row + len(from_rows) - from_playlist_length, 0)
|
||||
if overflow != 0:
|
||||
space_row = (
|
||||
to_row - overflow - len([a for a in from_rows if a > to_row])
|
||||
)
|
||||
|
||||
_playlist_shift_rows(session, to_playlist_id, space_row, len(from_rows))
|
||||
|
||||
# Move the PENDING_MOVE rows back and fixup row numbers
|
||||
# Update database
|
||||
# Build a dictionary of changes to make
|
||||
update_list: list[dict[str, int]] = []
|
||||
next_row = space_row
|
||||
# PLAYLIST_PENDING_MOVE may have gaps so don't check it
|
||||
for row_to_move in playlistrows_by_playlist(
|
||||
Config.PLAYLIST_PENDING_MOVE, check_playlist_itegrity=False
|
||||
):
|
||||
old_row_to_id = _playlist_rows_to_id(from_playlist_id)
|
||||
next_row = to_row
|
||||
|
||||
for from_row in from_rows:
|
||||
plrid = old_row_to_id[from_row]
|
||||
update_list.append(
|
||||
{"id": row_to_move.playlistrow_id, "row_number": next_row}
|
||||
{"id": plrid, "row_number": next_row}
|
||||
)
|
||||
update_list.append(
|
||||
{"id": row_to_move.playlistrow_id, "playlist_id": to_playlist_id}
|
||||
{"id": plrid, "playlist_id": to_playlist_id}
|
||||
)
|
||||
next_row += 1
|
||||
|
||||
session.execute(update(PlaylistRows), update_list)
|
||||
session.commit()
|
||||
|
||||
# Resequence row numbers in source
|
||||
_playlist_check_playlist(session, from_playlist_id, fix=True)
|
||||
# Sanity check destionation
|
||||
_playlist_check_playlist(session, from_playlist_id, fix=False)
|
||||
|
||||
|
||||
def _playlist_rows_to_id(playlist_id: int) -> dict[int, int]:
|
||||
"""
|
||||
Return a dict of {row_number: playlistrow_id} for passed playlist
|
||||
"""
|
||||
|
||||
row_to_id = {
|
||||
p.row_number: p.playlistrow_id
|
||||
for p in playlistrows_by_playlist(playlist_id)
|
||||
}
|
||||
|
||||
return row_to_id
|
||||
|
||||
|
||||
# @log_call
|
||||
def _playlist_move_rows_within_playlist(
|
||||
from_rows: list[int],
|
||||
from_playlist_id: int,
|
||||
to_row: int,
|
||||
) -> None:
|
||||
"""
|
||||
Move rows within playlists.
|
||||
|
||||
Algorithm:
|
||||
- Sanity checks
|
||||
- Create a list of row numbers in the new order
|
||||
- Update the database with the new order
|
||||
- Sanity check row numbers
|
||||
"""
|
||||
|
||||
# Sanity check destination not being moved
|
||||
if to_row in from_rows:
|
||||
log.error(
|
||||
f"ds._playlist_move_rows_within_playlist: {to_row=} in {from_rows=}"
|
||||
)
|
||||
return
|
||||
|
||||
with db.Session() as session:
|
||||
# Sanity check row numbers
|
||||
_playlist_check_playlist(session, from_playlist_id, fix=False)
|
||||
|
||||
# Create a list showing the new order of rows in playlist
|
||||
# Start with a list of rows excluding those to be moved
|
||||
from_playlist_length = len(playlistrows_by_playlist(from_playlist_id))
|
||||
new_row_order = [a for a in range(from_playlist_length) if a not in from_rows]
|
||||
# Insert the moved row numbers
|
||||
try:
|
||||
idx = new_row_order.index(to_row)
|
||||
except ValueError:
|
||||
raise ApplicationError(f"Can't find {to_row=} in {new_row_order=}")
|
||||
new_row_order[idx:idx] = from_rows
|
||||
|
||||
# Update database
|
||||
# Build a dictionary of {old_row_number: new_row_number} where
|
||||
# they differ
|
||||
row_changes = {old: new for new, old in enumerate(new_row_order) if old != new}
|
||||
# Build a dictionary of changes to make
|
||||
update_list: list[dict[str, int]] = []
|
||||
old_row_to_id = _playlist_rows_to_id(from_playlist_id)
|
||||
for old_row, new_row in row_changes.items():
|
||||
plrid = old_row_to_id[old_row]
|
||||
update_list.append({"id": plrid, "row_number": new_row})
|
||||
|
||||
# Updte database
|
||||
session.execute(update(PlaylistRows), update_list)
|
||||
session.commit()
|
||||
|
||||
# Sanity check row numbers
|
||||
_playlist_check_playlist(session, from_playlist_id, fix=False)
|
||||
if from_playlist_id != to_playlist_id:
|
||||
_playlist_check_playlist(session, to_playlist_id, fix=False)
|
||||
|
||||
|
||||
def playlists_open() -> list[PlaylistDTO]:
|
||||
@ -791,9 +858,7 @@ def playlist_rename(playlist_id: int, new_name: str) -> None:
|
||||
|
||||
with db.Session() as session:
|
||||
session.execute(
|
||||
update(Playlists)
|
||||
.where(Playlists.id == playlist_id)
|
||||
.values(name=new_name)
|
||||
update(Playlists).where(Playlists.id == playlist_id).values(name=new_name)
|
||||
)
|
||||
|
||||
session.commit()
|
||||
@ -903,7 +968,6 @@ def playlist_remove_rows(playlist_id: int, row_numbers: list[int]) -> None:
|
||||
)
|
||||
# Fixup row number to remove gaps
|
||||
_playlist_check_playlist(session, playlist_id, fix=True)
|
||||
session.commit()
|
||||
|
||||
|
||||
# @log_call
|
||||
@ -919,11 +983,9 @@ def playlist_save_tabs(playlist_id_to_tab: dict[int, int]) -> None:
|
||||
.where(Playlists.id.in_(playlist_id_to_tab.keys()))
|
||||
.values(tab=None)
|
||||
)
|
||||
for (playlist_id, tab) in playlist_id_to_tab.items():
|
||||
for playlist_id, tab in playlist_id_to_tab.items():
|
||||
session.execute(
|
||||
update(Playlists)
|
||||
.where(Playlists.id == playlist_id)
|
||||
.values(tab=tab)
|
||||
update(Playlists).where(Playlists.id == playlist_id).values(tab=tab)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
@ -943,6 +1005,7 @@ def playlist_update_template_favourite(template_id: int, favourite: bool) -> Non
|
||||
|
||||
# Playlist Rows
|
||||
|
||||
|
||||
# @log_call
|
||||
def playlistrow_by_id(playlistrow_id: int) -> PlaylistRowDTO | None:
|
||||
"""
|
||||
@ -951,7 +1014,9 @@ def playlistrow_by_id(playlistrow_id: int) -> PlaylistRowDTO | None:
|
||||
|
||||
with db.Session() as session:
|
||||
record = (
|
||||
session.execute(select(PlaylistRows).where(PlaylistRows.id == playlistrow_id))
|
||||
session.execute(
|
||||
select(PlaylistRows).where(PlaylistRows.id == playlistrow_id)
|
||||
)
|
||||
.scalars()
|
||||
.one_or_none()
|
||||
)
|
||||
@ -977,9 +1042,7 @@ def playlistrow_by_id(playlistrow_id: int) -> PlaylistRowDTO | None:
|
||||
def playlistrows_by_playlist(
|
||||
playlist_id: int, check_playlist_itegrity: bool = True
|
||||
) -> list[PlaylistRowDTO]:
|
||||
|
||||
with db.Session() as session:
|
||||
|
||||
# TODO: would be good to be confident at removing this
|
||||
if check_playlist_itegrity:
|
||||
_playlist_check_playlist(
|
||||
@ -994,7 +1057,6 @@ def playlistrows_by_playlist(
|
||||
|
||||
dto_list = []
|
||||
for record in records:
|
||||
|
||||
track = None
|
||||
if record.track_id:
|
||||
track = track_by_id(record.track_id)
|
||||
@ -1040,7 +1102,9 @@ def playlistrow_played(playlistrow_id: int, status: bool) -> None:
|
||||
|
||||
with db.Session() as session:
|
||||
session.execute(
|
||||
update(PlaylistRows).where(PlaylistRows.id == playlistrow_id).values(played=status)
|
||||
update(PlaylistRows)
|
||||
.where(PlaylistRows.id == playlistrow_id)
|
||||
.values(played=status)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
@ -1096,10 +1160,7 @@ def playdates_between_dates(
|
||||
Playdates.lastplayed,
|
||||
Playdates.track_id,
|
||||
Playdates.track,
|
||||
).where(
|
||||
Playdates.lastplayed >= start,
|
||||
Playdates.lastplayed <= end
|
||||
)
|
||||
).where(Playdates.lastplayed >= start, Playdates.lastplayed <= end)
|
||||
|
||||
results: list[PlaydatesDTO] = []
|
||||
|
||||
@ -1137,10 +1198,7 @@ def _queries_where(
|
||||
results: list[QueryDTO] = []
|
||||
|
||||
with db.Session() as session:
|
||||
records = session.scalars(
|
||||
select(Queries)
|
||||
.where(query)
|
||||
).all()
|
||||
records = session.scalars(select(Queries).where(query)).all()
|
||||
for record in records:
|
||||
dto = QueryDTO(
|
||||
favourite=record.favourite,
|
||||
@ -1273,4 +1331,3 @@ def db_name_get() -> str:
|
||||
dbname = session.bind.engine.url.database
|
||||
return dbname
|
||||
return Config.DB_NOT_FOUND
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ disable_existing_loggers: True
|
||||
formatters:
|
||||
colored:
|
||||
(): colorlog.ColoredFormatter
|
||||
format: "%(log_color)s[%(asctime)s] %(filename)s.%(funcName)s:%(lineno)s %(blue)s%(message)s"
|
||||
format: "%(log_color)s[%(asctime)s] %(filename)s.%(funcName)s:%(lineno)s %(light_blue)s%(message)s"
|
||||
datefmt: "%H:%M:%S"
|
||||
syslog:
|
||||
format: "[%(name)s] %(filename)s:%(lineno)s %(leveltag)s: %(message)s"
|
||||
@ -25,6 +25,7 @@ filters:
|
||||
musicmuster:
|
||||
- update_clocks
|
||||
- play_next
|
||||
- show_signal
|
||||
|
||||
handlers:
|
||||
stderr:
|
||||
|
||||
@ -34,14 +34,6 @@ import dbtables
|
||||
from log import log
|
||||
|
||||
|
||||
# Establish database connection
|
||||
DATABASE_URL = os.environ.get("DATABASE_URL")
|
||||
if DATABASE_URL is None:
|
||||
raise ValueError("DATABASE_URL is undefined")
|
||||
if "unittest" in sys.modules and "sqlite" not in DATABASE_URL:
|
||||
raise ValueError("Unit tests running on non-Sqlite database")
|
||||
db = DatabaseManager.get_instance(DATABASE_URL, engine_options=Config.ENGINE_OPTIONS).db
|
||||
|
||||
# Configure the cache region
|
||||
cache_region = make_region().configure(
|
||||
'dogpile.cache.memory', # Use in-memory caching for now (switch to Redis if needed)
|
||||
|
||||
@ -16,44 +16,11 @@ from PyQt6.QtCore import (
|
||||
)
|
||||
|
||||
# App imports
|
||||
from classes import singleton
|
||||
from classes import MusicMusterSignals, singleton
|
||||
from config import Config
|
||||
import helpers
|
||||
from log import log
|
||||
|
||||
# Define the VLC callback function type
|
||||
# import ctypes
|
||||
# import platform
|
||||
# VLC logging is very noisy so comment out unless needed
|
||||
# VLC_LOG_CB = ctypes.CFUNCTYPE(
|
||||
# None,
|
||||
# ctypes.c_void_p,
|
||||
# ctypes.c_int,
|
||||
# ctypes.c_void_p,
|
||||
# ctypes.c_char_p,
|
||||
# ctypes.c_void_p,
|
||||
# )
|
||||
|
||||
# # Determine the correct C library for vsnprintf based on the platform
|
||||
# if platform.system() == "Windows":
|
||||
# libc = ctypes.CDLL("msvcrt")
|
||||
# elif platform.system() == "Linux":
|
||||
# libc = ctypes.CDLL("libc.so.6")
|
||||
# elif platform.system() == "Darwin": # macOS
|
||||
# libc = ctypes.CDLL("libc.dylib")
|
||||
# else:
|
||||
# raise OSError("Unsupported operating system")
|
||||
|
||||
# # Define the vsnprintf function
|
||||
# libc.vsnprintf.argtypes = [
|
||||
# ctypes.c_char_p,
|
||||
# ctypes.c_size_t,
|
||||
# ctypes.c_char_p,
|
||||
# ctypes.c_void_p,
|
||||
# ]
|
||||
# libc.vsnprintf.restype = ctypes.c_int
|
||||
|
||||
|
||||
class _FadeTrack(QThread):
|
||||
finished = pyqtSignal()
|
||||
|
||||
@ -113,32 +80,7 @@ class Music:
|
||||
self.player: vlc.MediaPlayer | None = None
|
||||
self.max_volume: int = Config.VLC_VOLUME_DEFAULT
|
||||
self.start_dt: dt.datetime | None = None
|
||||
|
||||
# Set up logging
|
||||
# self._set_vlc_log()
|
||||
|
||||
# VLC logging very noisy so comment out unless needed
|
||||
# @VLC_LOG_CB
|
||||
# def log_callback(data, level, ctx, fmt, args):
|
||||
# try:
|
||||
# # Create a ctypes string buffer to hold the formatted message
|
||||
# buf = ctypes.create_string_buffer(1024)
|
||||
|
||||
# # Use vsnprintf to format the string with the va_list
|
||||
# libc.vsnprintf(buf, len(buf), fmt, args)
|
||||
|
||||
# # Decode the formatted message
|
||||
# message = buf.value.decode("utf-8", errors="replace")
|
||||
# log.debug("VLC: " + message)
|
||||
# except Exception as e:
|
||||
# log.error(f"Error in VLC log callback: {e}")
|
||||
|
||||
# def _set_vlc_log(self):
|
||||
# try:
|
||||
# vlc.libvlc_log_set(vlc_instance, self.log_callback, None)
|
||||
# log.debug("VLC logging set up successfully")
|
||||
# except Exception as e:
|
||||
# log.error(f"Failed to set up VLC logging: {e}")
|
||||
self.signals = MusicMusterSignals()
|
||||
|
||||
def fade(self, fade_seconds: int) -> None:
|
||||
"""
|
||||
|
||||
@ -2,8 +2,9 @@
|
||||
|
||||
# Standard library imports
|
||||
from __future__ import annotations
|
||||
from functools import partial
|
||||
from slugify import slugify # type: ignore
|
||||
from typing import Callable
|
||||
from typing import Any, Callable
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
import datetime as dt
|
||||
@ -93,6 +94,66 @@ import ds
|
||||
import helpers
|
||||
|
||||
|
||||
class SignalMonitor:
|
||||
def __init__(self):
|
||||
self.signals = MusicMusterSignals()
|
||||
|
||||
self.signals.enable_escape_signal.connect(
|
||||
partial(self.show_signal, "enable_escape_signal ")
|
||||
)
|
||||
self.signals.next_track_changed_signal.connect(
|
||||
partial(self.show_signal, "next_track_changed_signal ")
|
||||
)
|
||||
self.signals.resize_rows_signal.connect(
|
||||
partial(self.show_signal, "resize_rows_signal ")
|
||||
)
|
||||
self.signals.search_songfacts_signal.connect(
|
||||
partial(self.show_signal, "search_songfacts_signal ")
|
||||
)
|
||||
self.signals.search_wikipedia_signal.connect(
|
||||
partial(self.show_signal, "search_wikipedia_signal ")
|
||||
)
|
||||
self.signals.show_warning_signal.connect(
|
||||
partial(self.show_signal, "show_warning_signal ")
|
||||
)
|
||||
self.signals.signal_add_track_to_header.connect(
|
||||
partial(self.show_signal, "signal_add_track_to_header ")
|
||||
)
|
||||
self.signals.signal_begin_insert_rows.connect(
|
||||
partial(self.show_signal, "signal_begin_insert_rows ")
|
||||
)
|
||||
self.signals.signal_end_insert_rows.connect(
|
||||
partial(self.show_signal, "signal_end_insert_rows ")
|
||||
)
|
||||
self.signals.signal_insert_track.connect(
|
||||
partial(self.show_signal, "signal_insert_track ")
|
||||
)
|
||||
self.signals.signal_playlist_selected_rows.connect(
|
||||
partial(self.show_signal, "signal_playlist_selected_rows ")
|
||||
)
|
||||
self.signals.signal_set_next_row.connect(
|
||||
partial(self.show_signal, "signal_set_next_row ")
|
||||
)
|
||||
self.signals.signal_set_next_track.connect(
|
||||
partial(self.show_signal, "signal_set_next_track ")
|
||||
)
|
||||
self.signals.signal_track_started.connect(
|
||||
partial(self.show_signal, "signal_track_started ")
|
||||
)
|
||||
# self.signals.span_cells_signal.connect(
|
||||
# partial(self.show_signal, "span_cells_signal ")
|
||||
# )
|
||||
self.signals.status_message_signal.connect(
|
||||
partial(self.show_signal, "status_message_signal ")
|
||||
)
|
||||
self.signals.track_ended_signal.connect(
|
||||
partial(self.show_signal, "track_ended_signal ")
|
||||
)
|
||||
|
||||
def show_signal(self, name: str, *args: Any) -> None:
|
||||
log.debug(f"{name=}, args={args}")
|
||||
|
||||
|
||||
class Current:
|
||||
base_model: PlaylistModel
|
||||
proxy_model: PlaylistProxyModel
|
||||
@ -770,6 +831,7 @@ class QueryDialog(QDialog):
|
||||
super().__init__()
|
||||
self.playlist_id = playlist_id
|
||||
self.default = default
|
||||
self.signals = MusicMusterSignals()
|
||||
|
||||
# Build a list of (query-name, playlist-id) tuples
|
||||
self.selected_tracks: list[int] = []
|
||||
@ -862,9 +924,9 @@ class QueryDialog(QDialog):
|
||||
|
||||
# new_row_number = self.current_row_or_end()
|
||||
# base_model = self.current.base_model
|
||||
for track_id in self.query_dialog.selected_tracks:
|
||||
for track_id in self.selected_tracks:
|
||||
insert_track_data = InsertTrack(self.playlist_id, track_id, note="")
|
||||
self.signals.signal_insert_track.emit(InsertTrack(insert_track_data))
|
||||
self.signals.signal_insert_track.emit(insert_track_data)
|
||||
|
||||
self.accept()
|
||||
|
||||
@ -1180,6 +1242,9 @@ class Window(QMainWindow):
|
||||
self.action_quicklog = QShortcut(QKeySequence("Ctrl+L"), self)
|
||||
self.action_quicklog.activated.connect(self.quicklog)
|
||||
|
||||
# Optionally print signals
|
||||
self.signal_monitor = SignalMonitor()
|
||||
|
||||
# Load playlists
|
||||
self.load_last_playlists()
|
||||
|
||||
@ -1519,9 +1584,7 @@ class Window(QMainWindow):
|
||||
else:
|
||||
return None
|
||||
|
||||
def solicit_template_to_use(
|
||||
self, template_prompt: str | None = None
|
||||
) -> int | None:
|
||||
def solicit_template_to_use(self, template_prompt: str | None = None) -> int | None:
|
||||
"""
|
||||
Have user select a template. Return the template.id, or None if they cancel.
|
||||
template_id of zero means don't use a template.
|
||||
@ -1881,7 +1944,7 @@ class Window(QMainWindow):
|
||||
InsertTrack(
|
||||
playlist_id=self.current.base_model.playlist_id,
|
||||
track_id=None,
|
||||
note=dlg.textValue()
|
||||
note=dlg.textValue(),
|
||||
)
|
||||
)
|
||||
|
||||
@ -1936,8 +1999,7 @@ class Window(QMainWindow):
|
||||
# Save the selected PlaylistRows items ready for a later
|
||||
# paste
|
||||
self.move_source = MoveSource(
|
||||
model=self.current.base_model,
|
||||
rows=self.current.selected_row_numbers
|
||||
model=self.current.base_model, rows=self.current.selected_row_numbers
|
||||
)
|
||||
|
||||
log.debug(f"mark_rows_for_moving(): {self.move_source=}")
|
||||
@ -2054,9 +2116,7 @@ class Window(QMainWindow):
|
||||
to_playlist_model.set_next_row(to_row)
|
||||
|
||||
# @log_call
|
||||
def play_next(
|
||||
self, position: float | None = None, checked: bool = False
|
||||
) -> None:
|
||||
def play_next(self, position: float | None = None, checked: bool = False) -> None:
|
||||
"""
|
||||
Play next track, optionally from passed position.
|
||||
|
||||
|
||||
@ -717,12 +717,11 @@ class PlaylistModel(QAbstractTableModel):
|
||||
track_id=row_data.track_id,
|
||||
note=row_data.note,
|
||||
)
|
||||
super().endInsertRows()
|
||||
|
||||
# Need to refresh self.playlist_rows because row numbers will have
|
||||
# changed
|
||||
self.refresh_data()
|
||||
|
||||
super().endInsertRows()
|
||||
|
||||
self.signals.resize_rows_signal.emit(self.playlist_id)
|
||||
self.track_sequence.update()
|
||||
self.update_track_times()
|
||||
|
||||
@ -19,10 +19,10 @@ import pyqtgraph as pg # type: ignore
|
||||
# App imports
|
||||
from classes import ApplicationError, MusicMusterSignals, PlaylistRowDTO, singleton
|
||||
from config import Config
|
||||
import helpers
|
||||
from log import log
|
||||
from music_manager import Music
|
||||
import ds
|
||||
import helpers
|
||||
|
||||
|
||||
class PlaylistRow:
|
||||
@ -32,7 +32,8 @@ class PlaylistRow:
|
||||
|
||||
def __init__(self, dto: PlaylistRowDTO) -> None:
|
||||
"""
|
||||
The dto object will include a Tracks object if this row has a track.
|
||||
The dto object will include row information plus a Tracks object
|
||||
if this row has a track.
|
||||
"""
|
||||
|
||||
self.dto = dto
|
||||
@ -64,7 +65,7 @@ class PlaylistRow:
|
||||
|
||||
# Expose TrackDTO fields as properties
|
||||
@property
|
||||
def artist(self):
|
||||
def artist(self) -> str:
|
||||
if self.dto.track:
|
||||
return self.dto.track.artist
|
||||
else:
|
||||
@ -79,28 +80,28 @@ class PlaylistRow:
|
||||
ds.track_update(self.track_id, dict(artist=str(artist)))
|
||||
|
||||
@property
|
||||
def bitrate(self):
|
||||
def bitrate(self) -> int:
|
||||
if self.dto.track:
|
||||
return self.dto.track.bitrate
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
def duration(self) -> int:
|
||||
if self.dto.track:
|
||||
return self.dto.track.duration
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def fade_at(self):
|
||||
def fade_at(self) -> int:
|
||||
if self.dto.track:
|
||||
return self.dto.track.fade_at
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def intro(self):
|
||||
def intro(self) -> int:
|
||||
if self.dto.track:
|
||||
return self.dto.track.intro
|
||||
else:
|
||||
@ -115,35 +116,35 @@ class PlaylistRow:
|
||||
ds.track_update(self.track_id, dict(intro=str(intro)))
|
||||
|
||||
@property
|
||||
def lastplayed(self):
|
||||
def lastplayed(self) -> dt.datetime | None:
|
||||
if self.dto.track:
|
||||
return self.dto.track.lastplayed
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
def path(self) -> str:
|
||||
if self.dto.track:
|
||||
return self.dto.track.path
|
||||
else:
|
||||
return ""
|
||||
|
||||
@property
|
||||
def silence_at(self):
|
||||
def silence_at(self) -> int:
|
||||
if self.dto.track:
|
||||
return self.dto.track.silence_at
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def start_gap(self):
|
||||
def start_gap(self) -> int:
|
||||
if self.dto.track:
|
||||
return self.dto.track.start_gap
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
def title(self) -> str:
|
||||
if self.dto.track:
|
||||
return self.dto.track.title
|
||||
else:
|
||||
@ -158,7 +159,7 @@ class PlaylistRow:
|
||||
ds.track_update(self.track_id, dict(title=str(title)))
|
||||
|
||||
@property
|
||||
def track_id(self):
|
||||
def track_id(self) -> int:
|
||||
if self.dto.track:
|
||||
return self.dto.track.track_id
|
||||
else:
|
||||
@ -183,7 +184,7 @@ class PlaylistRow:
|
||||
|
||||
# Expose PlaylistRowDTO fields as properties
|
||||
@property
|
||||
def note(self):
|
||||
def note(self) -> str:
|
||||
return self.dto.note
|
||||
|
||||
@note.setter
|
||||
@ -192,7 +193,7 @@ class PlaylistRow:
|
||||
ds.playlistrow_update_note(self.playlistrow_id, str(note))
|
||||
|
||||
@property
|
||||
def played(self):
|
||||
def played(self) -> bool:
|
||||
return self.dto.played
|
||||
|
||||
@played.setter
|
||||
@ -201,15 +202,15 @@ class PlaylistRow:
|
||||
ds.playlistrow_played(self.playlistrow_id, value)
|
||||
|
||||
@property
|
||||
def playlist_id(self):
|
||||
def playlist_id(self) -> int:
|
||||
return self.dto.playlist_id
|
||||
|
||||
@property
|
||||
def playlistrow_id(self):
|
||||
def playlistrow_id(self) -> int:
|
||||
return self.dto.playlistrow_id
|
||||
|
||||
@property
|
||||
def row_number(self):
|
||||
def row_number(self) -> int:
|
||||
return self.dto.row_number
|
||||
|
||||
@row_number.setter
|
||||
|
||||
@ -165,7 +165,7 @@ class MyTestCase(unittest.TestCase):
|
||||
new_order = []
|
||||
for row in ds.playlistrows_by_playlist(playlist.playlist_id):
|
||||
new_order.append(int(row.note))
|
||||
assert new_order == [0, 1, 2, 4, 5, 3, 6, 7, 8, 9]
|
||||
assert new_order == [0, 1, 2, 4, 3, 5, 6, 7, 8, 9]
|
||||
|
||||
def test_move_rows_test2(self):
|
||||
# move row 4 to row 3
|
||||
@ -207,7 +207,7 @@ class MyTestCase(unittest.TestCase):
|
||||
new_order = []
|
||||
for row in ds.playlistrows_by_playlist(playlist.playlist_id):
|
||||
new_order.append(int(row.note))
|
||||
assert new_order == [0, 2, 3, 6, 7, 8, 1, 4, 5, 10, 9]
|
||||
assert new_order == [0, 2, 3, 6, 7, 1, 4, 5, 10, 8, 9]
|
||||
|
||||
def test_move_rows_test5(self):
|
||||
# move rows [3, 6] → 5
|
||||
@ -221,7 +221,7 @@ class MyTestCase(unittest.TestCase):
|
||||
new_order = []
|
||||
for row in ds.playlistrows_by_playlist(playlist.playlist_id):
|
||||
new_order.append(int(row.note))
|
||||
assert new_order == [0, 1, 2, 4, 5, 3, 6, 7, 8, 9, 10]
|
||||
assert new_order == [0, 1, 2, 4, 3, 6, 5, 7, 8, 9, 10]
|
||||
|
||||
def test_move_rows_test6(self):
|
||||
# move rows [3, 5, 6] → 8
|
||||
@ -235,7 +235,7 @@ class MyTestCase(unittest.TestCase):
|
||||
new_order = []
|
||||
for row in ds.playlistrows_by_playlist(playlist.playlist_id):
|
||||
new_order.append(int(row.note))
|
||||
assert new_order == [0, 1, 2, 4, 7, 8, 9, 10, 3, 5, 6]
|
||||
assert new_order == [0, 1, 2, 4, 7, 3, 5, 6, 8, 9, 10]
|
||||
|
||||
def test_move_rows_test7(self):
|
||||
# move rows [7, 8, 10] → 5
|
||||
@ -258,13 +258,13 @@ class MyTestCase(unittest.TestCase):
|
||||
number_of_rows = 11
|
||||
(playlist, model) = self.create_rows("test_move_rows_test8", number_of_rows)
|
||||
|
||||
ds.playlist_move_rows([0, 1, 2, 3], playlist.playlist_id, 0)
|
||||
ds.playlist_move_rows([1, 2, 3], playlist.playlist_id, 0)
|
||||
|
||||
# Check we have all rows and plr_rownums are correct
|
||||
new_order = []
|
||||
for row in ds.playlistrows_by_playlist(playlist.playlist_id):
|
||||
new_order.append(int(row.note))
|
||||
assert new_order == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
assert new_order == [1, 2, 3, 0, 4, 5, 6, 7, 8, 9, 10]
|
||||
|
||||
def test_move_rows_to_playlist(self):
|
||||
number_of_rows = 11
|
||||
|
||||
Loading…
Reference in New Issue
Block a user