From 705407f3a342e091eda3a070e5cc20b7898b2e6b Mon Sep 17 00:00:00 2001 From: tsi-billypom Date: Mon, 16 Sep 2024 16:36:19 -0400 Subject: [PATCH] batch delete files from table and db --- components/MusicTable.py | 53 ++++++++++--------- main.py | 10 ++-- utils/__init__.py | 1 + utils/batch_delete_filepaths_from_database.py | 50 +++++++++++++++++ 4 files changed, 84 insertions(+), 30 deletions(-) create mode 100644 utils/batch_delete_filepaths_from_database.py diff --git a/components/MusicTable.py b/components/MusicTable.py index 95407c9..ddc22ec 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -33,6 +33,9 @@ from components.AddToPlaylistWindow import AddToPlaylistWindow from components.MetadataWindow import MetadataWindow from main import Worker +from utils.batch_delete_filepaths_from_database import ( + batch_delete_filepaths_from_database, +) from utils.delete_song_id_from_database import delete_song_id_from_database from utils.add_files_to_library import add_files_to_library from utils.get_reorganize_vars import get_reorganize_vars @@ -163,32 +166,31 @@ class MusicTable(QTableView): QMessageBox.Yes, ) if reply: - try: - self.model.dataChanged.disconnect(self.on_cell_data_changed) - except Exception: - pass selected_filepaths = self.get_selected_songs_filepaths() - selected_indices = self.get_selected_rows() - # FIXME: this should be batch delete with a worker thread - # probably pass selected_filepaths to a worker thread + worker = Worker(batch_delete_filepaths_from_database, selected_filepaths) + worker.signals.signal_progress.connect(self.qapp.handle_progress) + worker.signals.signal_finished.connect(self.remove_selected_row_indices) + worker.signals.signal_finished.connect(self.load_music_table) + if self.qapp: + threadpool = self.qapp.threadpool + threadpool.start(worker) - for file in selected_filepaths: - with DBA.DBAccess() as db: - song_id = db.query( - "SELECT id FROM song WHERE filepath = ?", (file,) - )[0][0] - delete_song_id_from_database(song_id) - # This part cannot be thread...i think? bcus its Q stuff happening - for index in selected_indices: - try: - self.model.removeRow(index) - except Exception as e: - logging.info(f" delete_songs() failed | {e}") + def remove_selected_row_indices(self): + """Removes rows from the QTableView based on a list of indices""" + selected_indices = self.get_selected_rows() + try: + self.model.dataChanged.disconnect(self.on_cell_data_changed) + except Exception: + pass + for index in selected_indices: try: - self.model.dataChanged.connect(self.on_cell_data_changed) - except Exception: - pass - self.load_music_table() + self.model.removeRow(index) + except Exception as e: + logging.info(f" delete_songs() failed | {e}") + try: + self.model.dataChanged.connect(self.on_cell_data_changed) + except Exception: + pass def open_directory(self): """Opens the currently selected song in the system file manager""" @@ -358,7 +360,7 @@ class MusicTable(QTableView): worker.signals.signal_progress.connect(self.qapp.handle_progress) self.qapp.threadpool.start(worker) - def reorganize_selected_files(self, progress_callback): + def reorganize_selected_files(self, progress_callback=None): """Ctrl+Shift+R = Reorganize""" filepaths = self.get_selected_songs_filepaths() # Confirmation screen (yes, no) @@ -376,7 +378,8 @@ class MusicTable(QTableView): if str(filepath).startswith((target_dir)): continue try: - progress_callback.emit(filepath) + if progress_callback: + progress_callback.emit(f"Organizing: {filepath}") # Read file metadata artist, album = get_reorganize_vars(filepath) # Determine the new path that needs to be made diff --git a/main.py b/main.py index 83e1a9f..9ded0f4 100644 --- a/main.py +++ b/main.py @@ -61,11 +61,11 @@ from components import ( class WorkerSignals(QObject): """ - How to use signals for a QRunnable class; unlike most cases where signals - are defined as class attributes directly in the class, here we define a - class that inherits from QObject and define the signals as class - attributes in that class. Then we can instantiate that class and use it - as a signal object. + How to use signals for a QRunnable class; + Unlike most cases where signals are defined as class attributes directly in the class, + here we define a class that inherits from QObject + and define the signals as class attributes in that class. + Then we can instantiate that class and use it as a signal object. """ # 1) diff --git a/utils/__init__.py b/utils/__init__.py index b676f56..6fc78d8 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -7,6 +7,7 @@ from .get_id3_tags import get_id3_tags from .get_reorganize_vars import get_reorganize_vars from .set_id3_tag import set_id3_tag from .delete_song_id_from_database import delete_song_id_from_database +from .batch_delete_filepaths_from_database import batch_delete_filepaths_from_database from .delete_and_create_library_database import delete_and_create_library_database from .update_song_in_database import update_song_in_database from .scan_for_music import scan_for_music diff --git a/utils/batch_delete_filepaths_from_database.py b/utils/batch_delete_filepaths_from_database.py new file mode 100644 index 0000000..46b576c --- /dev/null +++ b/utils/batch_delete_filepaths_from_database.py @@ -0,0 +1,50 @@ +import DBA +from components.ErrorDialog import ErrorDialog + + +def batch_delete_filepaths_from_database( + files: list[str], chunk_size=1000, progress_callback=None +) -> bool: + """ + Handles deleting many songs from the database by filepath + Accounts for playlists and other song-linked tables + + Returns True on success + False on failure/error + """ + # Get song IDs from filepaths + try: + with DBA.DBAccess() as db: + placeholders = ", ".join("?" for _ in files) + query = f"SELECT id FROM song WHERE filepath in ({placeholders});" + result = db.query(query, files) + song_ids = [item[0] for item in result] + except Exception as e: + dialog = ErrorDialog( + f"batch_delete_filepaths_from_database.py | An error occurred during retrieval of song_ids: {e}" + ) + dialog.exec_() + return False + try: + with DBA.DBAccess() as db: + # Batch delete in chunks + for i in range(0, len(song_ids), chunk_size): + chunk = song_ids[i : i + chunk_size] + placeholders = ", ".join("?" for _ in chunk) + + # Delete from playlists + query = f"DELETE FROM song_playlist WHERE song_id IN ({placeholders});" + db.execute(query, chunk) + + # Delete from library + query = f"DELETE FROM song WHERE id in ({placeholders});" + db.execute(query, chunk) + if progress_callback: + progress_callback.emit(f"Deleting songs: {i}") + except Exception as e: + dialog = ErrorDialog( + f"batch_delete_filepaths_from_database.py | An error occurred during batch processing: {e}" + ) + dialog.exec_() + return False + return True