diff --git a/components/LyricsWindow.py b/components/LyricsWindow.py index 92d6331..d6f8fd7 100644 --- a/components/LyricsWindow.py +++ b/components/LyricsWindow.py @@ -2,10 +2,8 @@ from PyQt5.QtWidgets import ( QDialog, QPlainTextEdit, QVBoxLayout, - QLabel, QPushButton, ) -from PyQt5.QtGui import QFont from components.ErrorDialog import ErrorDialog from utils import set_tag from logging import debug diff --git a/components/MusicTable.py b/components/MusicTable.py index 74e2e4c..803a5ae 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -83,6 +83,8 @@ class MusicTable(QTableView): / "config.ini" ) _ = self.config.read(self.cfg_file) + self.sort_config: str = self.config.get('table', 'sort_order') + self.column_sort_order: list[tuple[str, Qt.SortOrder | None]] = [] font: QFont = QFont() font.setPointSize(11) @@ -363,18 +365,12 @@ class MusicTable(QTableView): # | | # |____________________| - def on_sort(self): - debug('on_sort') - self.find_current_and_selected_bits() - debug('on_sort(): done finding current and selected bits') - self.jump_to_selected_song() - # self.sortSignal.emit() - # debug('sortSignal emitted') - def on_header_clicked(self, logicalIndex: QModelIndex): - debug('on_header_clicked()') - debug(f'- logicalIndex: {logicalIndex}') - self.on_sort() + """ + self.horizontal_header.sectionClicked.connect(self.on_header_clicked) + """ + self.find_current_and_selected_bits() + self.jump_to_selected_song() def on_cell_clicked(self, index): @@ -382,7 +378,6 @@ class MusicTable(QTableView): When a cell is clicked, do some stuff :) - this func also runs when double click happens, fyi """ - # print(index.row(), index.column()) self.set_selected_song_filepath() self.set_selected_song_qmodel_index() self.viewport().update() # type: ignore @@ -413,9 +408,6 @@ class MusicTable(QTableView): error('ERROR: response failed') return - def handle_progress(self, data: object): - """Emits data to main""" - self.handleProgressSignal.emit(data) def on_get_audio_files_recursively_finished(self, result: list[str]): """file search completion handler""" @@ -452,6 +444,10 @@ class MusicTable(QTableView): # |____________________| + def handle_progress(self, data: object): + """Emits data to main""" + self.handleProgressSignal.emit(data) + def show_header_context_menu(self, position): """ Show context menu on right-click in the horizontal header. @@ -465,11 +461,8 @@ class MusicTable(QTableView): # i do not care and i did not ask # TODO: right click menu for sort by asc/desc, per column - debug(f'show_header_context_menu(position={position})') logical_index = self.horizontal_header.logicalIndexAt(position) - debug(f'- logicalIndexAt(position) = {logical_index}') column_name: str = self.headers.db_list[logical_index] - debug(f'- column_name = {column_name}') menu = QMenu() clear_action = QAction("Clear all sorts", self) clear_action.triggered.connect(self.clear_all_sorts) @@ -498,16 +491,21 @@ class MusicTable(QTableView): def mark_column_sort(self, logical_index, column_name, sort_order): """ + Marks a column as sorted in a particular direction + - logical_index: called from `show_header_context_menu()` + - column_name: database column name + - sort_order: 0 = asc, 1 = desc """ - debug(f'mark_column_sort(logical_index={logical_index}, column_name={column_name}, sort_order={sort_order})') # TODO: implement this # write field name and the sort direction into config # then run sort by logical fields or something to sort the config as is - raw = self.config["table"].get(option="sort_order", fallback="") - debug(f'- self.config[table][sort_order] = {raw}') - sort_list = [] - # newest sort at the beginning bcus we sort in reverse order - sort_list.append((column_name, sort_order)) + + debug(f'mark_column_sort(logical_index={logical_index}, column_name={column_name}, sort_order={sort_order})') + raw = self.sort_config + # Clear the sort order list + self.column_sort_order = [] + # Ensure newest sort at the beginning bcus we sort in reverse order + self.column_sort_order.append((column_name, sort_order)) # Parse current sort_order from config if raw: for item in raw.split(","): @@ -515,7 +513,7 @@ class MusicTable(QTableView): continue field, dir_str = item.strip().split(":") if field == column_name: - # skip newest sort + # skip newest marked sort continue direction = int(dir_str) if dir_str == "0": @@ -524,10 +522,9 @@ class MusicTable(QTableView): direction = Qt.SortOrder.DescendingOrder else: direction = None - sort_list.append((field, direction)) - debug(f'- sort list updated (field, direction): {sort_list}') - # Save back to config - self.save_sort_config(sort_list) + self.column_sort_order.append((field, direction)) + debug(f'- sort list updated (field, direction): {self.column_sort_order}') + self.sort_config = ",".join(f'{field}:{order}' for field, order in self.column_sort_order) # Re-apply the updated sort order self.sort_by_logical_fields() @@ -536,13 +533,9 @@ class MusicTable(QTableView): """ Clears all stored sort orders and refreshes the table view. """ - debug('clear_all_sorts()') - self.config["table"]["sort_order"] = "" - with open(self.cfg_file, "w") as f: - self.config.write(f) # Clear sort visually self.horizontal_header.setSortIndicator(-1, Qt.SortOrder.AscendingOrder) - debug('clear_all_sorts() - setSortIndicator at index -1 to AscendingOrder') + def find_qmodel_index_by_value(self, model, column: int, value) -> QModelIndex: """ @@ -550,7 +543,7 @@ class MusicTable(QTableView): Returns index at (0,0) if params are invalid/out of bounds Returns empty QModelIndex object if model is invalid """ - debug(f'find_qmodel_index_by_value(model={model}, column={column}, value={value})') + # debug(f'find_qmodel_index_by_value(model={model}, column={column}, value={value})') if not model: return QModelIndex() @@ -574,13 +567,14 @@ class MusicTable(QTableView): column_ratios = [] for i in range(self.model2.columnCount()): # for i in range(self.model2.columnCount() - 1): - debug(f'column count: {self.model2.columnCount()}') + # debug(f'column count: {self.model2.columnCount()}') column_width = self.columnWidth(i) ratio = column_width / total_table_width column_ratios.append(str(round(ratio, 4))) - debug(f'get_current_header_width_ratios = {column_ratios}') + # debug(f'get_current_header_width_ratios = {column_ratios}') return column_ratios + def save_header_ratios(self): """ Saves the current header widths to memory and file, as ratios @@ -596,7 +590,8 @@ class MusicTable(QTableView): self.config.write(configfile) except Exception as e: debug(f"wtf man {e}") - debug(f"Saved column ratios: {self.saved_column_ratios}") + # debug(f"Saved column ratios: {self.saved_column_ratios}") + def load_header_widths(self, ratios: list[str] | None = None): """ @@ -621,6 +616,7 @@ class MusicTable(QTableView): def set_qmodel_index(self, index: QModelIndex): self.current_song_qmodel_index = index + def play_selected_audio_file(self): """ Sets the current song filepath @@ -630,6 +626,7 @@ class MusicTable(QTableView): self.set_current_song_filepath() self.playSignal.emit(self.current_song_filepath) + def add_files_to_library(self, files: list[str]) -> None: """ Spawns a worker thread - adds a list of filepaths to the library @@ -648,12 +645,14 @@ class MusicTable(QTableView): 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)""" # NOTE: provide extra questionbox option? @@ -952,10 +951,8 @@ class MusicTable(QTableView): # cache the data # self.data_cache[self.selected_playlist_id] = data self.populate_model(data) - # self.sort_table_by_multiple_columns() + self.sort_by_logical_fields() self.current_playlist_id = self.selected_playlist_id - # self.model2.layoutChanged.emit() # emits a signal that the view should be updated - self.proxymodel.invalidateFilter() db_name: str = self.config.get("settings", "db").split("/").pop() db_filename = self.config.get("settings", "db") self.playlistStatsSignal.emit(f"Songs: {self.model2.rowCount()} | {db_name} | {db_filename}") @@ -968,14 +965,9 @@ class MusicTable(QTableView): When data changes in the model view, its nice to re-grab the current song index information might as well get the selected song too i guess? though nothing should be selected when reloading the table data """ - debug('find_current_and_selected_bits') search_col_num = self.headers.db_list.index("filepath") - debug('find_current_and_selected_bits - searching for column number of `filepath`') - debug(f'find_current_and_selected_bits - search_col_num: {search_col_num}') selected_qmodel_index = self.find_qmodel_index_by_value(self.proxymodel, search_col_num, self.selected_song_filepath) - debug(f'find_current_and_selected_bits - selected_qmodel_index: {selected_qmodel_index}') current_qmodel_index = self.find_qmodel_index_by_value(self.proxymodel, search_col_num, self.current_song_filepath) - debug(f'find_current_and_selected_bits - current_qmodel_index: {current_qmodel_index}') # Update the 2 proxy QModelIndexes that we track self.set_selected_song_qmodel_index(selected_qmodel_index) self.set_current_song_qmodel_index(current_qmodel_index) @@ -1001,71 +993,41 @@ class MusicTable(QTableView): for item in items: item.setData(id, Qt.ItemDataRole.UserRole) self.model2.appendRow(items) - # self.proxymodel.setSourceModel(self.model2) - # self.setModel(self.proxymodel) - def sort_table_by_multiple_columns(self): - """ - Sorts the data in QTableView (self) by multiple columns - as defined in config.ini - """ - # NOTE: STOP USING THIS - debug('sort_table_by_multiple_columns') - # self.horizontal_header.sortIndicatorChanged.disconnect() - sort_orders = [] - config_sort_orders: list[int] = [ - int(x) for x in self.config["table"]["sort_orders"].split(",") - ] - debug(f'sort_table_by_multiple_columns() - config_sort_orders = {config_sort_orders}') - 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) - debug(f'sort_table_by_multiple_columns() - determined sort_orders = {sort_orders}') - # 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.on_sort() - # self.horizontal_header.sortIndicatorChanged.connect(self.on_user_sort_change) - # self.model2.layoutChanged.emit() - self.proxymodel.invalidateFilter() - def save_sort_config(self, fields: list[tuple[str, Qt.SortOrder]]): - """ - Save sort config to ini file. - fields: List of tuples like [('artist', Qt.AscendingOrder), ('album', Qt.DescendingOrder)] - """ - debug('save_sort_config()') - # raw = ",".join(f"{field}:{1 if order == Qt.SortOrder.AscendingOrder else 2}" for field, order in fields) - raw = ",".join(f"{field}:{order}" for field, order in fields) - debug(f'- saving config [table][sort_order] = {raw}') - self.config["table"]["sort_order"] = raw - debug(f'- self.cfg_file = {self.cfg_file}') - with open(self.cfg_file, "w") as f: - debug('- writing to config file') - self.config.write(f) - debug(f'- self.config["table"]["sort_order"] = {self.config["table"]["sort_order"]}') + def get_sort_config_by_name(self) -> list[tuple[str, Qt.SortOrder]]: + debug('get_sort_config_by_name()') + sort_config = [] + raw_sort = self.sort_config + if not raw_sort: + raw_sort = self.config["table"].get("sort_order", "") + if not raw_sort: + return [] - def get_sort_config(self) -> list[tuple[int, Qt.SortOrder]]: + try: + sort_fields = [x.strip() for x in raw_sort.split(",")] + for entry in sort_fields: + if ":" not in entry: + continue + db_field, order_str = entry.split(":") + db_field = db_field.strip() + order = int(order_str.strip()) + sort_config.append((db_field, order)) + except Exception as e: + error(f"- Failed to parse sort config: {e}") + return sort_config + + def get_sort_config_by_index(self) -> list[tuple[int, Qt.SortOrder]]: """ Returns a list of (column_index, Qt.SortOrder) tuples based on config and headers """ - debug('get_sort_config()') + debug('get_sort_config_by_index()') sort_config = [] - raw_sort = self.config["table"].get("sort_order", "") + raw_sort = self.sort_config if not raw_sort: - return [] + raw_sort = self.config["table"].get("sort_order", "") + if not raw_sort: + return [] try: sort_fields = [x.strip() for x in raw_sort.split(",")] @@ -1090,8 +1052,7 @@ class MusicTable(QTableView): Sorts the table using logical field names defined in config. """ debug('sort_by_logical_fields()') - sort_config = self.get_sort_config() - debug(f'- retrieved sort_config: {sort_config}') + sort_config = self.get_sort_config_by_index() # Sort in reverse order (primary sort last) for col_index, order in reversed(sort_config): if order == 0: @@ -1102,11 +1063,8 @@ class MusicTable(QTableView): continue self.sortByColumn(col_index, new_order) debug(f'- sorted column index {col_index} by {new_order}') - # self.model2.layoutChanged.emit() - # debug('- emitted layoutChanged signal') self.proxymodel.invalidateFilter() - debug('- proxymodel invalidateFilter()') - # self.on_sort() + def get_audio_files_recursively(self, directories: list[str], progress_callback=None) -> list[str]: """Scans a directories for files""" @@ -1200,14 +1158,10 @@ class MusicTable(QTableView): converts to model2 index stores it """ - debug('set_current_song_qmodel_index') - debug(f'got index: {index}') if index is None: index = self.currentIndex() - debug(f'new index: {index}') # map proxy (sortable) model to the original model (used for interactions) real_index: QModelIndex = self.proxymodel.mapToSource(index) - debug(f'map index: {real_index}') self.current_song_qmodel_index = real_index def set_selected_song_qmodel_index(self, index: QModelIndex | None = None): @@ -1216,14 +1170,10 @@ class MusicTable(QTableView): converts to model2 index stores it """ - debug('set_selected_song_qmodel_index') - debug(f'got index: {index}') if index is None: index = self.currentIndex() - debug(f'new index: {index}') # map proxy (sortable) model to the original model (used for interactions) real_index: QModelIndex = self.proxymodel.mapToSource(index) - debug(f'map index: {real_index}') self.selected_song_qmodel_index = real_index def set_search_string(self, text: str): diff --git a/main.py b/main.py index 95baf22..5e00111 100644 --- a/main.py +++ b/main.py @@ -169,7 +169,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # QUICK ACTIONS MENU self.actionScanLibraries.triggered.connect(self.scan_libraries) self.actionDeleteDatabase.triggered.connect(self.delete_database) - self.actionSortColumns.triggered.connect(self.tableView.sort_table_by_multiple_columns) + self.actionSortColumns.triggered.connect(self.nothing) # QTableView # for drag & drop functionality @@ -197,6 +197,10 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.albumGraphicsView.albumArtDropped.connect(self.set_album_art_for_selected_songs) self.albumGraphicsView.albumArtDeleted.connect(self.delete_album_art_for_current_song) + + def nothing(self): + pass + # _________________ # | | # | | @@ -214,6 +218,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.config["settings"]["volume"] = str(self.current_volume) self.config["settings"]["window_size"] = (str(self.width()) + "," + str(self.height())) self.config['table']['column_ratios'] = ",".join(self.tableView.get_current_header_width_ratios()) + self.config["table"]["sort_order"] = self.tableView.sort_config # Save the config try: with open(self.cfg_file, "w") as configfile: diff --git a/sample_config.ini b/sample_config.ini index d3a8bc7..456fba0 100644 --- a/sample_config.ini +++ b/sample_config.ini @@ -13,5 +13,5 @@ window_size=1152,894 # Music table user options columns = title,artist,album,track_number,genre,album_date,codec,length_seconds,filepath column_ratios = 0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01 -# 0 = no sort, 1 = ascending, 2 = descending -sort_orders = 0,1,1,1,0,0,0,0,0 +# 0 = ascending, 1 = descending +sort_order = artist:0,track_number:0,album:0 diff --git a/utils/set_tag.py b/utils/set_tag.py index ac92658..ebfe230 100644 --- a/utils/set_tag.py +++ b/utils/set_tag.py @@ -5,20 +5,20 @@ from mutagen.id3 import ID3 from mutagen.id3._util import ID3NoHeaderError from mutagen.id3._frames import USLT, Frame -def set_tag(filepath: str, db_column: str, value: str): +def set_tag(filepath: str, tag_name: str, value: str): """ - Sets the ID3 tag for a file given a filepath, db_column, and a value for the tag + Sets the ID3 tag for a file given a filepath, tag_name, and a value for the tag Args: filepath: path to the mp3 file - db_column: db column name of the ID3 tag + tag_name: db column name of the ID3 tag value: value to set for the tag Returns: True / False """ headers = HeaderTags2() - debug(f"filepath: {filepath} | db_column: {db_column} | value: {value}") + debug(f"filepath: {filepath} | tag_name: {tag_name} | value: {value}") try: try: # Load existing tags @@ -26,7 +26,7 @@ def set_tag(filepath: str, db_column: str, value: str): except ID3NoHeaderError: # Create new tags if none exist audio_file = ID3() # Lyrics get handled differently - if db_column == "lyrics": + if tag_name == "lyrics": try: audio = ID3(filepath) except Exception as e: @@ -38,14 +38,14 @@ def set_tag(filepath: str, db_column: str, value: str): audio.save() return True # DB Tag into Mutagen Frame Class - if db_column in headers.db: - frame_class = headers.db[db_column].frame_class + if tag_name in headers.db: + frame_class = headers.db[tag_name].frame_class assert frame_class is not None # ooo scary if issubclass(frame_class, Frame): frame = frame_class(encoding=3, text=[value]) audio_file.add(frame) else: - warning(f'Tag "{db_column}" not found - ID3 tag update skipped') + warning(f'Tag "{tag_name}" not found - ID3 tag update skipped') audio_file.save(filepath) return True except Exception as e: