Use vlc events to trigger end-of-track actions

This commit is contained in:
Keith Edmunds 2025-04-19 12:26:44 +01:00
parent edd8c36c53
commit 223c7cd3ab
5 changed files with 76 additions and 15 deletions

View File

@ -270,15 +270,39 @@ def leading_silence(
return min(trim_ms, len(audio_segment)) return min(trim_ms, len(audio_segment))
def ms_to_mmss(ms: int | None, none: str = "-") -> str: def ms_to_mmss(
ms: Optional[int],
decimals: int = 0,
negative: bool = False,
none: Optional[str] = None,
) -> str:
"""Convert milliseconds to mm:ss""" """Convert milliseconds to mm:ss"""
if ms is None: minutes: int
return none remainder: int
seconds: float
minutes, seconds = divmod(ms // 1000, 60) if not ms:
if none:
return none
else:
return "-"
sign = ""
if ms < 0:
if negative:
sign = "-"
else:
ms = 0
return f"{minutes}:{seconds:02d}" minutes, remainder = divmod(ms, 60 * 1000)
seconds = remainder / 1000
# if seconds >= 59.5, it will be represented as 60, which looks odd.
# So, fake it under those circumstances
if seconds >= 59.5:
seconds = 59.0
return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}"
def normalise_track(path: str) -> None: def normalise_track(path: str) -> None:

View File

@ -53,6 +53,7 @@ class _FadeTrack(QThread):
) )
sleep(1 / Config.FADEOUT_STEPS_PER_SECOND) sleep(1 / Config.FADEOUT_STEPS_PER_SECOND)
self.player.stop()
self.finished.emit() self.finished.emit()
@ -80,9 +81,11 @@ class Music:
self.vlc_instance = vlc_manager.get_instance() self.vlc_instance = vlc_manager.get_instance()
self.vlc_instance.set_user_agent(name, name) self.vlc_instance.set_user_agent(name, name)
self.player: vlc.MediaPlayer | None = None self.player: vlc.MediaPlayer | None = None
self.vlc_event_manager: vlc.EventManager | None = None
self.max_volume: int = Config.VLC_VOLUME_DEFAULT self.max_volume: int = Config.VLC_VOLUME_DEFAULT
self.start_dt: dt.datetime | None = None self.start_dt: dt.datetime | None = None
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
self.end_of_track_signalled = False
def fade(self, fade_seconds: int) -> None: def fade(self, fade_seconds: int) -> None:
""" """
@ -98,6 +101,8 @@ 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
self.signal_track_ended()
self.fader_worker = _FadeTrack(self.player, fade_seconds=fade_seconds) self.fader_worker = _FadeTrack(self.player, fade_seconds=fade_seconds)
self.fader_worker.finished.connect(self.player.release) self.fader_worker.finished.connect(self.player.release)
self.fader_worker.start() self.fader_worker.start()
@ -142,10 +147,12 @@ class Music:
< dt.timedelta(microseconds=Config.PLAY_SETTLE) < dt.timedelta(microseconds=Config.PLAY_SETTLE)
) )
# @log_call
def play( def play(
self, self,
path: str, path: str,
start_time: dt.datetime, start_time: dt.datetime,
playlist_id: int,
position: float | None = None, position: float | None = None,
) -> None: ) -> None:
""" """
@ -157,7 +164,7 @@ class Music:
the start time is the same the start time is the same
""" """
log.debug(f"Music[{self.name}].play({path=}, {position=}") self.playlist_id = playlist_id
if helpers.file_is_unreadable(path): if helpers.file_is_unreadable(path):
log.error(f"play({path}): path not readable") log.error(f"play({path}): path not readable")
@ -171,6 +178,10 @@ class Music:
) )
return return
self.events = self.player.event_manager()
self.events.event_attach(vlc.EventType.MediaPlayerEndReached, self.track_end_event_handler)
self.events.event_attach(vlc.EventType.MediaPlayerStopped, self.track_end_event_handler)
_ = self.player.play() _ = self.player.play()
self.set_volume(self.max_volume) self.set_volume(self.max_volume)
@ -213,6 +224,21 @@ class Music:
log.debug(f"Reset from {volume=}") log.debug(f"Reset from {volume=}")
sleep(0.1) sleep(0.1)
def signal_track_ended(self) -> None:
"""
Multiple parts of the Music class can signal that the track has
ended. Handle them all here to ensure that only one such signal
is raised. Make this thead safe.
"""
lock = threading.Lock()
with lock:
if self.end_of_track_signalled:
return
self.signals.track_ended_signal.emit(self.playlist_id)
self.end_of_track_signalled = True
def stop(self) -> None: def stop(self) -> None:
"""Immediately stop playing""" """Immediately stop playing"""
@ -227,3 +253,11 @@ class Music:
self.player.stop() self.player.stop()
self.player.release() self.player.release()
self.player = None self.player = None
def track_end_event_handler(self, event: vlc.Event) -> None:
"""
Handler for MediaPlayerEndReached
"""
log.debug("track_end_event_handler() called")
self.signal_track_ended()

View File

@ -2563,8 +2563,6 @@ class Window(QMainWindow):
if self.track_sequence.current: if self.track_sequence.current:
try: try:
self.track_sequence.current.check_for_end_of_track()
# Update intro counter if applicable and, if updated, # Update intro counter if applicable and, if updated,
# return because playing an intro uses the intro field to # return because playing an intro uses the intro field to
# show timing and this takes precedence over timing a # show timing and this takes precedence over timing a

View File

@ -1057,7 +1057,7 @@ class PlaylistModel(QAbstractTableModel):
Remove track from row, retaining row as a header row Remove track from row, retaining row as a header row
""" """
self.playlist_rows[row_number].track_id = None self.playlist_rows[row_number].track_id = 0
# only invalidate required roles # only invalidate required roles
roles = [ roles = [
@ -1349,13 +1349,13 @@ class PlaylistModel(QAbstractTableModel):
plr = self.playlist_rows[row_number] plr = self.playlist_rows[row_number]
if column == Col.NOTE.value: if column == Col.NOTE.value:
plr.note = value plr.note = str(value)
elif column == Col.TITLE.value: elif column == Col.TITLE.value:
plr.title = value plr.title = str(value)
elif column == Col.ARTIST.value: elif column == Col.ARTIST.value:
plr.artist = value plr.artist = str(value)
elif column == Col.INTRO.value: elif column == Col.INTRO.value:
intro = int(round(float(value), 1) * 1000) intro = int(round(float(value), 1) * 1000)

View File

@ -103,7 +103,7 @@ class PlaylistRow:
@property @property
def intro(self) -> int: def intro(self) -> int:
if self.dto.track: if self.dto.track:
return self.dto.track.intro return self.dto.track.intro or 0
else: else:
return 0 return 0
@ -531,12 +531,17 @@ class TrackSequence:
""" """
if self.current is None: if self.current is None:
raise ApplicationError("Tried to move non-existent track from current to previous") raise ApplicationError(
"Tried to move non-existent track from current to previous"
)
# Dereference the fade curve so it can be garbage collected # Dereference the fade curve so it can be garbage collected
self.current.fade_graph = None if self.current.fade_graph:
self.current.fade_graph.clear()
self.current.fade_graph = None
self.previous = self.current self.previous = self.current
self.current = None self.current = None
self.start_time = None
def move_previous_to_next(self) -> None: def move_previous_to_next(self) -> None:
""" """