From dfc1344c69d5dccbb3528d5a414708989f8faf96 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sun, 14 Aug 2022 10:25:10 +0100 Subject: [PATCH] Insert track working --- app/models.py | 66 ++++++----- app/musicmuster.py | 233 +++++++++++++++++++++------------------ app/playlists.py | 103 ++++------------- app/ui/main_window.ui | 13 ++- app/ui/main_window_ui.py | 21 ++-- 5 files changed, 202 insertions(+), 234 deletions(-) diff --git a/app/models.py b/app/models.py index fdcff78..ba69322 100644 --- a/app/models.py +++ b/app/models.py @@ -448,17 +448,17 @@ class PlaylistRows(Base): f"note={self.note} row_number={self.row_number}>" ) -# def __init__( -# self, session: Session, playlist_id: int, track_id: int, -# row: int) -> None: -# log.debug(f"xPlaylistTracks.__init__({playlist_id=}, {track_id=}, {row=})") -# -# self.playlist_id = playlist_id -# self.track_id = track_id -# self.row = row -# session.add(self) -# session.flush() -# + def __init__( + self, session: Session, playlist_id: int, track_id: int, + row_number: int) -> None: + """Create PlaylistRows object""" + + self.playlist_id = playlist_id + self.track_id = track_id + self.row_number = row_number + session.add(self) + session.flush() + @staticmethod def delete_higher_rows(session: Session, playlist_id: int, row: int) \ -> None: @@ -806,23 +806,33 @@ class Tracks(Base): # session.flush() # except IntegrityError as exception: # log.error(f"Can't remove track with {path=} ({exception=})") -# -# @classmethod -# def search_artists(cls, session: Session, text: str) -> List["Tracks"]: -# -# return ( -# session.query(cls) -# .filter(cls.artist.ilike(f"%{text}%")) -# .order_by(cls.title) -# ).all() -# -# @classmethod -# def search_titles(cls, session: Session, text: str) -> List["Tracks"]: -# return ( -# session.query(cls) -# .filter(cls.title.ilike(f"%{text}%")) -# .order_by(cls.title) -# ).all() + + @classmethod + def search_artists(cls, session: Session, text: str) -> List["Tracks"]: + """Search case-insenstively for artists containing str""" + + return ( + session.execute( + select(cls) + .where(cls.artist.ilike(f"%{text}%")) + .order_by(cls.title) + ) + .scalars() + .all() + ) + + @classmethod + def search_titles(cls, session: Session, text: str) -> List["Tracks"]: + """Search case-insenstively for titles containing str""" + return ( + session.execute( + select(cls) + .where(cls.title.ilike(f"%{text}%")) + .order_by(cls.title) + ) + .scalars() + .all() + ) # # @staticmethod # def update_lastplayed(session: Session, track_id: int) -> None: diff --git a/app/musicmuster.py b/app/musicmuster.py index 32260ab..4815941 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -36,7 +36,7 @@ from models import ( ) from playlists import PlaylistTab from sqlalchemy.orm.exc import DetachedInstanceError -# from ui.dlg_search_database_ui import Ui_Dialog +from ui.dlg_search_database_ui import Ui_Dialog # type: ignore from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore from ui.downloadcsv_ui import Ui_DateSelect # type: ignore from config import Config @@ -148,7 +148,7 @@ class Window(QMainWindow, Ui_MainWindow): event.accept() def connect_signals_slots(self) -> None: - # self.actionAdd_note.triggered.connect(self.create_note) + # self.actionInsertSectionHeader.triggered.connect(self.insert_header) self.action_Clear_selection.triggered.connect(self.clear_selection) self.actionClosePlaylist.triggered.connect(self.close_playlist_tab) self.actionDownload_CSV_of_played_tracks.triggered.connect( @@ -163,7 +163,7 @@ class Window(QMainWindow, Ui_MainWindow): self.actionOpenPlaylist.triggered.connect(self.open_playlist) self.actionPlay_next.triggered.connect(self.play_next) # self.actionSearch.triggered.connect(self.search_playlist) -# self.actionSearch_database.triggered.connect(self.search_database) + self.actionInsertTrack.triggered.connect(self.insert_track) self.actionSelect_next_track.triggered.connect(self.select_next_row) # self.actionSelect_played_tracks.triggered.connect(self.select_played) self.actionSelect_previous_track.triggered.connect( @@ -214,7 +214,7 @@ class Window(QMainWindow, Ui_MainWindow): self.tabPlaylist.widget(tab_index).close() self.tabPlaylist.removeTab(tab_index) # -# def create_note(self) -> None: +# def insert_header(self) -> None: # """Call playlist to create note""" # # try: @@ -607,12 +607,12 @@ class Window(QMainWindow, Ui_MainWindow): self.label_end_time.setText( end_at.strftime(Config.TRACK_TIME_FORMAT)) -# def search_database(self) -> None: -# """Show dialog box to select and cue track from database""" -# -# with Session() as session: -# dlg = DbDialog(self, session) -# dlg.exec() + def insert_track(self) -> None: + """Show dialog box to select and add track from database""" + + with Session() as session: + dlg = DbDialog(self, session) + dlg.exec() # # def search_playlist(self): # """Show text box to search playlist""" @@ -881,104 +881,121 @@ class Window(QMainWindow, Ui_MainWindow): ) except AttributeError: self.hdrNextTrack.setText("") -# -# -# class DbDialog(QDialog): -# """Select track from database""" -# -# def __init__(self, parent, session): # review -# super().__init__(parent) -# self.session = session -# self.ui = Ui_Dialog() -# self.ui.setupUi(self) -# self.ui.btnAdd.clicked.connect(self.add_selected) -# self.ui.btnAddClose.clicked.connect(self.add_selected_and_close) -# self.ui.btnClose.clicked.connect(self.close) -# self.ui.matchList.itemDoubleClicked.connect(self.double_click) -# self.ui.matchList.itemSelectionChanged.connect(self.selection_changed) -# self.ui.radioTitle.toggled.connect(self.title_artist_toggle) -# self.ui.searchString.textEdited.connect(self.chars_typed) -# -# record = Settings.get_int_settings(self.session, "dbdialog_width") -# width = record.f_int or 800 -# record = Settings.get_int_settings(self.session, "dbdialog_height") -# height = record.f_int or 600 -# self.resize(width, height) -# -# def __del__(self): # review -# record = Settings.get_int_settings(self.session, "dbdialog_height") -# if record.f_int != self.height(): -# record.update(self.session, {'f_int': self.height()}) -# -# record = Settings.get_int_settings(self.session, "dbdialog_width") -# if record.f_int != self.width(): -# record.update(self.session, {'f_int': self.width()}) -# -# def add_selected(self): # review -# if not self.ui.matchList.selectedItems(): -# return -# -# item = self.ui.matchList.currentItem() -# track = item.data(Qt.UserRole) -# self.add_track(track) -# -# def add_selected_and_close(self): # review -# self.add_selected() -# self.close() -# -# def title_artist_toggle(self): # review -# """ -# Handle switching between searching for artists and searching for -# titles -# """ -# -# # Logic is handled already in chars_typed(), so just call that. -# self.chars_typed(self.ui.searchString.text()) -# -# def chars_typed(self, s): # review -# if len(s) > 0: -# if self.ui.radioTitle.isChecked(): -# matches = Tracks.search_titles(self.session, s) -# else: -# matches = Tracks.search_artists(self.session, s) -# self.ui.matchList.clear() -# if matches: -# for track in matches: -# t = QListWidgetItem() -# t.setText( -# f"{track.title} - {track.artist} " -# f"[{helpers.ms_to_mmss(track.duration)}] " -# f"({helpers.get_relative_date(track.lastplayed)})" -# ) -# t.setData(Qt.UserRole, track) -# self.ui.matchList.addItem(t) -# -# def double_click(self, entry): # review -# track = entry.data(Qt.UserRole) -# self.add_track(track) -# # Select search text to make it easier for next search -# self.select_searchtext() -# -# def add_track(self, track): # review -# # Add to playlist on screen -# self.parent().visible_playlist_tab().insert_track( -# self.session, track) -# # Commit session to get correct row numbers if more tracks added -# self.session.commit() -# # Select search text to make it easier for next search -# self.select_searchtext() -# -# def select_searchtext(self): # review -# self.ui.searchString.selectAll() -# self.ui.searchString.setFocus() -# -# def selection_changed(self): # review -# if not self.ui.matchList.selectedItems(): -# return -# -# item = self.ui.matchList.currentItem() -# track = item.data(Qt.UserRole) -# self.ui.dbPath.setText(track.path) + + +class DbDialog(QDialog): + """Select track from database""" + + def __init__(self, parent: QMainWindow, session: Session) -> None: + """Subclassed QDialog to manage track selection""" + + super().__init__(parent) + self.session = session + self.ui = Ui_Dialog() + self.ui.setupUi(self) + self.ui.btnAdd.clicked.connect(self.add_selected) + self.ui.btnAddClose.clicked.connect(self.add_selected_and_close) + self.ui.btnClose.clicked.connect(self.close) + self.ui.matchList.itemDoubleClicked.connect(self.double_click) + self.ui.matchList.itemSelectionChanged.connect(self.selection_changed) + self.ui.radioTitle.toggled.connect(self.title_artist_toggle) + self.ui.searchString.textEdited.connect(self.chars_typed) + + record = Settings.get_int_settings(self.session, "dbdialog_width") + width = record.f_int or 800 + record = Settings.get_int_settings(self.session, "dbdialog_height") + height = record.f_int or 600 + self.resize(width, height) + + def __del__(self) -> None: + """Save dialog size and position""" + + record = Settings.get_int_settings(self.session, "dbdialog_height") + if record.f_int != self.height(): + record.update(self.session, {'f_int': self.height()}) + + record = Settings.get_int_settings(self.session, "dbdialog_width") + if record.f_int != self.width(): + record.update(self.session, {'f_int': self.width()}) + + def add_selected(self) -> None: + """Handle Add button""" + + if not self.ui.matchList.selectedItems(): + return + + item = self.ui.matchList.currentItem() + track = item.data(Qt.UserRole) + self.add_track(track) + + def add_selected_and_close(self) -> None: + """Handle Add and Close button""" + + self.add_selected() + self.close() + + def title_artist_toggle(self) -> None: + """ + Handle switching between searching for artists and searching for + titles + """ + + # Logic is handled already in chars_typed(), so just call that. + self.chars_typed(self.ui.searchString.text()) + + def chars_typed(self, s: str) -> None: + """Handle text typed in search box""" + + self.ui.matchList.clear() + if len(s) > 1: + if self.ui.radioTitle.isChecked(): + matches = Tracks.search_titles(self.session, s) + else: + matches = Tracks.search_artists(self.session, s) + if matches: + for track in matches: + last_played = Playdates.last_played(self.session, track.id) + t = QListWidgetItem() + t.setText( + f"{track.title} - {track.artist} " + f"[{helpers.ms_to_mmss(track.duration)}] " + f"({helpers.get_relative_date(last_played)})" + ) + t.setData(Qt.UserRole, track) + self.ui.matchList.addItem(t) + + def double_click(self, entry: QListWidgetItem) -> None: + """Add items that are double-clicked""" + + track = entry.data(Qt.UserRole) + self.add_track(track) + # Select search text to make it easier for next search + self.select_searchtext() + + def add_track(self, track: Tracks) -> None: + """Add passed track to playlist on screen""" + + self.parent().visible_playlist_tab().insert_track(self.session, track) + # Commit session to get correct row numbers if more tracks added + self.session.commit() + # Select search text to make it easier for next search + self.select_searchtext() + + def select_searchtext(self) -> None: + """Select the searchbox""" + + self.ui.searchString.selectAll() + self.ui.searchString.setFocus() + + def selection_changed(self) -> None: + """Display selected track path in dialog box""" + + if not self.ui.matchList.selectedItems(): + return + + item = self.ui.matchList.currentItem() + track = item.data(Qt.UserRole) + self.ui.dbPath.setText(track.path) class DownloadCSV(QDialog): diff --git a/app/playlists.py b/app/playlists.py index 5f5f946..d345cd1 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -337,15 +337,6 @@ class PlaylistTab(QTableWidget): # closeEditor() # _cell_edit_ended() - - # def _edit_note_cell(self, row: int, column: int): # review - # """Called when table is single-clicked""" - - # print(f"_edit_note_cell({row=}, {column=}") - # # if column in [FIXUP.COL_ROW_NOTES]: - # # item = self.item(row, column) - # # self.editItem(item) - def _cell_changed(self, row: int, column: int) -> None: """Called when cell content has changed""" @@ -607,80 +598,26 @@ class PlaylistTab(QTableWidget): if repaint: self.save_playlist(session) self.update_display(session, clear_selection=False) -# -# def insert_track(self, session: Session, track: Tracks, -# repaint: bool = True) -> None: -# """ -# Insert track into playlist tab. -# -# If a row is selected, add track above. Otherwise, add to end of -# playlist. -# """ -# -# if self.selectionModel().hasSelection(): -# row = self.currentRow() -# else: -# row = self.rowCount() -# log.debug( -# f"playlists.insert_track({session=}, {track=}, {repaint=}), " -# f"{row=}" -# ) -# -# self.insertRow(row) -# -# # Put an item in COL_USERDATA for later -# item: QTableWidgetItem = QTableWidgetItem() -# # Add row metadata -# item.setData(self.ROW_FLAGS, 0) -# self.setItem(row, FIXUP.COL_USERDATA, item) -# -# # Add track details to columns -# mss_item: QTableWidgetItem = QTableWidgetItem(str(track.start_gap)) -# if track.start_gap and track.start_gap >= 500: -# mss_item.setBackground(QColor(Config.COLOUR_LONG_START)) -# self.setItem(row, FIXUP.COL_MSS, mss_item) -# -# title_item: QTableWidgetItem = QTableWidgetItem(track.title) -# self.setItem(row, FIXUP.COL_TITLE, title_item) -# -# artist_item: QTableWidgetItem = QTableWidgetItem(track.artist) -# self.setItem(row, FIXUP.COL_ARTIST, artist_item) -# -# duration_item: QTableWidgetItem = QTableWidgetItem( -# ms_to_mmss(track.duration) -# ) -# self._set_row_duration(row, track.duration) -# self.setItem(row, FIXUP.COL_DURATION, duration_item) -# -# last_playtime: Optional[datetime] = Playdates.last_played( -# session, track.id) -# last_played_str: str = get_relative_date(last_playtime) -# last_played_item: QTableWidgetItem = QTableWidgetItem(last_played_str) -# self.setItem(row, FIXUP.COL_LAST_PLAYED, last_played_item) -# -# row_note: Optional[str] = "Play text" -# row_note_item: QTableWidgetItem = QTableWidgetItem(row_note) -# self.setItem(row, FIXUP.COL_ROW_NOTES, row_note_item) -# -# # Add empty start and stop time because background -# # colour won't be set for columns without items -# start_item: QTableWidgetItem = QTableWidgetItem() -# self.setItem(row, FIXUP.COL_START_TIME, start_item) -# stop_item: QTableWidgetItem = QTableWidgetItem() -# self.setItem(row, FIXUP.COL_END_TIME, stop_item) -# -# # Attach track.id object to row -# self._set_row_content(row, track.id) -# -# # Mark track if file is unreadable -# if not file_is_readable(track.path): -# self._set_unreadable_row(row) -# # Scroll to new row -# self.scrollToItem(title_item, QAbstractItemView.PositionAtCenter) -# -# if repaint: -# self.save_playlist(session) -# self.update_display(session, clear_selection=False) + + def insert_track(self, session: Session, track: Tracks, + repaint: bool = True) -> None: + """ + Insert track into playlist tab. + + If a row is selected, add track above. Otherwise, add to end of + playlist. + + We simply build a PlaylistRows object and pass it to insert_row() + to do the heavy lifing. + """ + + # PlaylistRows object requires a row number, but that number + # can be reset by calling PlaylistRows.fixup_rownumbers() later, + # so just fudge a row number for now. + row_number = 0 + plr = PlaylistRows(session, self.playlist_id, track.id, row_number) + self.insert_row(session, plr) + PlaylistRows.fixup_rownumbers(session, self.playlist_id) # # def move_selected_to_playlist(self, session: Session, playlist_id: int) \ # -> None: diff --git a/app/ui/main_window.ui b/app/ui/main_window.ui index 67d0a05..130ac5c 100644 --- a/app/ui/main_window.ui +++ b/app/ui/main_window.ui @@ -780,12 +780,12 @@ border: 1px solid rgb(85, 87, 83); - + + - @@ -828,7 +828,7 @@ border: 1px solid rgb(85, 87, 83); Ctrl+Alt+Return - + ../../../../.designer/backup/icon_search_database.png../../../../.designer/backup/icon_search_database.png @@ -837,7 +837,7 @@ border: 1px solid rgb(85, 87, 83); Insert &track... - Ctrl+D + Ctrl+T @@ -1030,10 +1030,13 @@ border: 1px solid rgb(85, 87, 83); / - + Insert &section header... + + Ctrl+H + diff --git a/app/ui/main_window_ui.py b/app/ui/main_window_ui.py index 029521f..1b19c02 100644 --- a/app/ui/main_window_ui.py +++ b/app/ui/main_window_ui.py @@ -366,11 +366,11 @@ class Ui_MainWindow(object): icon4.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.actionSkipToNext.setIcon(icon4) self.actionSkipToNext.setObjectName("actionSkipToNext") - self.actionInsert = QtWidgets.QAction(MainWindow) + self.actionInsertTrack = QtWidgets.QAction(MainWindow) icon5 = QtGui.QIcon() icon5.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.actionInsert.setIcon(icon5) - self.actionInsert.setObjectName("actionInsert") + self.actionInsertTrack.setIcon(icon5) + self.actionInsertTrack.setObjectName("actionInsertTrack") self.actionAdd_file = QtWidgets.QAction(MainWindow) icon6 = QtGui.QIcon() icon6.addPixmap(QtGui.QPixmap("app/ui/../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) @@ -440,8 +440,8 @@ class Ui_MainWindow(object): self.actionDownload_CSV_of_played_tracks.setObjectName("actionDownload_CSV_of_played_tracks") self.actionSearch = QtWidgets.QAction(MainWindow) self.actionSearch.setObjectName("actionSearch") - self.actionInsert_section_header = QtWidgets.QAction(MainWindow) - self.actionInsert_section_header.setObjectName("actionInsert_section_header") + self.actionInsertSectionHeader = QtWidgets.QAction(MainWindow) + self.actionInsertSectionHeader.setObjectName("actionInsertSectionHeader") self.actionRemove = QtWidgets.QAction(MainWindow) self.actionRemove.setObjectName("actionRemove") self.menuFile.addAction(self.actionNewPlaylist) @@ -464,12 +464,12 @@ class Ui_MainWindow(object): self.menuPlaylist.addSeparator() self.menuPlaylist.addAction(self.actionSkipToNext) self.menuPlaylist.addSeparator() - self.menuPlaylist.addAction(self.actionInsert) + self.menuPlaylist.addAction(self.actionInsertSectionHeader) + self.menuPlaylist.addAction(self.actionInsertTrack) self.menuPlaylist.addAction(self.actionRemove) self.menuPlaylist.addAction(self.actionImport) self.menuPlaylist.addAction(self.actionSetNext) self.menuPlaylist.addAction(self.action_Clear_selection) - self.menuPlaylist.addAction(self.actionInsert_section_header) self.menuPlaylist.addSeparator() self.menuPlaylist.addAction(self.actionSearch) self.menuPlaylist.addAction(self.actionSelect_next_track) @@ -518,8 +518,8 @@ class Ui_MainWindow(object): self.actionPlay_next.setShortcut(_translate("MainWindow", "Return")) self.actionSkipToNext.setText(_translate("MainWindow", "Skip to &next")) self.actionSkipToNext.setShortcut(_translate("MainWindow", "Ctrl+Alt+Return")) - self.actionInsert.setText(_translate("MainWindow", "Insert &track...")) - self.actionInsert.setShortcut(_translate("MainWindow", "Ctrl+D")) + self.actionInsertTrack.setText(_translate("MainWindow", "Insert &track...")) + self.actionInsertTrack.setShortcut(_translate("MainWindow", "Ctrl+T")) self.actionAdd_file.setText(_translate("MainWindow", "Add &file")) self.actionAdd_file.setShortcut(_translate("MainWindow", "Ctrl+F")) self.actionFade.setText(_translate("MainWindow", "F&ade")) @@ -557,7 +557,8 @@ class Ui_MainWindow(object): self.actionDownload_CSV_of_played_tracks.setText(_translate("MainWindow", "Download CSV of played tracks...")) self.actionSearch.setText(_translate("MainWindow", "Search...")) self.actionSearch.setShortcut(_translate("MainWindow", "/")) - self.actionInsert_section_header.setText(_translate("MainWindow", "Insert §ion header...")) + self.actionInsertSectionHeader.setText(_translate("MainWindow", "Insert §ion header...")) + self.actionInsertSectionHeader.setShortcut(_translate("MainWindow", "Ctrl+H")) self.actionRemove.setText(_translate("MainWindow", "&Remove track")) from infotabs import InfoTabs import icons_rc