From 223c7cd3ab0fe3d8480ec527d81933ba74ddf5b4 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sat, 19 Apr 2025 12:26:44 +0100 Subject: [PATCH] Use vlc events to trigger end-of-track actions --- app/helpers.py | 34 +++++++++++++++++++++++++++++----- app/music_manager.py | 36 +++++++++++++++++++++++++++++++++++- app/musicmuster.py | 2 -- app/playlistmodel.py | 8 ++++---- app/playlistrow.py | 11 ++++++++--- 5 files changed, 76 insertions(+), 15 deletions(-) diff --git a/app/helpers.py b/app/helpers.py index d1c8492..40b2d9c 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -270,15 +270,39 @@ def leading_silence( 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""" - if ms is None: - return none + minutes: int + 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: diff --git a/app/music_manager.py b/app/music_manager.py index 0ba64bc..db29860 100644 --- a/app/music_manager.py +++ b/app/music_manager.py @@ -53,6 +53,7 @@ class _FadeTrack(QThread): ) sleep(1 / Config.FADEOUT_STEPS_PER_SECOND) + self.player.stop() self.finished.emit() @@ -80,9 +81,11 @@ class Music: self.vlc_instance = vlc_manager.get_instance() self.vlc_instance.set_user_agent(name, name) self.player: vlc.MediaPlayer | None = None + self.vlc_event_manager: vlc.EventManager | None = None self.max_volume: int = Config.VLC_VOLUME_DEFAULT self.start_dt: dt.datetime | None = None self.signals = MusicMusterSignals() + self.end_of_track_signalled = False 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(): return + self.signal_track_ended() + self.fader_worker = _FadeTrack(self.player, fade_seconds=fade_seconds) self.fader_worker.finished.connect(self.player.release) self.fader_worker.start() @@ -142,10 +147,12 @@ class Music: < dt.timedelta(microseconds=Config.PLAY_SETTLE) ) +# @log_call def play( self, path: str, start_time: dt.datetime, + playlist_id: int, position: float | None = None, ) -> None: """ @@ -157,7 +164,7 @@ class Music: 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): log.error(f"play({path}): path not readable") @@ -171,6 +178,10 @@ class Music: ) 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.set_volume(self.max_volume) @@ -213,6 +224,21 @@ class Music: log.debug(f"Reset from {volume=}") 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: """Immediately stop playing""" @@ -227,3 +253,11 @@ class Music: self.player.stop() self.player.release() 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() diff --git a/app/musicmuster.py b/app/musicmuster.py index 0376a29..f73ad07 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -2563,8 +2563,6 @@ class Window(QMainWindow): if self.track_sequence.current: try: - self.track_sequence.current.check_for_end_of_track() - # Update intro counter if applicable and, if updated, # return because playing an intro uses the intro field to # show timing and this takes precedence over timing a diff --git a/app/playlistmodel.py b/app/playlistmodel.py index 5fce3c7..42a9948 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -1057,7 +1057,7 @@ class PlaylistModel(QAbstractTableModel): 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 roles = [ @@ -1349,13 +1349,13 @@ class PlaylistModel(QAbstractTableModel): plr = self.playlist_rows[row_number] if column == Col.NOTE.value: - plr.note = value + plr.note = str(value) elif column == Col.TITLE.value: - plr.title = value + plr.title = str(value) elif column == Col.ARTIST.value: - plr.artist = value + plr.artist = str(value) elif column == Col.INTRO.value: intro = int(round(float(value), 1) * 1000) diff --git a/app/playlistrow.py b/app/playlistrow.py index f77df54..491a491 100644 --- a/app/playlistrow.py +++ b/app/playlistrow.py @@ -103,7 +103,7 @@ class PlaylistRow: @property def intro(self) -> int: if self.dto.track: - return self.dto.track.intro + return self.dto.track.intro or 0 else: return 0 @@ -531,12 +531,17 @@ class TrackSequence: """ 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 - 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.current = None + self.start_time = None def move_previous_to_next(self) -> None: """