diff --git a/components/MusicTable.py b/components/MusicTable.py index b0db049..5147fb3 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -4,6 +4,7 @@ from PyQt5.QtGui import QStandardItem, QStandardItemModel, QKeySequence from PyQt5.QtWidgets import QTableView, QShortcut, QMessageBox, QAbstractItemView from PyQt5.QtCore import QModelIndex, Qt, pyqtSignal, QTimer from utils import add_files_to_library +from utils import update_song_in_library from utils import get_id3_tags from utils import get_album_art from utils import set_id3_tag @@ -50,21 +51,23 @@ class MusicTable(QTableView): def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex): """Handles updating ID3 tags when data changes in a cell""" print('on_cell_data_changed') - filepath_column_idx = self.model.columnCount() - 1 # always the last column - filepath_index = self.model.index(topLeft.row(), filepath_column_idx) # exact index of the edited cell + id_index = self.model.index(topLeft.row(), 0) # ID is column 0, always + library_id = self.model.data(id_index, Qt.UserRole) + filepath_column_idx = self.model.columnCount() - 1 # filepath is always the last column + filepath_index = self.model.index(topLeft.row(), filepath_column_idx) # exact index of the edited cell in 2d space filepath = self.model.data(filepath_index) # filepath # update the ID3 information user_input_data = topLeft.data() edited_column_name = self.database_columns[topLeft.column()] response = set_id3_tag(filepath, edited_column_name, user_input_data) if response: - update_song_in_library(filepath, edited_column_name, user_input_data) + # Update the library with new metadata + update_song_in_library(library_id, edited_column_name, user_input_data) def reorganize_selected_files(self): """Ctrl+Shift+R = Reorganize""" filepaths = self.get_selected_songs_filepaths() - print(f'yay: {filepaths}') # Confirmation screen (yes, no) reply = QMessageBox.question(self, 'Confirmation', 'Are you sure you want to reorganize these files?', QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if reply: @@ -82,16 +85,17 @@ class MusicTable(QTableView): os.makedirs(os.path.dirname(new_path), exist_ok=True) # Move the file to the new directory shutil.move(filepath, new_path) - # Update the db? + # Update the db with DBA.DBAccess() as db: db.query('UPDATE library SET filepath = ? WHERE filepath = ?', (new_path, filepath)) print(f'Moved: {filepath} -> {new_path}') except Exception as e: print(f'Error moving file: {filepath} | {e}') - + # Draw the rest of the owl + self.model.dataChanged.disconnect(self.on_cell_data_changed) self.fetch_library() + self.model.dataChanged.connect(self.on_cell_data_changed) QMessageBox.information(self, 'Reorganization complete', 'Files successfully reorganized') - # add new files to library def keyPressEvent(self, event): @@ -125,11 +129,49 @@ class MusicTable(QTableView): self.playPauseSignal.emit() + def fetch_library(self): + """Initialize the tableview model""" + self.vertical_scroll_position = self.verticalScrollBar().value() # Get my scroll position before clearing + # temporarily disconnect the datachanged signal to avoid EVERY SONG getting triggered + self.model.clear() + self.model.setHorizontalHeaderLabels(self.table_headers) + # Fetch library data + with DBA.DBAccess() as db: + data = db.query('SELECT id, title, artist, album, genre, codec, album_date, filepath FROM library;', ()) + # Populate the model + for row_data in data: + id, *rest_of_data = row_data + items = [QStandardItem(str(item)) for item in rest_of_data] + self.model.appendRow(items) + # store id using setData - useful for later faster db fetching + row = self.model.rowCount() - 1 + for item in items: + item.setData(id, Qt.UserRole) + # Update the viewport/model + self.model.layoutChanged.emit() # emits a signal that the view should be updated + + + def restore_scroll_position(self): + """Restores the scroll position""" + print(f'Returning to {self.vertical_scroll_position}') + QTimer.singleShot(100, lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position)) + + + def add_files(self, files): + """When song(s) added to the library, update the tableview model + - Drag & Drop song(s) on tableView + - File > Open > List of song(s) + """ + number_of_files_added = add_files_to_library(files) + if number_of_files_added: + self.model.dataChanged.disconnect(self.on_cell_data_changed) + self.fetch_library() + self.model.dataChanged.connect(self.on_cell_data_changed) + + def set_selected_song_filepath(self): """Sets the filepath of the currently selected song""" self.selected_song_filepath = self.currentIndex().siblingAtColumn(self.table_headers.index('path')).data() - print(f'Selected song: {self.selected_song_filepath}') - # print(get_id3_tags(self.selected_song_filepath)) def set_current_song_filepath(self): @@ -137,17 +179,12 @@ class MusicTable(QTableView): # Setting the current song filepath automatically plays that song # self.tableView listens to this function and plays the audio file located at self.current_song_filepath self.current_song_filepath = self.currentIndex().siblingAtColumn(self.table_headers.index('path')).data() - # print(f'Current song: {self.current_song_filepath}') def get_selected_rows(self): """Returns a list of indexes for every selected row""" selection_model = self.selectionModel() return [index.row() for index in selection_model.selectedRows()] - # rows = [] - # for idx in self.selectionModel().siblingAtColumn(): - # rows.append(idx.row()) - # return rows def get_selected_songs_filepaths(self): @@ -185,39 +222,6 @@ class MusicTable(QTableView): return get_album_art(self.current_song_filepath) - def fetch_library(self): - """Initialize the tableview model""" - self.vertical_scroll_position = self.verticalScrollBar().value() # Get my scroll position before clearing - self.model.clear() - self.model.setHorizontalHeaderLabels(self.table_headers) - # Fetch library data - with DBA.DBAccess() as db: - data = db.query('SELECT title, artist, album, genre, codec, album_date, filepath FROM library;', ()) - # Populate the model - for row_data in data: - items = [QStandardItem(str(item)) for item in row_data] - self.model.appendRow(items) - # Update the viewport/model - # self.viewport().update() - self.model.layoutChanged.emit() - - def restore_scroll_position(self): - """Restores the scroll position""" - print(f'Returning to {self.vertical_scroll_position}') - QTimer.singleShot(100, lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position)) - - - def add_files(self, files): - """When song(s) added to the library, update the tableview model - - Drag & Drop song(s) on tableView - - File > Open > List of song(s) - """ - print(f'tableView - adding files: {files}') - number_of_files_added = add_files_to_library(files) - if number_of_files_added: - self.fetch_library() - - def load_qapp(self, qapp): # why was this necessary again? :thinking: self.qapp = qapp diff --git a/main.py b/main.py index cd0696c..100e92d 100644 --- a/main.py +++ b/main.py @@ -6,7 +6,7 @@ from PyQt5.QtCore import QUrl, QTimer, QEvent, Qt, QModelIndex from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe from PyQt5.QtGui import QPixmap, QStandardItemModel from utils import scan_for_music -from utils import initialize_library_database +from utils import delete_and_create_library_database from components import AudioVisualizer from components import PreferencesWindow from pyqtgraph import mkBrush @@ -241,7 +241,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.tableView.fetch_library() def clear_database(self): - initialize_library_database() + delete_and_create_library_database() self.tableView.fetch_library() def process_probe(self, buff): diff --git a/utils/__init__.py b/utils/__init__.py index 822bebc..e45d430 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -2,7 +2,8 @@ from .safe_get import safe_get from .get_album_art import get_album_art from .get_id3_tags import get_id3_tags from .set_id3_tag import set_id3_tag -from .initialize_library_database import initialize_library_database +from .delete_and_create_library_database import delete_and_create_library_database +from .update_song_in_library import update_song_in_library from .scan_for_music import scan_for_music from .fft_analyser import FFTAnalyser from .add_files_to_library import add_files_to_library diff --git a/utils/add_files_to_library.py b/utils/add_files_to_library.py index 1de37f6..3a2bfda 100644 --- a/utils/add_files_to_library.py +++ b/utils/add_files_to_library.py @@ -10,10 +10,10 @@ config.read("config.ini") def add_files_to_library(files): """Adds audio file(s) to the sqllite db files = list() of fully qualified paths to audio file(s) - Returns true if any files were added + Returns a list of dictionaries of metadata """ if not files: - return False + return [] print(f"utils/add_files_to_library: {files}") extensions = config.get("settings", "extensions").split(",") insert_data = [] # To store data for batch insert @@ -21,9 +21,9 @@ def add_files_to_library(files): if any(filepath.lower().endswith(ext) for ext in extensions): filename = filepath.split("/")[-1] audio = get_id3_tags(filepath) - if "title" not in audio: # This should never run - # get_id3_tags sets the title when no tags exist - return False + # Skip if no title is found (but should never happen + if "title" not in audio: + continue # Append data tuple to insert_data list insert_data.append( ( @@ -39,7 +39,6 @@ def add_files_to_library(files): safe_get(audio, "bitrate", [])[0] if "birate" in audio else None, ) ) - # Check if batch size is reached if len(insert_data) >= 1000: with DBA.DBAccess() as db: @@ -48,7 +47,6 @@ def add_files_to_library(files): insert_data, ) insert_data = [] # Reset the insert_data list - # Insert any remaining data if insert_data: with DBA.DBAccess() as db: diff --git a/utils/create_library.sql b/utils/delete_and_create_library.sql similarity index 100% rename from utils/create_library.sql rename to utils/delete_and_create_library.sql diff --git a/utils/initialize_library_database.py b/utils/delete_and_create_library_database.py similarity index 52% rename from utils/initialize_library_database.py rename to utils/delete_and_create_library_database.py index 7981b24..27a6370 100644 --- a/utils/initialize_library_database.py +++ b/utils/delete_and_create_library_database.py @@ -1,9 +1,9 @@ import DBA -def initialize_library_database(): - with open('utils/create_library.sql', 'r') as file: +def delete_and_create_library_database(): + with open('utils/delete_and_create_library.sql', 'r') as file: lines = file.read() for statement in lines.split(';'): print(f'executing [{statement}]') with DBA.DBAccess() as db: - db.execute(statement, ()) \ No newline at end of file + db.execute(statement, ()) diff --git a/utils/update_song_in_library.py b/utils/update_song_in_library.py new file mode 100644 index 0000000..d857bb1 --- /dev/null +++ b/utils/update_song_in_library.py @@ -0,0 +1,24 @@ +import DBA +from components.ErrorDialog import ErrorDialog + + +def update_song_in_library(library_id: int, edited_column_name: str, user_input_data: str): + """Updates a field in the library database based on an ID + + Args: + library_id: the database ID of the song + edited_column_name: the name of the database column + user_input_data: the data to input + + Returns: + True or False""" + try: + with DBA.DBAccess() as db: + # yeah yeah this is bad... the column names are defined in the program by me so im ok with it because it works + db.execute(f'UPDATE library SET {edited_column_name} = ? WHERE id = ?', (user_input_data, library_id)) + except Exception as e: + dialog = ErrorDialog(f'Unable to update [{edited_column_name}] to [{user_input_data}]. ID: {library_id} | {e}') + dialog.exec_() + return False + return True +