Noah Vogt 1 рік тому
батько
коміт
935cfb62e6
4 змінених файлів з 182 додано та 24 видалено
  1. 1 0
      choose_cd_dialog.py
  2. 177 21
      input/gui.py
  3. 1 0
      input/validate_config.py
  4. 3 3
      utils/scripts.py

+ 1 - 0
choose_cd_dialog.py

@@ -53,6 +53,7 @@ def choose_cd_day() -> list[str]:
 
         dialog = RadioButtonDialog(dirs, "Choose a CD to Burn")
         if dialog.exec_() == QDialog.Accepted:
+            # TODO: update text for possibly mutliple CD's
             log(f"Burning CD for day: {dialog.chosen}")
             return ["", dialog.chosen]
         return ["ignore", ""]

+ 177 - 21
input/gui.py

@@ -15,6 +15,13 @@ You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 """
 
+import sys
+from os import listdir, path
+from re import match
+from dataclasses import dataclass
+
+from soundfile import SoundFile, LibsndfileError  # pyright: ignore
+
 from PyQt5.QtWidgets import (  # pylint: disable=no-name-in-module
     QApplication,
     QDialog,
@@ -23,13 +30,38 @@ from PyQt5.QtWidgets import (  # pylint: disable=no-name-in-module
     QStyle,
     QLabel,
     QRadioButton,
+    QCheckBox,
     QPushButton,
     QMessageBox,
     QButtonGroup,
     QScrollArea,
     QWidget,
 )
-from PyQt5.QtCore import QSize
+from PyQt5.QtMultimedia import (  # pylint: disable=no-name-in-module
+    QMediaPlayer,
+    QMediaContent,
+)
+from PyQt5.QtCore import (  # pylint: disable=no-name-in-module
+    QSize,
+    QUrl,
+    QTimer,
+)
+
+
+class CustomException(Exception):
+    pass
+
+
+@dataclass
+class LabelConstruct:
+    rel_path: str
+    label: QLabel
+
+
+@dataclass
+class CheckBoxConstruct:
+    rel_path: str
+    check_box: QCheckBox
 
 
 # pylint: disable=too-few-public-methods
@@ -100,50 +132,94 @@ class RadioButtonDialog(QDialog):  # pylint: disable=too-few-public-methods
             )
 
 
+def get_wave_duration_in_secs(file_name: str) -> int:
+    try:
+        wav = SoundFile(file_name)
+        return int(wav.frames / wav.samplerate)
+    except LibsndfileError:
+        QMessageBox.critical(
+            None,
+            "Error",
+            f"Error: Could not get duration of {file_name}",
+        )
+        sys.exit(1)
+
+
 class SheetAndPreviewChooser(QDialog):  # pylint: disable=too-few-public-methods
     def __init__(
         self, base_dir: str, options: list[str], window_title: str
     ) -> None:
         super().__init__()
+        self.base_dir = base_dir
         self.setWindowTitle(window_title)
 
         self.master_layout = QVBoxLayout(self)
 
         scroll_area_layout = self.get_scroll_area_layout()
 
-        self.radio_button_group = QButtonGroup(self)
+        self.check_buttons = []
 
-        self.chosen = ""
+        self.player = QMediaPlayer()
+
+        self.chosen_sheets = []
+        self.position_labels = []
         for num, item in enumerate(options):
-            radio_button = QRadioButton(item)
+            rel_wave_path = self.get_wav_relative_path_from_cue_sheet(item)
+
+            check_box = QCheckBox(item)
             if num == 0:
-                radio_button.setChecked(True)
+                check_box.setChecked(True)
             button_layout = QHBoxLayout()
-            self.radio_button_group.addButton(radio_button)
-
-            button_layout.addWidget(radio_button)
+            check_box_construct = CheckBoxConstruct(item, check_box)
+            self.check_buttons.append(check_box_construct)
+            button_layout.addWidget(check_box)
 
             play_button = self.get_player_button("SP_MediaPlay")
             play_button.setToolTip("Play CD Preview")
+            play_button.clicked.connect(
+                lambda _, x=rel_wave_path: self.play_audio(x)
+            )
 
             pause_button = self.get_player_button("SP_MediaPause")
             pause_button.setToolTip("Pause CD Preview")
+            pause_button.clicked.connect(
+                lambda _, x=rel_wave_path: self.pause_player(x)
+            )
 
             stop_button = self.get_player_button("SP_MediaStop")
             stop_button.setToolTip("Stop CD Preview")
+            stop_button.clicked.connect(
+                lambda _, x=rel_wave_path: self.stop_player(x)
+            )
 
             seek_bwd_button = self.get_player_button("SP_MediaSeekBackward")
             seek_bwd_button.setToolTip("Seek 10 seconds backwards")
+            seek_bwd_button.clicked.connect(
+                lambda _, x=rel_wave_path: self.seek_bwd_10secs(x)
+            )
 
             seek_fwd_button = self.get_player_button("SP_MediaSeekForward")
             seek_fwd_button.setToolTip("Seek 10 seconds forwards")
+            seek_fwd_button.clicked.connect(
+                lambda _, x=rel_wave_path: self.seek_fwd_10secs(x)
+            )
 
             button_layout.addWidget(play_button)
             button_layout.addWidget(pause_button)
             button_layout.addWidget(stop_button)
             button_layout.addWidget(seek_bwd_button)
             button_layout.addWidget(seek_fwd_button)
-            button_layout.addWidget(QLabel("01:23 / 80:00"))
+
+            secs = get_wave_duration_in_secs(path.join(base_dir, rel_wave_path))
+            mins = secs // 60
+            secs %= 60
+            time_label = QLabel(f"00:00 / {mins:02}:{secs:02}")
+            label_construct = LabelConstruct(rel_wave_path, time_label)
+            self.position_labels.append(label_construct)
+            button_layout.addWidget(time_label)
+            timer = QTimer(self)
+            timer.timeout.connect(self.update_position)
+            timer.start(1000)
 
             scroll_area_layout.addLayout(button_layout)
 
@@ -151,6 +227,63 @@ class SheetAndPreviewChooser(QDialog):  # pylint: disable=too-few-public-methods
         ok_button.clicked.connect(self.accept)
         self.master_layout.addWidget(ok_button)
 
+    def play_audio(self, rel_path: str) -> None:
+        if self.player.state() == QMediaPlayer.PausedState:  # pyright: ignore
+            media = self.player.media()
+            if media.isNull():
+                return
+            if path.split(media.canonicalUrl().toString())[1] == rel_path:
+                self.player.play()
+        else:
+            url = QUrl.fromLocalFile(path.join(self.base_dir, rel_path))
+            content = QMediaContent(url)
+            self.player.setMedia(content)
+            self.player.play()
+
+    def update_position(self) -> None:
+        media = self.player.media()
+        if media.isNull():
+            return
+        playing_path = path.split(media.canonicalUrl().toString())[1]
+        for label_construct in self.position_labels:
+            if label_construct.rel_path == playing_path:
+                old_text = label_construct.label.text()
+                old_text = old_text[old_text.find(" / ") :]
+                secs = self.player.position() // 1000
+                mins = secs // 60
+                secs %= 60
+                label_construct.label.setText(f"{mins:02}:{secs:02}{old_text}")
+
+    def stop_player(self, rel_path: str) -> None:
+        media = self.player.media()
+        if media.isNull():
+            return
+        if path.split(media.canonicalUrl().toString())[1] == rel_path:
+            self.player.stop()
+            self.update_position()
+
+    def seek_by(self, rel_path: str, seek_by_milis) -> None:
+        media = self.player.media()
+        if media.isNull():
+            return
+        if path.split(media.canonicalUrl().toString())[1] == rel_path:
+            position = self.player.position()
+            self.player.setPosition(position + seek_by_milis)
+            self.update_position()
+
+    def seek_fwd_10secs(self, rel_path: str) -> None:
+        self.seek_by(rel_path, 10000)
+
+    def seek_bwd_10secs(self, rel_path: str) -> None:
+        self.seek_by(rel_path, -10000)
+
+    def pause_player(self, rel_path: str) -> None:
+        media = self.player.media()
+        if media.isNull():
+            return
+        if path.split(media.canonicalUrl().toString())[1] == rel_path:
+            self.player.pause()
+
     def get_scroll_area_layout(self):
         scroll_area = QScrollArea()
         scroll_area.setWidgetResizable(True)
@@ -170,17 +303,40 @@ class SheetAndPreviewChooser(QDialog):  # pylint: disable=too-few-public-methods
         stop_button.setIcon(icon)
         return stop_button
 
-    def accept(self):
-        selected_button = self.radio_button_group.checkedButton()
-        if selected_button:
-            self.chosen = selected_button.text()
-            # QMessageBox.information(
-            #     self, "Selection", f"You selected: {selected_button.text()}"
-            # )
-            super().accept()
-        else:
-            QMessageBox.warning(
+    def accept(self) -> None:
+        for check_box_construct in self.check_buttons:
+            if check_box_construct.check_box.isChecked():
+                self.chosen_sheets.append(check_box_construct.rel_path)
+        super().accept()
+
+    def get_wav_relative_path_from_cue_sheet(
+        self, sheet_relative_path: str
+    ) -> str:
+        full_path = path.join(self.base_dir, sheet_relative_path)
+
+        try:
+            with open(
+                full_path,
+                mode="r",
+                encoding="utf-8-sig",
+            ) as cachefile_reader:
+                cachefile_content = cachefile_reader.readlines()
+            first_line = cachefile_content[0].strip()
+            if not match(r"^FILE \".+\" WAVE$", first_line):
+                raise CustomException("invalid first cue sheet line")
+            full_path_found = first_line[first_line.find('"') + 1 :]
+            full_path_found = full_path_found[: full_path_found.rfind('"')]
+            return path.split(full_path_found)[1]
+        except (
+            FileNotFoundError,
+            PermissionError,
+            IOError,
+            IndexError,
+            CustomException,
+        ) as error:
+            QMessageBox.critical(
                 self,
-                "No Selection",
-                "Please select an option before proceeding.",
+                "Error",
+                f"Could not parse cue sheet: '{full_path}', Reason: {error}",
             )
+            sys.exit(1)

+ 1 - 0
input/validate_config.py

@@ -51,6 +51,7 @@ def validate_cd_record_config() -> None:
         "CD_RECORD_OUTPUT_BASEDIR": const.CD_RECORD_OUTPUT_BASEDIR,
         "CD_RECORD_FFMPEG_INPUT_ARGS": const.CD_RECORD_FFMPEG_INPUT_ARGS,
         "CD_RECORD_MAX_SECONDS": const.CD_RECORD_MAX_SECONDS,
+        "CD_RECORD_MIN_TRACK_MILIS": const.CD_RECORD_MIN_TRACK_MILIS,
     }
     general_config_validator(needed_constants)
 

+ 3 - 3
utils/scripts.py

@@ -74,8 +74,8 @@ def choose_right_cd_drive(drives: list) -> str:
 
         dialog = RadioButtonDialog(drives, "Choose a CD to Burn")
         if dialog.exec_() == QDialog.Accepted:
-            print(f"Dialog accepted: {dialog.chosen}")
-            return dialog.chosen
+            print(f"Dialog accepted: {dialog.chosen_sheets}")
+            return dialog.chosen_sheets
         log("Warning: Choosing first cd drive...", color="yellow")
 
     return drives[0]
@@ -245,7 +245,7 @@ def burn_cds_of_day(yyyy_mm_dd: str) -> None:
                 target_dir, cue_sheets, "Preview CD's"
             )
             if dialog.exec_() == QDialog.Accepted:
-                log(f"Burning CD from sheet: {dialog.chosen}")
+                log(f"Burning CD from sheet: {dialog.chosen_sheets}")
 
     except (FileNotFoundError, PermissionError, IOError):
         InfoMsgBox(