MusicTable logical organization, selected cell highlighting, add_files_to_library add_files_to_database rename for clarity

This commit is contained in:
tsi-billypom 2025-04-01 14:33:39 -04:00
parent 29182d5ca6
commit 257e09c241
5 changed files with 143 additions and 135 deletions

View File

@ -41,7 +41,7 @@ from utils.batch_delete_filepaths_from_database import (
batch_delete_filepaths_from_database, batch_delete_filepaths_from_database,
) )
from utils.delete_song_id_from_database import delete_song_id_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.get_reorganize_vars import get_reorganize_vars
from utils.update_song_in_database import update_song_in_database from utils.update_song_in_database import update_song_in_database
from utils.get_id3_tags import get_id3_tags from utils.get_id3_tags import get_id3_tags
@ -190,19 +190,17 @@ class MusicTable(QTableView):
super().paintEvent(e) super().paintEvent(e)
# Check if we have a current cell # 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 # 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 # Create a painter for custom drawing
painter = QPainter(self.viewport()) with QPainter(self.viewport()) as painter:
# Draw a border around the current cell # Draw a border around the current cell
pen = QPen(QColor("#4a90e2"), 2) # Blue, 2px width pen = QPen(QColor("#4a90e2"), 2) # Blue, 2px width
painter.setPen(pen) painter.setPen(pen)
painter.drawRect(rect.adjusted(1, 1, -1, -1)) painter.drawRect(rect.adjusted(1, 1, -1, -1))
painter.end()
def contextMenuEvent(self, a0): def contextMenuEvent(self, a0):
"""Right-click context menu for rows in Music Table""" """Right-click context menu for rows in Music Table"""
@ -283,7 +281,7 @@ class MusicTable(QTableView):
threadpool = self.qapp.threadpool threadpool = self.qapp.threadpool
threadpool.start(worker) threadpool.start(worker)
if files: if files:
self.add_files(files) self.add_files_to_library(files)
else: else:
e.ignore() e.ignore()
@ -293,44 +291,48 @@ class MusicTable(QTableView):
return return
key = e.key() key = e.key()
if key == Qt.Key.Key_Space: # Spacebar to play/pause if key == Qt.Key.Key_Space:
self.toggle_play_pause() self.toggle_play_pause()
elif key == Qt.Key.Key_Right: elif key == Qt.Key.Key_Right:
current_index = self.currentIndex() index = self.currentIndex()
new_index = self.model2.index( new_index = self.model2.index(index.row(), index.column() + 1)
current_index.row(), current_index.column() + 1
)
if new_index.isValid(): if new_index.isValid():
print(f"right -> ({new_index.row()},{new_index.column()})")
self.setCurrentIndex(new_index) self.setCurrentIndex(new_index)
self.viewport().update() # type: ignore
super().keyPressEvent(e) super().keyPressEvent(e)
return
elif key == Qt.Key.Key_Left: elif key == Qt.Key.Key_Left:
current_index = self.currentIndex() index = self.currentIndex()
new_index = self.model2.index( new_index = self.model2.index(index.row(), index.column() - 1)
current_index.row(), current_index.column() - 1
)
if new_index.isValid(): if new_index.isValid():
print(f"left -> ({new_index.row()},{new_index.column()})")
self.setCurrentIndex(new_index) self.setCurrentIndex(new_index)
self.viewport().update() # type: ignore
super().keyPressEvent(e) super().keyPressEvent(e)
return
elif key == Qt.Key.Key_Up: elif key == Qt.Key.Key_Up:
current_index = self.currentIndex() index = self.currentIndex()
new_index = self.model2.index( new_index = self.model2.index(index.row() - 1, index.column())
current_index.row() - 1, current_index.column()
)
if new_index.isValid(): if new_index.isValid():
print(f"up -> ({new_index.row()},{new_index.column()})")
self.setCurrentIndex(new_index) self.setCurrentIndex(new_index)
self.viewport().update() # type: ignore
super().keyPressEvent(e) super().keyPressEvent(e)
return
elif key == Qt.Key.Key_Down: elif key == Qt.Key.Key_Down:
current_index = self.currentIndex() index = self.currentIndex()
new_index = self.model2.index( new_index = self.model2.index(index.row() + 1, index.column())
current_index.row() + 1, current_index.column()
)
if new_index.isValid(): if new_index.isValid():
print(f"down -> ({new_index.row()},{new_index.column()})")
self.setCurrentIndex(new_index) self.setCurrentIndex(new_index)
self.viewport().update() # type: ignore
super().keyPressEvent(e) super().keyPressEvent(e)
return
elif key in (Qt.Key.Key_Return, Qt.Key.Key_Enter): elif key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
if self.state() != QAbstractItemView.EditingState: if self.state() != QAbstractItemView.EditingState:
@ -351,10 +353,11 @@ class MusicTable(QTableView):
""" """
When a cell is clicked, do some stuff :) When a cell is clicked, do some stuff :)
""" """
if index == self.current_index: print(f"click - ({index.row()}, {index.column()})")
print("nope") current_index = self.currentIndex()
if index == current_index:
return return
self.current_index = index self.setCurrentIndex(index)
self.set_selected_song_filepath() self.set_selected_song_filepath()
self.viewport().update() # type: ignore self.viewport().update() # type: ignore
@ -408,66 +411,19 @@ class MusicTable(QTableView):
def on_recursive_search_finished(self, result): def on_recursive_search_finished(self, result):
"""file search completion handler""" """file search completion handler"""
if result: 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): def disconnect_data_changed(self):
"""Disconnects the dataChanged signal from QTableView.model""" """Disconnects the dataChanged signal from QTableView.model"""
try: try:
@ -496,14 +452,32 @@ class MusicTable(QTableView):
except Exception: except Exception:
pass 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: # | Verbs |
return # | |
current_song = self.get_selected_song_metadata() # |____________________|
lyrics_window = DebugWindow(selected_song_filepath, str(current_song))
lyrics_window.exec_() 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): def delete_songs(self):
"""Asks to delete the currently selected songs from the db and music table (not the filesystem)""" """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() selected_filepaths = self.get_selected_songs_filepaths()
worker = Worker(batch_delete_filepaths_from_database, selected_filepaths) worker = Worker(batch_delete_filepaths_from_database, selected_filepaths)
worker.signals.signal_progress.connect(self.qapp.handle_progress) 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) worker.signals.signal_finished.connect(self.load_music_table)
if self.qapp: if self.qapp:
threadpool = self.qapp.threadpool threadpool = self.qapp.threadpool
threadpool.start(worker) 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""" """Removes rows from the QTableView based on a list of indices"""
selected_indices = self.get_selected_rows() selected_indices = self.get_selected_rows()
self.disconnect_data_changed() self.disconnect_data_changed()
@ -535,6 +509,14 @@ class MusicTable(QTableView):
debug(f" delete_songs() failed | {e}") debug(f" delete_songs() failed | {e}")
self.connect_data_changed() 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): def open_directory(self):
"""Opens the currently selected song in the system file manager""" """Opens the currently selected song in the system file manager"""
if self.get_selected_song_filepath() is None: if self.get_selected_song_filepath() is None:
@ -551,18 +533,14 @@ class MusicTable(QTableView):
path = "/".join(filepath) path = "/".join(filepath)
Popen(["xdg-open", path]) Popen(["xdg-open", path])
def edit_selected_files_metadata(self): def show_id3_tags_debug_menu(self):
"""Opens a form with metadata from the selected audio files""" """Shows ID3 tags for a specific .mp3 file"""
files = self.get_selected_songs_filepaths() selected_song_filepath = self.get_selected_song_filepath()
song_ids = self.get_selected_songs_db_ids() if selected_song_filepath is None:
window = MetadataWindow(self.refreshMusicTable, files, song_ids) return
window.refreshMusicTableSignal.connect(self.load_music_table) current_song = self.get_selected_song_metadata()
window.exec_() # Display the preferences window modally lyrics_window = DebugWindow(selected_song_filepath, str(current_song))
lyrics_window.exec_()
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_lyrics_menu(self): def show_lyrics_menu(self):
"""Shows the lyrics for the currently selected song""" """Shows the lyrics for the currently selected song"""
@ -590,10 +568,6 @@ class MusicTable(QTableView):
shortcut = QShortcut(QKeySequence("Delete"), self) shortcut = QShortcut(QKeySequence("Delete"), self)
shortcut.activated.connect(self.delete_songs) 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: def confirm_reorganize_files(self) -> None:
""" """
Ctrl+Shift+R = Reorganize Ctrl+Shift+R = Reorganize
@ -665,21 +639,6 @@ class MusicTable(QTableView):
self.set_current_song_filepath() self.set_current_song_filepath()
self.playPauseSignal.emit() 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): def load_music_table(self, *playlist_id):
""" """
Loads data into self (QTableView) Loads data into self (QTableView)
@ -741,6 +700,57 @@ class MusicTable(QTableView):
self.connect_data_changed() self.connect_data_changed()
self.connect_layout_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: def restore_scroll_position(self) -> None:
"""Restores the scroll position""" """Restores the scroll position"""
debug("restore_scroll_position") debug("restore_scroll_position")

View File

@ -11,7 +11,6 @@ from mutagen.id3 import ID3
from mutagen.id3._frames import APIC from mutagen.id3._frames import APIC
from configparser import ConfigParser from configparser import ConfigParser
from pathlib import Path from pathlib import Path
from numpy import where as npwhere
from appdirs import user_config_dir from appdirs import user_config_dir
from logging import debug, error, warning, basicConfig, INFO, DEBUG from logging import debug, error, warning, basicConfig, INFO, DEBUG
from ui import Ui_MainWindow from ui import Ui_MainWindow
@ -40,7 +39,7 @@ from PyQt5.QtGui import QClipboard, QCloseEvent, QPixmap, QResizeEvent
from utils import ( from utils import (
scan_for_music, scan_for_music,
initialize_db, initialize_db,
add_files_to_library, add_files_to_database,
) )
from components import ( from components import (
PreferencesWindow, PreferencesWindow,
@ -545,7 +544,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow):
open_files_window.exec_() open_files_window.exec_()
filenames = open_files_window.selectedFiles() filenames = open_files_window.selectedFiles()
# Adds files to the library in a new thread # 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_finished.connect(self.tableView.load_music_table)
worker.signals.signal_progress.connect(self.handle_progress) worker.signals.signal_progress.connect(self.handle_progress)
self.threadpool.start(worker) self.threadpool.start(worker)

View File

@ -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 .delete_and_create_library_database import delete_and_create_library_database
from .update_song_in_database import update_song_in_database from .update_song_in_database import update_song_in_database
from .scan_for_music import scan_for_music 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 from .convert_date_str_to_tyer_tdat_id3_tag import convert_date_str_to_tyer_tdat_id3_tag

View File

@ -8,9 +8,9 @@ from appdirs import user_config_dir
import platform 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: Args:
files: list() of fully qualified paths to audio file(s) files: list() of fully qualified paths to audio file(s)
progress_callback: emit data for user feedback progress_callback: emit data for user feedback

View File

@ -1,12 +1,11 @@
import os import os
from PyQt5.QtCore import pyqtSignal 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 configparser import ConfigParser
from pathlib import Path from pathlib import Path
from appdirs import user_config_dir from appdirs import user_config_dir
def scan_for_music(): def scan_for_music():
config = ConfigParser() config = ConfigParser()
cfg_file = ( cfg_file = (
@ -22,4 +21,4 @@ def scan_for_music():
filename = os.path.join(dirpath, file) filename = os.path.join(dirpath, file)
if any(filename.lower().endswith(ext) for ext in extensions): if any(filename.lower().endswith(ext) for ext in extensions):
files_to_add.append(filename) files_to_add.append(filename)
add_files_to_library(files_to_add) add_files_to_database(files_to_add)