diff --git a/components/MusicTable.py b/components/MusicTable.py index 2b9b595..596df78 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -1,4 +1,3 @@ -from mutagen.id3 import ID3 import DBA from PyQt5.QtGui import ( QColor, @@ -14,6 +13,7 @@ from PyQt5.QtGui import ( ) from PyQt5.QtWidgets import ( QAction, + QApplication, QHeaderView, QMenu, QTableView, @@ -26,11 +26,9 @@ from PyQt5.QtCore import ( QSortFilterProxyModel, Qt, QModelIndex, - QThreadPool, pyqtSignal, ) from components.DebugWindow import DebugWindow -from components.ErrorDialog import ErrorDialog from components.LyricsWindow import LyricsWindow from components.AddToPlaylistWindow import AddToPlaylistWindow from components.MetadataWindow import MetadataWindow @@ -40,11 +38,9 @@ from components.HeaderTags import HeaderTags from utils import ( batch_delete_filepaths_from_database, batch_delete_filepaths_from_playlist, - delete_song_id_from_database, add_files_to_database, get_reorganize_vars, update_song_in_database, - get_album_art, id3_remap, get_tags, set_tag, @@ -59,21 +55,20 @@ from pathlib import Path from appdirs import user_config_dir from configparser import ConfigParser - class MusicTable(QTableView): - playlistStatsSignal = pyqtSignal(str) - loadMusicTableSignal = pyqtSignal() - sortSignal = pyqtSignal() - playPauseSignal = pyqtSignal() - playSignal = pyqtSignal(str) - enterKey = pyqtSignal() - deleteKey = pyqtSignal() - refreshMusicTableSignal = pyqtSignal() - handleProgressSignal = pyqtSignal(str) - getThreadPoolSignal = pyqtSignal() - searchBoxSignal = pyqtSignal() - focusEnterSignal = pyqtSignal() - focusLeaveSignal = pyqtSignal() + playlistStatsSignal: pyqtSignal = pyqtSignal(str) + loadMusicTableSignal: pyqtSignal = pyqtSignal() + sortSignal: pyqtSignal = pyqtSignal() + playPauseSignal: pyqtSignal = pyqtSignal() + playSignal: pyqtSignal = pyqtSignal(str) + enterKey: pyqtSignal = pyqtSignal() + deleteKey: pyqtSignal = pyqtSignal() + refreshMusicTableSignal: pyqtSignal = pyqtSignal() + handleProgressSignal: pyqtSignal = pyqtSignal(str) + getThreadPoolSignal: pyqtSignal = pyqtSignal() + searchBoxSignal: pyqtSignal = pyqtSignal() + focusEnterSignal: pyqtSignal = pyqtSignal() + focusLeaveSignal: pyqtSignal = pyqtSignal() def __init__(self, parent=None, application_window=None): super().__init__(parent) @@ -85,7 +80,7 @@ class MusicTable(QTableView): Path(user_config_dir(appname="musicpom", appauthor="billypom")) / "config.ini" ) - self.config.read(cfg_file) + _ = self.config.read(cfg_file) debug(f"music table config: {self.config}") # NOTE: @@ -96,13 +91,12 @@ class MusicTable(QTableView): # Create QSortFilterProxyModel # Set QSortFilterProxyModel source to QStandardItemModel # Set QTableView model to the Proxy model - # so it looks like that, i guess + # so it looks like the above note, i guess # need a QStandardItemModel to do actions on cells self.model2: QStandardItemModel = QStandardItemModel() - self.proxymodel = QSortFilterProxyModel() - self.search_string = None - self.threadpool = QThreadPool + self.proxymodel: QSortFilterProxyModel = QSortFilterProxyModel() + self.search_string: str | None = None self.headers = HeaderTags() # db names of headers self.database_columns: list[str] = str( @@ -144,8 +138,7 @@ 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,35 +195,32 @@ 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) - edit_lyrics_action.triggered.connect(self.show_lyrics_menu) + _ = edit_lyrics_action.triggered.connect(self.show_lyrics_menu) 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.triggered.connect(self.open_directory) + _ = open_containing_folder_action.triggered.connect(self.open_directory) menu.addAction(open_containing_folder_action) # view id3 tags (debug) view_id3_tags_debug = QAction("View ID3 tags (debug)", self) - view_id3_tags_debug.triggered.connect(self.view_id3_tags_debug_menu) + _ = view_id3_tags_debug.triggered.connect(self.view_id3_tags_debug_menu) menu.addAction(view_id3_tags_debug) # delete song delete_action = QAction("Delete", self) - delete_action.triggered.connect(self.delete_songs) + _ = delete_action.triggered.connect(self.delete_songs) menu.addAction(delete_action) # show self.set_selected_song_filepath() @@ -261,8 +251,8 @@ class MusicTable(QTableView): data = e.mimeData() debug("dropEvent") if data and data.hasUrls(): - directories = [] - files = [] + directories: list[str] = [] + files: list[str] = [] for url in data.urls(): if url.isLocalFile(): path = url.toLocalFile() @@ -275,11 +265,9 @@ class MusicTable(QTableView): e.accept() if directories: worker = Worker(self.get_audio_files_recursively, directories) - worker.signals.signal_progress.connect(self.handle_progress) - worker.signals.signal_result.connect( - self.on_get_audio_files_recursively_finished - ) - worker.signals.signal_finished.connect(self.load_music_table) + _ = worker.signals.signal_progress.connect(self.handle_progress) + _ = worker.signals.signal_result.connect(self.on_get_audio_files_recursively_finished) + _ = worker.signals.signal_finished.connect(self.load_music_table) if self.qapp: threadpool = self.qapp.threadpool threadpool.start(worker) @@ -301,9 +289,8 @@ class MusicTable(QTableView): index = self.currentIndex() new_index = self.model2.index(index.row(), index.column() + 1) if new_index.isValid(): - # print(f"right -> ({new_index.row()},{new_index.column()})") self.setCurrentIndex(new_index) - self.viewport().update() # type: ignore + self.viewport().update() super().keyPressEvent(e) return @@ -311,9 +298,8 @@ class MusicTable(QTableView): index = self.currentIndex() new_index = self.model2.index(index.row(), index.column() - 1) if new_index.isValid(): - # print(f"left -> ({new_index.row()},{new_index.column()})") self.setCurrentIndex(new_index) - self.viewport().update() # type: ignore + self.viewport().update() super().keyPressEvent(e) return @@ -321,9 +307,8 @@ class MusicTable(QTableView): index = self.currentIndex() new_index = self.model2.index(index.row() - 1, index.column()) if new_index.isValid(): - # print(f"up -> ({new_index.row()},{new_index.column()})") self.setCurrentIndex(new_index) - self.viewport().update() # type: ignore + self.viewport().update() super().keyPressEvent(e) return @@ -331,15 +316,14 @@ class MusicTable(QTableView): index = self.currentIndex() new_index = self.model2.index(index.row() + 1, index.column()) if new_index.isValid(): - # print(f"down -> ({new_index.row()},{new_index.column()})") self.setCurrentIndex(new_index) - self.viewport().update() # type: ignore + self.viewport().update() super().keyPressEvent(e) return elif key in (Qt.Key.Key_Return, Qt.Key.Key_Enter): if self.state() != QAbstractItemView.EditingState: - self.enterKey.emit() # Enter key detected + self.enterKey.emit() else: super().keyPressEvent(e) else: # Default behavior @@ -450,8 +434,10 @@ class MusicTable(QTableView): - data returned from the original worker process function are returned here as the first item in a tuple """ - _, details = args[0][:2] + print('hello?') + print(args) try: + _, details = args[0][:2] details = dict(tuple(details)[0]) if details: window = DebugWindow(details) @@ -484,14 +470,15 @@ class MusicTable(QTableView): 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) + debug('add_files_to_library()') + worker = Worker(add_files_to_database, files, None) _ = 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) + _ = 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 threadpool.start(worker) @@ -711,7 +698,7 @@ class MusicTable(QTableView): self.set_current_song_filepath() self.playPauseSignal.emit() - def load_music_table(self, *playlist_id): + def load_music_table(self, *playlist_id: int): """ Loads data into self (QTableView) Loads all songs in library, by default @@ -721,7 +708,7 @@ class MusicTable(QTableView): """ self.disconnect_data_changed() self.disconnect_layout_changed() - self.vertical_scroll_position = self.verticalScrollBar().value() # type: ignore + self.vertical_scroll_position = self.verticalScrollBar().value() self.model2.clear() self.model2.setHorizontalHeaderLabels( self.headers.get_user_gui_headers()) @@ -845,10 +832,8 @@ class MusicTable(QTableView): 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 + # Disconnect these signals to prevent unnecessary reloads debug("sort_table_by_multiple_columns()") self.disconnect_data_changed() self.disconnect_layout_changed() @@ -879,6 +864,8 @@ class MusicTable(QTableView): self.connect_data_changed() self.connect_layout_changed() # self.model2.layoutChanged.emit() + # 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. def restore_scroll_position(self) -> None: """Restores the scroll position""" @@ -888,10 +875,10 @@ class MusicTable(QTableView): # lambda: self.verticalScrollBar().setValue(self.vertical_scroll_position), # ) - def get_audio_files_recursively(self, directories, progress_callback=None): + def get_audio_files_recursively(self, directories: list[str], progress_callback=None) -> list[str]: """Scans a directories for files""" extensions = self.config.get("settings", "extensions").split(",") - audio_files = [] + audio_files: list[str] = [] for directory in directories: for root, _, files in os.walk(directory): for file in files: @@ -965,9 +952,7 @@ class MusicTable(QTableView): self.proxymodel.index(row, 0), Qt.ItemDataRole.UserRole ) with DBA.DBAccess() as db: - filepath = db.query("SELECT filepath FROM song WHERE id = ?", (id,))[0][ - 0 - ] + filepath = db.query("SELECT filepath FROM song WHERE id = ?", (id,))[0][0] self.selected_song_filepath = filepath def set_current_song_filepath(self, filepath=None) -> None: @@ -984,7 +969,7 @@ class MusicTable(QTableView): else: self.current_song_filepath = filepath - def set_current_song_qmodel_index(self, index=None): + def set_current_song_qmodel_index(self, index: QModelIndex | None = None): """ Takes in the proxy model index for current song - QModelIndex converts to model2 index @@ -994,9 +979,9 @@ class MusicTable(QTableView): index = self.currentIndex() # map proxy (sortable) model to the original model (used for interactions) real_index: QModelIndex = self.proxymodel.mapToSource(index) - self.current_song_qmodel_index: QModelIndex = real_index + self.current_song_qmodel_index = real_index - def set_selected_song_qmodel_index(self, index=None): + def set_selected_song_qmodel_index(self, index: QModelIndex | None = None): """ Takes in the proxy model index for current song - QModelIndex converts to model2 index @@ -1006,15 +991,15 @@ class MusicTable(QTableView): index = self.currentIndex() # map proxy (sortable) model to the original model (used for interactions) real_index: QModelIndex = self.proxymodel.mapToSource(index) - self.selected_song_qmodel_index: QModelIndex = real_index + self.selected_song_qmodel_index = real_index def set_search_string(self, text: str): """set the search string""" self.search_string = text - def load_qapp(self, qapp) -> None: + def load_qapp(self, qapp: QApplication) -> None: """Necessary for using members and methods of main application window""" - self.qapp = qapp + self.qapp: QApplication = qapp # ____________________ # | | @@ -1033,7 +1018,7 @@ class MusicTable(QTableView): def connect_data_changed(self): """Connects the dataChanged signal from QTableView.model""" try: - self.model2.dataChanged.connect(self.on_cell_data_changed) + _ = self.model2.dataChanged.connect(self.on_cell_data_changed) except Exception: pass @@ -1047,7 +1032,7 @@ class MusicTable(QTableView): def connect_layout_changed(self): """Connects the layoutChanged signal from QTableView.model""" try: - self.model2.layoutChanged.connect(self.restore_scroll_position) + _ = self.model2.layoutChanged.connect(self.restore_scroll_position) except Exception: pass diff --git a/main.py b/main.py index 6799046..dd3b2b0 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ from mutagen.id3 import ID3 from configparser import ConfigParser from pathlib import Path from appdirs import user_config_dir -from logging import debug, error, warning, basicConfig, INFO, DEBUG +from logging import debug, error, basicConfig, DEBUG from ui import Ui_MainWindow from PyQt5.QtWidgets import ( QFileDialog, @@ -21,34 +21,25 @@ from PyQt5.QtWidgets import ( QMainWindow, QApplication, QGraphicsScene, - QGraphicsPixmapItem, QMessageBox, QPushButton, QStatusBar, QStyle, - QTableView, ) from PyQt5.QtCore import ( QModelIndex, QSize, - QThread, QUrl, QTimer, - Qt, pyqtSignal, - QObject, - pyqtSlot, QThreadPool, - QRunnable, ) from PyQt5.QtMultimedia import ( QMediaPlayer, QMediaContent, QAudioProbe, - QMediaPlaylist, - QMultimedia, ) -from PyQt5.QtGui import QClipboard, QCloseEvent, QFont, QPixmap, QResizeEvent +from PyQt5.QtGui import QCloseEvent, QFont, QResizeEvent from utils import ( delete_album_art, get_tags, @@ -68,7 +59,6 @@ from components import ( AudioVisualizer, CreatePlaylistWindow, ExportPlaylistWindow, - SearchLineEdit, ) # good help with signals slots in threads @@ -79,14 +69,14 @@ from components import ( class ApplicationWindow(QMainWindow, Ui_MainWindow): - reloadConfigSignal = pyqtSignal() - reloadDatabaseSignal = pyqtSignal() + reloadConfigSignal: pyqtSignal = pyqtSignal() + reloadDatabaseSignal: pyqtSignal = pyqtSignal() def __init__(self, clipboard): super(ApplicationWindow, self).__init__() self.clipboard = clipboard self.config: ConfigParser = ConfigParser() - self.cfg_file = ( + self.cfg_file: Path = ( Path(user_config_dir(appname="musicpom", appauthor="billypom")) / "config.ini" ) @@ -119,14 +109,15 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): self.audio_visualizer: AudioVisualizer = AudioVisualizer( self.player, self.probe, self.PlotWidget ) - self.timer = QTimer(self) # for playback slider and such + self.timer: QTimer = QTimer(parent=self) # for playback slider and such # Button styles + style: QStyle | None if not self.style(): style = QStyle() else: style = self.style() - assert style is not None # i hate linting errors + assert style is not None pixmapi = QStyle.StandardPixmap.SP_MediaSkipForward icon = style.standardIcon(pixmapi) self.nextButton.setIcon(icon) @@ -380,7 +371,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): QtCore.Qt.TextInteractionFlag.TextSelectableByMouse ) - font: QFont = QFont() font.setPointSize(12) font.setBold(False) self.titleLabel.setFont(font) @@ -388,7 +378,6 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): QtCore.Qt.TextInteractionFlag.TextSelectableByMouse ) - font: QFont = QFont() font.setPointSize(12) font.setItalic(True) self.albumLabel.setFont(font) @@ -585,10 +574,8 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): preferences_window = PreferencesWindow( self.reloadConfigSignal, self.reloadDatabaseSignal ) - preferences_window.reloadConfigSignal.connect( - self.load_config) # type: ignore - preferences_window.reloadDatabaseSignal.connect( - self.tableView.load_music_table) # type: ignore + preferences_window.reloadConfigSignal.connect(self.load_config) + preferences_window.reloadDatabaseSignal.connect(self.tableView.load_music_table) preferences_window.exec_() # Display the preferences window modally # Quick Actions @@ -606,11 +593,11 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): def delete_database(self) -> None: """Deletes the entire database""" reply = QMessageBox.question( - self, - "Confirmation", - "Delete database?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.Yes, + parent=self, + title="Confirmation", + text="Delete database?", + buttons=QMessageBox.Yes | QMessageBox.No, + defaultButton=QMessageBox.Yes, ) if reply == QMessageBox.Yes: initialize_db() @@ -629,7 +616,8 @@ def update_database_file() -> bool: appname="musicpom", appauthor="billypom"))) config = ConfigParser() config.read(cfg_file) - db_filepath: str = config.get("settings", "db") + db_filepath: str + db_filepath= config.get("settings", "db") # If the database location isnt set at the config location, move it if not db_filepath.startswith(cfg_path): @@ -643,7 +631,7 @@ def update_database_file() -> bool: with open(cfg_file, "w") as configfile: config.write(configfile) config.read(cfg_file) - db_filepath: str = config.get("settings", "db") + db_filepath = config.get("settings", "db") db_path = db_filepath.split("/") db_path.pop() @@ -722,8 +710,8 @@ if __name__ == "__main__": format="{%(filename)s:%(lineno)d} %(levelname)s - %(message)s", handlers=handlers, ) - debug(f'--------- musicpom debug started') - debug(f'---------------------| ') + debug('--------- musicpom debug started') + debug('---------------------| ') debug(f'----------------------> {handlers} ') # Initialization config: ConfigParser = update_config_file() diff --git a/utils/add_files_to_database.py b/utils/add_files_to_database.py index a675cf9..4808b76 100644 --- a/utils/add_files_to_database.py +++ b/utils/add_files_to_database.py @@ -1,5 +1,3 @@ -from PyQt5.QtWidgets import QMessageBox -from mutagen.id3 import ID3 import DBA from logging import debug from utils import get_tags, convert_id3_timestamp_to_datetime, id3_remap @@ -8,7 +6,7 @@ from pathlib import Path from appdirs import user_config_dir -def add_files_to_database(files, progress_callback=None): +def add_files_to_database(files: list[str], playlist_id: int | None = None, progress_callback=None) -> tuple[bool, dict[str, str]]: """ Adds audio file(s) to the sqllite db "song" table Args: @@ -21,26 +19,40 @@ def add_files_to_database(files, progress_callback=None): (True, {"filename.mp3":"failed because i said so"}) ``` """ + # yea + if playlist_id: + pass + config = ConfigParser() cfg_file = ( Path(user_config_dir(appname="musicpom", appauthor="billypom")) / "config.ini" ) - config.read(cfg_file) + _ = config.read(cfg_file) if not files: return False, {"Failure": "All operations failed in add_files_to_database()"} - failed_dict = {} - insert_data = [] # To store data for batch insert + failed_dict: dict[str, str] = {} + insert_data: list[tuple[ + str, + str | int | None, + str | int | None, + str | int | None, + str | int | None, + str | int | None, + str, + str | int | None, + str | int | None, + str | int | None]] = [] # To store data for batch insert for filepath in files: if progress_callback: progress_callback.emit(filepath) filename = filepath.split("/")[-1] - - tags, details = get_tags(filepath) - if details: - failed_dict[filepath] = details + tags, fail_reason = get_tags(filepath) + if fail_reason: + # if we fail to get audio tags, skip to next song + failed_dict[filepath] = fail_reason continue - audio = id3_remap(tags) - + # remap tags from ID3 to database tags + audio: dict[str, str | int | None] = id3_remap(tags) # Append data tuple to insert_data list insert_data.append( ( @@ -60,21 +72,22 @@ def add_files_to_database(files, progress_callback=None): if len(insert_data) >= 1000: debug(f"inserting a LOT of songs: {len(insert_data)}") with DBA.DBAccess() as db: - db.executemany( - "INSERT OR IGNORE INTO song (filepath, title, album, artist, track_number, genre, codec, album_date, bitrate, length_seconds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + result = db.executemany( + "INSERT OR IGNORE INTO song (filepath, title, album, artist, track_number, genre, codec, album_date, bitrate, length_seconds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id", insert_data, ) + debug('IMPORTANT') + debug(f'batch insert result: {result}') + debug('IMPORTANT') insert_data = [] # Reset the insert_data list - else: - # continue adding files if we havent reached big length - continue - # Insert any remaining data - debug("i check for insert data") + # Insert any remaining data after reading every file if insert_data: - debug(f"inserting some songs: {len(insert_data)}") with DBA.DBAccess() as db: - db.executemany( - "INSERT OR IGNORE INTO song (filepath, title, album, artist, track_number, genre, codec, album_date, bitrate, length_seconds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + result = db.executemany( + "INSERT OR IGNORE INTO song (filepath, title, album, artist, track_number, genre, codec, album_date, bitrate, length_seconds) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id", insert_data, ) + debug('IMPORTANT') + debug(f'batch insert result: {result}') + debug('IMPORTANT') return True, failed_dict diff --git a/utils/get_tags.py b/utils/get_tags.py index 1f89511..e6cc3df 100644 --- a/utils/get_tags.py +++ b/utils/get_tags.py @@ -34,7 +34,7 @@ def get_mp3_tags(filename: str) -> tuple[MP3 | ID3 | FLAC, str]: return MP3(), f"Could not assign ID3 tag to file: {e}" -def id3_remap(audio: MP3 | ID3 | FLAC) -> dict: +def id3_remap(audio: MP3 | ID3 | FLAC) -> dict[str, str | int | None]: """ Turns the ID3 dict of an audio file into a normal dict that I, the human, can use. Add extra fields too :D yahooo