initial gui added for pyrip

This commit is contained in:
Dirk Alders 2025-08-02 11:41:52 +02:00
parent 893e27f417
commit 7622f1fa38
7 changed files with 337 additions and 388 deletions

View File

@ -1,3 +1,14 @@
# pyrip
PyRip - CD ripper
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

180
gui/__init__.py Normal file
View File

@ -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()

2
gui/requirements.txt Normal file
View File

@ -0,0 +1,2 @@
pyside6

2
media

@ -1 +1 @@
Subproject commit 2e497e2d9e3617e5b50d50556e35cd631c36fd87
Subproject commit f38f34e9a5f85a263ae6856d5cea7241160dea79

View File

@ -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... - [ "

384
pyrip.wxg
View File

@ -1,384 +0,0 @@
<?xml version="1.0"?>
<!-- generated by wxGlade 0.7.1 on Tue Jul 12 10:30:16 2016 -->
<application class="" encoding="UTF-8" for_version="3.0" header_extension=".h" indent_amount="4" indent_symbol="space" is_template="0" language="python" name="" option="0" overwrite="0" path="pyrip.py" source_extension=".cpp" top_window="frame_1" use_gettext="1" use_new_namespace="1">
<object class="MainFrame" name="frame_1" base="EditFrame">
<title>PyRip</title>
<size>1033, 761</size>
<object class="wxBoxSizer" name="sizer_main" base="EditBoxSizer">
<orient>wxVERTICAL</orient>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>1</option>
<object class="wxNotebook" name="notebook_main" base="EditNotebook">
<tabs>
<tab window="notebook_main_rip">Rip</tab>
<tab window="notebook_main_config">Config</tab>
</tabs>
<object class="wxPanel" name="notebook_main_rip" base="EditPanel">
<style>wxTAB_TRAVERSAL</style>
<object class="wxBoxSizer" name="sizer_rip" base="EditBoxSizer">
<orient>wxVERTICAL</orient>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>1</option>
<object class="wxBoxSizer" name="sizer_rip_cd_data" base="EditBoxSizer">
<orient>wxHORIZONTAL</orient>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>1</option>
<object class="wxListBox" name="track_list" base="EditListBox">
<style>wxLB_MULTIPLE</style>
<selection>0</selection>
<choices>
</choices>
<events>
<handler event="EVT_LISTBOX">evt_tracklist</handler>
</events>
</object>
</object>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>1</option>
<object class="wxBoxSizer" name="sizer_rip_track_data" base="EditBoxSizer">
<orient>wxVERTICAL</orient>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxStaticBoxSizer" name="sizer_artist" base="EditStaticBoxSizer">
<orient>wxVERTICAL</orient>
<label>Artist</label>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxTextCtrl" name="text_ctrl_artist" base="EditTextCtrl">
<events>
<handler event="EVT_TEXT">evt_artist_changed</handler>
</events>
</object>
</object>
</object>
</object>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxStaticBoxSizer" name="sizer_album" base="EditStaticBoxSizer">
<orient>wxVERTICAL</orient>
<label>Album</label>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxTextCtrl" name="text_ctrl_album" base="EditTextCtrl">
<events>
<handler event="EVT_TEXT">evt_album_changed</handler>
</events>
</object>
</object>
</object>
</object>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxStaticBoxSizer" name="sizer_year" base="EditStaticBoxSizer">
<orient>wxVERTICAL</orient>
<label>Year</label>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxTextCtrl" name="text_ctrl_year" base="EditTextCtrl">
<events>
<handler event="EVT_TEXT">evt_year_changed</handler>
</events>
</object>
</object>
</object>
</object>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxStaticBoxSizer" name="sizer_genre" base="EditStaticBoxSizer">
<orient>wxVERTICAL</orient>
<label>Genre</label>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxTextCtrl" name="text_ctrl_genre" base="EditTextCtrl">
<events>
<handler event="EVT_TEXT">evt_genre_changed</handler>
</events>
</object>
</object>
</object>
</object>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxStaticBoxSizer" name="sizer_title" base="EditStaticBoxSizer">
<orient>wxVERTICAL</orient>
<label>Title</label>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxTextCtrl" name="text_ctrl_title" base="EditTextCtrl">
<events>
<handler event="EVT_TEXT">evt_title_changed</handler>
</events>
</object>
</object>
</object>
</object>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxStaticBoxSizer" name="sizer_track_no" base="EditStaticBoxSizer">
<orient>wxVERTICAL</orient>
<label>Trucknumber</label>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxTextCtrl" name="text_ctrl_track_no" base="EditTextCtrl">
<events>
<handler event="EVT_TEXT">evt_track_no_changed</handler>
</events>
</object>
</object>
</object>
</object>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxStaticBoxSizer" name="sizer_comment" base="EditStaticBoxSizer">
<orient>wxVERTICAL</orient>
<label>Comment</label>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxTextCtrl" name="text_ctrl_comment" base="EditTextCtrl">
<events>
<handler event="EVT_TEXT">evt_comment_changed</handler>
</events>
</object>
</object>
</object>
</object>
<object class="sizeritem">
<border>0</border>
<option>1</option>
<object class="spacer" name="spacer" base="EditSpacer">
<height>20</height>
<width>20</width>
</object>
</object>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxStaticBoxSizer" name="sizer_overall_progress" base="EditStaticBoxSizer">
<orient>wxVERTICAL</orient>
<label>Overall Progress</label>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxGauge" name="gauge_overall_progress" base="EditGauge">
<range>1000</range>
</object>
</object>
</object>
</object>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxStaticBoxSizer" name="sizer_rip_progress" base="EditStaticBoxSizer">
<orient>wxVERTICAL</orient>
<label>Rip Progress</label>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxGauge" name="gauge_rip_progress" base="EditGauge">
<range>1000</range>
</object>
</object>
</object>
</object>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxStaticBoxSizer" name="sizer_encode_progress" base="EditStaticBoxSizer">
<orient>wxVERTICAL</orient>
<label>Encode Progress</label>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxGauge" name="gauge_encode_progress" base="EditGauge">
<range>1000</range>
</object>
</object>
</object>
</object>
</object>
</object>
</object>
</object>
<object class="sizeritem">
<border>0</border>
<option>0</option>
<object class="wxBoxSizer" name="sizer_rip_buttons" base="EditBoxSizer">
<orient>wxHORIZONTAL</orient>
<object class="sizeritem">
<border>0</border>
<option>0</option>
<object class="spacer" name="spacer" base="EditSpacer">
<height>20</height>
<width>20</width>
</object>
</object>
<object class="sizeritem">
<border>0</border>
<option>0</option>
<object class="wxButton" name="button_rip" base="EditButton">
<label>Rip</label>
<events>
<handler event="EVT_BUTTON">evt_rip</handler>
</events>
</object>
</object>
<object class="sizeritem">
<border>0</border>
<option>0</option>
<object class="spacer" name="spacer" base="EditSpacer">
<height>20</height>
<width>20</width>
</object>
</object>
<object class="sizeritem">
<border>0</border>
<option>0</option>
<object class="wxButton" name="button_read_disc" base="EditButton">
<label>Read Disc</label>
<events>
<handler event="EVT_BUTTON">evt_new_disc</handler>
</events>
</object>
</object>
<object class="sizeritem">
<border>0</border>
<option>0</option>
<object class="spacer" name="spacer" base="EditSpacer">
<height>20</height>
<width>20</width>
</object>
</object>
</object>
</object>
</object>
</object>
<object class="wxPanel" name="notebook_main_config" base="EditPanel">
<style>wxTAB_TRAVERSAL</style>
<object class="wxBoxSizer" name="sizer_notebook_main_config" base="EditBoxSizer">
<orient>wxVERTICAL</orient>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxStaticBoxSizer" name="sizer_target_path" base="EditStaticBoxSizer">
<orient>wxVERTICAL</orient>
<label>Target Path</label>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxTextCtrl" name="text_ctrl_target_path" base="EditTextCtrl">
<events>
<handler event="EVT_TEXT">evt_target_path_changed</handler>
</events>
</object>
</object>
</object>
</object>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxStaticBoxSizer" name="sizer_device" base="EditStaticBoxSizer">
<orient>wxVERTICAL</orient>
<label>Device</label>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxTextCtrl" name="text_ctrl_device" base="EditTextCtrl">
<events>
<handler event="EVT_TEXT">evt_device_changed</handler>
</events>
</object>
</object>
</object>
</object>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxStaticBoxSizer" name="sizer_default_comment" base="EditStaticBoxSizer">
<orient>wxVERTICAL</orient>
<label>Default Comment</label>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxTextCtrl" name="text_ctrl_default_comment" base="EditTextCtrl">
<events>
<handler event="EVT_TEXT">evt_default_comment_changed</handler>
</events>
</object>
</object>
</object>
</object>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxStaticBoxSizer" name="sizer_track_list_string" base="EditStaticBoxSizer">
<orient>wxVERTICAL</orient>
<label>Track List Sting</label>
<object class="sizeritem">
<flag>wxEXPAND</flag>
<border>0</border>
<option>0</option>
<object class="wxTextCtrl" name="text_ctrl_track_list_string" base="EditTextCtrl">
<events>
<handler event="EVT_TEXT">evt_track_list_string_changed</handler>
</events>
</object>
</object>
</object>
</object>
</object>
</object>
</object>
</object>
</object>
</object>
</application>

140
pyripgui.py Normal file
View File

@ -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())