diff --git a/components/MusicTable.py b/components/MusicTable.py index 8c83302..fb68467 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -41,7 +41,7 @@ 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.add_files_to_database import add_files_to_database from utils.get_reorganize_vars import get_reorganize_vars from utils.update_song_in_database import update_song_in_database from utils.get_id3_tags import get_id3_tags @@ -190,19 +190,17 @@ class MusicTable(QTableView): super().paintEvent(e) # Check if we have a current cell - if self.current_index and self.current_index.isValid(): + current_index = self.currentIndex() + if current_index and current_index.isValid(): # Get the visual rect for the current cell - rect = self.visualRect(self.current_index) + rect = self.visualRect(current_index) # Create a painter for custom drawing - painter = QPainter(self.viewport()) - - # Draw a border around the current cell - pen = QPen(QColor("#4a90e2"), 2) # Blue, 2px width - - painter.setPen(pen) - painter.drawRect(rect.adjusted(1, 1, -1, -1)) - painter.end() + with QPainter(self.viewport()) as painter: + # Draw a border around the current cell + pen = QPen(QColor("#4a90e2"), 2) # Blue, 2px width + painter.setPen(pen) + painter.drawRect(rect.adjusted(1, 1, -1, -1)) def contextMenuEvent(self, a0): """Right-click context menu for rows in Music Table""" @@ -283,7 +281,7 @@ class MusicTable(QTableView): threadpool = self.qapp.threadpool threadpool.start(worker) if files: - self.add_files(files) + self.add_files_to_library(files) else: e.ignore() @@ -293,44 +291,48 @@ class MusicTable(QTableView): return key = e.key() - if key == Qt.Key.Key_Space: # Spacebar to play/pause + if key == Qt.Key.Key_Space: self.toggle_play_pause() elif key == Qt.Key.Key_Right: - current_index = self.currentIndex() - new_index = self.model2.index( - current_index.row(), current_index.column() + 1 - ) + index = self.currentIndex() + new_index = self.model2.index(index.row(), index.column() + 1) if new_index.isValid(): + print(f"right -> ({new_index.row()},{new_index.column()})") self.setCurrentIndex(new_index) + self.viewport().update() # type: ignore super().keyPressEvent(e) + return elif key == Qt.Key.Key_Left: - current_index = self.currentIndex() - new_index = self.model2.index( - current_index.row(), current_index.column() - 1 - ) + index = self.currentIndex() + new_index = self.model2.index(index.row(), index.column() - 1) if new_index.isValid(): + print(f"left -> ({new_index.row()},{new_index.column()})") self.setCurrentIndex(new_index) + self.viewport().update() # type: ignore super().keyPressEvent(e) + return elif key == Qt.Key.Key_Up: - current_index = self.currentIndex() - new_index = self.model2.index( - current_index.row() - 1, current_index.column() - ) + index = self.currentIndex() + new_index = self.model2.index(index.row() - 1, index.column()) if new_index.isValid(): + print(f"up -> ({new_index.row()},{new_index.column()})") self.setCurrentIndex(new_index) + self.viewport().update() # type: ignore super().keyPressEvent(e) + return elif key == Qt.Key.Key_Down: - current_index = self.currentIndex() - new_index = self.model2.index( - current_index.row() + 1, current_index.column() - ) + index = self.currentIndex() + new_index = self.model2.index(index.row() + 1, index.column()) if new_index.isValid(): + print(f"down -> ({new_index.row()},{new_index.column()})") self.setCurrentIndex(new_index) + self.viewport().update() # type: ignore super().keyPressEvent(e) + return elif key in (Qt.Key.Key_Return, Qt.Key.Key_Enter): if self.state() != QAbstractItemView.EditingState: @@ -351,10 +353,11 @@ class MusicTable(QTableView): """ When a cell is clicked, do some stuff :) """ - if index == self.current_index: - print("nope") + print(f"click - ({index.row()}, {index.column()})") + current_index = self.currentIndex() + if index == current_index: return - self.current_index = index + self.setCurrentIndex(index) self.set_selected_song_filepath() self.viewport().update() # type: ignore @@ -408,66 +411,19 @@ class MusicTable(QTableView): def on_recursive_search_finished(self, result): """file search completion handler""" if result: - self.add_files(result) + self.add_files_to_library(result) + + def handle_progress(self, data): + """Emits data to main""" + self.handleProgressSignal.emit(data) # ____________________ # | | # | | - # | Verbs | + # | Connection Mgmt | # | | # |____________________| - def load_header_widths(self): - """ - Loads the header widths from the last application close. - """ - table_view_column_widths = str(self.config["table"]["column_widths"]).split(",") - if not isinstance(table_view_column_widths[0], int): - return - if not isinstance(table_view_column_widths, list): - for i in range(self.model2.columnCount() - 1): - self.setColumnWidth(i, int(table_view_column_widths[i])) - - def sort_table_by_multiple_columns(self): - """ - Sorts the data in QTableView (self) by multiple columns - as defined in config.ini - """ - # TODO: Rewrite this function to use self.load_music_table() with dynamic SQL queries - # in order to sort the data more effectively & have more control over UI refreshes. - - # Disconnect these signals to prevent unnecessary loads - debug("sort_table_by_multiple_columns()") - self.disconnect_data_changed() - self.disconnect_layout_changed() - sort_orders = [] - config_sort_orders: list[int] = [ - int(x) for x in self.config["table"]["sort_orders"].split(",") - ] - for order in config_sort_orders: - if order == 0: - sort_orders.append(None) - elif order == 1: - sort_orders.append(Qt.SortOrder.AscendingOrder) - elif order == 2: - sort_orders.append(Qt.SortOrder.DescendingOrder) - - # QTableView sorts need to happen in reverse order - # The primary sort column is the last column sorted. - for i in reversed(range(len(sort_orders))): - if sort_orders[i] is not None: - debug(f"sorting column {i} by {sort_orders[i]}") - self.sortByColumn(i, sort_orders[i]) - # WARNING: - # sortByColumn calls a SELECT statement, - # and will do this for as many sorts that are needed - # maybe not a huge deal for a small music application...? - # `len(config_sort_orders)` number of SELECTs - - self.connect_data_changed() - self.connect_layout_changed() - # self.model2.layoutChanged.emit() - def disconnect_data_changed(self): """Disconnects the dataChanged signal from QTableView.model""" try: @@ -496,14 +452,32 @@ class MusicTable(QTableView): except Exception: pass - def show_id3_tags_debug_menu(self): - """Shows ID3 tags for a specific .mp3 file""" - selected_song_filepath = self.get_selected_song_filepath() - if selected_song_filepath is None: - return - current_song = self.get_selected_song_metadata() - lyrics_window = DebugWindow(selected_song_filepath, str(current_song)) - lyrics_window.exec_() + # ____________________ + # | | + # | | + # | Verbs | + # | | + # |____________________| + + def add_files_to_library(self, files: list[str]) -> None: + """ + Spawns a worker thread - adds a list of filepaths to the library + - Drag & Drop song(s) on tableView + - File > Open > List of song(s) + """ + worker = Worker(add_files_to_database, files) + worker.signals.signal_progress.connect(self.qapp.handle_progress) + worker.signals.signal_finished.connect(self.load_music_table) + if self.qapp: + threadpool = self.qapp.threadpool + threadpool.start(worker) + else: + error("Application window could not be found") + + def add_selected_files_to_playlist(self): + """Opens a playlist choice menu and adds the currently selected files to the chosen playlist""" + playlist_choice_window = AddToPlaylistWindow(self.get_selected_songs_db_ids()) + playlist_choice_window.exec_() def delete_songs(self): """Asks to delete the currently selected songs from the db and music table (not the filesystem)""" @@ -518,13 +492,13 @@ class MusicTable(QTableView): selected_filepaths = self.get_selected_songs_filepaths() 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.delete_selected_row_indices) worker.signals.signal_finished.connect(self.load_music_table) if self.qapp: threadpool = self.qapp.threadpool threadpool.start(worker) - def remove_selected_row_indices(self): + def delete_selected_row_indices(self): """Removes rows from the QTableView based on a list of indices""" selected_indices = self.get_selected_rows() self.disconnect_data_changed() @@ -535,6 +509,14 @@ class MusicTable(QTableView): debug(f" delete_songs() failed | {e}") self.connect_data_changed() + def edit_selected_files_metadata(self): + """Opens a form with metadata from the selected audio files""" + files = self.get_selected_songs_filepaths() + song_ids = self.get_selected_songs_db_ids() + window = MetadataWindow(self.refreshMusicTable, files, song_ids) + window.refreshMusicTableSignal.connect(self.load_music_table) + window.exec_() # Display the preferences window modally + def open_directory(self): """Opens the currently selected song in the system file manager""" if self.get_selected_song_filepath() is None: @@ -551,18 +533,14 @@ class MusicTable(QTableView): path = "/".join(filepath) Popen(["xdg-open", path]) - def edit_selected_files_metadata(self): - """Opens a form with metadata from the selected audio files""" - files = self.get_selected_songs_filepaths() - song_ids = self.get_selected_songs_db_ids() - window = MetadataWindow(self.refreshMusicTable, files, song_ids) - window.refreshMusicTableSignal.connect(self.load_music_table) - window.exec_() # Display the preferences window modally - - def add_selected_files_to_playlist(self): - """Opens a playlist choice menu and adds the currently selected files to the chosen playlist""" - playlist_choice_window = AddToPlaylistWindow(self.get_selected_songs_db_ids()) - playlist_choice_window.exec_() + def show_id3_tags_debug_menu(self): + """Shows ID3 tags for a specific .mp3 file""" + selected_song_filepath = self.get_selected_song_filepath() + if selected_song_filepath is None: + return + current_song = self.get_selected_song_metadata() + lyrics_window = DebugWindow(selected_song_filepath, str(current_song)) + lyrics_window.exec_() def show_lyrics_menu(self): """Shows the lyrics for the currently selected song""" @@ -590,10 +568,6 @@ class MusicTable(QTableView): shortcut = QShortcut(QKeySequence("Delete"), self) shortcut.activated.connect(self.delete_songs) - def handle_progress(self, data): - """Emits data to main""" - self.handleProgressSignal.emit(data) - def confirm_reorganize_files(self) -> None: """ Ctrl+Shift+R = Reorganize @@ -665,21 +639,6 @@ class MusicTable(QTableView): self.set_current_song_filepath() self.playPauseSignal.emit() - def add_files(self, files: list[str]) -> None: - """ - Spawns a worker thread - adds a list of filepaths to the library - - Drag & Drop song(s) on tableView - - File > Open > List of song(s) - """ - worker = Worker(add_files_to_library, files) - worker.signals.signal_progress.connect(self.qapp.handle_progress) - worker.signals.signal_finished.connect(self.load_music_table) - if self.qapp: - threadpool = self.qapp.threadpool - threadpool.start(worker) - else: - error("Application window could not be found") - def load_music_table(self, *playlist_id): """ Loads data into self (QTableView) @@ -741,6 +700,57 @@ class MusicTable(QTableView): self.connect_data_changed() self.connect_layout_changed() + def load_header_widths(self): + """ + Loads the header widths from the last application close. + """ + table_view_column_widths = str(self.config["table"]["column_widths"]).split(",") + if not isinstance(table_view_column_widths[0], int): + return + if not isinstance(table_view_column_widths, list): + for i in range(self.model2.columnCount() - 1): + self.setColumnWidth(i, int(table_view_column_widths[i])) + + def sort_table_by_multiple_columns(self): + """ + Sorts the data in QTableView (self) by multiple columns + as defined in config.ini + """ + # TODO: Rewrite this function to use self.load_music_table() with dynamic SQL queries + # in order to sort the data more effectively & have more control over UI refreshes. + + # Disconnect these signals to prevent unnecessary loads + debug("sort_table_by_multiple_columns()") + self.disconnect_data_changed() + self.disconnect_layout_changed() + sort_orders = [] + config_sort_orders: list[int] = [ + int(x) for x in self.config["table"]["sort_orders"].split(",") + ] + for order in config_sort_orders: + if order == 0: + sort_orders.append(None) + elif order == 1: + sort_orders.append(Qt.SortOrder.AscendingOrder) + elif order == 2: + sort_orders.append(Qt.SortOrder.DescendingOrder) + + # QTableView sorts need to happen in reverse order + # The primary sort column is the last column sorted. + for i in reversed(range(len(sort_orders))): + if sort_orders[i] is not None: + debug(f"sorting column {i} by {sort_orders[i]}") + self.sortByColumn(i, sort_orders[i]) + # WARNING: + # sortByColumn calls a SELECT statement, + # and will do this for as many sorts that are needed + # maybe not a huge deal for a small music application...? + # `len(config_sort_orders)` number of SELECTs + + self.connect_data_changed() + self.connect_layout_changed() + # self.model2.layoutChanged.emit() + def restore_scroll_position(self) -> None: """Restores the scroll position""" debug("restore_scroll_position") diff --git a/main.py b/main.py index 1123bb3..d1609ac 100644 --- a/main.py +++ b/main.py @@ -11,7 +11,6 @@ from mutagen.id3 import ID3 from mutagen.id3._frames import APIC from configparser import ConfigParser from pathlib import Path -from numpy import where as npwhere from appdirs import user_config_dir from logging import debug, error, warning, basicConfig, INFO, DEBUG from ui import Ui_MainWindow @@ -40,7 +39,7 @@ from PyQt5.QtGui import QClipboard, QCloseEvent, QPixmap, QResizeEvent from utils import ( scan_for_music, initialize_db, - add_files_to_library, + add_files_to_database, ) from components import ( PreferencesWindow, @@ -545,7 +544,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): open_files_window.exec_() filenames = open_files_window.selectedFiles() # Adds files to the library in a new thread - worker = Worker(add_files_to_library, filenames) + worker = Worker(add_files_to_database, filenames) worker.signals.signal_finished.connect(self.tableView.load_music_table) worker.signals.signal_progress.connect(self.handle_progress) self.threadpool.start(worker) diff --git a/utils/__init__.py b/utils/__init__.py index 6fc78d8..0b7f08e 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -11,5 +11,5 @@ from .batch_delete_filepaths_from_database import batch_delete_filepaths_from_da 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 -from .add_files_to_library import add_files_to_library +from .add_files_to_database import add_files_to_database from .convert_date_str_to_tyer_tdat_id3_tag import convert_date_str_to_tyer_tdat_id3_tag diff --git a/utils/add_files_to_library.py b/utils/add_files_to_database.py similarity index 97% rename from utils/add_files_to_library.py rename to utils/add_files_to_database.py index 9abd954..9b56fdb 100644 --- a/utils/add_files_to_library.py +++ b/utils/add_files_to_database.py @@ -8,9 +8,9 @@ from appdirs import user_config_dir import platform -def add_files_to_library(files, progress_callback=None): +def add_files_to_database(files, progress_callback=None): """ - Adds audio file(s) to the sqllite db + Adds audio file(s) to the sqllite db "song" table Args: files: list() of fully qualified paths to audio file(s) progress_callback: emit data for user feedback diff --git a/utils/scan_for_music.py b/utils/scan_for_music.py index 70fd0b5..15c04ee 100644 --- a/utils/scan_for_music.py +++ b/utils/scan_for_music.py @@ -1,12 +1,11 @@ import os from PyQt5.QtCore import pyqtSignal -from utils.add_files_to_library import add_files_to_library +from utils.add_files_to_database import add_files_to_database from configparser import ConfigParser from pathlib import Path from appdirs import user_config_dir - def scan_for_music(): config = ConfigParser() cfg_file = ( @@ -22,4 +21,4 @@ def scan_for_music(): filename = os.path.join(dirpath, file) if any(filename.lower().endswith(ext) for ext in extensions): files_to_add.append(filename) - add_files_to_library(files_to_add) + add_files_to_database(files_to_add)