Compare commits

..

No commits in common. "c6be215bd4f270c02c368130aef886f003a322de" and "8fa98a2207083081bb1b98cf138fed14b3e3a7cf" have entirely different histories.

9 changed files with 204 additions and 268 deletions

View File

@ -1,6 +1,4 @@
# Standard library imports # Standard library imports
import os
import sys
# PyQt imports # PyQt imports
@ -8,7 +6,6 @@ import sys
from alchemical import Alchemical # type:ignore from alchemical import Alchemical # type:ignore
# App imports # App imports
from config import Config
class DatabaseManager: class DatabaseManager:
@ -32,12 +29,3 @@ class DatabaseManager:
if DatabaseManager.__instance is None: if DatabaseManager.__instance is None:
DatabaseManager(database_url, **kwargs) DatabaseManager(database_url, **kwargs)
return DatabaseManager.__instance 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
View File

@ -29,9 +29,9 @@ from classes import (
TrackDTO, TrackDTO,
) )
from config import Config from config import Config
from dbmanager import db
from log import log, log_call from log import log, log_call
from models import ( from models import (
db,
NoteColours, NoteColours,
Playdates, Playdates,
PlaylistRows, PlaylistRows,
@ -44,8 +44,8 @@ from models import (
# Configure the dogpile cache region # Configure the dogpile cache region
cache_region = make_region().configure( cache_region = make_region().configure(
"dogpile.cache.memory", # Use in-memory caching for now (switch to Redis if needed) 'dogpile.cache.memory', # Use in-memory caching for now (switch to Redis if needed)
expiration_time=600, # Cache expires after 10 minutes expiration_time=600 # Cache expires after 10 minutes
) )
@ -179,9 +179,7 @@ def notecolours_remove_colour_substring(text: str) -> str:
# Track functions # Track functions
# @log_call # @log_call
def _tracks_where( def _tracks_where(query: BinaryExpression | ColumnElement[bool],) -> list[TrackDTO]:
query: BinaryExpression | ColumnElement[bool],
) -> list[TrackDTO]:
""" """
filter_by_last_played: bool = False, filter_by_last_played: bool = False,
last_played_before: dt.datetime | None = None, last_played_before: dt.datetime | None = None,
@ -437,7 +435,9 @@ def tracks_filtered(filter: Filter) -> list[TrackDTO]:
# @log_call # @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 Update an existing track db entry return the DTO
""" """
@ -472,8 +472,6 @@ def _playlist_check_playlist(
else raise ApplicationError. else raise ApplicationError.
""" """
fixed = False
playlist_rows = ( playlist_rows = (
session.execute( session.execute(
select(PlaylistRows) select(PlaylistRows)
@ -494,13 +492,9 @@ def _playlist_check_playlist(
if fix: if fix:
log.debug(msg) log.debug(msg)
plr.row_number = idx plr.row_number = idx
fixed = True
else: else:
raise ApplicationError(msg) raise ApplicationError(msg)
if fixed:
session.commit()
# @log_call # @log_call
def _playlist_shift_rows( def _playlist_shift_rows(
@ -529,17 +523,13 @@ def _playlists_where(
Return playlists selected by query Return playlists selected by query
""" """
stmt = ( stmt = select(
select( Playlists.favourite,
Playlists.favourite, Playlists.is_template,
Playlists.is_template, Playlists.id.label("playlist_id"),
Playlists.id.label("playlist_id"), Playlists.name,
Playlists.name, Playlists.open,
Playlists.open, ).where(query).order_by(Playlists.tab)
)
.where(query)
.order_by(Playlists.tab)
)
results: list[PlaylistDTO] = [] results: list[PlaylistDTO] = []
@ -607,9 +597,7 @@ def playlists_closed() -> list[PlaylistDTO]:
# @log_call # @log_call
def playlist_create( def playlist_create(name: str, template_id: int, as_template: bool = False) -> PlaylistDTO:
name: str, template_id: int, as_template: bool = False
) -> PlaylistDTO:
""" """
Create playlist and return DTO. Create playlist and return DTO.
""" """
@ -688,7 +676,9 @@ def playlist_mark_status(playlist_id: int, open: bool) -> None:
with db.Session() as session: with db.Session() as session:
session.execute( session.execute(
update(Playlists).where(Playlists.id == playlist_id).values(open=open) update(Playlists)
.where(Playlists.id == playlist_id)
.values(open=open)
) )
session.commit() session.commit()
@ -702,145 +692,88 @@ def playlist_move_rows(
to_playlist_id: int | None = None, to_playlist_id: int | None = None,
) -> None: ) -> None:
""" """
Call helper function depending upon whether we are moving rows within Move rows with or between playlists.
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: Algorithm:
- Sanity check row numbers - 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 - Resequence remaining row numbers
- Make space for moved rows - Make space for moved rows
- Move the PENDING_MOVE rows back and fixup row numbers - Move the PENDING_MOVE rows back and fixup row numbers
- Sanity check row numbers - Sanity check row numbers
""" """
# Sanity check destination not being moved # If to_playlist_id isn't specified, we're moving within the one
if to_row in from_rows: # playlist.
log.error( if to_playlist_id is None:
f"ds._playlist_move_rows_within_playlist: {to_row=} in {from_rows=}" to_playlist_id = from_playlist_id
)
return
with db.Session() as session: with db.Session() as session:
# Sanity check row numbers # Sanity check row numbers
_playlist_check_playlist(session, from_playlist_id, fix=False) _playlist_check_playlist(session, from_playlist_id, fix=False)
_playlist_check_playlist(session, to_playlist_id, fix=False) if from_playlist_id != to_playlist_id:
_playlist_check_playlist(session, to_playlist_id, fix=False)
# Make space in destination playlist # Check there are no playlist rows with playlist_id == PENDING_MOVE
_playlist_shift_rows(session, to_playlist_id, to_row, len(from_rows)) pending_move_rows = playlistrows_by_playlist(Config.PLAYLIST_PENDING_MOVE)
if pending_move_rows:
raise ApplicationError(f"move_rows_to_playlist: {pending_move_rows=}")
# Update database # We need playlist length if we're moving within a playlist. Get
# Build a dictionary of changes to make # 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_list: list[dict[str, int]] = [] update_list: list[dict[str, int]] = []
old_row_to_id = _playlist_rows_to_id(from_playlist_id) next_row = space_row
next_row = to_row # PLAYLIST_PENDING_MOVE may have gaps so don't check it
for row_to_move in playlistrows_by_playlist(
for from_row in from_rows: Config.PLAYLIST_PENDING_MOVE, check_playlist_itegrity=False
plrid = old_row_to_id[from_row] ):
update_list.append( update_list.append(
{"id": plrid, "row_number": next_row} {"id": row_to_move.playlistrow_id, "row_number": next_row}
) )
update_list.append( update_list.append(
{"id": plrid, "playlist_id": to_playlist_id} {"id": row_to_move.playlistrow_id, "playlist_id": to_playlist_id}
) )
next_row += 1 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.execute(update(PlaylistRows), update_list)
session.commit() session.commit()
# Sanity check row numbers # Sanity check row numbers
_playlist_check_playlist(session, from_playlist_id, fix=False) _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]: def playlists_open() -> list[PlaylistDTO]:
@ -858,7 +791,9 @@ def playlist_rename(playlist_id: int, new_name: str) -> None:
with db.Session() as session: with db.Session() as session:
session.execute( 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() session.commit()
@ -968,6 +903,7 @@ def playlist_remove_rows(playlist_id: int, row_numbers: list[int]) -> None:
) )
# Fixup row number to remove gaps # Fixup row number to remove gaps
_playlist_check_playlist(session, playlist_id, fix=True) _playlist_check_playlist(session, playlist_id, fix=True)
session.commit()
# @log_call # @log_call
@ -983,9 +919,11 @@ def playlist_save_tabs(playlist_id_to_tab: dict[int, int]) -> None:
.where(Playlists.id.in_(playlist_id_to_tab.keys())) .where(Playlists.id.in_(playlist_id_to_tab.keys()))
.values(tab=None) .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( session.execute(
update(Playlists).where(Playlists.id == playlist_id).values(tab=tab) update(Playlists)
.where(Playlists.id == playlist_id)
.values(tab=tab)
) )
session.commit() session.commit()
@ -1005,7 +943,6 @@ def playlist_update_template_favourite(template_id: int, favourite: bool) -> Non
# Playlist Rows # Playlist Rows
# @log_call # @log_call
def playlistrow_by_id(playlistrow_id: int) -> PlaylistRowDTO | None: def playlistrow_by_id(playlistrow_id: int) -> PlaylistRowDTO | None:
""" """
@ -1014,9 +951,7 @@ def playlistrow_by_id(playlistrow_id: int) -> PlaylistRowDTO | None:
with db.Session() as session: with db.Session() as session:
record = ( record = (
session.execute( session.execute(select(PlaylistRows).where(PlaylistRows.id == playlistrow_id))
select(PlaylistRows).where(PlaylistRows.id == playlistrow_id)
)
.scalars() .scalars()
.one_or_none() .one_or_none()
) )
@ -1042,7 +977,9 @@ def playlistrow_by_id(playlistrow_id: int) -> PlaylistRowDTO | None:
def playlistrows_by_playlist( def playlistrows_by_playlist(
playlist_id: int, check_playlist_itegrity: bool = True playlist_id: int, check_playlist_itegrity: bool = True
) -> list[PlaylistRowDTO]: ) -> list[PlaylistRowDTO]:
with db.Session() as session: with db.Session() as session:
# TODO: would be good to be confident at removing this # TODO: would be good to be confident at removing this
if check_playlist_itegrity: if check_playlist_itegrity:
_playlist_check_playlist( _playlist_check_playlist(
@ -1057,6 +994,7 @@ def playlistrows_by_playlist(
dto_list = [] dto_list = []
for record in records: for record in records:
track = None track = None
if record.track_id: if record.track_id:
track = track_by_id(record.track_id) track = track_by_id(record.track_id)
@ -1102,9 +1040,7 @@ def playlistrow_played(playlistrow_id: int, status: bool) -> None:
with db.Session() as session: with db.Session() as session:
session.execute( session.execute(
update(PlaylistRows) update(PlaylistRows).where(PlaylistRows.id == playlistrow_id).values(played=status)
.where(PlaylistRows.id == playlistrow_id)
.values(played=status)
) )
session.commit() session.commit()
@ -1160,7 +1096,10 @@ def playdates_between_dates(
Playdates.lastplayed, Playdates.lastplayed,
Playdates.track_id, Playdates.track_id,
Playdates.track, Playdates.track,
).where(Playdates.lastplayed >= start, Playdates.lastplayed <= end) ).where(
Playdates.lastplayed >= start,
Playdates.lastplayed <= end
)
results: list[PlaydatesDTO] = [] results: list[PlaydatesDTO] = []
@ -1198,7 +1137,10 @@ def _queries_where(
results: list[QueryDTO] = [] results: list[QueryDTO] = []
with db.Session() as session: 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: for record in records:
dto = QueryDTO( dto = QueryDTO(
favourite=record.favourite, favourite=record.favourite,
@ -1331,3 +1273,4 @@ def db_name_get() -> str:
dbname = session.bind.engine.url.database dbname = session.bind.engine.url.database
return dbname return dbname
return Config.DB_NOT_FOUND return Config.DB_NOT_FOUND

View File

@ -4,7 +4,7 @@ disable_existing_loggers: True
formatters: formatters:
colored: colored:
(): colorlog.ColoredFormatter (): colorlog.ColoredFormatter
format: "%(log_color)s[%(asctime)s] %(filename)s.%(funcName)s:%(lineno)s %(light_blue)s%(message)s" format: "%(log_color)s[%(asctime)s] %(filename)s.%(funcName)s:%(lineno)s %(blue)s%(message)s"
datefmt: "%H:%M:%S" datefmt: "%H:%M:%S"
syslog: syslog:
format: "[%(name)s] %(filename)s:%(lineno)s %(leveltag)s: %(message)s" format: "[%(name)s] %(filename)s:%(lineno)s %(leveltag)s: %(message)s"
@ -25,7 +25,6 @@ filters:
musicmuster: musicmuster:
- update_clocks - update_clocks
- play_next - play_next
- show_signal
handlers: handlers:
stderr: stderr:

View File

@ -34,6 +34,14 @@ import dbtables
from log import log 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 # Configure the cache region
cache_region = make_region().configure( cache_region = make_region().configure(
'dogpile.cache.memory', # Use in-memory caching for now (switch to Redis if needed) 'dogpile.cache.memory', # Use in-memory caching for now (switch to Redis if needed)

View File

@ -16,11 +16,44 @@ from PyQt6.QtCore import (
) )
# App imports # App imports
from classes import MusicMusterSignals, singleton from classes import singleton
from config import Config from config import Config
import helpers import helpers
from log import log 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): class _FadeTrack(QThread):
finished = pyqtSignal() finished = pyqtSignal()
@ -80,7 +113,32 @@ class Music:
self.player: vlc.MediaPlayer | None = None self.player: vlc.MediaPlayer | None = None
self.max_volume: int = Config.VLC_VOLUME_DEFAULT self.max_volume: int = Config.VLC_VOLUME_DEFAULT
self.start_dt: dt.datetime | None = None self.start_dt: dt.datetime | None = None
self.signals = MusicMusterSignals()
# 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}")
def fade(self, fade_seconds: int) -> None: def fade(self, fade_seconds: int) -> None:
""" """

View File

@ -2,9 +2,8 @@
# Standard library imports # Standard library imports
from __future__ import annotations from __future__ import annotations
from functools import partial
from slugify import slugify # type: ignore from slugify import slugify # type: ignore
from typing import Any, Callable from typing import Callable
import argparse import argparse
from dataclasses import dataclass from dataclasses import dataclass
import datetime as dt import datetime as dt
@ -94,66 +93,6 @@ import ds
import helpers 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: class Current:
base_model: PlaylistModel base_model: PlaylistModel
proxy_model: PlaylistProxyModel proxy_model: PlaylistProxyModel
@ -831,7 +770,6 @@ class QueryDialog(QDialog):
super().__init__() super().__init__()
self.playlist_id = playlist_id self.playlist_id = playlist_id
self.default = default self.default = default
self.signals = MusicMusterSignals()
# Build a list of (query-name, playlist-id) tuples # Build a list of (query-name, playlist-id) tuples
self.selected_tracks: list[int] = [] self.selected_tracks: list[int] = []
@ -924,9 +862,9 @@ class QueryDialog(QDialog):
# new_row_number = self.current_row_or_end() # new_row_number = self.current_row_or_end()
# base_model = self.current.base_model # base_model = self.current.base_model
for track_id in self.selected_tracks: for track_id in self.query_dialog.selected_tracks:
insert_track_data = InsertTrack(self.playlist_id, track_id, note="") insert_track_data = InsertTrack(self.playlist_id, track_id, note="")
self.signals.signal_insert_track.emit(insert_track_data) self.signals.signal_insert_track.emit(InsertTrack(insert_track_data))
self.accept() self.accept()
@ -1242,9 +1180,6 @@ class Window(QMainWindow):
self.action_quicklog = QShortcut(QKeySequence("Ctrl+L"), self) self.action_quicklog = QShortcut(QKeySequence("Ctrl+L"), self)
self.action_quicklog.activated.connect(self.quicklog) self.action_quicklog.activated.connect(self.quicklog)
# Optionally print signals
self.signal_monitor = SignalMonitor()
# Load playlists # Load playlists
self.load_last_playlists() self.load_last_playlists()
@ -1584,7 +1519,9 @@ class Window(QMainWindow):
else: else:
return None 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. Have user select a template. Return the template.id, or None if they cancel.
template_id of zero means don't use a template. template_id of zero means don't use a template.
@ -1944,7 +1881,7 @@ class Window(QMainWindow):
InsertTrack( InsertTrack(
playlist_id=self.current.base_model.playlist_id, playlist_id=self.current.base_model.playlist_id,
track_id=None, track_id=None,
note=dlg.textValue(), note=dlg.textValue()
) )
) )
@ -1999,7 +1936,8 @@ class Window(QMainWindow):
# Save the selected PlaylistRows items ready for a later # Save the selected PlaylistRows items ready for a later
# paste # paste
self.move_source = MoveSource( 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=}") log.debug(f"mark_rows_for_moving(): {self.move_source=}")
@ -2116,7 +2054,9 @@ class Window(QMainWindow):
to_playlist_model.set_next_row(to_row) to_playlist_model.set_next_row(to_row)
# @log_call # @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. Play next track, optionally from passed position.

View File

@ -717,11 +717,12 @@ class PlaylistModel(QAbstractTableModel):
track_id=row_data.track_id, track_id=row_data.track_id,
note=row_data.note, note=row_data.note,
) )
super().endInsertRows()
# Need to refresh self.playlist_rows because row numbers will have # Need to refresh self.playlist_rows because row numbers will have
# changed # changed
self.refresh_data() self.refresh_data()
super().endInsertRows()
self.signals.resize_rows_signal.emit(self.playlist_id) self.signals.resize_rows_signal.emit(self.playlist_id)
self.track_sequence.update() self.track_sequence.update()
self.update_track_times() self.update_track_times()

View File

@ -19,10 +19,10 @@ import pyqtgraph as pg # type: ignore
# App imports # App imports
from classes import ApplicationError, MusicMusterSignals, PlaylistRowDTO, singleton from classes import ApplicationError, MusicMusterSignals, PlaylistRowDTO, singleton
from config import Config from config import Config
import helpers
from log import log from log import log
from music_manager import Music from music_manager import Music
import ds import ds
import helpers
class PlaylistRow: class PlaylistRow:
@ -32,8 +32,7 @@ class PlaylistRow:
def __init__(self, dto: PlaylistRowDTO) -> None: def __init__(self, dto: PlaylistRowDTO) -> None:
""" """
The dto object will include row information plus a Tracks object The dto object will include a Tracks object if this row has a track.
if this row has a track.
""" """
self.dto = dto self.dto = dto
@ -65,7 +64,7 @@ class PlaylistRow:
# Expose TrackDTO fields as properties # Expose TrackDTO fields as properties
@property @property
def artist(self) -> str: def artist(self):
if self.dto.track: if self.dto.track:
return self.dto.track.artist return self.dto.track.artist
else: else:
@ -80,28 +79,28 @@ class PlaylistRow:
ds.track_update(self.track_id, dict(artist=str(artist))) ds.track_update(self.track_id, dict(artist=str(artist)))
@property @property
def bitrate(self) -> int: def bitrate(self):
if self.dto.track: if self.dto.track:
return self.dto.track.bitrate return self.dto.track.bitrate
else: else:
return 0 return 0
@property @property
def duration(self) -> int: def duration(self):
if self.dto.track: if self.dto.track:
return self.dto.track.duration return self.dto.track.duration
else: else:
return 0 return 0
@property @property
def fade_at(self) -> int: def fade_at(self):
if self.dto.track: if self.dto.track:
return self.dto.track.fade_at return self.dto.track.fade_at
else: else:
return 0 return 0
@property @property
def intro(self) -> int: def intro(self):
if self.dto.track: if self.dto.track:
return self.dto.track.intro return self.dto.track.intro
else: else:
@ -116,35 +115,35 @@ class PlaylistRow:
ds.track_update(self.track_id, dict(intro=str(intro))) ds.track_update(self.track_id, dict(intro=str(intro)))
@property @property
def lastplayed(self) -> dt.datetime | None: def lastplayed(self):
if self.dto.track: if self.dto.track:
return self.dto.track.lastplayed return self.dto.track.lastplayed
else: else:
return None return None
@property @property
def path(self) -> str: def path(self):
if self.dto.track: if self.dto.track:
return self.dto.track.path return self.dto.track.path
else: else:
return "" return ""
@property @property
def silence_at(self) -> int: def silence_at(self):
if self.dto.track: if self.dto.track:
return self.dto.track.silence_at return self.dto.track.silence_at
else: else:
return 0 return 0
@property @property
def start_gap(self) -> int: def start_gap(self):
if self.dto.track: if self.dto.track:
return self.dto.track.start_gap return self.dto.track.start_gap
else: else:
return 0 return 0
@property @property
def title(self) -> str: def title(self):
if self.dto.track: if self.dto.track:
return self.dto.track.title return self.dto.track.title
else: else:
@ -159,7 +158,7 @@ class PlaylistRow:
ds.track_update(self.track_id, dict(title=str(title))) ds.track_update(self.track_id, dict(title=str(title)))
@property @property
def track_id(self) -> int: def track_id(self):
if self.dto.track: if self.dto.track:
return self.dto.track.track_id return self.dto.track.track_id
else: else:
@ -184,7 +183,7 @@ class PlaylistRow:
# Expose PlaylistRowDTO fields as properties # Expose PlaylistRowDTO fields as properties
@property @property
def note(self) -> str: def note(self):
return self.dto.note return self.dto.note
@note.setter @note.setter
@ -193,7 +192,7 @@ class PlaylistRow:
ds.playlistrow_update_note(self.playlistrow_id, str(note)) ds.playlistrow_update_note(self.playlistrow_id, str(note))
@property @property
def played(self) -> bool: def played(self):
return self.dto.played return self.dto.played
@played.setter @played.setter
@ -202,15 +201,15 @@ class PlaylistRow:
ds.playlistrow_played(self.playlistrow_id, value) ds.playlistrow_played(self.playlistrow_id, value)
@property @property
def playlist_id(self) -> int: def playlist_id(self):
return self.dto.playlist_id return self.dto.playlist_id
@property @property
def playlistrow_id(self) -> int: def playlistrow_id(self):
return self.dto.playlistrow_id return self.dto.playlistrow_id
@property @property
def row_number(self) -> int: def row_number(self):
return self.dto.row_number return self.dto.row_number
@row_number.setter @row_number.setter

View File

@ -165,7 +165,7 @@ class MyTestCase(unittest.TestCase):
new_order = [] new_order = []
for row in ds.playlistrows_by_playlist(playlist.playlist_id): for row in ds.playlistrows_by_playlist(playlist.playlist_id):
new_order.append(int(row.note)) new_order.append(int(row.note))
assert new_order == [0, 1, 2, 4, 3, 5, 6, 7, 8, 9] assert new_order == [0, 1, 2, 4, 5, 3, 6, 7, 8, 9]
def test_move_rows_test2(self): def test_move_rows_test2(self):
# move row 4 to row 3 # move row 4 to row 3
@ -207,7 +207,7 @@ class MyTestCase(unittest.TestCase):
new_order = [] new_order = []
for row in ds.playlistrows_by_playlist(playlist.playlist_id): for row in ds.playlistrows_by_playlist(playlist.playlist_id):
new_order.append(int(row.note)) new_order.append(int(row.note))
assert new_order == [0, 2, 3, 6, 7, 1, 4, 5, 10, 8, 9] assert new_order == [0, 2, 3, 6, 7, 8, 1, 4, 5, 10, 9]
def test_move_rows_test5(self): def test_move_rows_test5(self):
# move rows [3, 6] → 5 # move rows [3, 6] → 5
@ -221,7 +221,7 @@ class MyTestCase(unittest.TestCase):
new_order = [] new_order = []
for row in ds.playlistrows_by_playlist(playlist.playlist_id): for row in ds.playlistrows_by_playlist(playlist.playlist_id):
new_order.append(int(row.note)) new_order.append(int(row.note))
assert new_order == [0, 1, 2, 4, 3, 6, 5, 7, 8, 9, 10] assert new_order == [0, 1, 2, 4, 5, 3, 6, 7, 8, 9, 10]
def test_move_rows_test6(self): def test_move_rows_test6(self):
# move rows [3, 5, 6] → 8 # move rows [3, 5, 6] → 8
@ -235,7 +235,7 @@ class MyTestCase(unittest.TestCase):
new_order = [] new_order = []
for row in ds.playlistrows_by_playlist(playlist.playlist_id): for row in ds.playlistrows_by_playlist(playlist.playlist_id):
new_order.append(int(row.note)) new_order.append(int(row.note))
assert new_order == [0, 1, 2, 4, 7, 3, 5, 6, 8, 9, 10] assert new_order == [0, 1, 2, 4, 7, 8, 9, 10, 3, 5, 6]
def test_move_rows_test7(self): def test_move_rows_test7(self):
# move rows [7, 8, 10] → 5 # move rows [7, 8, 10] → 5
@ -258,13 +258,13 @@ class MyTestCase(unittest.TestCase):
number_of_rows = 11 number_of_rows = 11
(playlist, model) = self.create_rows("test_move_rows_test8", number_of_rows) (playlist, model) = self.create_rows("test_move_rows_test8", number_of_rows)
ds.playlist_move_rows([1, 2, 3], playlist.playlist_id, 0) ds.playlist_move_rows([0, 1, 2, 3], playlist.playlist_id, 0)
# Check we have all rows and plr_rownums are correct # Check we have all rows and plr_rownums are correct
new_order = [] new_order = []
for row in ds.playlistrows_by_playlist(playlist.playlist_id): for row in ds.playlistrows_by_playlist(playlist.playlist_id):
new_order.append(int(row.note)) new_order.append(int(row.note))
assert new_order == [1, 2, 3, 0, 4, 5, 6, 7, 8, 9, 10] assert new_order == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def test_move_rows_to_playlist(self): def test_move_rows_to_playlist(self):
number_of_rows = 11 number_of_rows = 11