From 69ce4bc7632134ccaf9b81b65e23d05c91508412 Mon Sep 17 00:00:00 2001 From: billypom on debian Date: Sun, 3 Aug 2025 22:49:04 -0400 Subject: [PATCH] stuffg --- components/HeaderTags.py | 20 ++--- components/MusicTable.py | 155 ++++++++++++++++++++--------------- components/PlaylistsPane.py | 41 +++++---- components/SearchLineEdit.py | 2 +- main.py | 12 +-- utils/Worker.py | 14 ++-- 6 files changed, 131 insertions(+), 113 deletions(-) diff --git a/components/HeaderTags.py b/components/HeaderTags.py index c08898a..0819da2 100644 --- a/components/HeaderTags.py +++ b/components/HeaderTags.py @@ -7,16 +7,16 @@ from typing import Optional @dataclass class SQLiteMap: - title: Optional[str] = None - artist: Optional[str] = None - album: Optional[str] = None - album_artist: Optional[str] = None - track_number: Optional[str] = None - genre: Optional[str] = None - length_seconds: Optional[str] = None - album_date: Optional[str] = None - codec: Optional[str] = None - filepath: Optional[str] = None + title: str | None = None + artist: str | None = None + album: str | None = None + album_artist: str | None = None + track_number: str | None = None + genre: str | None = None + length_seconds: str | None = None + album_date: str | None = None + codec: str | None = None + filepath: str | None = None """ diff --git a/components/MusicTable.py b/components/MusicTable.py index 3acabcf..b02dc40 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -118,8 +118,7 @@ class MusicTable(QTableView): ) self.config = ConfigParser() self.config.read(cfg_file) - print("music table config:") - print(self.config) + debug(f"music table config: {self.config}") # Threads self.threadpool = QThreadPool @@ -138,7 +137,8 @@ class MusicTable(QTableView): # Properties self.setAcceptDrops(True) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.setHorizontalScrollBarPolicy( + Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.setEditTriggers(QAbstractItemView.EditTrigger.EditKeyPressed) self.setAlternatingRowColors(True) self.setSelectionMode(QAbstractItemView.ExtendedSelection) @@ -146,7 +146,7 @@ class MusicTable(QTableView): # header self.horizontal_header: QHeaderView = self.horizontalHeader() assert self.horizontal_header is not None # i hate look at linting errors - self.horizontal_header.setStretchLastSection(True) + self.horizontal_header.setStretchLastSection(False) self.horizontal_header.setSectionResizeMode(QHeaderView.Interactive) self.horizontal_header.sortIndicatorChanged.connect(self.on_sort) # dumb vertical estupido @@ -159,7 +159,8 @@ class MusicTable(QTableView): self.deleteKey.connect(self.delete_songs) self.doubleClicked.connect(self.play_selected_audio_file) self.enterKey.connect(self.play_selected_audio_file) - self.model2.dataChanged.connect(self.on_cell_data_changed) # editing cells + self.model2.dataChanged.connect( + self.on_cell_data_changed) # editing cells # self.model2.layoutChanged.connect(self.restore_scroll_position) self.horizontal_header.sectionResized.connect(self.on_header_resized) # Final actions @@ -202,11 +203,13 @@ class MusicTable(QTableView): """Right-click context menu""" menu = QMenu(self) add_to_playlist_action = QAction("Add to playlist", self) - add_to_playlist_action.triggered.connect(self.add_selected_files_to_playlist) + add_to_playlist_action.triggered.connect( + self.add_selected_files_to_playlist) menu.addAction(add_to_playlist_action) # edit metadata edit_metadata_action = QAction("Edit metadata", self) - edit_metadata_action.triggered.connect(self.edit_selected_files_metadata) + edit_metadata_action.triggered.connect( + self.edit_selected_files_metadata) menu.addAction(edit_metadata_action) # edit lyrics edit_lyrics_action = QAction("Lyrics (View/Edit)", self) @@ -214,10 +217,12 @@ class MusicTable(QTableView): menu.addAction(edit_lyrics_action) # jump to current song in table jump_to_current_song_action = QAction("Jump to current song", self) - jump_to_current_song_action.triggered.connect(self.jump_to_current_song) + jump_to_current_song_action.triggered.connect( + self.jump_to_current_song) menu.addAction(jump_to_current_song_action) # open in file explorer - open_containing_folder_action = QAction("Open in system file manager", self) + open_containing_folder_action = QAction( + "Open in system file manager", self) open_containing_folder_action.triggered.connect(self.open_directory) menu.addAction(open_containing_folder_action) # view id3 tags (debug) @@ -380,59 +385,58 @@ class MusicTable(QTableView): self.set_selected_song_qmodel_index() self.viewport().update() # type: ignore - def on_header_resized(self, logicalIndex, oldSize, newSize): + def on_header_resized(self, logicalIndex: int, oldSize: int, newSize: int): """Handles keeping headers inside the viewport""" # FIXME: how resize good + pass # https://stackoverflow.com/questions/46775438/how-to-limit-qheaderview-size-when-resizing-sections - col_count = self.model2.columnCount() - qtableview_width = self.size().width() - sum_of_cols = self.horizontal_header.length() + # col_count = self.model2.columnCount() + # qtableview_width = self.size().width() + # sum_of_cols = self.horizontal_header.length() # debug(f'qtable_width: {qtableview_width}') # debug(f'sum of cols: {sum_of_cols}') - # check for discrepancy - if sum_of_cols != qtableview_width: - # if not the last header - if logicalIndex < col_count: - next_header_size = self.horizontal_header.sectionSize(logicalIndex + 1) - # If it should shrink - if next_header_size > (sum_of_cols - qtableview_width): - # shrink it - self.horizontal_header.resizeSection( - logicalIndex + 1, - next_header_size - (sum_of_cols - qtableview_width), - ) - else: - # block the resize - self.horizontal_header.resizeSection(logicalIndex, oldSize) + # if sum_of_cols != qtableview_width: # check for discrepancy + # if logicalIndex < col_count: # if not the last header + # next_header_size = self.horizontal_header.sectionSize(logicalIndex + 1) + # if next_header_size > (sum_of_cols - qtableview_width): # if it should shrink + # self.horizontal_header.resizeSection( + # logicalIndex + 1, + # next_header_size - (sum_of_cols - qtableview_width), + # ) # shrink it + # else: + # self.horizontal_header.resizeSection(logicalIndex, oldSize) # block the resize def on_cell_data_changed(self, topLeft: QModelIndex, bottomRight: QModelIndex): """Handles updating ID3 tags when data changes in a cell""" # FIXME: broken - if isinstance(self.model2, QStandardItemModel): - debug("on_cell_data_changed") - # get the ID of the row that was edited - id_index = self.model2.index(topLeft.row(), 0) - # get the db song_id from the row - song_id = self.model2.data(id_index, Qt.ItemDataRole.UserRole) - user_index = self.headers.user_fields.index("filepath") - filepath = self.currentIndex().siblingAtColumn(user_index).data() - # update the ID3 information - user_input_data = topLeft.data() - edited_column_name = self.headers.user_fields[topLeft.column()] - debug(f"on_cell_data_changed | edited column name: {edited_column_name}") - response = set_tag(filepath, edited_column_name, user_input_data) - if response: - # Update the library with new metadata - update_song_in_database(song_id, edited_column_name, user_input_data) - return - def handle_progress(self, data): + # if isinstance(self.model2, QStandardItemModel): + debug("on_cell_data_changed") + # get the ID of the row that was edited + id_index = self.model2.index(topLeft.row(), 0) + # get the db song_id from the row + song_id: int = self.model2.data(id_index, Qt.ItemDataRole.UserRole) + user_index = self.headers.user_fields.index("filepath") + filepath = self.currentIndex().siblingAtColumn(user_index).data() + # update the ID3 information + user_input_data: str = topLeft.data() + edited_column_name: str = self.headers.user_fields[topLeft.column()] + debug( + f"on_cell_data_changed | edited column name: {edited_column_name}") + response = set_tag(filepath, edited_column_name, user_input_data) + if response: + # Update the library with new metadata + _ = update_song_in_database( + song_id, edited_column_name, user_input_data) + return + + def handle_progress(self, data: object): """Emits data to main""" self.handleProgressSignal.emit(data) - def on_get_audio_files_recursively_finished(self, result): + def on_get_audio_files_recursively_finished(self, result: list[str]): """file search completion handler""" if result: self.add_files_to_library(result) @@ -456,7 +460,8 @@ class MusicTable(QTableView): except IndexError: pass except Exception as e: - debug(f"on_add_files_to_database_finished() | Something went wrong: {e}") + debug( + f"on_add_files_to_database_finished() | Something went wrong: {e}") # ____________________ # | | @@ -484,8 +489,8 @@ class MusicTable(QTableView): - 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_result.connect(self.on_add_files_to_database_finished) + _ = worker.signals.signal_progress.connect(self.qapp.handle_progress) + _ = worker.signals.signal_result.connect(self.on_add_files_to_database_finished) worker.signals.signal_finished.connect(self.load_music_table) if self.qapp: threadpool = self.qapp.threadpool @@ -495,15 +500,15 @@ class MusicTable(QTableView): 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 = 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)""" - # FIXME: determine if we are in a playlist or not - # then delete songs from only the playlist - # or provide extra questionbox option + # NOTE: provide extra questionbox option? # | Delete from playlist & lib | Delete from playlist only | Cancel | + # Currently, this just deletes from the playlist, or the main lib & any playlists selected_filepaths = self.get_selected_songs_filepaths() if self.selected_playlist_id: question_dialog = QuestionBoxDetails( @@ -513,9 +518,12 @@ class MusicTable(QTableView): ) reply = question_dialog.execute() if reply: - worker = Worker(batch_delete_filepaths_from_playlist, selected_filepaths, self.selected_playlist_id) - worker.signals.signal_progress.connect(self.qapp.handle_progress) - worker.signals.signal_finished.connect(self.delete_selected_row_indices) + worker = Worker(batch_delete_filepaths_from_playlist, + selected_filepaths, self.selected_playlist_id) + worker.signals.signal_progress.connect( + self.qapp.handle_progress) + worker.signals.signal_finished.connect( + self.delete_selected_row_indices) if self.qapp: threadpool = self.qapp.threadpool threadpool.start(worker) @@ -527,9 +535,12 @@ class MusicTable(QTableView): ) reply = question_dialog.execute() if reply: - worker = Worker(batch_delete_filepaths_from_database, selected_filepaths) - worker.signals.signal_progress.connect(self.qapp.handle_progress) - worker.signals.signal_finished.connect(self.delete_selected_row_indices) + worker = Worker( + batch_delete_filepaths_from_database, selected_filepaths) + worker.signals.signal_progress.connect( + self.qapp.handle_progress) + worker.signals.signal_finished.connect( + self.delete_selected_row_indices) if self.qapp: threadpool = self.qapp.threadpool threadpool.start(worker) @@ -563,7 +574,8 @@ class MusicTable(QTableView): """Moves screen to the selected song, then selects the row""" debug("jump_to_selected_song") # get the proxy model index - proxy_index = self.proxymodel.mapFromSource(self.selected_song_qmodel_index) + proxy_index = self.proxymodel.mapFromSource( + self.selected_song_qmodel_index) self.scrollTo(proxy_index) self.selectRow(proxy_index.row()) @@ -573,7 +585,8 @@ class MusicTable(QTableView): # get the proxy model index debug(self.current_song_filepath) debug(self.current_song_qmodel_index) - proxy_index = self.proxymodel.mapFromSource(self.current_song_qmodel_index) + proxy_index = self.proxymodel.mapFromSource( + self.current_song_qmodel_index) self.scrollTo(proxy_index) self.selectRow(proxy_index.row()) @@ -684,7 +697,8 @@ class MusicTable(QTableView): ) # debug(f"reorganize_files() | Moved: {filepath} -> {new_path}") except Exception as e: - error(f"reorganize_files() | Error moving file: {filepath} | {e}") + error( + f"reorganize_files() | Error moving file: {filepath} | {e}") # Draw the rest of the owl # QMessageBox.information( # self, "Reorganization complete", "Files successfully reorganized" @@ -709,7 +723,8 @@ class MusicTable(QTableView): self.disconnect_layout_changed() self.vertical_scroll_position = self.verticalScrollBar().value() # type: ignore self.model2.clear() - self.model2.setHorizontalHeaderLabels(self.headers.get_user_gui_headers()) + self.model2.setHorizontalHeaderLabels( + self.headers.get_user_gui_headers()) fields = ", ".join(self.headers.user_fields) search_clause = ( "title LIKE ? OR artist LIKE ? OR album LIKE ?" @@ -731,7 +746,8 @@ class MusicTable(QTableView): query = f"{query} WHERE {search_clause};" else: query = f"{query} AND {search_clause};" - data = db.query(query, (self.selected_playlist_id, params)) + data = db.query( + query, (self.selected_playlist_id, params)) else: data = db.query(query, (self.selected_playlist_id,)) @@ -788,7 +804,8 @@ class MusicTable(QTableView): # reloading the model destroys and makes new indexes # so we look for the new index of the current song on load current_song_filepath = self.get_current_song_filepath() - debug(f"load_music_table() | current filepath: {current_song_filepath}") + debug( + f"load_music_table() | current filepath: {current_song_filepath}") for row in range(self.model2.rowCount()): real_index = self.model2.index( row, self.headers.user_fields.index("filepath") @@ -814,7 +831,8 @@ class MusicTable(QTableView): """ Loads the header widths from the last application close. """ - table_view_column_widths = str(self.config["table"]["column_widths"]).split(",") + table_view_column_widths = str( + self.config["table"]["column_widths"]).split(",") debug(f"loaded header widths: {table_view_column_widths}") if not isinstance(table_view_column_widths, list): for i in range(self.model2.columnCount() - 1): @@ -898,7 +916,8 @@ class MusicTable(QTableView): selected_rows = self.get_selected_rows() filepaths = [] for row in selected_rows: - idx = self.proxymodel.index(row, self.headers.user_fields.index("filepath")) + idx = self.proxymodel.index( + row, self.headers.user_fields.index("filepath")) filepaths.append(idx.data()) return filepaths diff --git a/components/PlaylistsPane.py b/components/PlaylistsPane.py index af6b93b..5b26761 100644 --- a/components/PlaylistsPane.py +++ b/components/PlaylistsPane.py @@ -1,7 +1,6 @@ from PyQt5.QtWidgets import ( QAction, QInputDialog, - QListWidget, QMenu, QMessageBox, QTreeWidget, @@ -10,6 +9,7 @@ from PyQt5.QtWidgets import ( from PyQt5.QtCore import QThreadPool, pyqtSignal, Qt, QPoint import DBA from logging import debug +from components.ErrorDialog import ErrorDialog from utils import Worker from components import CreatePlaylistWindow @@ -43,10 +43,6 @@ class PlaylistsPane(QTreeWidget): self.customContextMenuRequested.connect(self.showContextMenu) self.currentItemChanged.connect(self.playlist_clicked) self.playlist_db_id_choice: int | None = None - self.threadpool: QThreadPool | None = None - - def set_threadpool(self, threadpool: QThreadPool): - self.threadpool = threadpool def reload_playlists(self, progress_callback=None): """ @@ -60,7 +56,7 @@ class PlaylistsPane(QTreeWidget): playlists = db.query( "SELECT id, name FROM playlist ORDER BY date_created DESC;", () ) - debug(f'PlaylistsPane: | playlists = {playlists}') + # debug(f'PlaylistsPane: | playlists = {playlists}') for playlist in playlists: branch = PlaylistWidgetItem(self, playlist[0], playlist[1]) self._playlists_root.addChild(branch) @@ -86,7 +82,7 @@ class PlaylistsPane(QTreeWidget): def create_playlist(self): """Creates a database record for a playlist, given a name""" window = CreatePlaylistWindow(self.playlistCreatedSignal) - window.playlistCreatedSignal.connect(self.reload_playlists) # type: ignore + window.playlistCreatedSignal.connect(self.reload_playlists) window.exec_() def rename_playlist(self, *args): @@ -110,8 +106,9 @@ class PlaylistsPane(QTreeWidget): (text, self.playlist_db_id_choice), ) worker = Worker(self.reload_playlists) - if self.threadpool: - self.threadpool.start(worker) + if self.qapp: + threadpool = self.qapp.threadpool + threadpool.start(worker) def delete_playlist(self, *args): """Deletes a playlist""" @@ -123,14 +120,18 @@ class PlaylistsPane(QTreeWidget): QMessageBox.Yes, ) if reply == QMessageBox.Yes: - with DBA.DBAccess() as db: - db.execute( - "DELETE FROM playlist WHERE id = ?;", (self.playlist_db_id_choice,) - ) - # reload - worker = Worker(self.reload_playlists) - if self.threadpool: - self.threadpool.start(worker) + if self.qapp: + with DBA.DBAccess() as db: + db.execute( + "DELETE FROM playlist WHERE id = ?;", (self.playlist_db_id_choice,) + ) + # reload + worker = Worker(self.reload_playlists) + threadpool = self.qapp.threadpool + threadpool.start(worker) + else: + error_dialog = ErrorDialog("Main application [qapp] not loaded - aborting operation") + error_dialog.exec() def playlist_clicked(self, item): """Specific playlist pane index was clicked""" @@ -139,10 +140,14 @@ class PlaylistsPane(QTreeWidget): # self.all_songs_selected() self.allSongsSignal.emit() elif isinstance(item, PlaylistWidgetItem): - debug(f"ID: {item.id}, name: {item.text(0)}") + # debug(f"ID: {item.id}, name: {item.text(0)}") self.playlist_db_id_choice = item.id self.playlistChoiceSignal.emit(int(item.id)) + def load_qapp(self, qapp) -> None: + """Necessary for using members and methods of main application window""" + self.qapp = qapp + # def all_songs_selected(self): # """Emits a signal to display all songs in the library""" # # I have no idea why this has to be in its own function, but it does diff --git a/components/SearchLineEdit.py b/components/SearchLineEdit.py index c004112..40e4d2d 100644 --- a/components/SearchLineEdit.py +++ b/components/SearchLineEdit.py @@ -42,7 +42,7 @@ class SearchLineEdit(QLineEdit): def on_text_changed(self): """Reset a timer each time text is changed""" - self.timer.start(300) + self.timer.start(1500) def on_typing_stopped(self): """When timer reaches end, emit the text that is currently entered""" diff --git a/main.py b/main.py index b9f2662..1fd2544 100644 --- a/main.py +++ b/main.py @@ -92,7 +92,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): / "config.ini" ) self.config.read(self.cfg_file) - debug(f"\tmain config: {self.config}") self.threadpool: QThreadPool = QThreadPool() # UI self.setupUi(self) @@ -105,6 +104,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.status_bar.addPermanentWidget(self.permanent_status_label) self.setStatusBar(self.status_bar) + # table self.selected_song_filepath: str | None = None self.current_song_filepath: str | None = None self.current_song_metadata: ID3 | dict | None = None @@ -113,7 +113,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # widget bits self.tableView: MusicTable self.album_art_scene: QGraphicsScene = QGraphicsScene() - # self.player: QMediaPlayer = QMediaPlayer() # Audio player object self.player: QMediaPlayer = MediaPlayer() # set index on choose song # index is the model2's row number? i guess? @@ -143,6 +142,7 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # sharing functions with other classes and that self.tableView.load_qapp(self) self.albumGraphicsView.load_qapp(self) + self.playlistTreeView.load_qapp(self) self.headers = HeaderTags() # Settings init @@ -214,7 +214,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.tableView.load_music_table ) self.playlistTreeView.allSongsSignal.connect(self.tableView.load_music_table) - self.playlistTreeView.set_threadpool(self.threadpool) # albumGraphicsView self.albumGraphicsView.albumArtDropped.connect( @@ -393,16 +392,11 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): def load_config(self) -> None: """does what it says""" cfg_file = ( - Path(user_config_dir(appname="musicpom", appauthor="billypom")) - / "config.ini" + Path(user_config_dir(appname="musicpom", appauthor="billypom")) / "config.ini" ) self.config.read(cfg_file) debug("load_config()") - def get_thread_pool(self) -> QThreadPool: - """Returns the threadpool instance""" - return self.threadpool - def set_permanent_status_bar_message(self, message: str) -> None: """ Sets the permanent message label in the status bar diff --git a/utils/Worker.py b/utils/Worker.py index 75ff6f1..713e6bf 100644 --- a/utils/Worker.py +++ b/utils/Worker.py @@ -27,10 +27,10 @@ class WorkerSignals(QObject): # pandas DataFrames or numpy arrays. Instead, pass the minimum amount of information needed # (i.e. lists of filepaths) - signal_started = pyqtSignal() - signal_result = pyqtSignal(object) - signal_finished = pyqtSignal() - signal_progress = pyqtSignal(str) + signal_started: pyqtSignal = pyqtSignal() + signal_result: pyqtSignal = pyqtSignal(object) + signal_finished: pyqtSignal = pyqtSignal() + signal_progress: pyqtSignal = pyqtSignal(str) class Worker(QRunnable): @@ -46,12 +46,12 @@ class Worker(QRunnable): :param kwargs: Keywords to pass to the callback function """ - def __init__(self, fn, *args, **kwargs): + def __init__(self, fn, *args: object, **kwargs: object): super(Worker, self).__init__() # Store constructor arguments (re-used for processing) self.fn = fn - self.args = args - self.kwargs = kwargs + self.args: object = args + self.kwargs: object = kwargs self.signals: WorkerSignals = WorkerSignals() # Add a callback to our kwargs