WIP moving player to _PlaylistTrack

Playing and stopping track works
This commit is contained in:
Keith Edmunds 2024-05-25 21:22:56 +01:00
parent b1f682d2e6
commit 2c55f64fd4
5 changed files with 287 additions and 295 deletions

View File

@ -15,7 +15,8 @@ from sqlalchemy.orm import scoped_session
# App imports # App imports
from config import Config from config import Config
from log import log from log import log
from models import PlaylistRows from models import db, PlaylistRows
from music import Music
import helpers import helpers
@ -116,93 +117,87 @@ class MusicMusterSignals(QObject):
super().__init__() super().__init__()
class PlaylistTrack: class _PlaylistTrack:
""" """
Used to provide a single reference point for specific playlist tracks, Object to manage active playlist tracks,
typically the previous, current and next track. typically the previous, current and next track.
""" """
def __init__(self) -> None: def __init__(self, player_name: str, plrid: int) -> None:
""" """
Only initialises data structure. Call set_plr to populate. Initialises data structure.
Define a player.
Raise ValueError if no track in passed plr.
""" """
self.artist: Optional[str] = None with db.Session() as session:
self.duration: Optional[int] = None plr = session.get(PlaylistRows, plrid)
self.end_time: Optional[dt.datetime] = None if not plr.track:
self.fade_at: Optional[int] = None raise ValueError("No track defined in plr passed to PlaylistTrack")
self.fade_graph: Optional[FadeCurve] = None if helpers.file_is_unreadable(plr.track.path):
self.fade_graph_start_updates: Optional[dt.datetime] = None raise ValueError(f"_PlaylistTrack({plrid=}): {plr.track.path} is unreadable")
self.fade_length: Optional[int] = None track = plr.track
self.path: Optional[str] = None
self.playlist_id: Optional[int] = None self.artist = track.artist
self.plr_id: Optional[int] = None self.duration = track.duration
self.plr_rownum: Optional[int] = None self.end_time: Optional[dt.datetime] = None
self.resume_marker: Optional[float] = None self.fade_at = track.fade_at
self.silence_at: Optional[int] = None self.fade_graph: Optional[FadeCurve] = None
self.start_gap: Optional[int] = None self.fade_graph_start_updates: Optional[dt.datetime] = None
self.start_time: Optional[dt.datetime] = None self.fade_length: Optional[int] = None
self.title: Optional[str] = None self.intro = track.intro
self.track_id: Optional[int] = None self.path = track.path
self.player_name = player_name
self.playlist_id = plr.playlist_id
self.plr_id = plr.id
self.plr_rownum = plr.plr_rownum
self.resume_marker: Optional[float]
self.silence_at = track.silence_at
self.start_gap = track.start_gap
self.start_time: Optional[dt.datetime] = None
self.title = track.title
self.track_id = track.id
if track.silence_at and track.fade_at:
self.fade_length = track.silence_at - track.fade_at
self.player = Music(name=player_name)
# Initialise and add FadeCurve in a thread as it's slow
self.fadecurve_thread = QThread()
self.worker = AddFadeCurve(
self,
track_path=track.path,
track_fade_at=track.fade_at,
track_silence_at=track.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 __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
f"<PlaylistTrack(title={self.title}, artist={self.artist}, " f"<PlaylistTrack(title={self.title}, artist={self.artist}, "
f"plr_rownum={self.plr_rownum}, playlist_id={self.playlist_id}>" f"plr_rownum={self.plr_rownum}, playlist_id={self.playlist_id}>"
f"player_name={self.player_name}>"
) )
def set_plr(self, session: scoped_session, plr: PlaylistRows) -> None: def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None:
""" """Fade music"""
Update with new plr information
"""
session.add(plr) self.player.fade(fade_seconds)
self.plr_rownum = plr.plr_rownum
if not plr.track:
return
track = plr.track
self.artist = track.artist def play(self, position: Optional[float] = None) -> None:
self.duration = track.duration """Play track"""
self.end_time = None
self.fade_at = track.fade_at
self.intro = track.intro
self.path = track.path
self.playlist_id = plr.playlist_id
self.plr_id = plr.id
self.silence_at = track.silence_at
self.start_gap = track.start_gap
self.start_time = None
self.title = track.title
self.track_id = track.id
if track.silence_at and track.fade_at:
self.fade_length = track.silence_at - track.fade_at
# Initialise and add FadeCurve in a thread as it's slow
self.fadecurve_thread = QThread()
self.worker = AddFadeCurve(
self,
track_path=track.path,
track_fade_at=track.fade_at,
track_silence_at=track.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 start(self) -> None:
"""
Called when track starts playing
"""
now = dt.datetime.now() now = dt.datetime.now()
self.start_time = now self.start_time = now
if self.duration: self.player.play(self.path, position)
self.end_time = self.start_time + dt.timedelta(milliseconds=self.duration)
self.end_time = self.start_time + dt.timedelta(milliseconds=self.duration)
# Calculate time fade_graph should start updating # Calculate time fade_graph should start updating
if self.fade_at: if self.fade_at:
@ -213,13 +208,28 @@ class PlaylistTrack:
milliseconds=update_graph_at_ms milliseconds=update_graph_at_ms
) )
# Calculate time fade_graph should start updating def stop_playing(self, fade_seconds: int) -> None:
update_graph_at_ms = max( """
0, self.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1 Stop this track playing
) """
self.fade_graph_start_updates = now + dt.timedelta(
milliseconds=update_graph_at_ms self.resume_marker = self.player.get_position()
) self.player.fade(fade_seconds)
# Reset fade graph
if self.fade_graph:
self.fade_graph.clear()
class MainPlaylistTrack(_PlaylistTrack):
def __init__(self, plrid: int) -> None:
super().__init__(player_name=Config.VLC_MAIN_PLAYER_NAME, plrid=plrid)
class PreviewPlaylistTrack(_PlaylistTrack):
def __init__(self, plrid: int) -> None:
super().__init__(player_name=Config.VLC_PREVIEW_PLAYER_NAME, plrid=plrid)
@dataclass @dataclass
@ -246,7 +256,7 @@ class AddFadeCurve(QObject):
def __init__( def __init__(
self, self,
playlist_track: PlaylistTrack, playlist_track: _PlaylistTrack,
track_path: str, track_path: str,
track_fade_at: int, track_fade_at: int,
track_silence_at: int, track_silence_at: int,
@ -270,10 +280,11 @@ class AddFadeCurve(QObject):
self.finished.emit() self.finished.emit()
class TrackSequence: class PlaylistTracks:
next = PlaylistTrack() next: Optional[MainPlaylistTrack] = None
now = PlaylistTrack() now: Optional[MainPlaylistTrack] = None
previous = PlaylistTrack() previous: Optional[MainPlaylistTrack] = None
preview: Optional[PreviewPlaylistTrack] = None
track_sequence = TrackSequence() playlist_track = PlaylistTracks()

View File

@ -84,7 +84,7 @@ class Music:
else: else:
self.start_dt -= dt.timedelta(milliseconds=ms) self.start_dt -= dt.timedelta(milliseconds=ms)
def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None: def fade(self, fade_seconds: int) -> None:
""" """
Fade the currently playing track. Fade the currently playing track.
@ -100,6 +100,10 @@ class Music:
if not self.player.get_position() > 0 and self.player.is_playing(): if not self.player.get_position() > 0 and self.player.is_playing():
return return
if fade_seconds <= 0:
self._stop()
return
# Take a copy of current player to allow another track to be # Take a copy of current player to allow another track to be
# started without interfering here # started without interfering here
with lock: with lock:
@ -168,7 +172,7 @@ class Music:
self._adjust_by_ms(ms) self._adjust_by_ms(ms)
def play(self, path: str, position: Optional[float] = None) -> None: def play(self, path: str, position: Optional[float]) -> None:
""" """
Start playing the track at path. Start playing the track at path.
@ -238,7 +242,7 @@ class Music:
log.debug(f"Reset from {volume=}") log.debug(f"Reset from {volume=}")
sleep(0.1) sleep(0.1)
def stop(self) -> float: def _stop(self) -> None:
"""Immediately stop playing""" """Immediately stop playing"""
log.info(f"Music[{self.name}].stop()") log.info(f"Music[{self.name}].stop()")
@ -246,15 +250,13 @@ class Music:
self.start_dt = None self.start_dt = None
if not self.player: if not self.player:
return 0.0 return
p = self.player p = self.player
self.player = None self.player = None
self.start_dt = None self.start_dt = None
with lock: with lock:
position = p.get_position()
p.stop() p.stop()
p.release() p.release()
p = None p = None
return position

View File

@ -55,10 +55,10 @@ import stackprinter # type: ignore
# App imports # App imports
from classes import ( from classes import (
track_sequence, playlist_track,
FadeCurve, FadeCurve,
MusicMusterSignals, MusicMusterSignals,
PlaylistTrack, _PlaylistTrack,
TrackFileData, TrackFileData,
) )
from config import Config from config import Config
@ -430,7 +430,7 @@ class Window(QMainWindow, Ui_MainWindow):
Clear next track Clear next track
""" """
track_sequence.next = PlaylistTrack() playlist_track.next = None
self.update_headers() self.update_headers()
def clear_selection(self) -> None: def clear_selection(self) -> None:
@ -521,12 +521,13 @@ class Window(QMainWindow, Ui_MainWindow):
""" """
# Don't close current track playlist # Don't close current track playlist
current_track_playlist_id = track_sequence.now.playlist_id if playlist_track.now:
closing_tab_playlist_id = self.tabPlaylist.widget(tab_index).playlist_id current_track_playlist_id = playlist_track.now.playlist_id
if current_track_playlist_id: closing_tab_playlist_id = self.tabPlaylist.widget(tab_index).playlist_id
if closing_tab_playlist_id == current_track_playlist_id: if current_track_playlist_id:
self.show_status_message("Can't close current track playlist", 5000) if closing_tab_playlist_id == current_track_playlist_id:
return False self.show_status_message("Can't close current track playlist", 5000)
return False
# Record playlist as closed and update remaining playlist tabs # Record playlist as closed and update remaining playlist tabs
with db.Session() as session: with db.Session() as session:
@ -578,7 +579,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionMoveUnplayed.triggered.connect(self.move_unplayed) self.actionMoveUnplayed.triggered.connect(self.move_unplayed)
self.actionSetNext.triggered.connect(self.set_selected_track_next) self.actionSetNext.triggered.connect(self.set_selected_track_next)
self.actionSkipToNext.triggered.connect(self.play_next) self.actionSkipToNext.triggered.connect(self.play_next)
self.actionStop.triggered.connect(self.stop) self.actionStop.triggered.connect(self.stop_immediately)
self.btnDrop3db.clicked.connect(self.drop3db) self.btnDrop3db.clicked.connect(self.drop3db)
self.btnFade.clicked.connect(self.fade) self.btnFade.clicked.connect(self.fade)
self.btnHidePlayed.clicked.connect(self.hide_played) self.btnHidePlayed.clicked.connect(self.hide_played)
@ -589,7 +590,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnPreviewFwd.clicked.connect(self.preview_fwd) self.btnPreviewFwd.clicked.connect(self.preview_fwd)
self.btnPreviewMark.clicked.connect(self.preview_mark) self.btnPreviewMark.clicked.connect(self.preview_mark)
self.btnPreviewStart.clicked.connect(self.preview_start) self.btnPreviewStart.clicked.connect(self.preview_start)
self.btnStop.clicked.connect(self.stop) self.btnStop.clicked.connect(self.stop_immediately)
self.hdrCurrentTrack.clicked.connect(self.show_current) self.hdrCurrentTrack.clicked.connect(self.show_current)
self.hdrNextTrack.clicked.connect(self.show_next) self.hdrNextTrack.clicked.connect(self.show_next)
self.tabPlaylist.currentChanged.connect(self.tab_change) self.tabPlaylist.currentChanged.connect(self.tab_change)
@ -1147,38 +1148,34 @@ class Window(QMainWindow, Ui_MainWindow):
# Check for inadvertent press of 'return' # Check for inadvertent press of 'return'
if self.catch_return_key: if self.catch_return_key:
# Suppress inadvertent double press # Suppress inadvertent double press
if ( if playlist_track.now and playlist_track.now.start_time:
track_sequence.now.start_time if (
and track_sequence.now.start_time playlist_track.now.start_time
+ dt.timedelta(milliseconds=Config.RETURN_KEY_DEBOUNCE_MS) + dt.timedelta(milliseconds=Config.RETURN_KEY_DEBOUNCE_MS)
> dt.datetime.now() > dt.datetime.now()
): ):
return return
# If return is pressed during first PLAY_NEXT_GUARD_MS then
# default to NOT playing the next track, else default to # If return is pressed during first PLAY_NEXT_GUARD_MS then
# playing it. # default to NOT playing the next track, else default to
default_yes: bool = track_sequence.now.start_time is not None and ( # playing it.
(dt.datetime.now() - track_sequence.now.start_time).total_seconds() default_yes: bool = (
* 1000 dt.datetime.now() - playlist_track.now.start_time
> Config.PLAY_NEXT_GUARD_MS ).total_seconds() * 1000 > Config.PLAY_NEXT_GUARD_MS
) if not helpers.ask_yes_no(
if not helpers.ask_yes_no( "Track playing",
"Track playing", "Really play next track now?",
"Really play next track now?", default_yes=default_yes,
default_yes=default_yes, parent=self,
parent=self, ):
): return
return
log.info(f"play_next({position=})") log.info(f"play_next({position=})")
# If there is no next track set, return. # If there is no next track set, return.
if not track_sequence.next.track_id: if not playlist_track.next:
log.error("musicmuster.play_next(): no next track selected") log.error("musicmuster.play_next(): no next track selected")
return return
if not track_sequence.next.path:
log.error("musicmuster.play_next(): no path for next track")
return
# Issue #223 concerns a very short pause (maybe 0.1s) sometimes # Issue #223 concerns a very short pause (maybe 0.1s) sometimes
# when starting to play at track. # when starting to play at track.
@ -1195,7 +1192,7 @@ class Window(QMainWindow, Ui_MainWindow):
# Move next track to current track. # Move next track to current track.
# stop_playing() above has called end_of_track_actions() # stop_playing() above has called end_of_track_actions()
# which will have populated self.previous_track # which will have populated self.previous_track
track_sequence.now = track_sequence.next playlist_track.now = playlist_track.next
# Clear next track # Clear next track
self.clear_next() self.clear_next()
@ -1206,10 +1203,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnDrop3db.setChecked(False) self.btnDrop3db.setChecked(False)
# Play (new) current track # Play (new) current track
if not track_sequence.now.path: playlist_track.now.play()
log.error("No path for next track")
return
self.music.play(track_sequence.now.path, position)
# Show closing volume graph # Show closing volume graph
if track_sequence.now.fade_graph: if track_sequence.now.fade_graph:
@ -1252,14 +1246,14 @@ class Window(QMainWindow, Ui_MainWindow):
track_path = self.active_tab().get_selected_row_track_path() track_path = self.active_tab().get_selected_row_track_path()
if not track_path: if not track_path:
# Otherwise get path to next track to play # Otherwise get path to next track to play
track_path = track_sequence.next.path track_path = playlist_track.next.path
if not track_path: if not track_path:
self.btnPreview.setChecked(False) self.btnPreview.setChecked(False)
return return
self.preview_player.play(path=track_path) self.preview_player.play(path=track_path)
else: else:
self.preview_player.stop() self.preview_player._stop()
self.label_intro_timer.setText("0.0") self.label_intro_timer.setText("0.0")
self.btnPreviewMark.setEnabled(False) self.btnPreviewMark.setEnabled(False)
self.btnPreviewArm.setChecked(False) self.btnPreviewArm.setChecked(False)
@ -1417,27 +1411,27 @@ class Window(QMainWindow, Ui_MainWindow):
log.info("resume()") log.info("resume()")
# Return if no saved position # Return if no saved position
if not track_sequence.previous.resume_marker: if not playlist_track.previous.resume_marker:
log.error("No previous track position") log.error("No previous track position")
return return
# We want to use play_next() to resume, so copy the previous # We want to use play_next() to resume, so copy the previous
# track to the next track: # track to the next track:
track_sequence.next = track_sequence.previous playlist_track.next = playlist_track.previous
# Now resume playing the now-next track # Now resume playing the now-next track
self.play_next(track_sequence.next.resume_marker) self.play_next(playlist_track.next.resume_marker)
# Adjust track info so that clocks and graph are correct. # Adjust track info so that clocks and graph are correct.
# We need to fake the start time to reflect where we resumed the # We need to fake the start time to reflect where we resumed the
# track # track
if ( if (
track_sequence.now.start_time playlist_track.now.start_time
and track_sequence.now.duration and playlist_track.now.duration
and track_sequence.now.resume_marker and playlist_track.now.resume_marker
): ):
elapsed_ms = track_sequence.now.duration * track_sequence.now.resume_marker elapsed_ms = playlist_track.now.duration * playlist_track.now.resume_marker
track_sequence.now.start_time -= dt.timedelta(milliseconds=elapsed_ms) playlist_track.now.start_time -= dt.timedelta(milliseconds=elapsed_ms)
def save_as_template(self) -> None: def save_as_template(self) -> None:
"""Save current playlist as template""" """Save current playlist as template"""
@ -1563,7 +1557,7 @@ class Window(QMainWindow, Ui_MainWindow):
def show_current(self) -> None: def show_current(self) -> None:
"""Scroll to show current track""" """Scroll to show current track"""
self.show_track(track_sequence.now) self.show_track(playlist_track.now)
def show_warning(self, title: str, body: str) -> None: def show_warning(self, title: str, body: str) -> None:
""" """
@ -1576,7 +1570,7 @@ class Window(QMainWindow, Ui_MainWindow):
def show_next(self) -> None: def show_next(self) -> None:
"""Scroll to show next track""" """Scroll to show next track"""
self.show_track(track_sequence.next) self.show_track(playlist_track.next)
def show_status_message(self, message: str, timing: int) -> None: def show_status_message(self, message: str, timing: int) -> None:
""" """
@ -1585,7 +1579,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.statusbar.showMessage(message, timing) self.statusbar.showMessage(message, timing)
def show_track(self, plt: PlaylistTrack) -> None: def show_track(self, plt: _PlaylistTrack) -> None:
"""Scroll to show track in plt""" """Scroll to show track in plt"""
# Switch to the correct tab # Switch to the correct tab
@ -1636,7 +1630,7 @@ class Window(QMainWindow, Ui_MainWindow):
else: else:
return None return None
def stop(self) -> None: def stop_immediately(self) -> None:
"""Stop playing immediately""" """Stop playing immediately"""
self.stop_playing(fade=False) self.stop_playing(fade=False)
@ -1658,30 +1652,23 @@ class Window(QMainWindow, Ui_MainWindow):
- Enable controls - Enable controls
""" """
if not playlist_track.now:
return
# Set flag to say we're not playing a track so that timer ticks # Set flag to say we're not playing a track so that timer ticks
# don't see player=None and kick off end-of-track actions # don't see player=None and kick off end-of-track actions
if self.playing: if self.playing:
self.playing = False self.playing = False
else:
# Return if not playing
log.info("stop_playing() called but not playing")
return
# Stop/fade track # Stop/fade track
track_sequence.now.resume_marker = self.music.get_position()
if fade: if fade:
self.music.fade() playlist_track.now.stop_playing(fade_seconds=Config.FADEOUT_SECONDS)
else: else:
self.music.stop() playlist_track.now.stop_playing(fade_seconds=0)
# Reset fade graph # Reset playlist_track objects
if track_sequence.now.fade_graph: playlist_track.previous = playlist_track.now
track_sequence.now.fade_graph.clear() playlist_track.now = None
# Reset track_sequence objects
if track_sequence.now.track_id:
track_sequence.previous = track_sequence.now
track_sequence.now = PlaylistTrack()
# Tell model previous track has finished # Tell model previous track has finished
self.active_proxy_model().previous_track_ended() self.active_proxy_model().previous_track_ended()
@ -1712,20 +1699,20 @@ class Window(QMainWindow, Ui_MainWindow):
# Update volume fade curve # Update volume fade curve
if ( if (
track_sequence.now.fade_graph_start_updates is None playlist_track.now.fade_graph_start_updates is None
or track_sequence.now.fade_graph_start_updates > dt.datetime.now() or playlist_track.now.fade_graph_start_updates > dt.datetime.now()
): ):
return return
if ( if (
track_sequence.now.track_id playlist_track.now.track_id
and track_sequence.now.fade_graph and playlist_track.now.fade_graph
and track_sequence.now.start_time and playlist_track.now.start_time
): ):
play_time = ( play_time = (
dt.datetime.now() - track_sequence.now.start_time dt.datetime.now() - playlist_track.now.start_time
).total_seconds() * 1000 ).total_seconds() * 1000
track_sequence.now.fade_graph.tick(play_time) playlist_track.now.fade_graph.tick(play_time)
def tick_500ms(self) -> None: def tick_500ms(self) -> None:
""" """
@ -1800,7 +1787,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.label_elapsed_timer.setText( self.label_elapsed_timer.setText(
helpers.ms_to_mmss(playtime) helpers.ms_to_mmss(playtime)
+ " / " + " / "
+ helpers.ms_to_mmss(track_sequence.now.duration) + helpers.ms_to_mmss(playlist_track.now.duration)
) )
# Time to fade # Time to fade
@ -1845,25 +1832,37 @@ class Window(QMainWindow, Ui_MainWindow):
Update last / current / next track headers Update last / current / next track headers
""" """
if track_sequence.previous.title and track_sequence.previous.artist: if (
playlist_track.previous
and playlist_track.previous.title
and playlist_track.previous.artist
):
self.hdrPreviousTrack.setText( self.hdrPreviousTrack.setText(
f"{track_sequence.previous.title} - {track_sequence.previous.artist}" f"{playlist_track.previous.title} - {playlist_track.previous.artist}"
) )
else: else:
self.hdrPreviousTrack.setText("") self.hdrPreviousTrack.setText("")
if track_sequence.now.title and track_sequence.now.artist: if (
playlist_track.now
and playlist_track.now.title
and playlist_track.now.artist
):
self.hdrCurrentTrack.setText( self.hdrCurrentTrack.setText(
f"{track_sequence.now.title.replace('&', '&&')} - " f"{playlist_track.now.title.replace('&', '&&')} - "
f"{track_sequence.now.artist.replace('&', '&&')}" f"{playlist_track.now.artist.replace('&', '&&')}"
) )
else: else:
self.hdrCurrentTrack.setText("") self.hdrCurrentTrack.setText("")
if track_sequence.next.title and track_sequence.next.artist: if (
playlist_track.next
and playlist_track.next.title
and playlist_track.next.artist
):
self.hdrNextTrack.setText( self.hdrNextTrack.setText(
f"{track_sequence.next.title.replace('&', '&&')} - " f"{playlist_track.next.title.replace('&', '&&')} - "
f"{track_sequence.next.artist.replace('&', '&&')}" f"{playlist_track.next.artist.replace('&', '&&')}"
) )
else: else:
self.hdrNextTrack.setText("") self.hdrNextTrack.setText("")

View File

@ -30,7 +30,13 @@ import obswebsocket # type: ignore
# import snoop # type: ignore # import snoop # type: ignore
# App imports # App imports
from classes import Col, track_sequence, MusicMusterSignals, PlaylistTrack from classes import (
Col,
playlist_track,
MusicMusterSignals,
MainPlaylistTrack,
PreviewPlaylistTrack,
)
from config import Config from config import Config
from helpers import ( from helpers import (
file_is_unreadable, file_is_unreadable,
@ -195,10 +201,10 @@ class PlaylistModel(QAbstractTableModel):
if file_is_unreadable(prd.path): if file_is_unreadable(prd.path):
return QBrush(QColor(Config.COLOUR_UNREADABLE)) return QBrush(QColor(Config.COLOUR_UNREADABLE))
# Current track # Current track
if prd.plrid == track_sequence.now.plr_id: if playlist_track.now and prd.plrid == playlist_track.now.plr_id:
return QBrush(QColor(Config.COLOUR_CURRENT_PLAYLIST)) return QBrush(QColor(Config.COLOUR_CURRENT_PLAYLIST))
# Next track # Next track
if prd.plrid == track_sequence.next.plr_id: if playlist_track.next and prd.plrid == playlist_track.next.plr_id:
return QBrush(QColor(Config.COLOUR_NEXT_PLAYLIST)) return QBrush(QColor(Config.COLOUR_NEXT_PLAYLIST))
# Individual cell colouring # Individual cell colouring
@ -250,25 +256,15 @@ class PlaylistModel(QAbstractTableModel):
- find next track - find next track
""" """
row_number = track_sequence.now.plr_rownum if not playlist_track.now:
return
row_number = playlist_track.now.plr_rownum
if row_number is not None: if row_number is not None:
prd = self.playlist_rows[row_number] prd = self.playlist_rows[row_number]
else: else:
prd = None prd = None
# Sanity check
if not track_sequence.now.track_id:
log.error(
"playlistmodel:current_track_started called with no current track"
)
return
if row_number is None:
log.error(
"playlistmodel:current_track_started called with no row number "
f"({track_sequence.now=})"
)
return
# Check for OBS scene change # Check for OBS scene change
log.debug("Call OBS scene change") log.debug("Call OBS scene change")
self.obs_scene_change(row_number) self.obs_scene_change(row_number)
@ -276,29 +272,29 @@ class PlaylistModel(QAbstractTableModel):
with db.Session() as session: with db.Session() as session:
# Update Playdates in database # Update Playdates in database
log.debug("update playdates") log.debug("update playdates")
Playdates(session, track_sequence.now.track_id) Playdates(session, playlist_track.now.track_id)
# Mark track as played in playlist # Mark track as played in playlist
log.debug("Mark track as played") log.debug("Mark track as played")
plr = session.get(PlaylistRows, track_sequence.now.plr_id) plr = session.get(PlaylistRows, playlist_track.now.plr_id)
if plr: if plr:
plr.played = True plr.played = True
self.refresh_row(session, plr.plr_rownum) self.refresh_row(session, plr.plr_rownum)
else: else:
log.error(f"Can't retrieve plr, {track_sequence.now.plr_id=}") log.error(f"Can't retrieve plr, {playlist_track.now.plr_id=}")
# Update track times # Update track times
log.debug("Update track times") log.debug("Update track times")
if prd: if prd:
prd.start_time = track_sequence.now.start_time prd.start_time = playlist_track.now.start_time
prd.end_time = track_sequence.now.end_time prd.end_time = playlist_track.now.end_time
# Update colour and times for current row # Update colour and times for current row
self.invalidate_row(row_number) self.invalidate_row(row_number)
# Update previous row in case we're hiding played rows # Update previous row in case we're hiding played rows
if track_sequence.previous.plr_rownum: if playlist_track.previous and playlist_track.previous.plr_rownum:
self.invalidate_row(track_sequence.previous.plr_rownum) self.invalidate_row(playlist_track.previous.plr_rownum)
# Update all other track times # Update all other track times
self.update_track_times() self.update_track_times()
@ -391,7 +387,7 @@ class PlaylistModel(QAbstractTableModel):
session.commit() session.commit()
super().endRemoveRows() super().endRemoveRows()
self.reset_track_sequence_row_numbers() self.reset_playlist_track_row_numbers()
def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
""" """
@ -464,7 +460,7 @@ class PlaylistModel(QAbstractTableModel):
with db.Session() as session: with db.Session() as session:
self.refresh_data(session) self.refresh_data(session)
super().endResetModel() super().endResetModel()
self.reset_track_sequence_row_numbers() self.reset_playlist_track_row_numbers()
def edit_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: def edit_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant:
""" """
@ -703,16 +699,16 @@ class PlaylistModel(QAbstractTableModel):
# calculate end time if all tracks are played. # calculate end time if all tracks are played.
end_time_str = "" end_time_str = ""
if ( if (
track_sequence.now.plr_rownum playlist_track.now and playlist_track.now.plr_rownum
and track_sequence.now.end_time and playlist_track.now.end_time
and ( and (
row_number row_number
< track_sequence.now.plr_rownum < playlist_track.now.plr_rownum
< prd.plr_rownum < prd.plr_rownum
) )
): ):
section_end_time = ( section_end_time = (
track_sequence.now.end_time playlist_track.now.end_time
+ dt.timedelta(milliseconds=duration) + dt.timedelta(milliseconds=duration)
) )
end_time_str = ( end_time_str = (
@ -793,7 +789,7 @@ class PlaylistModel(QAbstractTableModel):
super().endInsertRows() super().endInsertRows()
self.signals.resize_rows_signal.emit(self.playlist_id) self.signals.resize_rows_signal.emit(self.playlist_id)
self.reset_track_sequence_row_numbers() self.reset_playlist_track_row_numbers()
self.invalidate_rows(list(range(new_row_number, len(self.playlist_rows)))) self.invalidate_rows(list(range(new_row_number, len(self.playlist_rows))))
def invalidate_row(self, modified_row: int) -> None: def invalidate_row(self, modified_row: int) -> None:
@ -905,15 +901,15 @@ class PlaylistModel(QAbstractTableModel):
if old_row != new_row: if old_row != new_row:
row_map[old_row] = new_row row_map[old_row] = new_row
# Check to see whether any rows in track_sequence have moved # Check to see whether any rows in playlist_tracks have moved
if track_sequence.previous.plr_rownum in row_map: if playlist_track.previous and playlist_track.previous.plr_rownum in row_map:
track_sequence.previous.plr_rownum = row_map[ playlist_track.previous.plr_rownum = row_map[
track_sequence.previous.plr_rownum playlist_track.previous.plr_rownum
] ]
if track_sequence.now.plr_rownum in row_map: if playlist_track.now and playlist_track.now.plr_rownum in row_map:
track_sequence.now.plr_rownum = row_map[track_sequence.now.plr_rownum] playlist_track.now.plr_rownum = row_map[playlist_track.now.plr_rownum]
if track_sequence.next.plr_rownum in row_map: if playlist_track.next and playlist_track.next.plr_rownum in row_map:
track_sequence.next.plr_rownum = row_map[track_sequence.next.plr_rownum] playlist_track.next.plr_rownum = row_map[playlist_track.next.plr_rownum]
# For SQLAlchemy, build a list of dictionaries that map plrid to # For SQLAlchemy, build a list of dictionaries that map plrid to
# new row number: # new row number:
@ -929,7 +925,7 @@ class PlaylistModel(QAbstractTableModel):
self.refresh_data(session) self.refresh_data(session)
# Update display # Update display
self.reset_track_sequence_row_numbers() self.reset_playlist_track_row_numbers()
self.invalidate_rows(list(row_map.keys())) self.invalidate_rows(list(row_map.keys()))
def move_rows_between_playlists( def move_rows_between_playlists(
@ -973,7 +969,7 @@ class PlaylistModel(QAbstractTableModel):
self.playlist_id, self.playlist_id,
[self.playlist_rows[a].plrid for a in row_group], [self.playlist_rows[a].plrid for a in row_group],
): ):
if plr.id == track_sequence.now.plr_id: if playlist_track.now and plr.id == playlist_track.now.plr_id:
# Don't move current track # Don't move current track
continue continue
plr.playlist_id = to_playlist_id plr.playlist_id = to_playlist_id
@ -988,7 +984,7 @@ class PlaylistModel(QAbstractTableModel):
session.commit() session.commit()
# Reset of model must come after session has been closed # Reset of model must come after session has been closed
self.reset_track_sequence_row_numbers() self.reset_playlist_track_row_numbers()
self.signals.row_order_changed_signal.emit(to_playlist_id) self.signals.row_order_changed_signal.emit(to_playlist_id)
self.signals.end_reset_model_signal.emit(to_playlist_id) self.signals.end_reset_model_signal.emit(to_playlist_id)
self.update_track_times() self.update_track_times()
@ -1080,18 +1076,18 @@ class PlaylistModel(QAbstractTableModel):
log.info("previous_track_ended()") log.info("previous_track_ended()")
# Sanity check # Sanity check
if not track_sequence.previous.track_id: if not playlist_track.previous:
log.error("playlistmodel:previous_track_ended called with no current track") log.error("playlistmodel:previous_track_ended called with no current track")
return return
if track_sequence.previous.plr_rownum is None: if playlist_track.previous.plr_rownum is None:
log.error( log.error(
"playlistmodel:previous_track_ended called with no row number " "playlistmodel:previous_track_ended called with no row number "
f"({track_sequence.previous=})" f"({playlist_track.previous=})"
) )
return return
# Update display # Update display
self.invalidate_row(track_sequence.previous.plr_rownum) self.invalidate_row(playlist_track.previous.plr_rownum)
def refresh_data(self, session: db.session): def refresh_data(self, session: db.session):
"""Populate dicts for data calls""" """Populate dicts for data calls"""
@ -1138,28 +1134,28 @@ class PlaylistModel(QAbstractTableModel):
self.signals.resize_rows_signal.emit(self.playlist_id) self.signals.resize_rows_signal.emit(self.playlist_id)
session.commit() session.commit()
def reset_track_sequence_row_numbers(self) -> None: def reset_playlist_track_row_numbers(self) -> None:
""" """
Signal handler for when row ordering has changed Signal handler for when row ordering has changed
""" """
log.debug("reset_track_sequence_row_numbers()") log.debug("reset_playlist_track_row_numbers()")
# Check the track_sequence next, now and previous plrs and # Check the playlist_track next, now and previous plrs and
# update the row number # update the row number
with db.Session() as session: with db.Session() as session:
if track_sequence.next.plr_rownum: if playlist_track.next and playlist_track.next.plr_rownum:
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id) next_plr = session.get(PlaylistRows, playlist_track.next.plr_id)
if next_plr: if next_plr:
track_sequence.next.plr_rownum = next_plr.plr_rownum playlist_track.next.plr_rownum = next_plr.plr_rownum
if track_sequence.now.plr_rownum: if playlist_track.now and playlist_track.now.plr_rownum:
now_plr = session.get(PlaylistRows, track_sequence.now.plr_id) now_plr = session.get(PlaylistRows, playlist_track.now.plr_id)
if now_plr: if now_plr:
track_sequence.now.plr_rownum = now_plr.plr_rownum playlist_track.now.plr_rownum = now_plr.plr_rownum
if track_sequence.previous.plr_rownum: if playlist_track.previous and playlist_track.previous.plr_rownum:
previous_plr = session.get(PlaylistRows, track_sequence.previous.plr_id) previous_plr = session.get(PlaylistRows, playlist_track.previous.plr_id)
if previous_plr: if previous_plr:
track_sequence.previous.plr_rownum = previous_plr.plr_rownum playlist_track.previous.plr_rownum = previous_plr.plr_rownum
self.update_track_times() self.update_track_times()
@ -1210,7 +1206,7 @@ class PlaylistModel(QAbstractTableModel):
if playlist_id != self.playlist_id: if playlist_id != self.playlist_id:
return return
self.reset_track_sequence_row_numbers() self.reset_playlist_track_row_numbers()
def selection_is_sortable(self, row_numbers: List[int]) -> bool: def selection_is_sortable(self, row_numbers: List[int]) -> bool:
""" """
@ -1240,59 +1236,43 @@ class PlaylistModel(QAbstractTableModel):
Set row_number as next track. If row_number is None, clear next track. Set row_number as next track. If row_number is None, clear next track.
""" """
log.info(f"set_next_row({row_number=})") log.debug(f"set_next_row({row_number=})")
next_row_was = track_sequence.next.plr_rownum
if row_number is None: if row_number is None:
if next_row_was is None: # Clear next track
if playlist_track.next:
playlist_track.next = None
self.signals.next_track_changed_signal.emit()
else:
return return
track_sequence.next = PlaylistTrack() else:
self.signals.next_track_changed_signal.emit() # Update playlist_track
return
# Update track_sequence
with db.Session() as session:
track_sequence.next = PlaylistTrack()
try: try:
plrid = self.playlist_rows[row_number].plrid plrid = self.playlist_rows[row_number].plrid
except IndexError: except IndexError:
log.error( log.error(
f"playlistmodel.set_next_track({row_number=}, " f"playlistmodel.set_next_track({row_number=}, " f"{self.playlist_id=}"
f"{self.playlist_id=}"
) )
return return
plr = session.get(PlaylistRows, plrid) try:
if plr: playlist_track.next = MainPlaylistTrack(plrid)
# Check this isn't a header row except ValueError as e:
if self.is_header_row(row_number): log.error(f"Error creating MainPlaylistTrack({plrid=}): ({str(e)})")
log.error( return
"Tried to set next row on header row: "
f"playlistmodel.set_next_track({row_number=}, "
f"{self.playlist_id=}"
)
return
# Check track is readable
if file_is_unreadable(plr.track.path):
log.error(
"Tried to set next row on unreadable row: "
f"playlistmodel.set_next_track({row_number=}, "
f"{self.playlist_id=}"
)
return
track_sequence.next.set_plr(session, plr)
self.signals.next_track_changed_signal.emit()
self.signals.search_wikipedia_signal.emit(
self.playlist_rows[row_number].title
)
self.invalidate_row(row_number)
if next_row_was is not None: self.signals.search_wikipedia_signal.emit(
self.invalidate_row(next_row_was) self.playlist_rows[row_number].title
)
self.invalidate_row(row_number)
self.signals.next_track_changed_signal.emit()
self.update_track_times() self.update_track_times()
def setData( def setData(
self, index: QModelIndex, value: str | float, role: int = Qt.ItemDataRole.EditRole self,
index: QModelIndex,
value: str | float,
role: int = Qt.ItemDataRole.EditRole,
) -> bool: ) -> bool:
""" """
Update model with edited data Update model with edited data
@ -1434,9 +1414,9 @@ class PlaylistModel(QAbstractTableModel):
prd = self.playlist_rows[row_number] prd = self.playlist_rows[row_number]
# Reset start_time if this is the current row # Reset start_time if this is the current row
if row_number == track_sequence.now.plr_rownum: if row_number == playlist_track.now.plr_rownum:
prd.start_time = track_sequence.now.start_time prd.start_time = playlist_track.now.start_time
prd.end_time = track_sequence.now.end_time prd.end_time = playlist_track.now.end_time
update_rows.append(row_number) update_rows.append(row_number)
if not next_start_time: if not next_start_time:
next_start_time = prd.end_time next_start_time = prd.end_time
@ -1444,10 +1424,10 @@ class PlaylistModel(QAbstractTableModel):
# Set start time for next row if we have a current track # Set start time for next row if we have a current track
if ( if (
row_number == track_sequence.next.plr_rownum row_number == playlist_track.next.plr_rownum
and track_sequence.now.end_time and playlist_track.now.end_time
): ):
prd.start_time = track_sequence.now.end_time prd.start_time = playlist_track.now.end_time
prd.end_time = prd.start_time + dt.timedelta(milliseconds=prd.duration) prd.end_time = prd.start_time + dt.timedelta(milliseconds=prd.duration)
next_start_time = prd.end_time next_start_time = prd.end_time
update_rows.append(row_number) update_rows.append(row_number)
@ -1460,11 +1440,11 @@ class PlaylistModel(QAbstractTableModel):
# If we're between the current and next row, zero out # If we're between the current and next row, zero out
# times # times
if ( if (
track_sequence.now.plr_rownum is not None playlist_track.now.plr_rownum is not None
and track_sequence.next.plr_rownum is not None and playlist_track.next.plr_rownum is not None
and track_sequence.now.plr_rownum and playlist_track.now.plr_rownum
< row_number < row_number
< track_sequence.next.plr_rownum < playlist_track.next.plr_rownum
): ):
prd.start_time = None prd.start_time = None
prd.end_time = None prd.end_time = None
@ -1539,16 +1519,16 @@ class PlaylistProxyModel(QSortFilterProxyModel):
if self.source_model.is_played_row(source_row): if self.source_model.is_played_row(source_row):
# Don't hide current or next track # Don't hide current or next track
with db.Session() as session: with db.Session() as session:
if track_sequence.next.plr_id: if playlist_track.next and playlist_track.next.plr_id:
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id) next_plr = session.get(PlaylistRows, playlist_track.next.plr_id)
if ( if (
next_plr next_plr
and next_plr.plr_rownum == source_row and next_plr.plr_rownum == source_row
and next_plr.playlist_id == self.source_model.playlist_id and next_plr.playlist_id == self.source_model.playlist_id
): ):
return True return True
if track_sequence.now.plr_id: if playlist_track.now and playlist_track.now.plr_id:
now_plr = session.get(PlaylistRows, track_sequence.now.plr_id) now_plr = session.get(PlaylistRows, playlist_track.now.plr_id)
if ( if (
now_plr now_plr
and now_plr.plr_rownum == source_row and now_plr.plr_rownum == source_row
@ -1558,9 +1538,9 @@ class PlaylistProxyModel(QSortFilterProxyModel):
# Don't hide previous track until # Don't hide previous track until
# HIDE_AFTER_PLAYING_OFFSET milliseconds after # HIDE_AFTER_PLAYING_OFFSET milliseconds after
# current track has started # current track has started
if track_sequence.previous.plr_id: if playlist_track.previous and playlist_track.previous.plr_id:
previous_plr = session.get( previous_plr = session.get(
PlaylistRows, track_sequence.previous.plr_id PlaylistRows, playlist_track.previous.plr_id
) )
if ( if (
previous_plr previous_plr
@ -1568,9 +1548,9 @@ class PlaylistProxyModel(QSortFilterProxyModel):
and previous_plr.playlist_id and previous_plr.playlist_id
== self.source_model.playlist_id == self.source_model.playlist_id
): ):
if track_sequence.now.start_time: if playlist_track.now and playlist_track.now.start_time:
if dt.datetime.now() > ( if dt.datetime.now() > (
track_sequence.now.start_time playlist_track.now.start_time
+ dt.timedelta( + dt.timedelta(
milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET
) )

View File

@ -35,7 +35,7 @@ from PyQt6.QtWidgets import (
# Third party imports # Third party imports
# App imports # App imports
from classes import Col, MusicMusterSignals, track_sequence from classes import Col, MusicMusterSignals, playlist_track
from config import Config from config import Config
from dialogs import TrackSelectDialog from dialogs import TrackSelectDialog
from helpers import ( from helpers import (
@ -425,8 +425,8 @@ class PlaylistTab(QTableView):
header_row = proxy_model.is_header_row(model_row_number) header_row = proxy_model.is_header_row(model_row_number)
track_row = not header_row track_row = not header_row
current_row = model_row_number == track_sequence.now.plr_rownum current_row = model_row_number == playlist_track.now.plr_rownum
next_row = model_row_number == track_sequence.next.plr_rownum next_row = model_row_number == playlist_track.next.plr_rownum
track_path = self.source_model.get_row_info(model_row_number).path track_path = self.source_model.get_row_info(model_row_number).path
# Open/import in/from Audacity # Open/import in/from Audacity