pyrip/gui/__init__.py

441 lines
15 KiB
Python

import logging
from PySide6.QtWidgets import (
QMainWindow,
QMessageBox,
QDialog,
QDialogButtonBox,
QWidget,
QVBoxLayout,
QHBoxLayout,
QListWidget,
QListWidgetItem,
QLabel,
QPlainTextEdit,
QProgressBar,
QPushButton,
QLineEdit
)
from PySide6.QtCore import (
Qt,
SIGNAL,
QObject,
Signal,
Slot,
QThread
)
from queue import Queue
import time
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 QueueWorker(QObject):
progress_updated = Signal(int, int)
job_finished = Signal(int)
all_jobs_finished = Signal()
def __init__(self):
super().__init__()
self.queue = Queue()
self.__stop__ = False
self.__error_msg__ = None
self.__job_data__ = {}
@Slot()
def stop(self):
self.__stop__ = True
@Slot()
def stopped(self):
return self.__stop__
@Slot()
def get_error(self):
return self.__error_msg__
@Slot(str)
def set_error(self, error_msg):
if self.__error_msg__ is None:
self.__error_msg__ = error_msg
@Slot()
def empty(self):
return self.queue.empty()
@Slot(int)
def add_job(self, job_index, job_data):
self.__job_data__[job_index] = job_data
self.queue.put(job_index)
@Slot()
def run(self):
while not self.__stop__:
if self.queue.empty() or self.__error_msg__ is not None:
# do not execute, if job_index is not available or error_msg is set
time.sleep(0.1)
else:
job_index = self.queue.get()
self.action(job_index)
if job_index in self.__job_data__:
del self.__job_data__[job_index]
self.job_finished.emit(job_index)
self.all_jobs_finished.emit()
@Slot(int)
def action(self, job_index):
pass
class RipQueueWorker(QueueWorker):
def __init__(self):
super().__init__()
@Slot(int)
def action(self, job_index):
ERROR_AT_TRACK = None # Give a number here
#
logger.info("action: Start ripping track %02d...", job_index + 1)
for i in range(0, 51):
time.sleep(0.1)
if ERROR_AT_TRACK is not None:
if job_index == ERROR_AT_TRACK and i > 10:
msg = "Fehler beim rippen von track %02d" % (job_index + 1)
logger.warning("action: " + msg)
self.set_error(msg)
return
self.progress_updated.emit(i, job_index)
class EncQueueWorker(QueueWorker):
def __init__(self):
super().__init__()
@Slot(int)
def action(self, job_index):
ERROR_AT_TRACK = None # Give a number here
#
logger.info("action: Start encoding track %02d...", job_index + 1)
for i in range(50, 101):
time.sleep(0.05)
if ERROR_AT_TRACK is not None:
if job_index == ERROR_AT_TRACK and i > 60:
msg = "Fehler beim encoden von track %02d" % (job_index + 1)
logger.warning("action: " + msg)
self.set_error(msg)
return
self.progress_updated.emit(i, job_index)
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.text_edit.setTabChangesFocus(True)
self.progress_bar = QProgressBar()
self.progress_bar.setMaximumWidth(150)
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("PyRip")
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_all)
button_layout.addWidget(self.button_rip)
self.connect(self.list_widget, SIGNAL("itemDoubleClicked(QListWidgetItem *)"), self.single_track_selected)
def clear(self):
self.list_widget.clear()
for key in self.FIELD_NAMES:
self.set_album_field(key, "")
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)
@Slot(int, int)
def set_title_progress(self, value, bar_index):
if 0 <= bar_index < self.list_widget.count():
list_item = self.list_widget.item(bar_index)
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, adding example data...")
for key in self.FIELD_NAMES:
self.set_album_field(key, key)
for track in range(0, 5):
self.append_title("Track %02d" % (track + 1))
self.button_rip.setEnabled(True)
def get_rip_worker(self):
return RipQueueWorker()
def get_encode_worker(self):
return EncQueueWorker()
def rip_all(self, clicked_data: bool = None, single_job: int = None):
self.button_read.setEnabled(False)
self.button_rip.setEnabled(False)
self.rip_thread = QThread()
self.rip_worker = self.get_rip_worker()
self.rip_worker.moveToThread(self.rip_thread)
self.enc_thread = QThread()
self.enc_worker = self.get_encode_worker()
self.enc_worker.moveToThread(self.enc_thread)
# Connect Signals and Slots
self.rip_worker.progress_updated.connect(self.set_title_progress)
self.enc_worker.progress_updated.connect(self.set_title_progress)
# We route the signal through the main window.
self.rip_worker.job_finished.connect(self.rip_task_finished)
self.enc_worker.job_finished.connect(self.enc_task_finished)
self.rip_thread.started.connect(self.rip_worker.run)
self.enc_thread.started.connect(self.enc_worker.run)
self.rip_worker.all_jobs_finished.connect(self.rip_tasks_complete)
self.enc_worker.all_jobs_finished.connect(self.enc_tasks_complete)
self.rip_thread.start()
self.enc_thread.start()
self.set_status("rip_all: Start ripping...")
if single_job is None:
logger.info("rip_all: Starting rip process for whole disc...")
for i in range(0, self.list_widget.count()):
self.rip_worker.add_job(i, self.get_job_data(i))
elif 0 <= single_job < self.list_widget.count():
logger.info("rip_all: Starting rip process for track %02d", single_job + 1)
self.rip_worker.add_job(single_job, self.get_job_data(single_job))
@Slot(int)
def get_job_data(self, job_index):
# placeholder for disc function
return None
def single_track_selected(self, list_item):
list_index = self.list_widget.row(list_item)
if self.button_rip.isEnabled():
self.rip_all(single_job=list_index)
else:
logger.warning("single_track_selected: A rip process is running ignoring rip request of track %02d", list_index + 1)
def on_disc_removed(self, device):
self.clear()
def on_disc_new(self, device):
self.read_it()
def device_callback(self, device):
action = device.action
if 'ID_CDROM' in device and action == 'change':
if device.properties.get('ID_CDROM_MEDIA') is None:
self.on_disc_removed(device)
if int(device.properties.get('ID_CDROM_MEDIA_TRACK_COUNT_AUDIO', 0)) > 0:
self.on_disc_new(device)
@Slot(int)
def rip_task_finished(self, job_index):
"""
This slot receives the 'job_finished' signal from worker1
and explicitly adds the job to worker2.
"""
logger.info("rip_task_finished: Track %02d", job_index + 1)
if self.rip_worker.get_error() is not None or self.enc_worker.get_error() is not None:
# Error in one worker detected
if not self.rip_worker.stopped():
logger.warning("rip_task_finished: Error detected. Stopping rip worker and setting progress of track %02d to 0%%.", job_index + 1)
self.rip_worker.stop()
self.set_title_progress(0, job_index)
else:
# Add next encode task only, if no errors are detected
logger.debug("rip_task_finished: Starting encode task for track %02d", job_index + 1)
self.enc_worker.add_job(job_index, self.get_job_data(job_index))
if self.rip_worker.empty():
logger.debug("rip_task_finished: Rip worker queue is empty. Stopping worker")
self.rip_worker.stop()
@Slot(int)
def enc_task_finished(self, job_index):
logger.info("enc_task_finished: Track %02d", job_index + 1)
if self.rip_worker.get_error() is not None or self.enc_worker.get_error() is not None:
# Error in one worker detected
if not self.rip_worker.stopped():
logger.warning("enc_task_finished: Error detected. Stopping rip worker.")
self.rip_worker.stop()
if self.enc_worker.get_error() is not None:
# Error in enc worker detected
logger.warning("enc_task_finished: Encoding error detected. Setting progress of track %02d to 0%%.", job_index + 1)
self.set_title_progress(0, job_index)
@Slot()
def rip_tasks_complete(self):
logger.info("rip_tasks_complete: All rip tasks completed. Stopping encode worker which is already encoding the last track.")
self.enc_worker.stop()
@Slot()
def enc_tasks_complete(self):
logger.info("enc_tasks_complete: All encode tasks completed. Removing threads and enabling buttons.")
self.rip_thread.quit()
self.rip_thread.wait()
self.enc_thread.quit()
self.enc_thread.wait()
self.rip_worker.deleteLater()
self.enc_worker.deleteLater()
self.rip_thread.deleteLater()
self.enc_thread.deleteLater()
rip_error = self.rip_worker.get_error()
enc_error = self.enc_worker.get_error()
if rip_error is None and enc_error is None:
self.set_status("Finished ripping...")
else:
self.set_status("Ripping FAILED...")
if rip_error is not None:
msg = rip_error
if enc_error is not None:
msg += "\n"
msg += enc_error
else:
msg = enc_error
self.set_status(msg)
QMessageBox.critical(self, "Error!", msg)
self.button_read.setEnabled(True)
self.button_rip.setEnabled(True)
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()