sorting works now. mostly... also lyrics window small fix
This commit is contained in:
parent
c4e5105f91
commit
8420b31057
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
7
main.py
7
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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user