Compare commits

...

5 Commits

Author SHA1 Message Date
Keith Edmunds
ad391aedc6 Rewrite thread management for fade graph generation 2025-04-24 11:49:04 +01:00
Keith Edmunds
735c927864 Make Signals more apparent in logs (prefix SIGNAL) 2025-04-23 17:12:02 +01:00
Keith Edmunds
e2aac65dac Remove spurious '.id' references 2025-04-23 17:11:33 +01:00
Keith Edmunds
86c3c3fd80 Black 2025-04-22 21:56:44 +01:00
Keith Edmunds
a4ba013306 Change id columns to TABLENAME_id 2025-04-22 20:18:56 +01:00
15 changed files with 383 additions and 214 deletions

View File

@ -29,5 +29,3 @@ class DatabaseManager:
if DatabaseManager.__instance is None:
DatabaseManager(database_url, **kwargs)
return DatabaseManager.__instance

View File

@ -52,7 +52,7 @@ class JSONEncodedDict(TypeDecorator):
class NoteColours(Model):
__tablename__ = "notecolours"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
notecolour_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
substring: Mapped[str] = mapped_column(String(256), index=True, unique=True)
colour: Mapped[str] = mapped_column(String(21), index=False)
enabled: Mapped[bool] = mapped_column(default=True, index=True)
@ -64,7 +64,7 @@ class NoteColours(Model):
def __repr__(self) -> str:
return (
f"<NoteColours(id={self.id}, substring={self.substring}, "
f"<NoteColours(id={self.notecolour_id}, substring={self.substring}, "
f"colour={self.colour}>"
)
@ -92,9 +92,11 @@ class NoteColours(Model):
class Playdates(Model):
__tablename__ = "playdates"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
playdate_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
lastplayed: Mapped[dt.datetime] = mapped_column(index=True)
track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id", ondelete="CASCADE"))
track_id: Mapped[int] = mapped_column(
ForeignKey("tracks.track_id", ondelete="CASCADE")
)
track: Mapped["Tracks"] = relationship(
"Tracks",
back_populates="playdates",
@ -116,7 +118,7 @@ class Playdates(Model):
def __repr__(self) -> str:
return (
f"<Playdates(id={self.id}, track_id={self.track_id} "
f"<Playdates(id={self.playdate_id}, track_id={self.track_id} "
f"lastplayed={self.lastplayed}>"
)
@ -128,7 +130,7 @@ class Playlists(Model):
__tablename__ = "playlists"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
playlist_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(32), unique=True)
last_used: Mapped[Optional[dt.datetime]] = mapped_column(DateTime, default=None)
tab: Mapped[Optional[int]] = mapped_column(default=None)
@ -146,7 +148,7 @@ class Playlists(Model):
def __repr__(self) -> str:
return (
f"<Playlists(id={self.id}, name={self.name}, "
f"<Playlists(id={self.playlist_id}, name={self.name}, "
f"is_templatee={self.is_template}, open={self.open}>"
)
@ -161,24 +163,24 @@ class Playlists(Model):
# If a template is specified, copy from it
if template_id:
PlaylistRows.copy_playlist(session, template_id, self.id)
PlaylistRows.copy_playlist(session, template_id, self.playlist_id)
class PlaylistRows(Model):
__tablename__ = "playlist_rows"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
playlistrow_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
row_number: Mapped[int] = mapped_column(index=True)
note: Mapped[str] = mapped_column(
String(2048), index=False, default="", nullable=False
)
playlist_id: Mapped[int] = mapped_column(
ForeignKey("playlists.id", ondelete="CASCADE"), index=True
ForeignKey("playlists.playlist_id", ondelete="CASCADE"), index=True
)
playlist: Mapped[Playlists] = relationship(back_populates="rows")
track_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("tracks.id", ondelete="CASCADE")
ForeignKey("tracks.track_id", ondelete="CASCADE")
)
track: Mapped["Tracks"] = relationship(
"Tracks",
@ -190,7 +192,7 @@ class PlaylistRows(Model):
def __repr__(self) -> str:
return (
f"<PlaylistRows(id={self.id}, playlist_id={self.playlist_id}, "
f"<PlaylistRows(id={self.playlistrow_id}, playlist_id={self.playlist_id}, "
f"track_id={self.track_id}, "
f"note={self.note}, row_number={self.row_number}>"
)
@ -217,10 +219,14 @@ class PlaylistRows(Model):
class Queries(Model):
__tablename__ = "queries"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
query_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
_filter_data: Mapped[dict | None] = mapped_column("filter_data", JSONEncodedDict, nullable=False)
favourite: Mapped[bool] = mapped_column(Boolean, nullable=False, index=False, default=False)
_filter_data: Mapped[dict | None] = mapped_column(
"filter_data", JSONEncodedDict, nullable=False
)
favourite: Mapped[bool] = mapped_column(
Boolean, nullable=False, index=False, default=False
)
def _get_filter(self) -> Filter:
"""Convert stored JSON dictionary to a Filter object."""
@ -236,7 +242,7 @@ class Queries(Model):
filter = property(_get_filter, _set_filter)
def __repr__(self) -> str:
return f"<Queries(id={self.id}, name={self.name}, filter={self.filter})>"
return f"<Queries(id={self.query_id}, name={self.name}, filter={self.filter})>"
def __init__(
self,
@ -260,7 +266,7 @@ class Settings(Model):
__tablename__ = "settings"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
setting_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(64), unique=True)
f_datetime: Mapped[Optional[dt.datetime]] = mapped_column(default=None)
f_int: Mapped[Optional[int]] = mapped_column(default=None)
@ -268,7 +274,7 @@ class Settings(Model):
def __repr__(self) -> str:
return (
f"<Settings(id={self.id}, name={self.name}, "
f"<Settings(id={self.setting_id}, name={self.name}, "
f"f_datetime={self.f_datetime}, f_int={self.f_int}, f_string={self.f_string}>"
)
@ -282,7 +288,7 @@ class Settings(Model):
class Tracks(Model):
__tablename__ = "tracks"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
track_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
artist: Mapped[str] = mapped_column(String(256), index=True)
bitrate: Mapped[int] = mapped_column(default=None)
duration: Mapped[int] = mapped_column(index=True)
@ -308,7 +314,7 @@ class Tracks(Model):
def __repr__(self) -> str:
return (
f"<Track(id={self.id}, title={self.title}, "
f"<Track(id={self.track_id}, title={self.title}, "
f"artist={self.artist}, path={self.path}>"
)

107
app/ds.py
View File

@ -110,7 +110,7 @@ def _notecolours_all(session: Session) -> list[NoteColoursDTO]:
results: list[NoteColoursDTO] = []
for record in records:
result = NoteColoursDTO(
notecolour_id=record.id,
notecolour_id=record.notecolour_id,
substring=record.substring,
colour=record.colour,
enabled=record.enabled,
@ -211,7 +211,7 @@ def _tracks_where(
)
stmt = (
select(
Tracks.id.label("track_id"),
Tracks.track_id,
Tracks.artist,
Tracks.bitrate,
Tracks.duration,
@ -223,7 +223,9 @@ def _tracks_where(
Tracks.title,
latest_playdate_subq.c.lastplayed,
)
.outerjoin(latest_playdate_subq, Tracks.id == latest_playdate_subq.c.track_id)
.outerjoin(
latest_playdate_subq, Tracks.track_id == latest_playdate_subq.c.track_id
)
.where(query)
)
@ -259,7 +261,7 @@ def track_add_to_header(playlistrow_id: int, track_id: int) -> None:
with db.Session() as session:
session.execute(
update(PlaylistRows)
.where(PlaylistRows.id == playlistrow_id)
.where(PlaylistRows.playlistrow_id == playlistrow_id)
.values(track_id=track_id)
)
session.commit()
@ -268,7 +270,7 @@ def track_add_to_header(playlistrow_id: int, track_id: int) -> None:
def tracks_all() -> list[TrackDTO]:
"""Return a list of all tracks"""
return _tracks_where(Tracks.id > 0)
return _tracks_where(Tracks.track_id > 0)
def tracks_by_artist(filter_str: str) -> list[TrackDTO]:
@ -284,7 +286,7 @@ def track_by_id(track_id: int) -> TrackDTO | None:
Return track with specified id
"""
track_list = _tracks_where(Tracks.id == track_id)
track_list = _tracks_where(Tracks.track_id == track_id)
if not track_list:
return None
if len(track_list) > 1:
@ -333,7 +335,7 @@ def track_create(metadata: dict[str, str | int | float]) -> TrackDTO:
bitrate=int(metadata["bitrate"]),
)
track_id = track.id
track_id = track.track_id
session.commit()
except Exception:
raise ApplicationError("Can't create Track")
@ -387,8 +389,8 @@ def tracks_filtered(filter: Filter) -> list[TrackDTO]:
# Process comparator
if filter.last_played_comparator == Config.FILTER_PLAYED_COMPARATOR_NEVER:
# Select tracks that have never been played
query = query.outerjoin(Playdates, Tracks.id == Playdates.track_id).where(
Playdates.id.is_(None)
query = query.outerjoin(Playdates, Tracks.track_id == Playdates.track_id).where(
Playdates.playdate_id.is_(None)
)
else:
# Last played specification
@ -414,7 +416,7 @@ def tracks_filtered(filter: Filter) -> list[TrackDTO]:
.group_by(Playdates.track_id)
.subquery()
)
query = query.join(subquery, Tracks.id == subquery.c.track_id).where(
query = query.join(subquery, Tracks.track_id == subquery.c.track_id).where(
subquery.c.max_last_played < before
)
@ -438,7 +440,7 @@ def tracks_filtered(filter: Filter) -> list[TrackDTO]:
silence_at=record.silence_at,
start_gap=record.start_gap,
title=record.title,
track_id=record.id,
track_id=record.track_id,
)
results.append(dto)
@ -498,7 +500,7 @@ def _playlist_check_playlist(
msg = (
"_check_playlist_integrity: incorrect row number "
f"({plr.id=}, {plr.row_number=}, {idx=})"
f"({plr.playlistrow_id=}, {plr.row_number=}, {idx=})"
)
if fix:
log.debug(msg)
@ -542,7 +544,7 @@ def _playlists_where(
select(
Playlists.favourite,
Playlists.is_template,
Playlists.id.label("playlist_id"),
Playlists.playlist_id,
Playlists.name,
Playlists.open,
)
@ -570,7 +572,7 @@ def _playlists_where(
def playlists_all():
"""Return all playlists"""
return _playlists_where(Playlists.id > 0)
return _playlists_where(Playlists.playlist_id > 0)
# @log_call
@ -579,7 +581,7 @@ def playlist_by_id(playlist_id: int) -> PlaylistDTO | None:
Return playlist with specified id
"""
playlist_list = _playlists_where(Playlists.id == playlist_id)
playlist_list = _playlists_where(Playlists.playlist_id == playlist_id)
if not playlist_list:
return None
if len(playlist_list) > 1:
@ -627,7 +629,7 @@ def playlist_create(
try:
playlist = Playlists(session, name, template_id)
playlist.is_template = as_template
playlist_id = playlist.id
playlist_id = playlist.playlist_id
session.commit()
except Exception:
raise ApplicationError("Can't create Playlist")
@ -679,7 +681,7 @@ def playlist_insert_row(
track_id=track_id,
)
session.commit()
playlist_row_id = playlist_row.id
playlist_row_id = playlist_row.playlistrow_id
# Sanity check
_playlist_check_playlist(session, playlist_id, fix=False)
@ -697,7 +699,9 @@ 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.playlist_id == playlist_id)
.values(open=open)
)
session.commit()
@ -744,9 +748,7 @@ def _playlist_move_rows_between_playlists(
# 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=}"
)
log.error(f"ds._playlist_move_rows_within_playlist: {to_row=} in {from_rows=}")
return
with db.Session() as session:
@ -765,12 +767,8 @@ def _playlist_move_rows_between_playlists(
for from_row in from_rows:
plrid = old_row_to_id[from_row]
update_list.append(
{"id": plrid, "row_number": next_row}
)
update_list.append(
{"id": plrid, "playlist_id": to_playlist_id}
)
update_list.append({"playlistrow_id": plrid, "row_number": next_row})
update_list.append({"playlistrow_id": plrid, "playlist_id": to_playlist_id})
next_row += 1
session.execute(update(PlaylistRows), update_list)
@ -778,7 +776,7 @@ def _playlist_move_rows_between_playlists(
# Resequence row numbers in source
_playlist_check_playlist(session, from_playlist_id, fix=True)
# Sanity check destionation
# Sanity check destination
_playlist_check_playlist(session, from_playlist_id, fix=False)
@ -788,8 +786,7 @@ def _playlist_rows_to_id(playlist_id: int) -> dict[int, int]:
"""
row_to_id = {
p.row_number: p.playlistrow_id
for p in playlistrows_by_playlist(playlist_id)
p.row_number: p.playlistrow_id for p in playlistrows_by_playlist(playlist_id)
}
return row_to_id
@ -813,9 +810,7 @@ def _playlist_move_rows_within_playlist(
# 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=}"
)
log.error(f"ds._playlist_move_rows_within_playlist: {to_row=} in {from_rows=}")
return
with db.Session() as session:
@ -842,9 +837,9 @@ def _playlist_move_rows_within_playlist(
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})
update_list.append({"playlistrow_id": plrid, "row_number": new_row})
# Updte database
# Update database
session.execute(update(PlaylistRows), update_list)
session.commit()
@ -867,7 +862,9 @@ 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.playlist_id == playlist_id)
.values(name=new_name)
)
session.commit()
@ -989,12 +986,14 @@ def playlist_save_tabs(playlist_id_to_tab: dict[int, int]) -> None:
# Clear all existing tab numbers
session.execute(
update(Playlists)
.where(Playlists.id.in_(playlist_id_to_tab.keys()))
.where(Playlists.playlist_id.in_(playlist_id_to_tab.keys()))
.values(tab=None)
)
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.playlist_id == playlist_id)
.values(tab=tab)
)
session.commit()
@ -1006,7 +1005,7 @@ def playlist_update_template_favourite(template_id: int, favourite: bool) -> Non
with db.Session() as session:
session.execute(
update(Playlists)
.where(Playlists.id == template_id)
.where(Playlists.playlist_id == template_id)
.values(favourite=favourite)
)
session.commit()
@ -1024,7 +1023,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)
select(PlaylistRows).where(
PlaylistRows.playlistrow_id == playlistrow_id
)
)
.scalars()
.one_or_none()
@ -1040,7 +1041,7 @@ def playlistrow_by_id(playlistrow_id: int) -> PlaylistRowDTO | None:
note=record.note,
played=record.played,
playlist_id=record.playlist_id,
playlistrow_id=record.id,
playlistrow_id=record.playlistrow_id,
row_number=record.row_number,
track=track,
)
@ -1074,7 +1075,7 @@ def playlistrows_by_playlist(
note=record.note,
played=record.played,
playlist_id=record.playlist_id,
playlistrow_id=record.id,
playlistrow_id=record.playlistrow_id,
row_number=record.row_number,
track=track,
)
@ -1112,7 +1113,7 @@ def playlistrow_played(playlistrow_id: int, status: bool) -> None:
with db.Session() as session:
session.execute(
update(PlaylistRows)
.where(PlaylistRows.id == playlistrow_id)
.where(PlaylistRows.playlistrow_id == playlistrow_id)
.values(played=status)
)
session.commit()
@ -1165,7 +1166,7 @@ def playdates_between_dates(
end = dt.datetime.now()
stmt = select(
Playdates.id.label("playdate_id"),
Playdates.playdate_id,
Playdates.lastplayed,
Playdates.track_id,
Playdates.track,
@ -1213,7 +1214,7 @@ def _queries_where(
favourite=record.favourite,
filter=record.filter,
name=record.name,
query_id=record.id,
query_id=record.query_id,
)
results.append(dto)
@ -1223,14 +1224,14 @@ def _queries_where(
def queries_all(favourites_only: bool = False) -> list[QueryDTO]:
"""Return a list of all queries"""
query = Queries.id > 0
query = Queries.query_id > 0
return _queries_where(query)
def query_by_id(query_id: int) -> QueryDTO | None:
"""Return query"""
query_list = _queries_where(Queries.id == query_id)
query_list = _queries_where(Queries.query_id == query_id)
if not query_list:
return None
if len(query_list) > 1:
@ -1246,7 +1247,7 @@ def query_create(name: str, filter: Filter) -> QueryDTO:
with db.Session() as session:
try:
query = Queries(session=session, name=name, filter=filter)
query_id = query.id
query_id = query.query_id
session.commit()
except Exception:
raise ApplicationError("Can't create Query")
@ -1272,7 +1273,9 @@ def query_update_favourite(query_id: int, favourite: bool) -> None:
with db.Session() as session:
session.execute(
update(Queries).where(Queries.id == query_id).values(favourite=favourite)
update(Queries)
.where(Queries.query_id == query_id)
.values(favourite=favourite)
)
session.commit()
@ -1282,7 +1285,7 @@ def query_update_filter(query_id: int, filter: Filter) -> None:
with db.Session() as session:
session.execute(
update(Queries).where(Queries.id == query_id).values(filter=filter)
update(Queries).where(Queries.query_id == query_id).values(filter=filter)
)
session.commit()
@ -1291,7 +1294,9 @@ def query_update_name(query_id: int, name: str) -> None:
"""Update query name"""
with db.Session() as session:
session.execute(update(Queries).where(Queries.id == query_id).values(name=name))
session.execute(
update(Queries).where(Queries.query_id == query_id).values(name=name)
)
session.commit()

View File

@ -101,9 +101,13 @@ def handle_exception(exc_type, exc_value, exc_traceback):
log.error(logmsg)
else:
# Handle unexpected errors (log and display)
error_msg = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
error_msg = "".join(
traceback.format_exception(exc_type, exc_value, exc_traceback)
)
print(stackprinter.format(exc_value, suppressed_paths=['/.venv'], style='darkbg'))
print(
stackprinter.format(exc_value, suppressed_paths=["/.venv"], style="darkbg")
)
stack = stackprinter.format(exc_value)
log.error(stack)
@ -147,6 +151,7 @@ def log_call(func):
except Exception as e:
log.debug(f"exception in {func.__name__}: {e}", stacklevel=2)
raise
return wrapper

View File

@ -147,7 +147,7 @@ class Music:
< dt.timedelta(microseconds=Config.PLAY_SETTLE)
)
# @log_call
# @log_call
def play(
self,
path: str,
@ -179,8 +179,12 @@ class Music:
return
self.events = self.player.event_manager()
self.events.event_attach(vlc.EventType.MediaPlayerEndReached, self.track_end_event_handler)
self.events.event_attach(vlc.EventType.MediaPlayerStopped, self.track_end_event_handler)
self.events.event_attach(
vlc.EventType.MediaPlayerEndReached, self.track_end_event_handler
)
self.events.event_attach(
vlc.EventType.MediaPlayerStopped, self.track_end_event_handler
)
_ = self.player.play()
self.set_volume(self.max_volume)
@ -197,9 +201,7 @@ class Music:
if self.player:
self.player.set_position(position)
def set_volume(
self, volume: int | None = None, set_default: bool = True
) -> None:
def set_volume(self, volume: int | None = None, set_default: bool = True) -> None:
"""Set maximum volume used for player"""
if not self.player:

View File

@ -140,7 +140,7 @@ class SignalMonitor:
)
def show_signal(self, name: str, *args: Any) -> None:
log.debug(f"{name=}, args={args}")
log.debug(f"SIGNAL: {name=}, args={args}")
@dataclass
@ -1579,8 +1579,9 @@ class Window(QMainWindow):
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.
Have user select a template. Return the template.playlist_id, or
None if they cancel. template_id of zero means don't use a
template.
"""
template_name_id_list: list[tuple[str, int]] = []
@ -1742,7 +1743,9 @@ class Window(QMainWindow):
self.signals.signal_set_next_track.connect(self.set_next_track_handler)
self.signals.status_message_signal.connect(self.show_status_message)
self.signals.signal_track_ended.connect(self.track_ended_handler)
self.signals.signal_playlist_selected_rows.connect(self.playlist_selected_rows_handler)
self.signals.signal_playlist_selected_rows.connect(
self.playlist_selected_rows_handler
)
self.timer10.timeout.connect(self.tick_10ms)
self.timer500.timeout.connect(self.tick_500ms)
@ -1765,7 +1768,7 @@ class Window(QMainWindow):
if self.current.selected_row_numbers:
return self.current.selected_row_numbers[0]
if not self.current.base_model:
return 0 # hack, but mostly there WILL be a current model
return 0 # hack, but mostly there WILL be a current model
return self.current.base_model.rowCount()
def debug(self, checked: bool = False) -> None:
@ -2038,7 +2041,7 @@ class Window(QMainWindow):
source_playlist_id = self.current.playlist_id
for playlist in ds.playlists_all():
if playlist.id == source_playlist_id:
if playlist.playlist_id == source_playlist_id:
continue
else:
playlists.append(playlist)
@ -2562,9 +2565,7 @@ class Window(QMainWindow):
self._active_tab().scroll_to_top(playlist_track.row_number)
def playlist_selected_rows_handler(
self, selected_rows: SelectedRows
) -> None:
def playlist_selected_rows_handler(self, selected_rows: SelectedRows) -> None:
"""
Handle signal_playlist_selected_rows to keep track of which rows
are selected in the current model
@ -2746,7 +2747,7 @@ class Window(QMainWindow):
base_model: PlaylistModel,
proxy_model: PlaylistProxyModel,
playlist_id: int,
selected_row_numbers: list[int]
selected_row_numbers: list[int],
) -> None:
"""
Update self.current when playlist tab changes. Called by new playlist
@ -2756,7 +2757,7 @@ class Window(QMainWindow):
base_model=base_model,
proxy_model=proxy_model,
playlist_id=playlist_id,
selected_row_numbers=selected_row_numbers
selected_row_numbers=selected_row_numbers,
)
def update_headers(self) -> None:

View File

@ -93,11 +93,15 @@ class PlaylistModel(QAbstractTableModel):
self.played_tracks_hidden = False
# Connect signals
self.signals.signal_add_track_to_header.connect(self.signal_add_track_to_header_handler)
self.signals.signal_add_track_to_header.connect(
self.signal_add_track_to_header_handler
)
self.signals.signal_begin_insert_rows.connect(self.begin_insert_rows_handler)
self.signals.signal_end_insert_rows.connect(self.end_insert_rows_handler)
self.signals.signal_insert_track.connect(self.insert_row_signal_handler)
self.signals.signal_playlist_selected_rows.connect(self.playlist_selected_rows_handler)
self.signals.signal_playlist_selected_rows.connect(
self.playlist_selected_rows_handler
)
self.signals.signal_set_next_row.connect(self.set_next_row_handler)
self.signals.signal_track_started.connect(self.track_started_handler)
self.signals.signal_track_ended.connect(self.signal_track_ended_handler)
@ -150,7 +154,9 @@ class PlaylistModel(QAbstractTableModel):
return header_row
# @log_call
def signal_add_track_to_header_handler(self, track_and_playlist: TrackAndPlaylist) -> None:
def signal_add_track_to_header_handler(
self, track_and_playlist: TrackAndPlaylist
) -> None:
"""
Handle signal_add_track_to_header
"""
@ -1547,7 +1553,9 @@ class PlaylistModel(QAbstractTableModel):
current_track_row_number, current_track_start_time
)
if self.update_start_end_times(plr, current_track_start_time, current_track_end_time):
if self.update_start_end_times(
plr, current_track_start_time, current_track_end_time
):
update_rows.append(current_track_row_number)
# If we have a next track, note row number
@ -1582,7 +1590,9 @@ class PlaylistModel(QAbstractTableModel):
# Set start time for next row if we have a current track
if current_track_row_number is not None and row_number == next_track_row:
next_start_time = self.get_end_time(row_number, current_track_end_time)
if self.update_start_end_times(plr, current_track_end_time, next_start_time):
if self.update_start_end_times(
plr, current_track_end_time, next_start_time
):
update_rows.append(row_number)
# If we're between the current and next row, zero out

View File

@ -1,6 +1,6 @@
# Standard library imports
from collections import deque
import datetime as dt
from typing import Any
# PyQt imports
from PyQt6.QtCore import (
@ -25,6 +25,59 @@ import ds
import helpers
class FadeGraphGenerator(QObject):
finished = pyqtSignal(object, object)
task_completed = pyqtSignal()
def generate_graph(self, plr: "PlaylistRow") -> None:
fade_graph = FadeCurve(plr.path, plr.fade_at, plr.silence_at)
if not fade_graph:
log.error(f"Failed to create FadeCurve for {plr=}")
return
self.finished.emit(plr, fade_graph)
self.task_completed.emit()
class FadegraphThreadController(QObject):
def __init__(self):
super().__init__()
self._thread = None
self._generator = None
self._request_queue = deque()
def generate_fade_graph(self, playlist_row):
self._request_queue.append(playlist_row) # Use append for enqueue with deque
if self._thread is None or not self._thread.isRunning():
self._start_next_generation()
def _start_next_generation(self):
if not self._request_queue: # Check if deque is empty
return
playlist_row = self._request_queue.popleft() # Use popleft for dequeue with deque
self._start_thread(playlist_row)
def _start_thread(self, playlist_row):
self._thread = QThread()
self._generator = FadeGraphGenerator()
self._generator.moveToThread(self._thread)
self._generator.finished.connect(lambda row, graph: row.attach_fade_graph(graph))
self._generator.task_completed.connect(self._cleanup_thread)
self._thread.started.connect(lambda: self._generator.generate_graph(playlist_row))
self._thread.start()
def _cleanup_thread(self):
if self._thread:
self._thread.quit()
self._thread.wait()
self._thread.deleteLater()
self._thread = None
self._generator.deleteLater()
self._generator = None
# Start the next request if any
self._start_next_generation()
class PlaylistRow:
"""
Object to manage playlist row and track.
@ -41,7 +94,7 @@ class PlaylistRow:
self.signals = MusicMusterSignals()
self.end_of_track_signalled: bool = False
self.end_time: dt.datetime | None = None
self.fade_graph: Any | None = None
self.fade_graph: FadeCurve | None = None
self.fade_graph_start_updates: dt.datetime | None = None
self.forecast_end_time: dt.datetime | None = None
self.forecast_start_time: dt.datetime | None = None
@ -51,6 +104,7 @@ class PlaylistRow:
self.row_bg: str | None = None
self.row_fg: str | None = None
self.start_time: dt.datetime | None = None
self.fadegraph_thread_controller = FadegraphThreadController()
def __repr__(self) -> str:
track_id = None
@ -223,6 +277,9 @@ class PlaylistRow:
# the change to the database.
self.dto.row_number = value
def attach_fade_graph(self, fade_graph):
self.fade_graph = fade_graph
def drop3db(self, enable: bool) -> None:
"""
If enable is true, drop output by 3db else restore to full volume
@ -337,40 +394,6 @@ class PlaylistRow:
self.fade_graph.tick(self.time_playing())
class _AddFadeCurve(QObject):
"""
Initialising a fade curve introduces a noticeable delay so carry out in
a thread.
"""
finished = pyqtSignal()
def __init__(
self,
plr: PlaylistRow,
track_path: str,
track_fade_at: int,
track_silence_at: int,
) -> None:
super().__init__()
self.plr = plr
self.track_path = track_path
self.track_fade_at = track_fade_at
self.track_silence_at = track_silence_at
def run(self) -> None:
"""
Create fade curve and add to PlaylistTrack object
"""
fc = FadeCurve(self.track_path, self.track_fade_at, self.track_silence_at)
if not fc:
log.error(f"Failed to create FadeCurve for {self.track_path=}")
else:
self.plr.fade_graph = fc
self.finished.emit()
class FadeCurve:
GraphWidget: PlotWidget | None = None
@ -388,10 +411,10 @@ class FadeCurve:
# Start point of curve is Config.FADE_CURVE_MS_BEFORE_FADE
# milliseconds before fade starts to silence
self.start_ms: int = max(
self.start_ms = max(
0, track_fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1
)
self.end_ms: int = track_silence_at
self.end_ms = track_silence_at
audio_segment = audio[self.start_ms : self.end_ms]
self.graph_array = np.array(audio_segment.get_array_of_samples())
@ -470,7 +493,7 @@ class TrackSequence:
self.next = None
else:
self.next = plr
self.create_fade_graph()
plr.fadegraph_thread_controller.generate_fade_graph(plr)
def move_next_to_current(self) -> None:
"""
@ -506,27 +529,6 @@ class TrackSequence:
self.next = self.previous
self.previous = None
def create_fade_graph(self) -> None:
"""
Initialise and add FadeCurve in a thread as it's slow
"""
self.fadecurve_thread = QThread()
if self.next is None:
raise ApplicationError("hell in a handcart")
self.worker = _AddFadeCurve(
self.next,
track_path=self.next.path,
track_fade_at=self.next.fade_at,
track_silence_at=self.next.silence_at,
)
self.worker.moveToThread(self.fadecurve_thread)
self.fadecurve_thread.started.connect(self.worker.run)
self.worker.finished.connect(self.fadecurve_thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.fadecurve_thread.finished.connect(self.fadecurve_thread.deleteLater)
self.fadecurve_thread.start()
def update(self) -> None:
"""
If a PlaylistRow is edited (moved, title changed, etc), the

View File

@ -41,7 +41,7 @@ from classes import (
MusicMusterSignals,
PlaylistStyle,
SelectedRows,
TrackInfo
TrackInfo,
)
from config import Config
from dialogs import TrackInsertDialog
@ -190,9 +190,7 @@ class PlaylistDelegate(QStyledItemDelegate):
# Close editor if no changes have been made
data_modified = False
if isinstance(editor, QTextEdit):
data_modified = (
self.original_model_data != editor.toPlainText()
)
data_modified = self.original_model_data != editor.toPlainText()
elif isinstance(editor, QDoubleSpinBox):
data_modified = (
self.original_model_data != int(editor.value()) * 1000
@ -544,7 +542,9 @@ class PlaylistTab(QTableView):
header_row = self.get_base_model().is_header_row(model_row_number)
track_row = not header_row
if self.track_sequence.current:
this_is_current_row = model_row_number == self.track_sequence.current.row_number
this_is_current_row = (
model_row_number == self.track_sequence.current.row_number
)
else:
this_is_current_row = False
if self.track_sequence.next:
@ -844,7 +844,9 @@ class PlaylistTab(QTableView):
if not selected_indexes:
return []
return sorted(list(set([self.model().mapToSource(a).row() for a in selected_indexes])))
return sorted(
list(set([self.model().mapToSource(a).row() for a in selected_indexes]))
)
# @log_call
def get_top_visible_row(self) -> int:

View File

@ -0,0 +1,68 @@
"""notes substrings, indexing, playlist faviourites, bitrate not null
Revision ID: 6d36cde8dea0
Revises: 4fc2a9a82ab0
Create Date: 2025-04-22 17:03:00.497945
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '6d36cde8dea0'
down_revision = '4fc2a9a82ab0'
branch_labels = None
depends_on = None
def upgrade(engine_name: str) -> None:
globals()["upgrade_%s" % engine_name]()
def downgrade(engine_name: str) -> None:
globals()["downgrade_%s" % engine_name]()
def upgrade_() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('notecolours', schema=None) as batch_op:
batch_op.add_column(sa.Column('strip_substring', sa.Boolean(), nullable=False))
batch_op.create_index(batch_op.f('ix_notecolours_substring'), ['substring'], unique=True)
with op.batch_alter_table('playlist_rows', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_playlist_rows_playlist_id'), ['playlist_id'], unique=False)
with op.batch_alter_table('playlists', schema=None) as batch_op:
batch_op.add_column(sa.Column('favourite', sa.Boolean(), nullable=False))
with op.batch_alter_table('tracks', schema=None) as batch_op:
batch_op.alter_column('bitrate',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
# ### end Alembic commands ###
def downgrade_() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tracks', schema=None) as batch_op:
batch_op.alter_column('bitrate',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
with op.batch_alter_table('playlists', schema=None) as batch_op:
batch_op.drop_column('favourite')
with op.batch_alter_table('playlist_rows', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_playlist_rows_playlist_id'))
with op.batch_alter_table('notecolours', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_notecolours_substring'))
batch_op.drop_column('strip_substring')
# ### end Alembic commands ###

View File

@ -0,0 +1,86 @@
"""Have id field reflect table name
Revision ID: 8e06d465923a
Revises: 6d36cde8dea0
Create Date: 2025-04-22 13:23:18.813024
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
from dataclasses import dataclass
@dataclass
class TableInfo:
table: str
old: str
new: str
data = [
TableInfo("notecolours", "id", "notecolour_id"),
TableInfo("playdates", "id", "playdate_id"),
TableInfo("playlists", "id", "playlist_id"),
TableInfo("playlist_rows", "id", "playlistrow_id"),
TableInfo("queries", "id", "query_id"),
TableInfo("settings", "id", "setting_id"),
TableInfo("tracks", "id", "track_id"),
]
# revision identifiers, used by Alembic.
revision = '8e06d465923a'
down_revision = '6d36cde8dea0'
branch_labels = None
depends_on = None
def upgrade(engine_name: str) -> None:
globals()["upgrade_%s" % engine_name]()
def downgrade(engine_name: str) -> None:
globals()["downgrade_%s" % engine_name]()
def upgrade_() -> None:
# Drop foreign key constraints
op.drop_constraint('fk_playdates_track_id_tracks', 'playdates', type_='foreignkey')
op.drop_constraint('fk_playlist_rows_track_id_tracks', 'playlist_rows', type_='foreignkey')
for record in data:
op.alter_column(
record.table,
record.old,
new_column_name=record.new,
existing_type=sa.Integer(), # Specify the existing column type
existing_nullable=False # If the column is NOT NULL, specify that too
)
# Recreate the foreign key constraints
op.create_foreign_key('fk_playdates_track_id_tracks', 'playdates', 'tracks', ['track_id'], ['track_id'])
op.create_foreign_key('fk_playlist_rows_track_id_tracks', 'playlist_rows', 'tracks', ['track_id'], ['track_id'])
# ### end Alembic commands ###
def downgrade_() -> None:
# Drop foreign key constraints
op.drop_constraint('fk_playdates_track_id_tracks', 'playdates', type_='foreignkey')
op.drop_constraint('fk_playlist_rows_track_id_tracks', 'playlist_rows', type_='foreignkey')
for record in data:
op.alter_column(
record.table,
record.new,
new_column_name=record.old,
existing_type=sa.Integer(), # Specify the existing column type
existing_nullable=False # If the column is NOT NULL, specify that too
)
# Recreate the foreign key constraints
op.create_foreign_key('fk_playdates_track_id_tracks', 'playdates', 'tracks', ['track_id'], ['track_id'])
op.create_foreign_key('fk_playlist_rows_track_id_tracks', 'playlist_rows', 'tracks', ['track_id'], ['track_id'])
# ### end Alembic commands ###

View File

@ -177,9 +177,7 @@ class MyTestCase(unittest.TestCase):
track = tracks[0]
assert track.title == "I'm So Afraid"
assert track.artist == "Fleetwood Mac"
track_file = os.path.join(
self.musicstore, os.path.basename(test_track_path)
)
track_file = os.path.join(self.musicstore, os.path.basename(test_track_path))
assert track.path == track_file
assert os.path.exists(track_file)
assert os.listdir(self.import_source) == []
@ -222,9 +220,7 @@ class MyTestCase(unittest.TestCase):
track = tracks[1]
assert track.title == "The Lovecats"
assert track.artist == "The Cure"
track_file = os.path.join(
self.musicstore, os.path.basename(test_track_path)
)
track_file = os.path.join(self.musicstore, os.path.basename(test_track_path))
assert track.path == track_file
assert os.path.exists(track_file)
assert os.listdir(self.import_source) == []
@ -275,9 +271,7 @@ class MyTestCase(unittest.TestCase):
assert track.title == "The Lovecats"
assert track.artist == "The Cure"
assert track.track_id == 2
track_file = os.path.join(
self.musicstore, os.path.basename(test_track_path)
)
track_file = os.path.join(self.musicstore, os.path.basename(test_track_path))
assert track.path == track_file
assert os.path.exists(track_file)
assert os.listdir(self.import_source) == []
@ -470,9 +464,7 @@ class MyTestCase(unittest.TestCase):
assert track.title == "The Lovecats xyz"
assert track.artist == "The Cure"
assert track.track_id == 2
track_file = os.path.join(
self.musicstore, os.path.basename(test_track_path)
)
track_file = os.path.join(self.musicstore, os.path.basename(test_track_path))
assert track.path == track_file
assert os.path.exists(track_file)
assert os.listdir(self.import_source) == []

View File

@ -34,7 +34,9 @@ class TestMMMiscTracks(unittest.TestCase):
# Create a playlist and model
self.playlist = ds.playlist_create(PLAYLIST_NAME, template_id=0)
self.model = playlistmodel.PlaylistModel(self.playlist.playlist_id, is_template=False)
self.model = playlistmodel.PlaylistModel(
self.playlist.playlist_id, is_template=False
)
for row in range(len(self.test_tracks)):
track_path = self.test_tracks[row % len(self.test_tracks)]
@ -68,18 +70,12 @@ class TestMMMiscTracks(unittest.TestCase):
self.model.selected_rows = [self.model.playlist_rows[START_ROW]]
self.model.insert_row_signal_handler(
InsertTrack(
playlist_id=self.playlist.playlist_id,
track_id=None,
note="start+"
playlist_id=self.playlist.playlist_id, track_id=None, note="start+"
)
)
self.model.selected_rows = [self.model.playlist_rows[END_ROW]]
self.model.insert_row_signal_handler(
InsertTrack(
playlist_id=self.playlist.playlist_id,
track_id=None,
note="-+"
)
InsertTrack(playlist_id=self.playlist.playlist_id, track_id=None, note="-+")
)
prd = self.model.playlist_rows[START_ROW]
@ -144,7 +140,9 @@ class TestMMMiscRowMove(unittest.TestCase):
db.create_all()
self.playlist = ds.playlist_create(self.PLAYLIST_NAME, template_id=0)
self.model = playlistmodel.PlaylistModel(self.playlist.playlist_id, is_template=False)
self.model = playlistmodel.PlaylistModel(
self.playlist.playlist_id, is_template=False
)
for row in range(self.ROWS_TO_CREATE):
self.model.insert_row_signal_handler(
InsertTrack(
@ -165,9 +163,7 @@ class TestMMMiscRowMove(unittest.TestCase):
assert self.model.rowCount() == self.ROWS_TO_CREATE
self.model.insert_row_signal_handler(
InsertTrack(
playlist_id=self.playlist.playlist_id,
track_id=None,
note=note_text
playlist_id=self.playlist.playlist_id, track_id=None, note=note_text
)
)
assert self.model.rowCount() == self.ROWS_TO_CREATE + 1
@ -192,9 +188,7 @@ class TestMMMiscRowMove(unittest.TestCase):
self.model.insert_row_signal_handler(
InsertTrack(
playlist_id=self.playlist.playlist_id,
track_id=None,
note=note_text
playlist_id=self.playlist.playlist_id, track_id=None, note=note_text
)
)
assert self.model.rowCount() == self.ROWS_TO_CREATE + 1
@ -214,9 +208,7 @@ class TestMMMiscRowMove(unittest.TestCase):
self.model.insert_row_signal_handler(
InsertTrack(
playlist_id=self.playlist.playlist_id,
track_id=None,
note=note_text
playlist_id=self.playlist.playlist_id, track_id=None, note=note_text
)
)
assert self.model.rowCount() == self.ROWS_TO_CREATE + 1
@ -257,13 +249,13 @@ class TestMMMiscRowMove(unittest.TestCase):
for row in range(self.ROWS_TO_CREATE):
model_dst.insert_row_signal_handler(
InsertTrack(
playlist_id=playlist_dst.playlist_id,
track_id=None,
note=str(row)
playlist_id=playlist_dst.playlist_id, track_id=None, note=str(row)
)
)
model_src.move_rows_between_playlists(from_rows, to_row, playlist_dst.playlist_id)
model_src.move_rows_between_playlists(
from_rows, to_row, playlist_dst.playlist_id
)
assert model_src.rowCount() == self.ROWS_TO_CREATE - len(from_rows)
assert model_dst.rowCount() == self.ROWS_TO_CREATE + len(from_rows)
@ -284,13 +276,13 @@ class TestMMMiscRowMove(unittest.TestCase):
for row in range(self.ROWS_TO_CREATE):
model_dst.insert_row_signal_handler(
InsertTrack(
playlist_id=playlist_dst.playlist_id,
track_id=None,
note=str(row)
playlist_id=playlist_dst.playlist_id, track_id=None, note=str(row)
)
)
model_src.move_rows_between_playlists(from_rows, to_row, playlist_dst.playlist_id)
model_src.move_rows_between_playlists(
from_rows, to_row, playlist_dst.playlist_id
)
# Check the rows of the destination model
row_notes = []
@ -318,13 +310,13 @@ class TestMMMiscRowMove(unittest.TestCase):
for row in range(self.ROWS_TO_CREATE):
model_dst.insert_row_signal_handler(
InsertTrack(
playlist_id=playlist_dst.playlist_id,
track_id=None,
note=str(row)
playlist_id=playlist_dst.playlist_id, track_id=None, note=str(row)
)
)
model_src.move_rows_between_playlists(from_rows, to_row, playlist_dst.playlist_id)
model_src.move_rows_between_playlists(
from_rows, to_row, playlist_dst.playlist_id
)
# Check the rows of the destination model
row_notes = []

View File

@ -31,7 +31,7 @@ class MyTestCase(unittest.TestCase):
path="/alpha/bravo/charlie",
silence_at=0,
start_gap=0,
title="abc"
title="abc",
)
_ = ds.track_create(track1_meta)
track2_meta = dict(
@ -42,8 +42,8 @@ class MyTestCase(unittest.TestCase):
path="/xray/yankee/zulu",
silence_at=0,
start_gap=0,
title="xyz"
)
title="xyz",
)
track2 = ds.track_create(track2_meta)
# Add playdates
@ -74,7 +74,7 @@ class MyTestCase(unittest.TestCase):
results = ds.tracks_filtered(filter)
assert len(results) == 1
assert 'alpha' in results[0].path
assert "alpha" in results[0].path
def test_search_path_2(self):
"""Search for unplayed track that doesn't exist"""
@ -91,7 +91,7 @@ class MyTestCase(unittest.TestCase):
results = ds.tracks_filtered(filter)
assert len(results) == 1
assert 'zulu' in results[0].path
assert "zulu" in results[0].path
def test_played_over_two_years_ago(self):
"""Search for tracks played over 2 years ago"""
@ -108,7 +108,7 @@ class MyTestCase(unittest.TestCase):
results = ds.tracks_filtered(filter)
assert len(results) == 1
assert 'alpha' in results[0].path
assert "alpha" in results[0].path
def test_played_anytime(self):
"""Search for tracks played over a year ago"""
@ -117,4 +117,4 @@ class MyTestCase(unittest.TestCase):
results = ds.tracks_filtered(filter)
assert len(results) == 1
assert 'zulu' in results[0].path
assert "zulu" in results[0].path

View File

@ -100,7 +100,7 @@ class MyTestCase(unittest.TestCase):
InsertTrack(
playlist_id=playlist.playlist_id,
track_id=self.track1.track_id,
note=note_text
note=note_text,
)
)