sorting works now. mostly... also lyrics window small fix

This commit is contained in:
billy@pom 2026-01-11 12:00:11 -05:00
parent c4e5105f91
commit 8420b31057
5 changed files with 83 additions and 130 deletions

View File

@ -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

View File

@ -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,68 +993,38 @@ 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.sort_config
if not raw_sort:
raw_sort = self.config["table"].get("sort_order", "")
if not raw_sort:
return []
@ -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):

View File

@ -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:

View File

@ -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

View File

@ -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: