WIP: playlists load, can't play track
This commit is contained in:
parent
b520178e3a
commit
e733e7025d
@ -5,7 +5,7 @@ import datetime as dt
|
||||
from enum import auto, Enum
|
||||
import functools
|
||||
import threading
|
||||
from typing import NamedTuple
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
# Third party imports
|
||||
|
||||
@ -21,6 +21,7 @@ from PyQt6.QtWidgets import (
|
||||
)
|
||||
|
||||
# App imports
|
||||
# from music_manager import FadeCurve
|
||||
|
||||
|
||||
# Define singleton first as it's needed below
|
||||
@ -91,31 +92,6 @@ class Filter:
|
||||
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):
|
||||
def drawPrimitive(self, element, option, painter, widget=None):
|
||||
"""
|
||||
@ -165,7 +141,7 @@ class TrackDTO:
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlaylistRowDTO(TrackDTO):
|
||||
class PlaylistRowObj(TrackDTO):
|
||||
note: str
|
||||
played: bool
|
||||
playlist_id: int
|
||||
@ -177,14 +153,51 @@ class PlaylistRowDTO(TrackDTO):
|
||||
note_bg: str | None = None
|
||||
end_of_track_signalled: bool = False
|
||||
end_time: dt.datetime | None = None
|
||||
# fade_graph: FadeCurve | None = None
|
||||
fade_graph: Any | None = None
|
||||
fade_graph_start_updates: dt.datetime | None = None
|
||||
resume_marker: float = 0.0
|
||||
forecast_end_time: dt.datetime | None = None
|
||||
forecast_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):
|
||||
track_id: 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__()
|
||||
|
||||
@ -395,6 +395,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
|
||||
given playlist_id and row
|
||||
"""
|
||||
|
||||
# TODO: use selectinload?
|
||||
stmt = (
|
||||
select(PlaylistRows)
|
||||
.options(joinedload(cls.track))
|
||||
|
||||
@ -238,27 +238,6 @@ class _Music:
|
||||
# except Exception as 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:
|
||||
"""
|
||||
Fade the currently playing track.
|
||||
@ -353,21 +332,6 @@ class _Music:
|
||||
self.player.set_position(position)
|
||||
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:
|
||||
"""
|
||||
Set player position
|
||||
@ -519,25 +483,6 @@ class RowAndTrack:
|
||||
self.signals.track_ended_signal.emit()
|
||||
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:
|
||||
"""
|
||||
If enable is true, drop output by 3db else restore to full volume
|
||||
@ -565,20 +510,6 @@ class RowAndTrack:
|
||||
|
||||
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:
|
||||
"""Play track"""
|
||||
|
||||
@ -599,13 +530,6 @@ class RowAndTrack:
|
||||
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(
|
||||
self, modified_rows: list[int], start: Optional[dt.datetime]
|
||||
) -> Optional[dt.datetime]:
|
||||
@ -743,7 +667,28 @@ class TrackSequence:
|
||||
self.next = None
|
||||
else:
|
||||
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()
|
||||
|
||||
@ -2521,11 +2521,13 @@ class Window(QMainWindow):
|
||||
Set currently-selected row on visible playlist tab as next track
|
||||
"""
|
||||
|
||||
playlist_tab = self.active_tab()
|
||||
if playlist_tab:
|
||||
playlist_tab.set_row_as_next_track()
|
||||
else:
|
||||
log.error("No active tab")
|
||||
self.signals.signal_set_next_row.emit(self.current.playlist_id)
|
||||
self.clear_selection()
|
||||
# playlist_tab = self.active_tab()
|
||||
# if playlist_tab:
|
||||
# playlist_tab.set_row_as_next_track()
|
||||
# else:
|
||||
# log.error("No active tab")
|
||||
|
||||
def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None:
|
||||
"""
|
||||
|
||||
@ -35,7 +35,7 @@ from classes import (
|
||||
ApplicationError,
|
||||
Col,
|
||||
MusicMusterSignals,
|
||||
PlaylistRowDTO,
|
||||
PlaylistRowObj,
|
||||
)
|
||||
from config import Config
|
||||
from helpers import (
|
||||
@ -85,9 +85,10 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.playlist_id = playlist_id
|
||||
self.is_template = is_template
|
||||
|
||||
self.playlist_rows: dict[int, PlaylistRowDTO] = {}
|
||||
self.selected_rows: list[PlaylistRowDTO] = []
|
||||
self.playlist_rows: dict[int, PlaylistRowObj] = {}
|
||||
self.selected_rows: list[PlaylistRowObj] = []
|
||||
self.signals = MusicMusterSignals()
|
||||
self.signals.signal_set_next_row.connect(self.set_next_row)
|
||||
self.played_tracks_hidden = False
|
||||
|
||||
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
|
||||
# only invalidate required roles
|
||||
roles = [
|
||||
Qt.ItemDataRole.DisplayRole
|
||||
]
|
||||
roles = [Qt.ItemDataRole.DisplayRole]
|
||||
self.invalidate_row(row_number, roles)
|
||||
|
||||
# Update previous row in case we're hiding played rows
|
||||
@ -759,7 +758,9 @@ class PlaylistModel(QAbstractTableModel):
|
||||
Qt.ItemDataRole.FontRole,
|
||||
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:
|
||||
"""
|
||||
@ -771,10 +772,12 @@ class PlaylistModel(QAbstractTableModel):
|
||||
self.dataChanged.emit(
|
||||
self.index(modified_row, 0),
|
||||
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
|
||||
"""
|
||||
@ -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].row_number = p.row_number
|
||||
# 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):
|
||||
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]
|
||||
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.
|
||||
|
||||
Return True if successful else False.
|
||||
Handle signal_set_next_row
|
||||
"""
|
||||
|
||||
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:
|
||||
# Clear next track
|
||||
if len(self.selected_rows) == 0:
|
||||
# No row selected so clear next track
|
||||
if track_sequence.next is not None:
|
||||
track_sequence.set_next(None)
|
||||
else:
|
||||
# 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
|
||||
return
|
||||
|
||||
old_next_row: Optional[int] = None
|
||||
if track_sequence.next:
|
||||
old_next_row = track_sequence.next.row_number
|
||||
if len(self.selected_rows) > 1:
|
||||
self.signals.show_warning_signal.emit(
|
||||
"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:
|
||||
self.signals.search_wikipedia_signal.emit(
|
||||
self.playlist_rows[row_number].title
|
||||
)
|
||||
if Config.SONGFACTS_ON_NEXT:
|
||||
self.signals.search_songfacts_signal.emit(
|
||||
self.playlist_rows[row_number].title
|
||||
)
|
||||
roles = [
|
||||
Qt.ItemDataRole.BackgroundRole,
|
||||
]
|
||||
if old_next_row is not None:
|
||||
# only invalidate required roles
|
||||
self.invalidate_row(old_next_row, roles)
|
||||
old_next_row: Optional[int] = None
|
||||
if track_sequence.next:
|
||||
old_next_row = track_sequence.next.row_number
|
||||
|
||||
track_sequence.set_next(rat)
|
||||
|
||||
roles = [
|
||||
Qt.ItemDataRole.BackgroundRole,
|
||||
]
|
||||
if old_next_row is not None:
|
||||
# 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.update_track_times()
|
||||
@ -1826,7 +1819,9 @@ class PlaylistProxyModel(QSortFilterProxyModel):
|
||||
]
|
||||
QTimer.singleShot(
|
||||
Config.HIDE_AFTER_PLAYING_OFFSET + 100,
|
||||
lambda: self.sourceModel().invalidate_row(source_row, roles),
|
||||
lambda: self.sourceModel().invalidate_row(
|
||||
source_row, roles
|
||||
),
|
||||
)
|
||||
return True
|
||||
# Next track not playing yet so don't hide previous
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
# Third party imports
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.orm import aliased
|
||||
from classes import PlaylistRowDTO
|
||||
from classes import PlaylistRowObj
|
||||
|
||||
# App imports
|
||||
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
|
||||
LatestPlaydate = aliased(Playdates)
|
||||
|
||||
@ -75,7 +75,7 @@ def get_playlist_rows(playlist_id: int) -> list[PlaylistRowDTO]:
|
||||
for row in results:
|
||||
# Handle cases where track_id is None (no track associated)
|
||||
if row.track_id is None:
|
||||
dto = PlaylistRowDTO(
|
||||
dto = PlaylistRowObj(
|
||||
artist="",
|
||||
bitrate=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
|
||||
)
|
||||
else:
|
||||
dto = PlaylistRowDTO(
|
||||
dto = PlaylistRowObj(
|
||||
artist=row.artist,
|
||||
bitrate=row.bitrate,
|
||||
duration=row.duration,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user