From 553376a99ec1f166ebef9a02b4ead8c988652a4f Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Wed, 3 Jul 2024 17:55:09 +0100 Subject: [PATCH] Preview with pygame working --- app/classes.py | 7 ++- app/musicmuster.py | 113 +++++++++++++++++++++++++++++++++------------ app/playlists.py | 24 ++++++---- 3 files changed, 103 insertions(+), 41 deletions(-) diff --git a/app/classes.py b/app/classes.py index 32f6728..64ed162 100644 --- a/app/classes.py +++ b/app/classes.py @@ -1,7 +1,7 @@ # Standard library imports from dataclasses import dataclass, field from enum import auto, Enum -from typing import Any, Optional +from typing import Any, Optional, NamedTuple # PyQt imports from PyQt6.QtCore import pyqtSignal, QObject @@ -65,3 +65,8 @@ class TrackFileData: obsolete_path: Optional[str] = None tags: dict[str, Any] = field(default_factory=dict) audio_metadata: dict[str, str | int | float] = field(default_factory=dict) + + +class TrackInfo(NamedTuple): + track_id: int + row_number: int diff --git a/app/musicmuster.py b/app/musicmuster.py index 3bc8792..1cff60d 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -48,9 +48,11 @@ import stackprinter # type: ignore from classes import ( MusicMusterSignals, TrackFileData, + TrackInfo, ) from config import Config from dialogs import TrackSelectDialog, ReplaceFilesDialog +from helpers import file_is_unreadable from log import log from models import db, Playdates, PlaylistRows, Playlists, Settings, Tracks from playlistmodel import PlaylistModel, PlaylistProxyModel @@ -150,7 +152,10 @@ class PreviewManager: def __init__(self) -> None: mixer.init() + self.intro: Optional[int] = None self.path: str = "" + self.row_number: Optional[int] = None + self.track_id: int = 0 self.start_time: Optional[dt.datetime] = None def back(self, ms: int) -> None: @@ -187,17 +192,64 @@ class PreviewManager: def is_playing(self) -> bool: return mixer.music.get_busy() + def move_to_intro_end(self) -> None: + """ + Move play position to 'buffer' milliseconds before end of intro. + + If no intro defined, do nothing. + """ + + if self.intro is None: + return + + position = max(0, self.intro - Config.PREVIEW_END_BUFFER_MS) / 1000 + mixer.music.set_pos(position) + self.start_time = dt.datetime.now() - dt.timedelta(seconds=position) + def play(self) -> None: mixer.music.play() self.start_time = dt.datetime.now() - def set_path(self, path) -> None: - self.path = path - mixer.music.load(path) + def restart(self) -> None: + """ + Restart player from beginning + """ + + if not mixer.music.get_busy(): + return + + mixer.music.set_pos(0) + self.start_time = dt.datetime.now() + + def set_intro(self, ms: int) -> None: + """ + Set intro time + """ + + self.intro = ms + + def set_track_info(self, info: TrackInfo) -> None: + self.track_id = info.track_id + self.row_number = info.row_number + + with db.Session() as session: + track = session.get(Tracks, self.track_id) + if not track: + raise ValueError(f"PreviewManager: unable to retreive track {self.track_id=}") + self.intro = track.intro + self.path = track.path + + # Check file readable + if file_is_unreadable(self.path): + raise ValueError(f"PreviewManager.__init__: {track.path=} unreadable") + + mixer.music.load(self.path) def stop(self) -> None: mixer.music.stop() self.path = "" + self.row_number = None + self.track_id = 0 self.start_time = None @@ -1114,14 +1166,18 @@ class Window(QMainWindow, Ui_MainWindow): if self.btnPreview.isChecked(): # Get track path for first selected track if there is one - track_path = self.active_tab().get_selected_row_track_path() - if not track_path: + track_info = self.active_tab().get_selected_row_track_info() + if not track_info: # Otherwise get track_id to next track to play if track_sequence.next: - track_path = track_sequence.next.path - if track_path: - self.preview_manager.set_path(track_path) - self.preview_manager.play() + if track_sequence.next.path: + track_info = TrackInfo(track_sequence.next.track_id, + track_sequence.next.row_number + ) + else: + return + self.preview_manager.set_track_info(track_info) + self.preview_manager.play() else: self.preview_manager.stop() @@ -1138,8 +1194,8 @@ class Window(QMainWindow, Ui_MainWindow): def preview_end(self) -> None: """Advance preview file to Config.PREVIEW_END_BUFFER_MS before end of intro""" - # TODO - pass + if self.preview_manager: + self.preview_manager.move_to_intro_end() def preview_fwd(self) -> None: """Advance preview file""" @@ -1149,29 +1205,26 @@ class Window(QMainWindow, Ui_MainWindow): def preview_mark(self) -> None: """Set intro time""" - # TODO - pass - # if self.preview_track_manager: - # track_id = self.preview_track_manager.track_id - # row_number = self.preview_track_manager.row_number - # with db.Session() as session: - # track = session.get(Tracks, track_id) - # if track: - # # Save intro as millisends rounded to nearest 0.1 - # # second because editor spinbox only resolves to 0.1 - # # seconds - # intro = round(self.preview_track_manager.time_playing() / 100) * 100 - # track.intro = intro - # session.commit() - # self.preview_track_manager.intro = intro - # self.active_tab().source_model.refresh_row(session, row_number) - # self.active_tab().source_model.invalidate_row(row_number) + if self.preview_manager.is_playing(): + track_id = self.preview_manager.track_id + row_number = self.preview_manager.row_number + with db.Session() as session: + track = session.get(Tracks, track_id) + if track: + # Save intro as millisends rounded to nearest 0.1 + # second because editor spinbox only resolves to 0.1 + # seconds + intro = round(self.preview_manager.get_playtime() / 100) * 100 + track.intro = intro + session.commit() + self.preview_manager.set_intro(intro) + self.active_tab().source_model.refresh_row(session, row_number) + self.active_tab().source_model.invalidate_row(row_number) def preview_start(self) -> None: """Restart preview""" - # TODO - pass + self.preview_manager.restart() def rename_playlist(self) -> None: """ diff --git a/app/playlists.py b/app/playlists.py index 53dd2c9..6fd16c9 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -35,7 +35,7 @@ from PyQt6.QtWidgets import ( # Third party imports # App imports -from classes import Col, MusicMusterSignals +from classes import Col, MusicMusterSignals, TrackInfo from config import Config from dialogs import TrackSelectDialog from helpers import ( @@ -644,22 +644,26 @@ class PlaylistTab(QTableView): self.source_model.delete_rows(self.selected_model_row_numbers()) self.clear_selection() - def get_selected_row_track_path(self) -> str: + def get_selected_row_track_info(self) -> Optional[TrackInfo]: """ - Return the path of the selected row. If no row selected or selected - row does not have a track, return empty string. + Return the track_path, track_id and row number of the selected + row. If no row selected or selected row does not have a track, + return None. """ - log.debug("get_selected_row_track_path() called") + selected_row = self.get_selected_row() + if selected_row is None: + return None model_row_number = self.source_model_selected_row_number() if model_row_number is None: - result = "" + return None else: - result = self.source_model.get_row_track_path(model_row_number) - - log.debug(f"get_selected_row_track_path() returned: {result=}") - return result + track_id = self.source_model.get_row_track_id(model_row_number) + if not track_id: + return None + else: + return TrackInfo(track_id, selected_row) def get_selected_row(self) -> Optional[int]: """