diff --git a/README.md b/README.md index 6288e3f..9df3f4d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,14 @@ # pyrip -PyRip - CD ripper \ No newline at end of file +PyRip - CD ripper + +You might want to install: +* cdparnoia +* lame + + +Possibly these are also needed: +* qt6-base-dev +* qt6-base-devtools +* python3-pyside6.qtwidgets +* python3-pyqt6.qtdesigner diff --git a/gui/__init__.py b/gui/__init__.py new file mode 100644 index 0000000..38bb4b0 --- /dev/null +++ b/gui/__init__.py @@ -0,0 +1,180 @@ +import logging +import media +from PySide6.QtWidgets import ( + QMainWindow, + QDialog, + QDialogButtonBox, + QWidget, + QVBoxLayout, + QHBoxLayout, + QListWidget, + QListWidgetItem, + QLabel, + QPlainTextEdit, + QProgressBar, + QPushButton, + QLineEdit +) +from PySide6.QtCore import Qt + +try: + from config import APP_NAME as ROOT_LOGGER_NAME +except ImportError: + ROOT_LOGGER_NAME = 'root' +logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__) + + +class RowWidget(QWidget): + """ + Ein Widget, das eine einzelne Zeile in unserer Liste darstellt. + Es enthält eine Nummer (QLabel), ein Textfeld (QPlainTextEdit) + und einen Fortschrittsbalken (QProgressBar). + """ + + def __init__(self, number: int, parent=None): + super().__init__(parent) + + self.number_label = QLabel(f"{number}.") + self.text_edit = QPlainTextEdit() + self.progress_bar = QProgressBar() + + self.text_edit.setFixedHeight(31) + self.number_label.setFixedWidth(30) + self.number_label.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignRight) + + layout = QHBoxLayout() + layout.addWidget(self.number_label) + layout.addWidget(self.text_edit) + layout.addWidget(self.progress_bar) + self.setLayout(layout) + + +class MainWindow(QMainWindow): + KEY_ALBUM_ARTIST = "Interpret" + KEY_ALBUM_ALBUM = "Album" + KEY_ALBUM_YEAR = "Jahr" + KEY_ALBUM_GENRE = "Genre" + FIELD_NAMES = [KEY_ALBUM_ARTIST, KEY_ALBUM_ALBUM, KEY_ALBUM_YEAR, KEY_ALBUM_GENRE] + + def __init__(self, basepath): + super().__init__() + self.__basepath__ = basepath + # + self.setWindowTitle("PySide6 Listenanwendung") + self.setGeometry(100, 100, 900, 600) # Fensterbreite angepasst + self.set_status("Ready...") + + self.__album_lines__ = [] + main_widget = QWidget() + main_layout = QVBoxLayout(main_widget) + self.setCentralWidget(main_widget) + + disc_layout = QHBoxLayout() + album_layout = QVBoxLayout() + + # Create fields for Album Information + for i in range(0, len(self.FIELD_NAMES)): + # Add label + label = QLabel(self.FIELD_NAMES[i]) + # Add title + line_edit = QLineEdit() + line_edit.setMaximumWidth(300) + line_edit.setPlaceholderText("Title...") + # Add title to list for later use + self.__album_lines__.append(line_edit) + + # Add both to album_layout + album_layout.addWidget(label) + album_layout.addWidget(line_edit) + # Add stretch to place entries to top + album_layout.addStretch() + + # Add album and disc_layout + disc_layout.addLayout(album_layout) + main_layout.addLayout(disc_layout) + + self.list_widget = QListWidget() + disc_layout.addWidget(self.list_widget) + + button_layout = QHBoxLayout() + main_layout.addLayout(button_layout) + + self.button_read = QPushButton("Read Disc") + self.button_read.clicked.connect(self.read_it) + button_layout.addWidget(self.button_read) + + self.button_rip = QPushButton("Rip Disc") + self.button_rip.setDisabled(True) + self.button_rip.clicked.connect(self.rip_it) + button_layout.addWidget(self.button_rip) + + def set_album_field(self, key, value): + i = self.FIELD_NAMES.index(key) + line = self.__album_lines__[i] + line.setText(value) + + def get_album_field(self, key): + i = self.FIELD_NAMES.index(key) + if key == self.KEY_ALBUM_YEAR: + return int(self.__album_lines__[i].text()) + else: + return self.__album_lines__[i].text() + + def append_title(self, value): + num_tracks = self.list_widget.count() + 1 + row_widget = RowWidget(num_tracks) + row_widget.text_edit.setPlainText(value) + row_widget.progress_bar.setValue(0) + list_item = QListWidgetItem(self.list_widget) + list_item.setSizeHint(row_widget.sizeHint()) + self.list_widget.addItem(list_item) + self.list_widget.setItemWidget(list_item, row_widget) + + def set_status(self, status): + self.statusBar().showMessage(status) + + def set_title_progress(self, num, value): + list_item = self.list_widget.item(num) + row_widget = self.list_widget.itemWidget(list_item) + row_widget.progress_bar.setValue(value) + + def read_it(self): + print("read_it button action not yet implemented...") + + def rip_it(self): + print("rip_it button action not yet implemented...") + + +class SelectionDialog(QDialog): + """ + A custom dialog to select an item from a list. + """ + + def __init__(self, parent, title, items): + super().__init__(parent) + self.setWindowTitle(title) + self.setMinimumWidth(300) + + # Layout + layout = QVBoxLayout(self) + + # List widget for options + self.list_widget = QListWidget() + self.list_widget.addItems(items) + if items: + self.list_widget.setCurrentRow(0) # Select first item by default + + # Add a double-click shortcut to accept the dialog + self.list_widget.itemDoubleClicked.connect(self.accept) + + layout.addWidget(self.list_widget) + + # Standard OK and Cancel buttons + button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + layout.addWidget(button_box) + + def get_selected_index(self): + """Returns the index of the currently selected item.""" + return self.list_widget.currentRow() diff --git a/gui/requirements.txt b/gui/requirements.txt new file mode 100644 index 0000000..2a3748b --- /dev/null +++ b/gui/requirements.txt @@ -0,0 +1,2 @@ +pyside6 + diff --git a/media b/media index 2e497e2..f38f34e 160000 --- a/media +++ b/media @@ -1 +1 @@ -Subproject commit 2e497e2d9e3617e5b50d50556e35cd631c36fd87 +Subproject commit f38f34e9a5f85a263ae6856d5cea7241160dea79 diff --git a/pyrip.py b/pyrip.py index 3115c1e..2345ef0 100644 --- a/pyrip.py +++ b/pyrip.py @@ -8,7 +8,7 @@ import fstools logger = report.app_logging_config() -def progress_callback_rip(p: float): +def progress_callback_rip(p: float, track_num: int = None): bar_length = 40 progress = int(bar_length * p) out = "\rRipping.... - [ " @@ -18,7 +18,7 @@ def progress_callback_rip(p: float): sys.stdout.write(out) -def progress_callback_enc(p: float): +def progress_callback_enc(p: float, track_num: int = None): bar_length = 40 progress = int(bar_length * p) out = "\rEncoding... - [ " diff --git a/pyrip.wxg b/pyrip.wxg deleted file mode 100644 index b60ef78..0000000 --- a/pyrip.wxg +++ /dev/null @@ -1,384 +0,0 @@ - - - - - - PyRip - 1033, 761 - - wxVERTICAL - - wxEXPAND - 0 - - - - Rip - Config - - - - - wxVERTICAL - - wxEXPAND - 0 - - - wxHORIZONTAL - - wxEXPAND - 0 - - - - 0 - - - - evt_tracklist - - - - - wxEXPAND - 0 - - - wxVERTICAL - - wxEXPAND - 0 - - - wxVERTICAL - - - wxEXPAND - 0 - - - - evt_artist_changed - - - - - - - wxEXPAND - 0 - - - wxVERTICAL - - - wxEXPAND - 0 - - - - evt_album_changed - - - - - - - wxEXPAND - 0 - - - wxVERTICAL - - - wxEXPAND - 0 - - - - evt_year_changed - - - - - - - wxEXPAND - 0 - - - wxVERTICAL - - - wxEXPAND - 0 - - - - evt_genre_changed - - - - - - - wxEXPAND - 0 - - - wxVERTICAL - - - wxEXPAND - 0 - - - - evt_title_changed - - - - - - - wxEXPAND - 0 - - - wxVERTICAL - - - wxEXPAND - 0 - - - - evt_track_no_changed - - - - - - - wxEXPAND - 0 - - - wxVERTICAL - - - wxEXPAND - 0 - - - - evt_comment_changed - - - - - - - 0 - - - 20 - 20 - - - - wxEXPAND - 0 - - - wxVERTICAL - - - wxEXPAND - 0 - - - 1000 - - - - - - wxEXPAND - 0 - - - wxVERTICAL - - - wxEXPAND - 0 - - - 1000 - - - - - - wxEXPAND - 0 - - - wxVERTICAL - - - wxEXPAND - 0 - - - 1000 - - - - - - - - - - 0 - - - wxHORIZONTAL - - 0 - - - 20 - 20 - - - - 0 - - - - - evt_rip - - - - - 0 - - - 20 - 20 - - - - 0 - - - - - evt_new_disc - - - - - 0 - - - 20 - 20 - - - - - - - - - - wxVERTICAL - - wxEXPAND - 0 - - - wxVERTICAL - - - wxEXPAND - 0 - - - - evt_target_path_changed - - - - - - - wxEXPAND - 0 - - - wxVERTICAL - - - wxEXPAND - 0 - - - - evt_device_changed - - - - - - - wxEXPAND - 0 - - - wxVERTICAL - - - wxEXPAND - 0 - - - - evt_default_comment_changed - - - - - - - wxEXPAND - 0 - - - wxVERTICAL - - - wxEXPAND - 0 - - - - evt_track_list_string_changed - - - - - - - - - - - - diff --git a/pyripgui.py b/pyripgui.py new file mode 100644 index 0000000..a592a42 --- /dev/null +++ b/pyripgui.py @@ -0,0 +1,140 @@ +import argparse +import fstools +from gui import MainWindow, SelectionDialog +import media +import os +from PySide6.QtWidgets import QApplication, QMessageBox +import report +import sys +import task + +logger = report.app_logging_config() + +# TODO: Usage of QThread instead of task.delayed +# TODO: Error handling on cancelling cddb choose Dialog + + +MEDIA_GUI_DICT = { + MainWindow.KEY_ALBUM_ARTIST: media.KEY_ARTIST, + MainWindow.KEY_ALBUM_ALBUM: media.KEY_ALBUM, + MainWindow.KEY_ALBUM_YEAR: media.KEY_YEAR, + MainWindow.KEY_ALBUM_GENRE: media.KEY_GENRE +} + + +class RipMainWindow(MainWindow): + def __init__(self, basepath): + super().__init__(basepath) + self.__rip_task__ = task.delayed(0, self.__rip_it__) + + def join(self): + self.__rip_task__.join + + def stop(self): + self.__rip_task__.stop() + + def clear(self): + self.list_widget.clear() + for key in MEDIA_GUI_DICT: + self.set_album_field(key, "") + + def read_it(self): + self.set_status("Reading data from disc...") + disc_data = media.get_media_data(media.get_disc_device(), self.cddb_choose_dialog) + # disc_data = media.metadata.get_disc_data_dummy() + if disc_data is None: + msg = "Could not read disc data!" + logger.error(msg) + self.set_status(msg) + QMessageBox.critical(self, "Error!", msg) + else: + self.clear() + # Set album info to gui + for key in MEDIA_GUI_DICT: + self.set_album_field(key, str(disc_data[MEDIA_GUI_DICT[key]])) + # Set tracks to gui + for track in disc_data[media.KEY_TRACKLIST]: + self.append_title(track[media.KEY_TITLE]) + # Enable rip button + self.set_status("Disc data received") + self.button_rip.setEnabled(True) + + def cddb_choose_dialog(self, what: int, info: dict): + if what == media.CALLBACK_CDDB_CHOICE: + values = tuple(info.values()) + keys = tuple(info.keys()) + dlg = SelectionDialog(self, "Multiple CDDB entries found. Choose one.", values) + if dlg.exec(): + return keys[dlg.get_selected_index()] + else: + msg = "Invalid cddb selection!" + logger.error(msg) + self.set_status(msg) + QMessageBox.critical(self, "Error!", msg) + + def rip_it(self): + self.__rip_task__.run() + + def __rip_it__(self): + self.button_rip.setDisabled(True) + self.button_read.setDisabled(True) + for i in range(self.list_widget.count()): + list_item = self.list_widget.item(i) + row_widget = self.list_widget.itemWidget(list_item) + track_info = {} + track_info[media.KEY_TRACK] = i + 1 + for key in MEDIA_GUI_DICT: + track_info[MEDIA_GUI_DICT[key]] = self.get_album_field(key) + track_info[media.KEY_TITLE] = row_widget.text_edit.toPlainText() + wavfile = media.track_to_targetpath(self.__basepath__, track_info, 'wav') + try: + fstools.mkdir(os.path.dirname(wavfile)) + except PermissionError: + msg = f"Unable to create ripping target path: {os.path.dirname(wavfile)}" + logger.exception(msg) + self.set_status(msg) + QMessageBox.critical(self, "Error!", msg) + self.button_rip.setDisabled(False) + self.button_read.setDisabled(False) + return + self.set_status("Ripping track %02d..." % (i + 1)) + rv = media.disc_track_rip(i + 1, wavfile, self.progress_callback_rip) + if rv == 0: + self.set_status("Encoding track %02d..." % (i + 1)) + rv = media.wav_to_mp3(wavfile, self.__basepath__, track_info, self.progress_callback_enc) + os.remove(wavfile) + if rv != 0: + msg = f"Error while ripping or encoding track {i + 1}" + logger.error(msg) + self.set_status(msg) + self.set_title_progress(i, 0) + QMessageBox.critical(self, "Error!", msg) + self.button_rip.setDisabled(False) + self.button_read.setDisabled(False) + return + self.set_status("Ripping finished!") + self.button_rip.setDisabled(False) + self.button_read.setDisabled(False) + + def progress_callback_rip(self, p: float, track_num: int = None): + self.set_title_progress(track_num - 1, 50 * p) + + def progress_callback_enc(self, p: float, track_num: int = None): + self.set_title_progress(track_num - 1, 50 + 50 * p) + + +if __name__ == "__main__": + default_baspath = os.path.join(os.getenv("HOME"), "pyrip") + parser = argparse.ArgumentParser(description='Description') + parser.add_argument('-b', '--basepath', help=f'The rip and encode basepath (default is {default_baspath})', default=default_baspath) + args = parser.parse_args() + # + app = QApplication(sys.argv) + window = RipMainWindow(args.basepath) + window.show() + try: + window.join() + finally: + window.stop() + + sys.exit(app.exec())