WIP: moving player to PlaylistTrack. Player works.

This commit is contained in:
Keith Edmunds 2024-06-01 17:41:22 +01:00
parent b1f682d2e6
commit 8ea0a0dad5
5 changed files with 320 additions and 276 deletions

View File

@ -10,12 +10,12 @@ from PyQt6.QtCore import pyqtSignal, QObject, QThread
# Third party imports # Third party imports
import numpy as np import numpy as np
import pyqtgraph as pg # type: ignore import pyqtgraph as pg # type: ignore
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, Tracks
from music import Music
import helpers import helpers
@ -116,68 +116,42 @@ class MusicMusterSignals(QObject):
super().__init__() super().__init__()
class PlaylistTrack: class _TrackPlayer:
""" """
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, session: db.Session, player_name: str, track_id: 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 track = session.get(Tracks, track_id)
self.duration: Optional[int] = None if not track:
self.end_time: Optional[dt.datetime] = None raise ValueError(f"_TrackPlayer: unable to retreived {track_id=}")
self.fade_at: Optional[int] = None self.player_name = player_name
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
def __repr__(self) -> str:
return (
f"<PlaylistTrack(title={self.title}, artist={self.artist}, "
f"plr_rownum={self.plr_rownum}, playlist_id={self.playlist_id}>"
)
def set_plr(self, session: scoped_session, plr: PlaylistRows) -> None:
"""
Update with new plr information
"""
session.add(plr)
self.plr_rownum = plr.plr_rownum
if not plr.track:
return
track = plr.track
self.artist = track.artist self.artist = track.artist
self.bitrate = track.bitrate
self.duration = track.duration self.duration = track.duration
self.end_time = None
self.fade_at = track.fade_at self.fade_at = track.fade_at
self.intro = track.intro self.intro = track.intro
self.path = track.path self.path = track.path
self.playlist_id = plr.playlist_id
self.plr_id = plr.id
self.silence_at = track.silence_at self.silence_at = track.silence_at
self.start_gap = track.start_gap self.start_gap = track.start_gap
self.start_time = None
self.title = track.title self.title = track.title
self.track_id = track.id self.track_id = track.id
if track.silence_at and track.fade_at: self.end_time: Optional[dt.datetime] = None
self.fade_length = track.silence_at - track.fade_at self.fade_graph: Optional[FadeCurve] = None
self.fade_graph_start_updates: Optional[dt.datetime] = None
self.resume_marker: Optional[float]
self.start_time: Optional[dt.datetime] = None
self.player = Music(name=player_name)
# Initialise and add FadeCurve in a thread as it's slow # Initialise and add FadeCurve in a thread as it's slow
self.fadecurve_thread = QThread() self.fadecurve_thread = QThread()
@ -194,14 +168,24 @@ class PlaylistTrack:
self.fadecurve_thread.finished.connect(self.fadecurve_thread.deleteLater) self.fadecurve_thread.finished.connect(self.fadecurve_thread.deleteLater)
self.fadecurve_thread.start() self.fadecurve_thread.start()
def start(self) -> None: def __repr__(self) -> str:
""" return (
Called when track starts playing f"<_TrackPlayer(title={self.title}, artist={self.artist}, "
""" f"player_name={self.player_name}>"
)
def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None:
"""Fade music"""
self.player.fade(fade_seconds)
def play(self, position: Optional[float] = None) -> None:
"""Play track"""
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
@ -213,12 +197,67 @@ 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 = 0) -> None:
update_graph_at_ms = max( """
0, self.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1 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 MainTrackPlayer(_TrackPlayer):
def __init__(self, session: db.Session, track_id: int) -> None:
super().__init__(
session=session, player_name=Config.VLC_MAIN_PLAYER_NAME, track_id=track_id
) )
self.fade_graph_start_updates = now + dt.timedelta(
milliseconds=update_graph_at_ms
class PreviewTrackPlayer(_TrackPlayer):
def __init__(self, session: db.Session, track_id: int) -> None:
super().__init__(
session=session,
player_name=Config.VLC_PREVIEW_PLAYER_NAME,
track_id=track_id,
)
class PlaylistTrack:
"""
Used to provide a single reference point for specific playlist tracks,
typically the previous, current and next track.
"""
def __init__(self, plrid: int) -> None:
"""
Initialise
"""
with db.Session() as session:
# Ensure we have a track
plr = session.get(PlaylistRows, plrid)
if not plr:
raise ValueError(f"PlaylistTrack: unable to retreive plr {plrid=}")
self.track_id: int = plr.track_id
# Save non-track plr info
self.row_number: int = plr.plr_rownum
self.playlist_id: int = plr.playlist_id
self.plr_id: int = plr.id
# Initialise player
self.track_player = MainTrackPlayer(session=session, track_id=self.track_id)
def __repr__(self) -> str:
return (
f"<PlaylistTrack(title={self.track_player.title}, "
f"artist={self.track_player.artist}, "
f"plr_rownum={self.row_number}, playlist_id={self.playlist_id}>"
) )
@ -246,13 +285,13 @@ class AddFadeCurve(QObject):
def __init__( def __init__(
self, self,
playlist_track: PlaylistTrack, track_player: _TrackPlayer,
track_path: str, track_path: str,
track_fade_at: int, track_fade_at: int,
track_silence_at: int, track_silence_at: int,
): ):
super().__init__() super().__init__()
self.playlist_track = playlist_track self.track_player = track_player
self.track_path = track_path self.track_path = track_path
self.track_fade_at = track_fade_at self.track_fade_at = track_fade_at
self.track_silence_at = track_silence_at self.track_silence_at = track_silence_at
@ -266,14 +305,14 @@ class AddFadeCurve(QObject):
if not fc: if not fc:
log.error(f"Failed to create FadeCurve for {self.track_path=}") log.error(f"Failed to create FadeCurve for {self.track_path=}")
else: else:
self.playlist_track.fade_graph = fc self.track_player.fade_graph = fc
self.finished.emit() self.finished.emit()
class TrackSequence: class TrackSequence:
next = PlaylistTrack() next: Optional[PlaylistTrack] = None
now = PlaylistTrack() current: Optional[PlaylistTrack] = None
previous = PlaylistTrack() previous: Optional[PlaylistTrack] = None
track_sequence = TrackSequence() track_sequence = TrackSequence()

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

@ -59,6 +59,7 @@ from classes import (
FadeCurve, FadeCurve,
MusicMusterSignals, MusicMusterSignals,
PlaylistTrack, PlaylistTrack,
PreviewTrackPlayer,
TrackFileData, TrackFileData,
) )
from config import Config from config import Config
@ -231,9 +232,8 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer1000: QTimer = QTimer() self.timer1000: QTimer = QTimer()
self.music: music.Music = music.Music(name=Config.VLC_MAIN_PLAYER_NAME) self.music: music.Music = music.Music(name=Config.VLC_MAIN_PLAYER_NAME)
self.preview_player: music.Music = music.Music( self.preview_track_player: Optional[PreviewTrackPlayer] = None
name=Config.VLC_PREVIEW_PLAYER_NAME
)
self.playing: bool = False self.playing: bool = False
self.set_main_window_size() self.set_main_window_size()
@ -430,7 +430,7 @@ class Window(QMainWindow, Ui_MainWindow):
Clear next track Clear next track
""" """
track_sequence.next = PlaylistTrack() track_sequence.next = None
self.update_headers() self.update_headers()
def clear_selection(self) -> None: def clear_selection(self) -> None:
@ -521,7 +521,10 @@ 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 track_sequence.current is None:
return True
current_track_playlist_id = track_sequence.current.playlist_id
closing_tab_playlist_id = self.tabPlaylist.widget(tab_index).playlist_id closing_tab_playlist_id = self.tabPlaylist.widget(tab_index).playlist_id
if current_track_playlist_id: if current_track_playlist_id:
if closing_tab_playlist_id == current_track_playlist_id: if closing_tab_playlist_id == current_track_playlist_id:
@ -1145,11 +1148,12 @@ class Window(QMainWindow, Ui_MainWindow):
""" """
# Check for inadvertent press of 'return' # Check for inadvertent press of 'return'
if self.catch_return_key: if track_sequence.current and self.catch_return_key:
# Suppress inadvertent double press # Suppress inadvertent double press
if ( if (
track_sequence.now.start_time track_sequence.current
and track_sequence.now.start_time and track_sequence.current.track_player.start_time
and track_sequence.current.track_player.start_time
+ dt.timedelta(milliseconds=Config.RETURN_KEY_DEBOUNCE_MS) + dt.timedelta(milliseconds=Config.RETURN_KEY_DEBOUNCE_MS)
> dt.datetime.now() > dt.datetime.now()
): ):
@ -1157,11 +1161,17 @@ class Window(QMainWindow, Ui_MainWindow):
# If return is pressed during first PLAY_NEXT_GUARD_MS then # If return is pressed during first PLAY_NEXT_GUARD_MS then
# default to NOT playing the next track, else default to # default to NOT playing the next track, else default to
# playing it. # playing it.
default_yes: bool = track_sequence.now.start_time is not None and ( default_yes: bool = (
(dt.datetime.now() - track_sequence.now.start_time).total_seconds() track_sequence.current.track_player.start_time is not None
and (
(
dt.datetime.now()
- track_sequence.current.track_player.start_time
).total_seconds()
* 1000 * 1000
> Config.PLAY_NEXT_GUARD_MS > 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?",
@ -1173,12 +1183,9 @@ class Window(QMainWindow, Ui_MainWindow):
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 track_sequence.next is None:
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 +1202,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 track_sequence.current = track_sequence.next
# Clear next track # Clear next track
self.clear_next() self.clear_next()
@ -1206,21 +1213,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: track_sequence.current.track_player.play(position)
log.error("No path for next track")
return
self.music.play(track_sequence.now.path, position)
# Show closing volume graph
if track_sequence.now.fade_graph:
track_sequence.now.fade_graph.plot()
else:
log.error("No fade_graph")
# Note that track is playing
log.debug("set track_sequence")
track_sequence.now.start()
self.playing = True
# Disable play next controls # Disable play next controls
self.catch_return_key = True self.catch_return_key = True
@ -1248,18 +1241,22 @@ class Window(QMainWindow, Ui_MainWindow):
""" """
if self.btnPreview.isChecked(): if self.btnPreview.isChecked():
# Get track path for first selected track if there is one # Get track_id for first selected track if there is one
track_path = self.active_tab().get_selected_row_track_path() track_id = self.active_tab().get_selected_row_track_id()
if not track_path: if not track_id:
# Otherwise get path to next track to play # Otherwise get path to next track to play
track_path = track_sequence.next.path if track_sequence.next:
if not track_path: track_id = track_sequence.next.track_id
if not track_id:
self.btnPreview.setChecked(False) self.btnPreview.setChecked(False)
return return
self.preview_player.play(path=track_path) with db.Session() as session:
self.preview_track_player = PreviewTrackPlayer(session, track_id)
self.preview_track_player.play()
else: else:
self.preview_player.stop() if self.preview_track_player:
self.preview_track_player.stop_playing()
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)
@ -1272,7 +1269,7 @@ class Window(QMainWindow, Ui_MainWindow):
def preview_back(self) -> None: def preview_back(self) -> None:
"""Wind back preview file""" """Wind back preview file"""
self.preview_player.move_back(Config.PREVIEW_BACK_MS) self.preview_track_player.move_back(Config.PREVIEW_BACK_MS)
def preview_end(self) -> None: def preview_end(self) -> None:
"""Advance preview file to just before end of intro""" """Advance preview file to just before end of intro"""
@ -1316,7 +1313,7 @@ class Window(QMainWindow, Ui_MainWindow):
def preview_start(self) -> None: def preview_start(self) -> None:
"""Advance preview file""" """Advance preview file"""
self.preview_player.set_position(0) self.preview_track_player.set_position(0)
def rename_playlist(self) -> None: def rename_playlist(self) -> None:
""" """
@ -1432,12 +1429,14 @@ class Window(QMainWindow, Ui_MainWindow):
# 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 track_sequence.current.start_time
and track_sequence.now.duration and track_sequence.current.duration
and track_sequence.now.resume_marker and track_sequence.current.resume_marker
): ):
elapsed_ms = track_sequence.now.duration * track_sequence.now.resume_marker elapsed_ms = (
track_sequence.now.start_time -= dt.timedelta(milliseconds=elapsed_ms) track_sequence.current.duration * track_sequence.current.resume_marker
)
track_sequence.current.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 +1562,8 @@ 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) if track_sequence.current:
self.show_track(track_sequence.current)
def show_warning(self, title: str, body: str) -> None: def show_warning(self, title: str, body: str) -> None:
""" """
@ -1576,6 +1576,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"""
if track_sequence.next:
self.show_track(track_sequence.next) self.show_track(track_sequence.next)
def show_status_message(self, message: str, timing: int) -> None: def show_status_message(self, message: str, timing: int) -> None:
@ -1585,25 +1586,25 @@ 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, playlist_track: PlaylistTrack) -> None:
"""Scroll to show track in plt""" """Scroll to show track in plt"""
# Switch to the correct tab # Switch to the correct tab
plt_playlist_id = plt.playlist_id playlist_id = playlist_track.playlist_id
if not plt_playlist_id: if not playlist_id:
# No playlist # No playlist
return return
if plt_playlist_id != self.active_tab().playlist_id: if playlist_id != self.active_tab().playlist_id:
for idx in range(self.tabPlaylist.count()): for idx in range(self.tabPlaylist.count()):
if self.tabPlaylist.widget(idx).playlist_id == plt_playlist_id: if self.tabPlaylist.widget(idx).playlist_id == playlist_id:
self.tabPlaylist.setCurrentIndex(idx) self.tabPlaylist.setCurrentIndex(idx)
break break
display_row = ( display_row = (
self.active_proxy_model() self.active_proxy_model()
.mapFromSource( .mapFromSource(
self.active_proxy_model().source_model.index(plt.plr_rownum, 0) self.active_proxy_model().source_model.index(playlist_track.row_number, 0)
) )
.row() .row()
) )
@ -1710,22 +1711,23 @@ class Window(QMainWindow, Ui_MainWindow):
Called every 10ms Called every 10ms
""" """
return
# Update volume fade curve # Update volume fade curve
if ( if (
track_sequence.now.fade_graph_start_updates is None track_sequence.current.fade_graph_start_updates is None
or track_sequence.now.fade_graph_start_updates > dt.datetime.now() or track_sequence.current.fade_graph_start_updates > dt.datetime.now()
): ):
return return
if ( if (
track_sequence.now.track_id track_sequence.current.track_id
and track_sequence.now.fade_graph and track_sequence.current.fade_graph
and track_sequence.now.start_time and track_sequence.current.start_time
): ):
play_time = ( play_time = (
dt.datetime.now() - track_sequence.now.start_time dt.datetime.now() - track_sequence.current.start_time
).total_seconds() * 1000 ).total_seconds() * 1000
track_sequence.now.fade_graph.tick(play_time) track_sequence.current.fade_graph.tick(play_time)
def tick_500ms(self) -> None: def tick_500ms(self) -> None:
""" """
@ -1845,25 +1847,26 @@ 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 track_sequence.previous:
self.hdrPreviousTrack.setText( player = track_sequence.previous.track_player
f"{track_sequence.previous.title} - {track_sequence.previous.artist}" self.hdrPreviousTrack.setText(f"{player.title} - {player.artist}")
)
else: else:
self.hdrPreviousTrack.setText("") self.hdrPreviousTrack.setText("")
if track_sequence.now.title and track_sequence.now.artist: if track_sequence.current:
player = track_sequence.current.track_player
self.hdrCurrentTrack.setText( self.hdrCurrentTrack.setText(
f"{track_sequence.now.title.replace('&', '&&')} - " f"{player.title.replace('&', '&&')} - "
f"{track_sequence.now.artist.replace('&', '&&')}" f"{player.artist.replace('&', '&&')}"
) )
else: else:
self.hdrCurrentTrack.setText("") self.hdrCurrentTrack.setText("")
if track_sequence.next.title and track_sequence.next.artist: if track_sequence.next:
player = track_sequence.next.track_player
self.hdrNextTrack.setText( self.hdrNextTrack.setText(
f"{track_sequence.next.title.replace('&', '&&')} - " f"{player.title.replace('&', '&&')} - "
f"{track_sequence.next.artist.replace('&', '&&')}" f"{player.artist.replace('&', '&&')}"
) )
else: else:
self.hdrNextTrack.setText("") self.hdrNextTrack.setText("")

View File

@ -47,7 +47,7 @@ HEADER_NOTES_COLUMN = 1
scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]") scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]")
class PlaylistRowData: class _PlaylistRowData:
def __init__(self, plr: PlaylistRows) -> None: def __init__(self, plr: PlaylistRows) -> None:
""" """
Populate PlaylistRowData from database PlaylistRows record Populate PlaylistRowData from database PlaylistRows record
@ -117,7 +117,7 @@ class PlaylistModel(QAbstractTableModel):
self.playlist_id = playlist_id self.playlist_id = playlist_id
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.playlist_rows: dict[int, PlaylistRowData] = {} self.playlist_rows: dict[int, _PlaylistRowData] = {}
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
self.played_tracks_hidden = False self.played_tracks_hidden = False
@ -178,7 +178,7 @@ class PlaylistModel(QAbstractTableModel):
self.signals.resize_rows_signal.emit(self.playlist_id) self.signals.resize_rows_signal.emit(self.playlist_id)
def background_role(self, row: int, column: int, prd: PlaylistRowData) -> QBrush: def background_role(self, row: int, column: int, prd: _PlaylistRowData) -> QBrush:
"""Return background setting""" """Return background setting"""
# Handle entire row colouring # Handle entire row colouring
@ -195,10 +195,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 track_sequence.current and track_sequence.current.track_id == prd.track_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 track_sequence.next and track_sequence.next.track_id == prd.track_id:
return QBrush(QColor(Config.COLOUR_NEXT_PLAYLIST)) return QBrush(QColor(Config.COLOUR_NEXT_PLAYLIST))
# Individual cell colouring # Individual cell colouring
@ -250,24 +250,10 @@ class PlaylistModel(QAbstractTableModel):
- find next track - find next track
""" """
row_number = track_sequence.now.plr_rownum if not track_sequence.current:
if row_number is not None: return
prd = self.playlist_rows[row_number]
else:
prd = None
# Sanity check row_number = track_sequence.current.row_number
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")
@ -276,29 +262,23 @@ 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, track_sequence.current.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, track_sequence.current.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, {track_sequence.current.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
# 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 track_sequence.previous and track_sequence.previous.row_number:
self.invalidate_row(track_sequence.previous.plr_rownum) self.invalidate_row(track_sequence.previous.row_number)
# Update all other track times # Update all other track times
self.update_track_times() self.update_track_times()
@ -393,7 +373,7 @@ class PlaylistModel(QAbstractTableModel):
self.reset_track_sequence_row_numbers() self.reset_track_sequence_row_numbers()
def display_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: def display_role(self, row: int, column: int, prd: _PlaylistRowData) -> QVariant:
""" """
Return text for display Return text for display
""" """
@ -466,7 +446,7 @@ class PlaylistModel(QAbstractTableModel):
super().endResetModel() super().endResetModel()
self.reset_track_sequence_row_numbers() self.reset_track_sequence_row_numbers()
def edit_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: def edit_role(self, row: int, column: int, prd: _PlaylistRowData) -> QVariant:
""" """
Return text for editing Return text for editing
""" """
@ -510,7 +490,7 @@ class PlaylistModel(QAbstractTableModel):
return default return default
def font_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: def font_role(self, row: int, column: int, prd: _PlaylistRowData) -> QVariant:
""" """
Return font Return font
""" """
@ -569,7 +549,7 @@ class PlaylistModel(QAbstractTableModel):
log.debug(f"get_new_row_number() return: {new_row_number=}") log.debug(f"get_new_row_number() return: {new_row_number=}")
return new_row_number return new_row_number
def get_row_info(self, row_number: int) -> PlaylistRowData: def get_row_info(self, row_number: int) -> _PlaylistRowData:
""" """
Return info about passed row Return info about passed row
""" """
@ -659,7 +639,7 @@ class PlaylistModel(QAbstractTableModel):
return QVariant() return QVariant()
def header_text(self, prd: PlaylistRowData) -> str: def header_text(self, prd: _PlaylistRowData) -> str:
""" """
Process possible section timing directives embeded in header Process possible section timing directives embeded in header
""" """
@ -703,16 +683,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 track_sequence.current
and track_sequence.now.end_time and track_sequence.current.track_player.end_time
and ( and (
row_number row_number
< track_sequence.now.plr_rownum < track_sequence.current.row_number
< prd.plr_rownum < prd.plr_rownum
) )
): ):
section_end_time = ( section_end_time = (
track_sequence.now.end_time track_sequence.current.track_player.end_time
+ dt.timedelta(milliseconds=duration) + dt.timedelta(milliseconds=duration)
) )
end_time_str = ( end_time_str = (
@ -830,7 +810,7 @@ class PlaylistModel(QAbstractTableModel):
return self.playlist_rows[row_number].played return self.playlist_rows[row_number].played
def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]: def is_track_in_playlist(self, track_id: int) -> Optional[_PlaylistRowData]:
""" """
If this track_id is in the playlist, return the PlaylistRowData object If this track_id is in the playlist, return the PlaylistRowData object
else return None else return None
@ -906,14 +886,16 @@ class PlaylistModel(QAbstractTableModel):
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 track_sequence have moved
if track_sequence.previous.plr_rownum in row_map: if track_sequence.previous and track_sequence.previous.row_number in row_map:
track_sequence.previous.plr_rownum = row_map[ track_sequence.previous.row_number = row_map[
track_sequence.previous.plr_rownum track_sequence.previous.row_number
] ]
if track_sequence.now.plr_rownum in row_map: if track_sequence.current and track_sequence.current.row_number in row_map:
track_sequence.now.plr_rownum = row_map[track_sequence.now.plr_rownum] track_sequence.current.row_number = row_map[
if track_sequence.next.plr_rownum in row_map: track_sequence.current.row_number
track_sequence.next.plr_rownum = row_map[track_sequence.next.plr_rownum] ]
if track_sequence.next and track_sequence.next.row_number in row_map:
track_sequence.next.row_number = row_map[track_sequence.next.row_number]
# 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:
@ -973,7 +955,10 @@ 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 (
track_sequence.current
and plr.id == track_sequence.current.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
@ -994,7 +979,7 @@ class PlaylistModel(QAbstractTableModel):
self.update_track_times() self.update_track_times()
def move_track_add_note( def move_track_add_note(
self, new_row_number: int, existing_prd: PlaylistRowData, note: str self, new_row_number: int, existing_prd: _PlaylistRowData, note: str
) -> None: ) -> None:
""" """
Move existing_prd track to new_row_number and append note to any existing note Move existing_prd track to new_row_number and append note to any existing note
@ -1018,7 +1003,10 @@ class PlaylistModel(QAbstractTableModel):
self.signals.resize_rows_signal.emit(self.playlist_id) self.signals.resize_rows_signal.emit(self.playlist_id)
def move_track_to_header( def move_track_to_header(
self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str] self,
header_row_number: int,
existing_prd: _PlaylistRowData,
note: Optional[str],
) -> None: ) -> None:
""" """
Add the existing_prd track details to the existing header at header_row_number Add the existing_prd track details to the existing header at header_row_number
@ -1080,10 +1068,10 @@ 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 track_sequence.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 track_sequence.previous.row_number 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"({track_sequence.previous=})"
@ -1091,7 +1079,7 @@ class PlaylistModel(QAbstractTableModel):
return return
# Update display # Update display
self.invalidate_row(track_sequence.previous.plr_rownum) self.invalidate_row(track_sequence.previous.row_number)
def refresh_data(self, session: db.session): def refresh_data(self, session: db.session):
"""Populate dicts for data calls""" """Populate dicts for data calls"""
@ -1099,13 +1087,13 @@ class PlaylistModel(QAbstractTableModel):
# Populate self.playlist_rows with playlist data # Populate self.playlist_rows with playlist data
self.playlist_rows.clear() self.playlist_rows.clear()
for p in PlaylistRows.deep_rows(session, self.playlist_id): for p in PlaylistRows.deep_rows(session, self.playlist_id):
self.playlist_rows[p.plr_rownum] = PlaylistRowData(p) self.playlist_rows[p.plr_rownum] = _PlaylistRowData(p)
def refresh_row(self, session, row_number): def refresh_row(self, session, row_number):
"""Populate dict for one row from database""" """Populate dict for one row from database"""
p = PlaylistRows.deep_row(session, self.playlist_id, row_number) p = PlaylistRows.deep_row(session, self.playlist_id, row_number)
self.playlist_rows[row_number] = PlaylistRowData(p) self.playlist_rows[row_number] = _PlaylistRowData(p)
def remove_track(self, row_number: int) -> None: def remove_track(self, row_number: int) -> None:
""" """
@ -1145,21 +1133,23 @@ class PlaylistModel(QAbstractTableModel):
log.debug("reset_track_sequence_row_numbers()") log.debug("reset_track_sequence_row_numbers()")
# Check the track_sequence next, now and previous plrs and # Check the track_sequence next, current 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 track_sequence.next and track_sequence.next.row_number:
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id) next_plr = session.get(PlaylistRows, track_sequence.next.row_number)
if next_plr: if next_plr:
track_sequence.next.plr_rownum = next_plr.plr_rownum track_sequence.next.row_number = next_plr.plr_rownum
if track_sequence.now.plr_rownum: if track_sequence.current and track_sequence.current.row_number:
now_plr = session.get(PlaylistRows, track_sequence.now.plr_id) now_plr = session.get(PlaylistRows, track_sequence.current.row_number)
if now_plr: if now_plr:
track_sequence.now.plr_rownum = now_plr.plr_rownum track_sequence.current.row_number = now_plr.plr_rownum
if track_sequence.previous.plr_rownum: if track_sequence.previous and track_sequence.previous.row_number:
previous_plr = session.get(PlaylistRows, track_sequence.previous.plr_id) previous_plr = session.get(
PlaylistRows, track_sequence.previous.row_number
)
if previous_plr: if previous_plr:
track_sequence.previous.plr_rownum = previous_plr.plr_rownum track_sequence.previous.row_number = previous_plr.plr_rownum
self.update_track_times() self.update_track_times()
@ -1240,59 +1230,53 @@ 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 track_sequence.next:
track_sequence.next = None
else:
return return
track_sequence.next = PlaylistTrack() else:
self.signals.next_track_changed_signal.emit() # Get plrid of row
return
# Update track_sequence
with db.Session() as session:
track_sequence.next = PlaylistTrack()
try: try:
plrid = self.playlist_rows[row_number].plrid prd = self.playlist_rows[row_number]
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=}"
"IndexError"
) )
return return
plr = session.get(PlaylistRows, plrid) if prd.track_id is None or prd.plr_rownum is None:
if plr:
# Check this isn't a header row
if self.is_header_row(row_number):
log.error( log.error(
"Tried to set next row on header row: "
f"playlistmodel.set_next_track({row_number=}, " f"playlistmodel.set_next_track({row_number=}, "
f"{self.playlist_id=}" "No track / row number "
f"{self.playlist_id=}, {prd.track_id=}, {prd.plr_rownum=}"
) )
return return
# Check track is readable
if file_is_unreadable(plr.track.path): try:
log.error( track_sequence.next = PlaylistTrack(prd.plrid)
"Tried to set next row on unreadable row: " self.invalidate_row(row_number)
f"playlistmodel.set_next_track({row_number=}, " except ValueError as e:
f"{self.playlist_id=}" log.error(f"Error creating PlaylistTrack({prd=}): ({str(e)})")
)
return return
track_sequence.next.set_plr(session, plr)
self.signals.next_track_changed_signal.emit()
self.signals.search_wikipedia_signal.emit( self.signals.search_wikipedia_signal.emit(
self.playlist_rows[row_number].title self.playlist_rows[row_number].title
) )
self.invalidate_row(row_number) self.invalidate_row(row_number)
if next_row_was is not None: self.signals.next_track_changed_signal.emit()
self.invalidate_row(next_row_was)
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
@ -1396,7 +1380,7 @@ class PlaylistModel(QAbstractTableModel):
def supportedDropActions(self) -> Qt.DropAction: def supportedDropActions(self) -> Qt.DropAction:
return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction return Qt.DropAction.MoveAction | Qt.DropAction.CopyAction
def tooltip_role(self, row: int, column: int, prd: PlaylistRowData) -> QVariant: def tooltip_role(self, row: int, column: int, prd: _PlaylistRowData) -> QVariant:
""" """
Return tooltip. Currently only used for last_played column. Return tooltip. Currently only used for last_played column.
""" """
@ -1434,9 +1418,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 == track_sequence.current.plr_rownum:
prd.start_time = track_sequence.now.start_time prd.start_time = track_sequence.current.start_time
prd.end_time = track_sequence.now.end_time prd.end_time = track_sequence.current.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
@ -1445,9 +1429,9 @@ 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 == track_sequence.next.plr_rownum
and track_sequence.now.end_time and track_sequence.current.end_time
): ):
prd.start_time = track_sequence.now.end_time prd.start_time = track_sequence.current.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,9 +1444,9 @@ 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 track_sequence.current.plr_rownum is not None
and track_sequence.next.plr_rownum is not None and track_sequence.next.plr_rownum is not None
and track_sequence.now.plr_rownum and track_sequence.current.plr_rownum
< row_number < row_number
< track_sequence.next.plr_rownum < track_sequence.next.plr_rownum
): ):
@ -1539,7 +1523,7 @@ 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 track_sequence.next:
next_plr = session.get(PlaylistRows, track_sequence.next.plr_id) next_plr = session.get(PlaylistRows, track_sequence.next.plr_id)
if ( if (
next_plr next_plr
@ -1547,8 +1531,10 @@ class PlaylistProxyModel(QSortFilterProxyModel):
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 track_sequence.current:
now_plr = session.get(PlaylistRows, track_sequence.now.plr_id) now_plr = session.get(
PlaylistRows, track_sequence.current.plr_id
)
if ( if (
now_plr now_plr
and now_plr.plr_rownum == source_row and now_plr.plr_rownum == source_row
@ -1558,19 +1544,20 @@ 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 track_sequence.previous:
previous_plr = session.get( previous_plr = session.get(
PlaylistRows, track_sequence.previous.plr_id PlaylistRows, track_sequence.previous.plr_id
) )
if ( if (
previous_plr track_sequence.current
and previous_plr
and previous_plr.plr_rownum == source_row and previous_plr.plr_rownum == source_row
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 track_sequence.current.track_player.start_time:
if dt.datetime.now() > ( if dt.datetime.now() > (
track_sequence.now.start_time track_sequence.current.track_player.start_time
+ dt.timedelta( + dt.timedelta(
milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET
) )
@ -1623,7 +1610,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
def get_rows_duration(self, row_numbers: List[int]) -> int: def get_rows_duration(self, row_numbers: List[int]) -> int:
return self.source_model.get_rows_duration(row_numbers) return self.source_model.get_rows_duration(row_numbers)
def get_row_info(self, row_number: int) -> PlaylistRowData: def get_row_info(self, row_number: int) -> _PlaylistRowData:
return self.source_model.get_row_info(row_number) return self.source_model.get_row_info(row_number)
def get_row_track_path(self, row_number: int) -> str: def get_row_track_path(self, row_number: int) -> str:
@ -1649,7 +1636,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
def is_played_row(self, row_number: int) -> bool: def is_played_row(self, row_number: int) -> bool:
return self.source_model.is_played_row(row_number) return self.source_model.is_played_row(row_number)
def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]: def is_track_in_playlist(self, track_id: int) -> Optional[_PlaylistRowData]:
return self.source_model.is_track_in_playlist(track_id) return self.source_model.is_track_in_playlist(track_id)
def mark_unplayed(self, row_numbers: List[int]) -> None: def mark_unplayed(self, row_numbers: List[int]) -> None:
@ -1666,12 +1653,15 @@ class PlaylistProxyModel(QSortFilterProxyModel):
) )
def move_track_add_note( def move_track_add_note(
self, new_row_number: int, existing_prd: PlaylistRowData, note: str self, new_row_number: int, existing_prd: _PlaylistRowData, note: str
) -> None: ) -> None:
return self.source_model.move_track_add_note(new_row_number, existing_prd, note) return self.source_model.move_track_add_note(new_row_number, existing_prd, note)
def move_track_to_header( def move_track_to_header(
self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str] self,
header_row_number: int,
existing_prd: _PlaylistRowData,
note: Optional[str],
) -> None: ) -> None:
return self.source_model.move_track_to_header( return self.source_model.move_track_to_header(
header_row_number, existing_prd, note header_row_number, existing_prd, note

View File

@ -118,9 +118,13 @@ class EscapeDelegate(QStyledItemDelegate):
# Close editor if no changes have been made # Close editor if no changes have been made
data_modified = False data_modified = False
if isinstance(self.editor, QPlainTextEdit): if isinstance(self.editor, QPlainTextEdit):
data_modified = self.original_model_data == self.editor.toPlainText() data_modified = (
self.original_model_data == self.editor.toPlainText()
)
elif isinstance(self.editor, QDoubleSpinBox): elif isinstance(self.editor, QDoubleSpinBox):
data_modified = self.original_model_data == int(self.editor.value()) * 1000 data_modified = (
self.original_model_data == int(self.editor.value()) * 1000
)
if data_modified: if data_modified:
self.closeEditor.emit(editor) self.closeEditor.emit(editor)
return True return True
@ -425,12 +429,18 @@ 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 if track_sequence.current:
next_row = model_row_number == track_sequence.next.plr_rownum this_is_current_row = model_row_number == track_sequence.current.row_number
else:
this_is_current_row = False
if track_sequence.next:
this_is_next_row = model_row_number == track_sequence.next.row_number
else:
this_is_next_row = False
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
if track_row and not current_row: if track_row and not this_is_current_row:
if track_path == self.musicmuster.audacity_file_path: if track_path == self.musicmuster.audacity_file_path:
# This track was opened in Audacity # This track was opened in Audacity
self._add_context_menu( self._add_context_menu(
@ -447,7 +457,7 @@ class PlaylistTab(QTableView):
) )
# Rescan # Rescan
if track_row and not current_row: if track_row and not this_is_current_row:
self._add_context_menu( self._add_context_menu(
"Rescan track", lambda: self._rescan(model_row_number) "Rescan track", lambda: self._rescan(model_row_number)
) )
@ -456,11 +466,11 @@ class PlaylistTab(QTableView):
self.menu.addSeparator() self.menu.addSeparator()
# Delete row # Delete row
if not current_row and not next_row: if not this_is_current_row and not this_is_next_row:
self._add_context_menu("Delete row", lambda: self._delete_rows()) self._add_context_menu("Delete row", lambda: self._delete_rows())
# Remove track from row # Remove track from row
if track_row and not current_row and not next_row: if track_row and not this_is_current_row and not this_is_next_row:
self._add_context_menu( self._add_context_menu(
"Remove track from row", "Remove track from row",
lambda: proxy_model.remove_track(model_row_number), lambda: proxy_model.remove_track(model_row_number),
@ -481,7 +491,7 @@ class PlaylistTab(QTableView):
) )
# Unmark as next # Unmark as next
if next_row: if this_is_next_row:
self._add_context_menu( self._add_context_menu(
"Unmark as next track", lambda: self._unmark_as_next() "Unmark as next track", lambda: self._unmark_as_next()
) )