WIP: playlists load, can't play track

This commit is contained in:
Keith Edmunds 2025-03-16 10:36:10 +00:00
parent b520178e3a
commit e733e7025d
6 changed files with 121 additions and 165 deletions

View File

@ -5,7 +5,7 @@ import datetime as dt
from enum import auto, Enum from enum import auto, Enum
import functools import functools
import threading import threading
from typing import NamedTuple from typing import Any, NamedTuple
# Third party imports # Third party imports
@ -21,6 +21,7 @@ from PyQt6.QtWidgets import (
) )
# App imports # App imports
# from music_manager import FadeCurve
# Define singleton first as it's needed below # Define singleton first as it's needed below
@ -91,31 +92,6 @@ class Filter:
duration_unit: str = "minutes" duration_unit: str = "minutes"
@singleton
@dataclass
class MusicMusterSignals(QObject):
"""
Class for all MusicMuster signals. See:
- https://zetcode.com/gui/pyqt5/eventssignals/
- https://stackoverflow.com/questions/62654525/emit-a-signal-from-another-class-to-main-class
"""
begin_reset_model_signal = pyqtSignal(int)
enable_escape_signal = pyqtSignal(bool)
end_reset_model_signal = pyqtSignal(int)
next_track_changed_signal = pyqtSignal()
resize_rows_signal = pyqtSignal(int)
search_songfacts_signal = pyqtSignal(str)
search_wikipedia_signal = pyqtSignal(str)
show_warning_signal = pyqtSignal(str, str)
span_cells_signal = pyqtSignal(int, int, int, int, int)
status_message_signal = pyqtSignal(str, int)
track_ended_signal = pyqtSignal()
def __post_init__(self):
super().__init__()
class PlaylistStyle(QProxyStyle): class PlaylistStyle(QProxyStyle):
def drawPrimitive(self, element, option, painter, widget=None): def drawPrimitive(self, element, option, painter, widget=None):
""" """
@ -165,7 +141,7 @@ class TrackDTO:
@dataclass @dataclass
class PlaylistRowDTO(TrackDTO): class PlaylistRowObj(TrackDTO):
note: str note: str
played: bool played: bool
playlist_id: int playlist_id: int
@ -177,14 +153,51 @@ class PlaylistRowDTO(TrackDTO):
note_bg: str | None = None note_bg: str | None = None
end_of_track_signalled: bool = False end_of_track_signalled: bool = False
end_time: dt.datetime | None = None end_time: dt.datetime | None = None
# fade_graph: FadeCurve | None = None fade_graph: Any | None = None
fade_graph_start_updates: dt.datetime | None = None fade_graph_start_updates: dt.datetime | None = None
resume_marker: float = 0.0 resume_marker: float = 0.0
forecast_end_time: dt.datetime | None = None forecast_end_time: dt.datetime | None = None
forecast_start_time: dt.datetime | None = None forecast_start_time: dt.datetime | None = None
start_time: dt.datetime | None = None start_time: dt.datetime | None = None
def is_playing(self) -> bool:
"""
Return True if we're currently playing else False
"""
if self.start_time is None:
return False
return True
class TrackInfo(NamedTuple): class TrackInfo(NamedTuple):
track_id: int track_id: int
row_number: int row_number: int
@singleton
@dataclass
class MusicMusterSignals(QObject):
"""
Class for all MusicMuster signals. See:
- https://zetcode.com/gui/pyqt5/eventssignals/
- https://stackoverflow.com/questions/62654525/emit-a-signal-from-another-class-to-main-class
"""
begin_reset_model_signal = pyqtSignal(int)
enable_escape_signal = pyqtSignal(bool)
end_reset_model_signal = pyqtSignal(int)
next_track_changed_signal = pyqtSignal()
resize_rows_signal = pyqtSignal(int)
search_songfacts_signal = pyqtSignal(str)
search_wikipedia_signal = pyqtSignal(str)
show_warning_signal = pyqtSignal(str, str)
signal_set_next_row = pyqtSignal(int)
signal_set_next_track = pyqtSignal(PlaylistRowObj)
span_cells_signal = pyqtSignal(int, int, int, int, int)
status_message_signal = pyqtSignal(str, int)
track_ended_signal = pyqtSignal()
def __post_init__(self):
super().__init__()

View File

@ -395,6 +395,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
given playlist_id and row given playlist_id and row
""" """
# TODO: use selectinload?
stmt = ( stmt = (
select(PlaylistRows) select(PlaylistRows)
.options(joinedload(cls.track)) .options(joinedload(cls.track))

View File

@ -238,27 +238,6 @@ class _Music:
# except Exception as e: # except Exception as e:
# log.error(f"Failed to set up VLC logging: {e}") # log.error(f"Failed to set up VLC logging: {e}")
def adjust_by_ms(self, ms: int) -> None:
"""Move player position by ms milliseconds"""
if not self.player:
return
elapsed_ms = self.get_playtime()
position = self.get_position()
if not position:
position = 0.0
new_position = max(0.0, position + ((position * ms) / elapsed_ms))
self.set_position(new_position)
# Adjus start time so elapsed time calculations are correct
if new_position == 0:
self.start_dt = dt.datetime.now()
else:
if self.start_dt:
self.start_dt -= dt.timedelta(milliseconds=ms)
else:
self.start_dt = dt.datetime.now() - dt.timedelta(milliseconds=ms)
def fade(self, fade_seconds: int) -> None: def fade(self, fade_seconds: int) -> None:
""" """
Fade the currently playing track. Fade the currently playing track.
@ -353,21 +332,6 @@ class _Music:
self.player.set_position(position) self.player.set_position(position)
self.start_dt = start_time self.start_dt = start_time
# For as-yet unknown reasons. sometimes the volume gets
# reset to zero within 200mS or so of starting play. This
# only happened since moving to Debian 12, which uses
# Pipewire for sound (which may be irrelevant).
# It has been known for the volume to need correcting more
# than once in the first 200mS.
# Update August 2024: This no longer seems to be an issue
# for _ in range(3):
# if self.player:
# volume = self.player.audio_get_volume()
# if volume < Config.VLC_VOLUME_DEFAULT:
# self.set_volume(Config.VLC_VOLUME_DEFAULT)
# log.error(f"Reset from {volume=}")
# sleep(0.1)
def set_position(self, position: float) -> None: def set_position(self, position: float) -> None:
""" """
Set player position Set player position
@ -519,25 +483,6 @@ class RowAndTrack:
self.signals.track_ended_signal.emit() self.signals.track_ended_signal.emit()
self.end_of_track_signalled = True self.end_of_track_signalled = True
def create_fade_graph(self) -> None:
"""
Initialise and add FadeCurve in a thread as it's slow
"""
self.fadecurve_thread = QThread()
self.worker = _AddFadeCurve(
self,
track_path=self.path,
track_fade_at=self.fade_at,
track_silence_at=self.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 drop3db(self, enable: bool) -> None: def drop3db(self, enable: bool) -> None:
""" """
If enable is true, drop output by 3db else restore to full volume If enable is true, drop output by 3db else restore to full volume
@ -565,20 +510,6 @@ class RowAndTrack:
return self.music.is_playing() return self.music.is_playing()
def move_back(self, ms: int = Config.PREVIEW_BACK_MS) -> None:
"""
Rewind player by ms milliseconds
"""
self.music.adjust_by_ms(ms * -1)
def move_forward(self, ms: int = Config.PREVIEW_ADVANCE_MS) -> None:
"""
Rewind player by ms milliseconds
"""
self.music.adjust_by_ms(ms)
def play(self, position: Optional[float] = None) -> None: def play(self, position: Optional[float] = None) -> None:
"""Play track""" """Play track"""
@ -599,13 +530,6 @@ class RowAndTrack:
milliseconds=update_graph_at_ms milliseconds=update_graph_at_ms
) )
def restart(self) -> None:
"""
Restart player
"""
self.music.adjust_by_ms(self.time_playing() * -1)
def set_forecast_start_time( def set_forecast_start_time(
self, modified_rows: list[int], start: Optional[dt.datetime] self, modified_rows: list[int], start: Optional[dt.datetime]
) -> Optional[dt.datetime]: ) -> Optional[dt.datetime]:
@ -743,7 +667,28 @@ class TrackSequence:
self.next = None self.next = None
else: else:
self.next = rat self.next = rat
self.next.create_fade_graph() self.create_fade_graph()
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()
track_sequence = TrackSequence() track_sequence = TrackSequence()

View File

@ -2521,11 +2521,13 @@ class Window(QMainWindow):
Set currently-selected row on visible playlist tab as next track Set currently-selected row on visible playlist tab as next track
""" """
playlist_tab = self.active_tab() self.signals.signal_set_next_row.emit(self.current.playlist_id)
if playlist_tab: self.clear_selection()
playlist_tab.set_row_as_next_track() # playlist_tab = self.active_tab()
else: # if playlist_tab:
log.error("No active tab") # playlist_tab.set_row_as_next_track()
# else:
# log.error("No active tab")
def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None: def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None:
""" """

View File

@ -35,7 +35,7 @@ from classes import (
ApplicationError, ApplicationError,
Col, Col,
MusicMusterSignals, MusicMusterSignals,
PlaylistRowDTO, PlaylistRowObj,
) )
from config import Config from config import Config
from helpers import ( from helpers import (
@ -85,9 +85,10 @@ class PlaylistModel(QAbstractTableModel):
self.playlist_id = playlist_id self.playlist_id = playlist_id
self.is_template = is_template self.is_template = is_template
self.playlist_rows: dict[int, PlaylistRowDTO] = {} self.playlist_rows: dict[int, PlaylistRowObj] = {}
self.selected_rows: list[PlaylistRowDTO] = [] self.selected_rows: list[PlaylistRowObj] = []
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
self.signals.signal_set_next_row.connect(self.set_next_row)
self.played_tracks_hidden = False self.played_tracks_hidden = False
self.signals.begin_reset_model_signal.connect(self.begin_reset_model) self.signals.begin_reset_model_signal.connect(self.begin_reset_model)
@ -306,9 +307,7 @@ class PlaylistModel(QAbstractTableModel):
# Update colour and times for current row # Update colour and times for current row
# only invalidate required roles # only invalidate required roles
roles = [ roles = [Qt.ItemDataRole.DisplayRole]
Qt.ItemDataRole.DisplayRole
]
self.invalidate_row(row_number, roles) self.invalidate_row(row_number, roles)
# Update previous row in case we're hiding played rows # Update previous row in case we're hiding played rows
@ -759,7 +758,9 @@ class PlaylistModel(QAbstractTableModel):
Qt.ItemDataRole.FontRole, Qt.ItemDataRole.FontRole,
Qt.ItemDataRole.ForegroundRole, Qt.ItemDataRole.ForegroundRole,
] ]
self.invalidate_rows(list(range(new_row_number, len(self.playlist_rows))), roles) self.invalidate_rows(
list(range(new_row_number, len(self.playlist_rows))), roles
)
def invalidate_row(self, modified_row: int, roles: list[Qt.ItemDataRole]) -> None: def invalidate_row(self, modified_row: int, roles: list[Qt.ItemDataRole]) -> None:
""" """
@ -771,10 +772,12 @@ class PlaylistModel(QAbstractTableModel):
self.dataChanged.emit( self.dataChanged.emit(
self.index(modified_row, 0), self.index(modified_row, 0),
self.index(modified_row, self.columnCount() - 1), self.index(modified_row, self.columnCount() - 1),
roles roles,
) )
def invalidate_rows(self, modified_rows: list[int], roles: list[Qt.ItemDataRole]) -> None: def invalidate_rows(
self, modified_rows: list[int], roles: list[Qt.ItemDataRole]
) -> None:
""" """
Signal to view to refresh invlidated rows Signal to view to refresh invlidated rows
""" """
@ -838,7 +841,7 @@ class PlaylistModel(QAbstractTableModel):
# new_playlist_rows[p.row_number] = self.playlist_rows[plid_to_row[p.id]] # new_playlist_rows[p.row_number] = self.playlist_rows[plid_to_row[p.id]]
# new_playlist_rows[p.row_number].row_number = p.row_number # new_playlist_rows[p.row_number].row_number = p.row_number
# build a new playlist_rows # build a new playlist_rows
new_playlist_rows: dict[int, PlaylistRowDTO] = {} new_playlist_rows: dict[int, PlaylistRowObj] = {}
for p in get_playlist_rows(self.playlist_id): for p in get_playlist_rows(self.playlist_id):
new_playlist_rows[p.row_number] = p new_playlist_rows[p.row_number] = p
@ -1432,57 +1435,47 @@ class PlaylistModel(QAbstractTableModel):
""" """
self.selected_rows = [self.playlist_rows[a] for a in selected_rows] self.selected_rows = [self.playlist_rows[a] for a in selected_rows]
import pdb; pdb.set_trace()
def set_next_row(self, row_number: Optional[int]) -> None: def set_next_row(self, playlist_id: int) -> None:
""" """
Set row_number as next track. If row_number is None, clear next track. Handle signal_set_next_row
Return True if successful else False.
""" """
log.debug(f"{self}: set_next_row({row_number=})") log.debug(f"{self}: set_next_row({playlist_id=})")
if playlist_id != self.playlist_id:
# Not for us
return
if row_number is None: if len(self.selected_rows) == 0:
# Clear next track # No row selected so clear next track
if track_sequence.next is not None: if track_sequence.next is not None:
track_sequence.set_next(None) track_sequence.set_next(None)
else: return
# Get playlistrow_id of row
try:
rat = self.playlist_rows[row_number]
except IndexError:
log.error(f"{self} set_track_sequence.next({row_number=}, IndexError")
return
if rat.track_id is None or rat.row_number is None:
log.error(
f"{self} .set_track_sequence.next({row_number=}, "
f"No track / row number {rat.track_id=}, {rat.row_number=}"
)
return
old_next_row: Optional[int] = None if len(self.selected_rows) > 1:
if track_sequence.next: self.signals.show_warning_signal.emit(
old_next_row = track_sequence.next.row_number "Too many rows selected", "Select one row for next row"
)
return
track_sequence.set_next(rat) rat = self.selected_rows[0]
if rat.track_id is None:
raise ApplicationError(f"set_next_row: no track_id ({rat=})")
if Config.WIKIPEDIA_ON_NEXT: old_next_row: Optional[int] = None
self.signals.search_wikipedia_signal.emit( if track_sequence.next:
self.playlist_rows[row_number].title old_next_row = track_sequence.next.row_number
)
if Config.SONGFACTS_ON_NEXT: track_sequence.set_next(rat)
self.signals.search_songfacts_signal.emit(
self.playlist_rows[row_number].title roles = [
) Qt.ItemDataRole.BackgroundRole,
roles = [ ]
Qt.ItemDataRole.BackgroundRole, if old_next_row is not None:
]
if old_next_row is not None:
# only invalidate required roles
self.invalidate_row(old_next_row, roles)
# only invalidate required roles # only invalidate required roles
self.invalidate_row(row_number, roles) self.invalidate_row(old_next_row, roles)
# only invalidate required roles
self.invalidate_row(rat.row_number, roles)
self.signals.next_track_changed_signal.emit() self.signals.next_track_changed_signal.emit()
self.update_track_times() self.update_track_times()
@ -1826,7 +1819,9 @@ class PlaylistProxyModel(QSortFilterProxyModel):
] ]
QTimer.singleShot( QTimer.singleShot(
Config.HIDE_AFTER_PLAYING_OFFSET + 100, Config.HIDE_AFTER_PLAYING_OFFSET + 100,
lambda: self.sourceModel().invalidate_row(source_row, roles), lambda: self.sourceModel().invalidate_row(
source_row, roles
),
) )
return True return True
# Next track not playing yet so don't hide previous # Next track not playing yet so don't hide previous

View File

@ -5,7 +5,7 @@
# Third party imports # Third party imports
from sqlalchemy import select, func from sqlalchemy import select, func
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from classes import PlaylistRowDTO from classes import PlaylistRowObj
# App imports # App imports
from classes import TrackDTO from classes import TrackDTO
@ -29,7 +29,7 @@ def tracks_like_title(filter_str: str) -> list[TrackDTO]:
] ]
def get_playlist_rows(playlist_id: int) -> list[PlaylistRowDTO]: def get_playlist_rows(playlist_id: int) -> list[PlaylistRowObj]:
# Alias PlaydatesTable for subquery # Alias PlaydatesTable for subquery
LatestPlaydate = aliased(Playdates) LatestPlaydate = aliased(Playdates)
@ -75,7 +75,7 @@ def get_playlist_rows(playlist_id: int) -> list[PlaylistRowDTO]:
for row in results: for row in results:
# Handle cases where track_id is None (no track associated) # Handle cases where track_id is None (no track associated)
if row.track_id is None: if row.track_id is None:
dto = PlaylistRowDTO( dto = PlaylistRowObj(
artist="", artist="",
bitrate=0, bitrate=0,
duration=0, duration=0,
@ -95,7 +95,7 @@ def get_playlist_rows(playlist_id: int) -> list[PlaylistRowDTO]:
# Additional fields like row_fg, row_bg, etc., use default None values # Additional fields like row_fg, row_bg, etc., use default None values
) )
else: else:
dto = PlaylistRowDTO( dto = PlaylistRowObj(
artist=row.artist, artist=row.artist,
bitrate=row.bitrate, bitrate=row.bitrate,
duration=row.duration, duration=row.duration,