Compare commits

...

9 Commits

Author SHA1 Message Date
Keith Edmunds
c6be215bd4 Log signals; show debug to console 2025-04-17 19:13:13 +01:00
Keith Edmunds
22e16144e1 Remove more vlc loggins 2025-04-16 21:03:45 +01:00
Keith Edmunds
1932719fea WIP using signals and no sessions 2025-04-16 21:03:02 +01:00
Keith Edmunds
e9f4ecf5ef Remove vlc logging 2025-04-16 21:02:33 +01:00
Keith Edmunds
ce224a41d1 Implement signal monitor 2025-04-16 21:02:21 +01:00
Keith Edmunds
bf89172f8a Move definition of db into dbmanager 2025-04-16 21:01:35 +01:00
Keith Edmunds
9f234bb007 Fix segfault inserting tracks 2025-04-16 10:33:01 +01:00
Keith Edmunds
6fb6339843 Much improved row move implementation 2025-04-16 10:23:44 +01:00
Keith Edmunds
bf5563f2f1 Black changes 2025-04-16 10:23:17 +01:00
9 changed files with 268 additions and 204 deletions

View File

@ -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
View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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:
"""

View File

@ -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.

View File

@ -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()

View File

@ -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

View File

@ -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