From 4b440378a5766fd088d6f5aefc5d50048ef79eb9 Mon Sep 17 00:00:00 2001 From: tsi-billypom Date: Thu, 24 Apr 2025 11:43:17 -0400 Subject: [PATCH] full text search testing --- components/MusicTable.py | 71 +++++++++++++++++++++++++++++------- components/SearchLineEdit.py | 34 +++++++++++++++++ components/__init__.py | 1 + main.py | 51 +++++++++++++++++++++----- ui.py | 15 +++++--- ui.ui | 18 +++++++-- 6 files changed, 159 insertions(+), 31 deletions(-) create mode 100644 components/SearchLineEdit.py diff --git a/components/MusicTable.py b/components/MusicTable.py index 9df428c..cbc49ec 100644 --- a/components/MusicTable.py +++ b/components/MusicTable.py @@ -75,6 +75,7 @@ class MusicTable(QTableView): refreshMusicTableSignal = pyqtSignal() handleProgressSignal = pyqtSignal(str) getThreadPoolSignal = pyqtSignal() + searchBoxSignal = pyqtSignal() def __init__(self, parent=None, application_window=None): super().__init__(parent) @@ -96,6 +97,7 @@ class MusicTable(QTableView): self.proxymodel.setSourceModel(self.model2) self.setModel(self.proxymodel) self.setSortingEnabled(True) + self.search_string = None # Config cfg_file = ( @@ -591,6 +593,12 @@ class MusicTable(QTableView): # Delete key? shortcut = QShortcut(QKeySequence("Delete"), self) shortcut.activated.connect(self.delete_songs) + # Search box + shortcut = QShortcut(QKeySequence("Ctrl+F"), self) + shortcut.activated.connect(self.emit_search_box) + + def emit_search_box(self): + self.searchBoxSignal.emit() def confirm_reorganize_files(self) -> None: """ @@ -677,15 +685,28 @@ class MusicTable(QTableView): self.vertical_scroll_position = self.verticalScrollBar().value() # type: ignore self.model2.clear() self.model2.setHorizontalHeaderLabels(self.headers.get_user_gui_headers()) + search_clause = ( + "title LIKE %?% AND artist LIKE %?% and album LIKE %?%" + if self.search_string + else "" + ) fields = ", ".join(self.headers.user_headers) + params = "" if playlist_id: # Load a playlist - # Fetch playlist data selected_playlist_id = playlist_id[0] try: with DBA.DBAccess() as db: + query = f"SELECT id, {fields} FROM song JOIN song_playlist sp ON id = sp.song_id WHERE sp.playlist_id = ?" + # fulltext search + if self.search_string: + params = 3 * [self.search_string] + if query.find("WHERE") == -1: + query = f"{query} WHERE {search_clause};" + else: + query = f"{query} AND {search_clause};" data = db.query( - f"SELECT id, {fields} FROM song JOIN song_playlist sp ON id = sp.song_id WHERE sp.playlist_id = ?", - (selected_playlist_id,), + query, + (selected_playlist_id, params), ) except Exception as e: error(f"load_music_table() | Unhandled exception: {e}") @@ -693,9 +714,17 @@ class MusicTable(QTableView): else: # Load the library try: with DBA.DBAccess() as db: + query = f"SELECT id, {fields} FROM song" + # fulltext search + if self.search_string: + params = 3 * [self.search_string] + if query.find("WHERE") == -1: + query = f"{query} WHERE {search_clause};" + else: + query = f"{query} AND {search_clause};" data = db.query( - f"SELECT id, {fields} FROM song;", - (), + query, + (params), ) except Exception as e: error(f"load_music_table() | Unhandled exception: {e}") @@ -730,13 +759,15 @@ 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() - print(f'load music table current filepath: {current_song_filepath}') + print(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_headers.index("filepath")) + real_index = self.model2.index( + row, self.headers.user_headers.index("filepath") + ) if real_index.data() == current_song_filepath: - print('is it true?') - print(f'{real_index.data()} == {current_song_filepath}') - print('load music table real index:') + print("is it true?") + print(f"{real_index.data()} == {current_song_filepath}") + print("load music table real index:") print(real_index) self.current_song_qmodel_index = real_index self.model2.layoutChanged.emit() # emits a signal that the view should be updated @@ -850,7 +881,9 @@ class MusicTable(QTableView): return [] selected_rows = set(index.row() for index in indexes) id_list = [ - self.proxymodel.data(self.proxymodel.index(row, 0), Qt.ItemDataRole.UserRole) + self.proxymodel.data( + self.proxymodel.index(row, 0), Qt.ItemDataRole.UserRole + ) for row in selected_rows ] return id_list @@ -875,9 +908,13 @@ class MusicTable(QTableView): except ValueError: # if the user doesnt have filepath selected as a header, retrieve the file from db row = self.currentIndex().row() - id = self.proxymodel.data(self.proxymodel.index(row, 0), Qt.ItemDataRole.UserRole) + id = self.proxymodel.data( + 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: @@ -887,7 +924,9 @@ class MusicTable(QTableView): """ # update the filepath if not filepath: - path = self.current_song_qmodel_index.siblingAtColumn(self.headers.user_headers.index("filepath")).data() + path = self.current_song_qmodel_index.siblingAtColumn( + self.headers.user_headers.index("filepath") + ).data() self.current_song_filepath: str = path else: self.current_song_filepath = filepath @@ -916,6 +955,10 @@ class MusicTable(QTableView): real_index: QModelIndex = self.proxymodel.mapToSource(index) self.selected_song_qmodel_index: QModelIndex = real_index + def set_search_string(self, text: str): + """set the search string""" + self.search_string = text + def load_qapp(self, qapp) -> None: """Necessary for using members and methods of main application window""" self.qapp = qapp diff --git a/components/SearchLineEdit.py b/components/SearchLineEdit.py new file mode 100644 index 0000000..434b24b --- /dev/null +++ b/components/SearchLineEdit.py @@ -0,0 +1,34 @@ +from PyQt5.QtWidgets import QLineEdit + +""" +MusicTable.py holds a variable called self.search_string +MusicTable.py had a function called load_music_table(), which loads data + from the SQLite database to the QTableView. load_music_table() + checks for the self.search_string + +in main.py, on self.lineEditSearch.textChanged(), +this updates the self.search_string in MusicTable.py + +in MusicTable.py, when Ctrl+F is pressed, the line edit gets hidden or visible +""" + + +class SearchLineEdit(QLineEdit): + def __init__(self, parent=None): + super().__init__(parent) + self.setVisible(False) + + def toggle_visibility(self): + if self.isHidden(): + self.setHidden(False) + else: + self.setHidden(True) + self.setText(None) + + # def toggle_visibility(self): + # if self.is_hidden: + # self.button.setVisible(True) + # self.is_hidden = False + # else: + # self.button.setVisible(False) + # self.is_hidden = True diff --git a/components/__init__.py b/components/__init__.py index 460c842..5eb37b5 100644 --- a/components/__init__.py +++ b/components/__init__.py @@ -12,3 +12,4 @@ from .ExportPlaylistWindow import ExportPlaylistWindow from .QuestionBoxDetails import QuestionBoxDetails from .HeaderTags import HeaderTags from .MediaPlayer import MediaPlayer +from .SearchLineEdit import SearchLineEdit diff --git a/main.py b/main.py index 973622d..6ef457f 100644 --- a/main.py +++ b/main.py @@ -18,6 +18,7 @@ from ui import Ui_MainWindow from PyQt5.QtWidgets import ( QFileDialog, QLabel, + QLineEdit, QMainWindow, QApplication, QGraphicsScene, @@ -41,7 +42,13 @@ from PyQt5.QtCore import ( QThreadPool, QRunnable, ) -from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioProbe, QMediaPlaylist, QMultimedia +from PyQt5.QtMultimedia import ( + QMediaPlayer, + QMediaContent, + QAudioProbe, + QMediaPlaylist, + QMultimedia, +) from PyQt5.QtGui import QClipboard, QCloseEvent, QFont, QPixmap, QResizeEvent from utils import ( delete_album_art, @@ -50,7 +57,7 @@ from utils import ( initialize_db, add_files_to_database, set_album_art, - id3_remap + id3_remap, ) from components import ( HeaderTags, @@ -253,13 +260,18 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # for drag & drop functionality self.tableView.viewport().installEventFilter(self) + # Search box + self.lineEditSearch: QLineEdit + ## CONNECTIONS + self.lineEditSearch.textChanged.connect(self.handle_search_box_text) # tableView self.tableView.playSignal.connect(self.play_audio_file) self.tableView.playPauseSignal.connect( self.on_play_clicked ) # Spacebar toggle play/pause signal self.tableView.handleProgressSignal.connect(self.handle_progress) + self.tableView.searchBoxSignal.connect(self.handle_search_box) self.tableView.playlistStatsSignal.connect( self.set_permanent_status_bar_message ) @@ -328,7 +340,9 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): file_url = media.canonicalUrl().toLocalFile() metadata = id3_remap(get_tags(file_url)[0]) if metadata is not None: - self.set_ui_metadata(metadata["title"], metadata["artist"], metadata["album"], file_url) + self.set_ui_metadata( + metadata["title"], metadata["artist"], metadata["album"], file_url + ) def on_volume_changed(self) -> None: """Handles volume changes""" @@ -374,8 +388,12 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): return row: int = index.row() prev_row: int = row - 1 - prev_index: QModelIndex = self.tableView.proxymodel.index(prev_row, index.column()) - prev_filepath = prev_index.siblingAtColumn(self.headers.user_headers.index("filepath")).data() + prev_index: QModelIndex = self.tableView.proxymodel.index( + prev_row, index.column() + ) + prev_filepath = prev_index.siblingAtColumn( + self.headers.user_headers.index("filepath") + ).data() if prev_filepath is None: return @@ -394,8 +412,12 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): return row: int = index.row() next_row: int = row + 1 - next_index: QModelIndex = self.tableView.proxymodel.index(next_row, index.column()) - next_filepath = next_index.siblingAtColumn(self.headers.user_headers.index("filepath")).data() + next_index: QModelIndex = self.tableView.proxymodel.index( + next_row, index.column() + ) + next_filepath = next_index.siblingAtColumn( + self.headers.user_headers.index("filepath") + ).data() if next_filepath is None: return self.play_audio_file(next_filepath) @@ -465,13 +487,22 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): else: self.status_bar.showMessage(message) + def handle_search_box(self): + """show or hide the searchbox""" + self.lineEditSearch.toggle_visibility() + + def handle_search_box_text(self, text: str): + """when text changes, update the music table thingie""" + self.tableView.set_search_string(text) + self.tableView.load_music_table(text) + def play_audio_file(self, filepath=None) -> None: """ Start playback of filepath & moves playback slider filepath default value = `tableView.current_song_filepath` """ - print('play audio file') + print("play audio file") if not filepath: filepath = self.tableView.get_selected_song_filepath() metadata = id3_remap(get_tags(filepath)[0]) @@ -487,7 +518,9 @@ class ApplicationWindow(QMainWindow, Ui_MainWindow): # assign "now playing" labels & album artwork if metadata is not None: - self.set_ui_metadata(metadata["title"], metadata["artist"], metadata["album"], filepath) + self.set_ui_metadata( + metadata["title"], metadata["artist"], metadata["album"], filepath + ) def set_ui_metadata(self, title, artist, album, filepath): """ diff --git a/ui.py b/ui.py index 9157209..d4b7711 100644 --- a/ui.py +++ b/ui.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'ui.ui' # -# Created by: PyQt5 UI code generator 5.15.10 +# Created by: PyQt5 UI code generator 5.15.11 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. @@ -24,6 +24,9 @@ class Ui_MainWindow(object): self.verticalLayout.setContentsMargins(-1, -1, 0, -1) self.verticalLayout.setSpacing(6) self.verticalLayout.setObjectName("verticalLayout") + self.lineEditSearch = SearchLineEdit(self.centralwidget) + self.lineEditSearch.setObjectName("lineEditSearch") + self.verticalLayout.addWidget(self.lineEditSearch) self.hLayoutHead = QtWidgets.QHBoxLayout() self.hLayoutHead.setSizeConstraint(QtWidgets.QLayout.SetFixedSize) self.hLayoutHead.setObjectName("hLayoutHead") @@ -57,13 +60,15 @@ class Ui_MainWindow(object): self.hLayoutMusicTable.setSizeConstraint(QtWidgets.QLayout.SetMaximumSize) self.hLayoutMusicTable.setContentsMargins(0, -1, 0, -1) self.hLayoutMusicTable.setObjectName("hLayoutMusicTable") + self.verticalLayout_2 = QtWidgets.QVBoxLayout() + self.verticalLayout_2.setObjectName("verticalLayout_2") self.playlistTreeView = PlaylistsPane(self.centralwidget) self.playlistTreeView.setObjectName("playlistTreeView") - self.hLayoutMusicTable.addWidget(self.playlistTreeView) + self.verticalLayout_2.addWidget(self.playlistTreeView) + self.hLayoutMusicTable.addLayout(self.verticalLayout_2) self.tableView = MusicTable(self.centralwidget) self.tableView.setObjectName("tableView") self.hLayoutMusicTable.addWidget(self.tableView) - self.hLayoutMusicTable.setStretch(0, 2) self.hLayoutMusicTable.setStretch(1, 10) self.verticalLayout.addLayout(self.hLayoutMusicTable) self.hLayoutCurrentSongDetails = QtWidgets.QHBoxLayout() @@ -185,7 +190,7 @@ class Ui_MainWindow(object): self.verticalLayout_3.setStretch(0, 20) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1152, 24)) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1152, 21)) self.menubar.setObjectName("menubar") self.menuFile = QtWidgets.QMenu(self.menubar) self.menuFile.setObjectName("menuFile") @@ -252,5 +257,5 @@ class Ui_MainWindow(object): self.actionDeleteDatabase.setText(_translate("MainWindow", "Delete Database")) self.actionNewPlaylist.setText(_translate("MainWindow", "New playlist")) self.actionExportPlaylist.setText(_translate("MainWindow", "Export playlist")) -from components import AlbumArtGraphicsView, MusicTable, PlaylistsPane +from components import AlbumArtGraphicsView, MusicTable, PlaylistsPane, SearchLineEdit from pyqtgraph import PlotWidget diff --git a/ui.ui b/ui.ui index 625aec1..7b23071 100644 --- a/ui.ui +++ b/ui.ui @@ -26,6 +26,9 @@ 0 + + + @@ -73,7 +76,7 @@ - + QLayout::SetMaximumSize @@ -84,7 +87,11 @@ 0 - + + + + + @@ -311,7 +318,7 @@ 0 0 1152 - 24 + 21 @@ -414,6 +421,11 @@ QTreeView
components
+ + SearchLineEdit + QLineEdit +
components
+