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
from config import Config
from log import log
from models import PlaylistRows
from models import db, PlaylistRows
from music import Music
import helpers
@ -116,93 +117,87 @@ class MusicMusterSignals(QObject):
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.
"""
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
self.duration: Optional[int] = None
self.end_time: Optional[dt.datetime] = None
self.fade_at: Optional[int] = None
self.fade_graph: Optional[FadeCurve] = None
self.fade_graph_start_updates: Optional[dt.datetime] = None
self.fade_length: Optional[int] = None
self.path: Optional[str] = None
self.playlist_id: Optional[int] = None
self.plr_id: Optional[int] = None
self.plr_rownum: Optional[int] = None
self.resume_marker: Optional[float] = None
self.silence_at: Optional[int] = None
self.start_gap: Optional[int] = None
self.start_time: Optional[dt.datetime] = None
self.title: Optional[str] = None
self.track_id: Optional[int] = None
with db.Session() as session:
plr = session.get(PlaylistRows, plrid)
if not plr.track:
raise ValueError("No track defined in plr passed to PlaylistTrack")
if helpers.file_is_unreadable(plr.track.path):
raise ValueError(f"_PlaylistTrack({plrid=}): {plr.track.path} is unreadable")
track = plr.track
self.artist = track.artist
self.duration = track.duration
self.end_time: Optional[dt.datetime] = None
self.fade_at = track.fade_at
self.fade_graph: Optional[FadeCurve] = None
self.fade_graph_start_updates: Optional[dt.datetime] = None
self.fade_length: Optional[int] = None
self.intro = track.intro
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:
return (
f"<PlaylistTrack(title={self.title}, artist={self.artist}, "
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:
"""
Update with new plr information
"""
def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None:
"""Fade music"""
session.add(plr)
self.plr_rownum = plr.plr_rownum
if not plr.track:
return
track = plr.track
self.player.fade(fade_seconds)
self.artist = track.artist
self.duration = track.duration
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
"""
def play(self, position: Optional[float] = None) -> None:
"""Play track"""
now = dt.datetime.now()
self.start_time = now
if self.duration:
self.end_time = self.start_time + dt.timedelta(milliseconds=self.duration)
self.player.play(self.path, position)
self.end_time = self.start_time + dt.timedelta(milliseconds=self.duration)
# Calculate time fade_graph should start updating
if self.fade_at:
@ -213,13 +208,28 @@ class PlaylistTrack:
milliseconds=update_graph_at_ms
)
# Calculate time fade_graph should start updating
update_graph_at_ms = max(
0, self.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1
)
self.fade_graph_start_updates = now + dt.timedelta(
milliseconds=update_graph_at_ms
)
def stop_playing(self, fade_seconds: int) -> None:
"""
Stop this track playing
"""
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
@ -246,7 +256,7 @@ class AddFadeCurve(QObject):
def __init__(
self,
playlist_track: PlaylistTrack,
playlist_track: _PlaylistTrack,
track_path: str,
track_fade_at: int,
track_silence_at: int,
@ -270,10 +280,11 @@ class AddFadeCurve(QObject):
self.finished.emit()
class TrackSequence:
next = PlaylistTrack()
now = PlaylistTrack()
previous = PlaylistTrack()
class PlaylistTracks:
next: Optional[MainPlaylistTrack] = None
now: Optional[MainPlaylistTrack] = None
previous: Optional[MainPlaylistTrack] = None
preview: Optional[PreviewPlaylistTrack] = None
track_sequence = TrackSequence()
playlist_track = PlaylistTracks()

View File

@ -84,7 +84,7 @@ class Music:
else:
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.
@ -100,6 +100,10 @@ class Music:
if not self.player.get_position() > 0 and self.player.is_playing():
return
if fade_seconds <= 0:
self._stop()
return
# Take a copy of current player to allow another track to be
# started without interfering here
with lock:
@ -168,7 +172,7 @@ class Music:
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.
@ -238,7 +242,7 @@ class Music:
log.debug(f"Reset from {volume=}")
sleep(0.1)
def stop(self) -> float:
def _stop(self) -> None:
"""Immediately stop playing"""
log.info(f"Music[{self.name}].stop()")
@ -246,15 +250,13 @@ class Music:
self.start_dt = None
if not self.player:
return 0.0
return
p = self.player
self.player = None
self.start_dt = None
with lock:
position = p.get_position()
p.stop()
p.release()
p = None
return position

View File

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

View File

@ -30,7 +30,13 @@ import obswebsocket # type: ignore
# import snoop # type: ignore
# App imports
from classes import Col, track_sequence, MusicMusterSignals, PlaylistTrack
from classes import (
Col,
playlist_track,
MusicMusterSignals,
MainPlaylistTrack,
PreviewPlaylistTrack,
)
from config import Config
from helpers import (
file_is_unreadable,
@ -195,10 +201,10 @@ class PlaylistModel(QAbstractTableModel):
if file_is_unreadable(prd.path):
return QBrush(QColor(Config.COLOUR_UNREADABLE))
# 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))
# 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))
# Individual cell colouring
@ -250,25 +256,15 @@ class PlaylistModel(QAbstractTableModel):
- 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:
prd = self.playlist_rows[row_number]
else:
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
log.debug("Call OBS scene change")
self.obs_scene_change(row_number)
@ -276,29 +272,29 @@ class PlaylistModel(QAbstractTableModel):
with db.Session() as session:
# Update Playdates in database
log.debug("update playdates")
Playdates(session, track_sequence.now.track_id)
Playdates(session, playlist_track.now.track_id)
# Mark track as played in playlist
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:
plr.played = True
self.refresh_row(session, plr.plr_rownum)
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
log.debug("Update track times")
if prd:
prd.start_time = track_sequence.now.start_time
prd.end_time = track_sequence.now.end_time
prd.start_time = playlist_track.now.start_time
prd.end_time = playlist_track.now.end_time
# Update colour and times for current row
self.invalidate_row(row_number)
# Update previous row in case we're hiding played rows
if track_sequence.previous.plr_rownum:
self.invalidate_row(track_sequence.previous.plr_rownum)
if playlist_track.previous and playlist_track.previous.plr_rownum:
self.invalidate_row(playlist_track.previous.plr_rownum)
# Update all other track times
self.update_track_times()
@ -391,7 +387,7 @@ class PlaylistModel(QAbstractTableModel):
session.commit()
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:
"""
@ -464,7 +460,7 @@ class PlaylistModel(QAbstractTableModel):
with db.Session() as session:
self.refresh_data(session)
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:
"""
@ -703,16 +699,16 @@ class PlaylistModel(QAbstractTableModel):
# calculate end time if all tracks are played.
end_time_str = ""
if (
track_sequence.now.plr_rownum
and track_sequence.now.end_time
playlist_track.now and playlist_track.now.plr_rownum
and playlist_track.now.end_time
and (
row_number
< track_sequence.now.plr_rownum
< playlist_track.now.plr_rownum
< prd.plr_rownum
)
):
section_end_time = (
track_sequence.now.end_time
playlist_track.now.end_time
+ dt.timedelta(milliseconds=duration)
)
end_time_str = (
@ -793,7 +789,7 @@ class PlaylistModel(QAbstractTableModel):
super().endInsertRows()
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))))
def invalidate_row(self, modified_row: int) -> None:
@ -905,15 +901,15 @@ class PlaylistModel(QAbstractTableModel):
if old_row != new_row:
row_map[old_row] = new_row
# Check to see whether any rows in track_sequence have moved
if track_sequence.previous.plr_rownum in row_map:
track_sequence.previous.plr_rownum = row_map[
track_sequence.previous.plr_rownum
# Check to see whether any rows in playlist_tracks have moved
if playlist_track.previous and playlist_track.previous.plr_rownum in row_map:
playlist_track.previous.plr_rownum = row_map[
playlist_track.previous.plr_rownum
]
if track_sequence.now.plr_rownum in row_map:
track_sequence.now.plr_rownum = row_map[track_sequence.now.plr_rownum]
if track_sequence.next.plr_rownum in row_map:
track_sequence.next.plr_rownum = row_map[track_sequence.next.plr_rownum]
if playlist_track.now and playlist_track.now.plr_rownum in row_map:
playlist_track.now.plr_rownum = row_map[playlist_track.now.plr_rownum]
if playlist_track.next and playlist_track.next.plr_rownum in row_map:
playlist_track.next.plr_rownum = row_map[playlist_track.next.plr_rownum]
# For SQLAlchemy, build a list of dictionaries that map plrid to
# new row number:
@ -929,7 +925,7 @@ class PlaylistModel(QAbstractTableModel):
self.refresh_data(session)
# Update display
self.reset_track_sequence_row_numbers()
self.reset_playlist_track_row_numbers()
self.invalidate_rows(list(row_map.keys()))
def move_rows_between_playlists(
@ -973,7 +969,7 @@ class PlaylistModel(QAbstractTableModel):
self.playlist_id,
[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
continue
plr.playlist_id = to_playlist_id
@ -988,7 +984,7 @@ class PlaylistModel(QAbstractTableModel):
session.commit()
# 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.end_reset_model_signal.emit(to_playlist_id)
self.update_track_times()
@ -1080,18 +1076,18 @@ class PlaylistModel(QAbstractTableModel):
log.info("previous_track_ended()")
# 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")
return
if track_sequence.previous.plr_rownum is None:
if playlist_track.previous.plr_rownum is None:
log.error(
"playlistmodel:previous_track_ended called with no row number "
f"({track_sequence.previous=})"
f"({playlist_track.previous=})"
)
return
# 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):
"""Populate dicts for data calls"""
@ -1138,28 +1134,28 @@ class PlaylistModel(QAbstractTableModel):
self.signals.resize_rows_signal.emit(self.playlist_id)
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
"""
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
with db.Session() as session:
if track_sequence.next.plr_rownum:
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id)
if playlist_track.next and playlist_track.next.plr_rownum:
next_plr = session.get(PlaylistRows, playlist_track.next.plr_id)
if next_plr:
track_sequence.next.plr_rownum = next_plr.plr_rownum
if track_sequence.now.plr_rownum:
now_plr = session.get(PlaylistRows, track_sequence.now.plr_id)
playlist_track.next.plr_rownum = next_plr.plr_rownum
if playlist_track.now and playlist_track.now.plr_rownum:
now_plr = session.get(PlaylistRows, playlist_track.now.plr_id)
if now_plr:
track_sequence.now.plr_rownum = now_plr.plr_rownum
if track_sequence.previous.plr_rownum:
previous_plr = session.get(PlaylistRows, track_sequence.previous.plr_id)
playlist_track.now.plr_rownum = now_plr.plr_rownum
if playlist_track.previous and playlist_track.previous.plr_rownum:
previous_plr = session.get(PlaylistRows, playlist_track.previous.plr_id)
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()
@ -1210,7 +1206,7 @@ class PlaylistModel(QAbstractTableModel):
if playlist_id != self.playlist_id:
return
self.reset_track_sequence_row_numbers()
self.reset_playlist_track_row_numbers()
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.
"""
log.info(f"set_next_row({row_number=})")
next_row_was = track_sequence.next.plr_rownum
log.debug(f"set_next_row({row_number=})")
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
track_sequence.next = PlaylistTrack()
self.signals.next_track_changed_signal.emit()
return
# Update track_sequence
with db.Session() as session:
track_sequence.next = PlaylistTrack()
else:
# Update playlist_track
try:
plrid = self.playlist_rows[row_number].plrid
except IndexError:
log.error(
f"playlistmodel.set_next_track({row_number=}, "
f"{self.playlist_id=}"
f"playlistmodel.set_next_track({row_number=}, " f"{self.playlist_id=}"
)
return
plr = session.get(PlaylistRows, plrid)
if plr:
# Check this isn't a header row
if self.is_header_row(row_number):
log.error(
"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)
try:
playlist_track.next = MainPlaylistTrack(plrid)
except ValueError as e:
log.error(f"Error creating MainPlaylistTrack({plrid=}): ({str(e)})")
return
if next_row_was is not None:
self.invalidate_row(next_row_was)
self.signals.search_wikipedia_signal.emit(
self.playlist_rows[row_number].title
)
self.invalidate_row(row_number)
self.signals.next_track_changed_signal.emit()
self.update_track_times()
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:
"""
Update model with edited data
@ -1434,9 +1414,9 @@ class PlaylistModel(QAbstractTableModel):
prd = self.playlist_rows[row_number]
# Reset start_time if this is the current row
if row_number == track_sequence.now.plr_rownum:
prd.start_time = track_sequence.now.start_time
prd.end_time = track_sequence.now.end_time
if row_number == playlist_track.now.plr_rownum:
prd.start_time = playlist_track.now.start_time
prd.end_time = playlist_track.now.end_time
update_rows.append(row_number)
if not next_start_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
if (
row_number == track_sequence.next.plr_rownum
and track_sequence.now.end_time
row_number == playlist_track.next.plr_rownum
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)
next_start_time = prd.end_time
update_rows.append(row_number)
@ -1460,11 +1440,11 @@ class PlaylistModel(QAbstractTableModel):
# If we're between the current and next row, zero out
# times
if (
track_sequence.now.plr_rownum is not None
and track_sequence.next.plr_rownum is not None
and track_sequence.now.plr_rownum
playlist_track.now.plr_rownum is not None
and playlist_track.next.plr_rownum is not None
and playlist_track.now.plr_rownum
< row_number
< track_sequence.next.plr_rownum
< playlist_track.next.plr_rownum
):
prd.start_time = None
prd.end_time = None
@ -1539,16 +1519,16 @@ class PlaylistProxyModel(QSortFilterProxyModel):
if self.source_model.is_played_row(source_row):
# Don't hide current or next track
with db.Session() as session:
if track_sequence.next.plr_id:
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id)
if playlist_track.next and playlist_track.next.plr_id:
next_plr = session.get(PlaylistRows, playlist_track.next.plr_id)
if (
next_plr
and next_plr.plr_rownum == source_row
and next_plr.playlist_id == self.source_model.playlist_id
):
return True
if track_sequence.now.plr_id:
now_plr = session.get(PlaylistRows, track_sequence.now.plr_id)
if playlist_track.now and playlist_track.now.plr_id:
now_plr = session.get(PlaylistRows, playlist_track.now.plr_id)
if (
now_plr
and now_plr.plr_rownum == source_row
@ -1558,9 +1538,9 @@ class PlaylistProxyModel(QSortFilterProxyModel):
# Don't hide previous track until
# HIDE_AFTER_PLAYING_OFFSET milliseconds after
# current track has started
if track_sequence.previous.plr_id:
if playlist_track.previous and playlist_track.previous.plr_id:
previous_plr = session.get(
PlaylistRows, track_sequence.previous.plr_id
PlaylistRows, playlist_track.previous.plr_id
)
if (
previous_plr
@ -1568,9 +1548,9 @@ class PlaylistProxyModel(QSortFilterProxyModel):
and previous_plr.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() > (
track_sequence.now.start_time
playlist_track.now.start_time
+ dt.timedelta(
milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET
)

View File

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