multithreading working

This commit is contained in:
billypom on debian 2024-09-07 12:34:03 -04:00
parent 31f23efcc1
commit c4d181f1c1
3 changed files with 120 additions and 62 deletions

View File

@ -19,6 +19,7 @@ from PyQt5.QtWidgets import (
from PyQt5.QtCore import ( from PyQt5.QtCore import (
QAbstractItemModel, QAbstractItemModel,
QModelIndex, QModelIndex,
QThreadPool,
Qt, Qt,
pyqtSignal, pyqtSignal,
QTimer, QTimer,
@ -49,17 +50,21 @@ class MusicTable(QTableView):
enterKey = pyqtSignal() enterKey = pyqtSignal()
deleteKey = pyqtSignal() deleteKey = pyqtSignal()
refreshMusicTable = pyqtSignal() refreshMusicTable = pyqtSignal()
handleProgressSignal = pyqtSignal(str)
getThreadPoolSignal = pyqtSignal()
def __init__(self, parent): def __init__(self, parent=None, application_window=None):
super().__init__(parent) super().__init__(parent)
self.application_window = application_window
# FIXME: why does this give me pyright errors # FIXME: why does this give me pyright errors
self.model = QStandardItemModel() self.model = QStandardItemModel()
self.setModel(self.model) self.setModel(self.model)
# self.model: QAbstractItemModel | None = QAbstractItemModel(self)
# Config # Config
self.config = configparser.ConfigParser() self.config = configparser.ConfigParser()
self.config.read("config.ini") self.config.read("config.ini")
self.threadpool = QThreadPool
# gui names of headers # gui names of headers
self.table_headers = [ self.table_headers = [
"title", "title",
@ -98,12 +103,12 @@ class MusicTable(QTableView):
self.deleteKey.connect(self.delete_songs) self.deleteKey.connect(self.delete_songs)
self.model.dataChanged.connect(self.on_cell_data_changed) # editing cells self.model.dataChanged.connect(self.on_cell_data_changed) # editing cells
self.model.layoutChanged.connect(self.restore_scroll_position) self.model.layoutChanged.connect(self.restore_scroll_position)
self.load_music_table() self.load_music_table()
self.setup_keyboard_shortcuts() self.setup_keyboard_shortcuts()
def contextMenuEvent(self, event): def contextMenuEvent(self, a0):
"""Right-click context menu for rows in Music Table""" """Right-click context menu for rows in Music Table"""
assert a0 is not None
menu = QMenu(self) menu = QMenu(self)
add_to_playlist_action = QAction("Add to playlist", self) add_to_playlist_action = QAction("Add to playlist", self)
add_to_playlist_action.triggered.connect(self.add_selected_files_to_playlist) add_to_playlist_action.triggered.connect(self.add_selected_files_to_playlist)
@ -130,7 +135,7 @@ class MusicTable(QTableView):
menu.addAction(delete_action) menu.addAction(delete_action)
# show # show
self.set_selected_song_filepath() self.set_selected_song_filepath()
menu.exec_(event.globalPos()) menu.exec_(a0.globalPos())
def show_id3_tags_debug_menu(self): def show_id3_tags_debug_menu(self):
"""Shows ID3 tags for a specific .mp3 file""" """Shows ID3 tags for a specific .mp3 file"""
@ -151,6 +156,10 @@ class MusicTable(QTableView):
QMessageBox.Yes, QMessageBox.Yes,
) )
if reply: if reply:
try:
self.model.dataChanged.disconnect(self.on_cell_data_changed)
except Exception as e:
pass
selected_filepaths = self.get_selected_songs_filepaths() selected_filepaths = self.get_selected_songs_filepaths()
selected_indices = self.get_selected_rows() selected_indices = self.get_selected_rows()
for file in selected_filepaths: for file in selected_filepaths:
@ -159,14 +168,16 @@ class MusicTable(QTableView):
"SELECT id FROM song WHERE filepath = ?", (file,) "SELECT id FROM song WHERE filepath = ?", (file,)
)[0][0] )[0][0]
delete_song_id_from_database(song_id) delete_song_id_from_database(song_id)
self.model.dataChanged.disconnect(self.on_cell_data_changed)
for index in selected_indices: for index in selected_indices:
try: try:
self.model.removeRow(index) self.model.removeRow(index)
except Exception as e: except Exception as e:
logging.info(f" delete_songs() failed | {e}") logging.info(f" delete_songs() failed | {e}")
self.load_music_table() try:
self.model.dataChanged.connect(self.on_cell_data_changed) self.model.dataChanged.connect(self.on_cell_data_changed)
except Exception:
pass
self.load_music_table()
def open_directory(self): def open_directory(self):
"""Opens the currently selected song in the system file manager""" """Opens the currently selected song in the system file manager"""
@ -237,19 +248,44 @@ class MusicTable(QTableView):
if e is None: if e is None:
return return
data = e.mimeData() data = e.mimeData()
logging.info(f"dropEvent data: {data}")
if data and data.hasUrls(): if data and data.hasUrls():
directories = []
files = [] files = []
for url in data.urls(): for url in data.urls():
if url.isLocalFile(): if url.isLocalFile():
files.append(url.path()) path = url.toLocalFile()
if os.path.isdir(path):
# append 1 directory
directories.append(path)
else:
# append 1 file
files.append(path)
e.accept() e.accept()
worker = Worker(add_files_to_library, files) print(f"directories: {directories}")
print(f"files: {files}")
if directories:
worker = Worker(self.get_audio_files_recursively, directories)
worker.signals.signal_progress.connect(self.handle_progress) worker.signals.signal_progress.connect(self.handle_progress)
worker.signals.signal_result.connect(self.on_recursive_search_finished)
worker.signals.signal_finished.connect(self.load_music_table) worker.signals.signal_finished.connect(self.load_music_table)
self.threadpool.start(worker) if self.qapp:
threadpool = self.qapp.threadpool
threadpool.start(worker)
if files:
self.add_files(files)
else: else:
e.ignore() e.ignore()
def on_recursive_search_finished(self, result):
"""file search completion handler"""
if result:
self.add_files(result)
def handle_progress(self, data):
"""Emits data to main"""
self.handleProgressSignal.emit(data)
def keyPressEvent(self, e): def keyPressEvent(self, e):
"""Press a key. Do a thing""" """Press a key. Do a thing"""
if not e: if not e:
@ -287,6 +323,7 @@ class MusicTable(QTableView):
def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex): def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex):
"""Handles updating ID3 tags when data changes in a cell""" """Handles updating ID3 tags when data changes in a cell"""
logging.info("on_cell_data_changed") logging.info("on_cell_data_changed")
if isinstance(self.model, QStandardItemModel):
id_index = self.model.index(topLeft.row(), 0) # ID is column 0, always id_index = self.model.index(topLeft.row(), 0) # ID is column 0, always
song_id = self.model.data(id_index, Qt.UserRole) song_id = self.model.data(id_index, Qt.UserRole)
# filepath is always the last column # filepath is always the last column
@ -307,7 +344,7 @@ class MusicTable(QTableView):
"""""" """"""
worker = Worker(self.reorganize_selected_files) worker = Worker(self.reorganize_selected_files)
worker.signals.signal_progress.connect(self.qapp.handle_progress) worker.signals.signal_progress.connect(self.qapp.handle_progress)
self.threadpool.start(worker) self.qapp.threadpool.start(worker)
def reorganize_selected_files(self, progress_callback): def reorganize_selected_files(self, progress_callback):
"""Ctrl+Shift+R = Reorganize""" """Ctrl+Shift+R = Reorganize"""
@ -352,9 +389,7 @@ class MusicTable(QTableView):
f"reorganize_selected_files() | Error moving file: {filepath} | {e}" f"reorganize_selected_files() | Error moving file: {filepath} | {e}"
) )
# Draw the rest of the owl # Draw the rest of the owl
# self.model.dataChanged.disconnect(self.on_cell_data_changed)
self.load_music_table() self.load_music_table()
# self.model.dataChanged.connect(self.on_cell_data_changed)
QMessageBox.information( QMessageBox.information(
self, "Reorganization complete", "Files successfully reorganized" self, "Reorganization complete", "Files successfully reorganized"
) )
@ -365,12 +400,21 @@ class MusicTable(QTableView):
self.set_current_song_filepath() self.set_current_song_filepath()
self.playPauseSignal.emit() self.playPauseSignal.emit()
def add_files(self, files, progress_callback) -> None: def add_files(self, files) -> None:
"""When song(s) added to the library, update the tableview model """Thread handles adding songs to library
- Drag & Drop song(s) on tableView - Drag & Drop song(s) on tableView
- File > Open > List of song(s) - File > Open > List of song(s)
""" """
add_files_to_library(files, progress_callback) logging.info(f"add files, files: {files}")
worker = Worker(add_files_to_library, files)
worker.signals.signal_progress.connect(self.handle_progress)
worker.signals.signal_finished.connect(self.load_music_table)
if self.qapp:
threadpool = self.qapp.threadpool
threadpool.start(worker)
else:
logging.warning("Application window could not be found")
# add_files_to_library(files, progress_callback)
return return
def load_music_table(self, *playlist_id): def load_music_table(self, *playlist_id):
@ -384,7 +428,6 @@ class MusicTable(QTableView):
# Loading the table also causes cell data to change, technically # Loading the table also causes cell data to change, technically
# so we must disconnect the dataChanged trigger before loading # so we must disconnect the dataChanged trigger before loading
# then re-enable after we are done loading # then re-enable after we are done loading
pass
self.model.dataChanged.disconnect(self.on_cell_data_changed) self.model.dataChanged.disconnect(self.on_cell_data_changed)
except Exception as e: except Exception as e:
logging.info( logging.info(
@ -394,7 +437,6 @@ class MusicTable(QTableView):
self.vertical_scroll_position = ( self.vertical_scroll_position = (
self.verticalScrollBar().value() self.verticalScrollBar().value()
) # Get my scroll position before clearing ) # Get my scroll position before clearing
# temporarily disconnect the datachanged signal to avoid EVERY SONG getting triggered
self.model.clear() self.model.clear()
self.model.setHorizontalHeaderLabels(self.table_headers) self.model.setHorizontalHeaderLabels(self.table_headers)
if playlist_id: if playlist_id:
@ -434,10 +476,13 @@ class MusicTable(QTableView):
item.setData(id, Qt.UserRole) item.setData(id, Qt.UserRole)
self.model.layoutChanged.emit() # emits a signal that the view should be updated self.model.layoutChanged.emit() # emits a signal that the view should be updated
try: try:
# self.model.dataChanged.connect(self.on_cell_data_changed)
self.restore_scroll_position() self.restore_scroll_position()
except Exception: except Exception:
pass pass
try:
self.model.dataChanged.connect(self.on_cell_data_changed)
except Exception:
pass
def restore_scroll_position(self) -> None: def restore_scroll_position(self) -> None:
"""Restores the scroll position""" """Restores the scroll position"""
@ -447,6 +492,19 @@ class MusicTable(QTableView):
lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position), lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position),
) )
def get_audio_files_recursively(self, directories, progress_callback=None):
"""Scans a directories for files"""
extensions = self.config.get("settings", "extensions").split(",")
audio_files = []
for directory in directories:
for root, _, files in os.walk(directory):
for file in files:
if any(file.lower().endswith(ext) for ext in extensions):
audio_files.append(os.path.join(root, file))
if progress_callback:
progress_callback.emit(file)
return audio_files
def get_selected_rows(self) -> list[int]: def get_selected_rows(self) -> list[int]:
"""Returns a list of indexes for every selected row""" """Returns a list of indexes for every selected row"""
selection_model = self.selectionModel() selection_model = self.selectionModel()
@ -509,5 +567,5 @@ class MusicTable(QTableView):
) )
def load_qapp(self, qapp) -> None: def load_qapp(self, qapp) -> None:
"""Necessary for talking between components...""" """Necessary for using members and methods of main application window"""
self.qapp = qapp self.qapp = qapp

46
main.py
View File

@ -78,8 +78,8 @@ class WorkerSignals(QObject):
# (i.e. lists of filepaths) # (i.e. lists of filepaths)
signal_started = pyqtSignal() signal_started = pyqtSignal()
signal_finished = pyqtSignal()
signal_result = pyqtSignal(object) signal_result = pyqtSignal(object)
signal_finished = pyqtSignal()
signal_progress = pyqtSignal(str) signal_progress = pyqtSignal(str)
@ -117,12 +117,16 @@ class Worker(QRunnable):
""" """
self.signals.signal_started.emit() self.signals.signal_started.emit()
try: try:
self.fn(*self.args, **self.kwargs) result = self.fn(*self.args, **self.kwargs)
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
exctype, value = sys.exc_info()[:2] exctype, value = sys.exc_info()[:2]
self.signals.signal_finished.emit((exctype, value, traceback.format_exc())) self.signals.signal_finished.emit((exctype, value, traceback.format_exc()))
finally: else:
if result:
self.signals.signal_finished.emit()
self.signals.signal_result.emit(result)
else:
self.signals.signal_finished.emit() self.signals.signal_finished.emit()
@ -153,8 +157,8 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.audio_visualizer: AudioVisualizer = AudioVisualizer(self.player) self.audio_visualizer: AudioVisualizer = AudioVisualizer(self.player)
self.current_volume: int = 50 self.current_volume: int = 50
self.qapp = qapp self.qapp = qapp
self.tableView.load_qapp(self.qapp) self.tableView.load_qapp(self)
self.albumGraphicsView.load_qapp(self.qapp) self.albumGraphicsView.load_qapp(self)
self.config.read("config.ini") self.config.read("config.ini")
# Initialization # Initialization
self.timer = QTimer(self) # Audio timing things self.timer = QTimer(self) # Audio timing things
@ -209,7 +213,8 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.actionDeleteLibrary.triggered.connect(self.clear_database) self.actionDeleteLibrary.triggered.connect(self.clear_database)
self.actionDeleteDatabase.triggered.connect(self.delete_database) self.actionDeleteDatabase.triggered.connect(self.delete_database)
## tableView triggers ## CONNECTIONS
# tableView
self.tableView.doubleClicked.connect( self.tableView.doubleClicked.connect(
self.play_audio_file self.play_audio_file
) # Listens for the double click event, then plays the song ) # Listens for the double click event, then plays the song
@ -220,7 +225,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.on_play_clicked self.on_play_clicked
) # Spacebar toggle play/pause signal ) # Spacebar toggle play/pause signal
## Playlist triggers # playlistTreeView
self.playlistTreeView.playlistChoiceSignal.connect( self.playlistTreeView.playlistChoiceSignal.connect(
self.tableView.load_music_table self.tableView.load_music_table
) )
@ -233,9 +238,12 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.albumGraphicsView.albumArtDeleted.connect( self.albumGraphicsView.albumArtDeleted.connect(
self.delete_album_art_for_selected_songs self.delete_album_art_for_selected_songs
) )
# multithreading
# whatever
self.tableView.viewport().installEventFilter( self.tableView.viewport().installEventFilter(
self self
) # for drag & drop functionality ) # for drag & drop functionality
self.tableView.handleProgressSignal.connect(self.handle_progress)
# set column widths # set column widths
table_view_column_widths = str(self.config["table"]["column_widths"]).split(",") table_view_column_widths = str(self.config["table"]["column_widths"]).split(",")
for i in range(self.tableView.model.columnCount()): for i in range(self.tableView.model.columnCount()):
@ -244,6 +252,14 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.tableView.horizontalHeader().setStretchLastSection(False) self.tableView.horizontalHeader().setStretchLastSection(False)
def reload_config(self) -> None:
"""does what it says"""
self.config.read("config.ini")
def get_thread_pool(self) -> QThreadPool:
"""Returns the threadpool instance"""
return self.threadpool
def closeEvent(self, a0: QCloseEvent | None) -> None: def closeEvent(self, a0: QCloseEvent | None) -> None:
"""Save settings when closing the application""" """Save settings when closing the application"""
# MusicTable/tableView column widths # MusicTable/tableView column widths
@ -483,22 +499,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
worker = Worker(add_files_to_library, filenames) worker = Worker(add_files_to_library, filenames)
worker.signals.signal_finished.connect(self.tableView.load_music_table) worker.signals.signal_finished.connect(self.tableView.load_music_table)
worker.signals.signal_progress.connect(self.handle_progress) worker.signals.signal_progress.connect(self.handle_progress)
# try:
# worker.signals.signal_started.connect(
# lambda: self.tableView.model.dataChanged.disconnect(
# self.tableView.on_cell_data_changed
# )
# )
# except:
# pass
# try:
# worker.signals.signal_finished.connect(
# lambda: self.tableView.model.dataChanged.connect(
# self.tableView.on_cell_data_changed
# )
# )
# except:
# pass
self.threadpool.start(worker) self.threadpool.start(worker)
def handle_progress(self, data): def handle_progress(self, data):

View File

@ -8,7 +8,7 @@ config = ConfigParser()
config.read("config.ini") config.read("config.ini")
def add_files_to_library(files, progress_callback): def add_files_to_library(files, progress_callback=None):
"""Adds audio file(s) to the sqllite db """Adds audio file(s) to the sqllite db
files = list() of fully qualified paths to audio file(s) files = list() of fully qualified paths to audio file(s)
Returns a list of dictionaries of metadata Returns a list of dictionaries of metadata