From aa6ab03555aee5db22b5887ad02ff9500865ae4b Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Fri, 28 Feb 2025 11:25:29 +0000 Subject: [PATCH] Make manage queries and manage templates into classes --- app/classes.py | 1 + app/helpers.py | 19 +- app/menu.yaml | 4 +- app/musicmuster.py | 505 +++++++++++++++++++++++++-------------------- 4 files changed, 300 insertions(+), 229 deletions(-) diff --git a/app/classes.py b/app/classes.py index 1120e73..aac3c94 100644 --- a/app/classes.py +++ b/app/classes.py @@ -80,6 +80,7 @@ class FileErrors(NamedTuple): @dataclass class Filter: + version: int = 1 path_type: str = "contains" path: Optional[str] = None last_played_number: Optional[int] = None diff --git a/app/helpers.py b/app/helpers.py index 0747af3..2971c08 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -10,7 +10,7 @@ import ssl import tempfile # PyQt imports -from PyQt6.QtWidgets import QMainWindow, QMessageBox, QWidget +from PyQt6.QtWidgets import QInputDialog, QMainWindow, QMessageBox, QWidget # Third party imports from mutagen.flac import FLAC # type: ignore @@ -150,6 +150,23 @@ def get_audio_metadata(filepath: str) -> AudioMetadata: ) +def get_name(prompt: str, default: str = "") -> str | None: + """Get a name from the user""" + + dlg = QInputDialog() + dlg.setInputMode(QInputDialog.InputMode.TextInput) + dlg.setLabelText(prompt) + while True: + if default: + dlg.setTextValue(default) + dlg.resize(500, 100) + ok = dlg.exec() + if ok: + return dlg.textValue() + + return None + + def get_relative_date( past_date: Optional[dt.datetime], reference_date: Optional[dt.datetime] = None ) -> str: diff --git a/app/menu.yaml b/app/menu.yaml index f9787eb..4e728a0 100644 --- a/app/menu.yaml +++ b/app/menu.yaml @@ -4,10 +4,10 @@ menus: - text: "Save as Template" handler: "save_as_template" - text: "Manage Templates" - handler: "manage_templates" + handler: "manage_templates_wrapper" - separator: true - text: "Manage Queries" - handler: "manage_queries" + handler: "manage_queries_wrapper" - separator: true - text: "Exit" handler: "close" diff --git a/app/musicmuster.py b/app/musicmuster.py index 32ef292..bde7fbe 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -68,7 +68,7 @@ from classes import ( from config import Config from dialogs import TrackSelectDialog from file_importer import FileImporter -from helpers import file_is_unreadable +from helpers import file_is_unreadable, get_name from log import log from models import db, Playdates, PlaylistRows, Playlists, Queries, Settings, Tracks from music_manager import RowAndTrack, track_sequence @@ -270,7 +270,7 @@ class FilterDialog(QDialog): self.ok_button = QPushButton("OK") self.cancel_button = QPushButton("Cancel") self.cancel_button.clicked.connect(self.reject) - self.ok_button.clicked.connect(self.accept) + self.ok_button.clicked.connect(self.ok_clicked) button_layout.addWidget(self.ok_button) button_layout.addWidget(self.cancel_button) layout.addLayout(button_layout) @@ -278,7 +278,9 @@ class FilterDialog(QDialog): self.setLayout(layout) # Connect signals - self.last_played_combo.currentIndexChanged.connect(self.toggle_last_played_controls) + self.last_played_combo.currentIndexChanged.connect( + self.toggle_last_played_controls + ) self.toggle_last_played_controls() @@ -287,6 +289,21 @@ class FilterDialog(QDialog): self.last_played_spinbox.setDisabled(disabled) self.last_played_unit.setDisabled(disabled) + def ok_clicked(self) -> None: + """ + Update filter to match selections + """ + self.filter.path_type = self.path_combo.currentText() + self.filter.path = self.path_text.text() + self.filter.last_played_number = self.last_played_spinbox.value() + self.filter.last_played_type = self.last_played_combo.currentText() + self.filter.last_played_unit = self.last_played_unit.currentText() + self.filter.duration_type = self.duration_combo.currentText() + self.filter.duration_number = self.duration_spinbox.value() + self.filter.duration_unit = self.duration_unit.currentText() + + self.accept() + @dataclass class ItemlistItem: @@ -296,18 +313,14 @@ class ItemlistItem: class ItemlistManager(QDialog): - def __init__( - self, items: list[ItemlistItem], callbacks: ItemlistManagerCallbacks - ) -> None: + def __init__(self) -> None: super().__init__() self.setWindowTitle("Item Manager") self.setMinimumSize(600, 400) - self.items = items - self.callbacks = callbacks - layout = QVBoxLayout(self) - self.table = QTableWidget(len(items), 2, self) + self.table = QTableWidget(self) + self.table.setColumnCount(2) self.table.setHorizontalHeaderLabels(["Item", "Actions"]) hh = self.table.horizontalHeader() if not hh: @@ -316,8 +329,6 @@ class ItemlistManager(QDialog): self.table.setColumnWidth(0, 288) self.table.setColumnWidth(1, 300) - self.populate_table() - layout.addWidget(self.table) button_layout = QHBoxLayout() @@ -331,8 +342,10 @@ class ItemlistManager(QDialog): layout.addLayout(button_layout) - def populate_table(self) -> None: + def populate_table(self, items: list[ItemlistItem]) -> None: """Populates the table with items and action buttons.""" + + self.items = items self.table.setRowCount(len(self.items)) for row, item in enumerate(self.items): @@ -371,25 +384,35 @@ class ItemlistManager(QDialog): self.table.setCellWidget(row, 1, widget) def delete_item(self, item_id: int) -> None: - self.callbacks.delete(item_id) + """Subclass must implement this method""" + raise NotImplementedError def edit_item(self, item_id: int) -> None: - self.callbacks.edit(item_id) + """Subclass must implement this method""" + raise NotImplementedError + + def new_item(self) -> None: + """Subclass must implement this method""" + raise NotImplementedError def rename_item(self, item_id: int) -> None: - new_name = self.callbacks.rename(item_id) - if not new_name: - return - # Rename item in list + """Subclass must implement this method""" + raise NotImplementedError + + def change_text(self, item_id: int, new_text: str) -> None: + """ + Update text for one row + """ + for row in range(self.table.rowCount()): item = self.table.item(row, 0) if item and self.items[row].id == item_id: - item.setText(new_name) - self.items[row].name = new_name + item.setText(new_text) + self.items[row].name = new_text break def toggle_favourite(self, item_id: int, checked: bool) -> None: - self.callbacks.favourite(item_id, checked) + """Subclass must udpate database if required""" for row in range(self.table.rowCount()): item = self.table.item(row, 0) @@ -402,8 +425,230 @@ class ItemlistManager(QDialog): self.items[row].favourite = checked break + +class ManageQueries(ItemlistManager): + """ + Delete / edit queries + """ + + def __init__(self, session: Session, musicmuster: Window) -> None: + super().__init__() + + self.session = session + self.musicmuster = musicmuster + self.refresh_table() + self.exec() + + def refresh_table(self) -> None: + """ + Update table in widget + """ + + # Build a list of queries + query_list: list[ItemlistItem] = [] + + for query in Queries.get_all_queries(self.session): + query_list.append( + ItemlistItem(name=query.name, id=query.id, favourite=query.favourite) + ) + + self.populate_table(query_list) + + def delete_item(self, query_id: int) -> None: + """delete query""" + + query = self.session.get(Queries, query_id) + if not query: + raise ApplicationError( + f"manage_template.delete({query_id=}) can't load query" + ) + if helpers.ask_yes_no( + "Delete query", + f"Delete query '{query.name}': " "Are you sure?", + ): + log.debug(f"manage_queries: delete {query=}") + self.session.delete(query) + self.session.commit() + + self.refresh_table() + + def _edit_item(self, query: Queries) -> None: + """Edit query""" + + dlg = FilterDialog(query.name, query.filter) + if dlg.exec(): + query.filter = dlg.filter + self.session.commit() + + def edit_item(self, query_id: int) -> None: + """Edit query""" + + query = self.session.get(Queries, query_id) + if not query: + raise ApplicationError( + f"manage_template.edit_item({query_id=}) can't load query" + ) + return self._edit_item(query) + + def toggle_favourite(self, query_id: int, favourite: bool) -> None: + """Mark query as (not) favourite""" + + query = self.session.get(Queries, query_id) + if not query: + return + query.favourite = favourite + self.session.commit() + def new_item(self) -> None: - self.callbacks.new_item() + """Create new query""" + + query_name = get_name(prompt="New query name:") + if not query_name: + return + + query = Queries(self.session, query_name, Filter()) + self._edit_item(query) + self.refresh_table() + + def rename_item(self, query_id: int) -> None: + """rename query""" + + query = self.session.get(Queries, query_id) + if not query: + raise ApplicationError( + f"manage_template.delete({query_id=}) can't load query" + ) + new_name = get_name(prompt="New query name", default=query.name) + if not new_name: + return + + query.name = new_name + self.session.commit() + + self.change_text(query_id, new_name) + + +class ManageTemplates(ItemlistManager): + """ + Delete / edit templates + """ + + def __init__(self, session: Session, musicmuster: Window) -> None: + super().__init__() + + self.session = session + self.musicmuster = musicmuster + self.refresh_table() + self.exec() + + def refresh_table(self) -> None: + """ + Update table in widget + """ + + # Build a list of templates + template_list: list[ItemlistItem] = [] + + for template in Playlists.get_all_templates(self.session): + template_list.append( + ItemlistItem( + name=template.name, id=template.id, favourite=template.favourite + ) + ) + + self.populate_table(template_list) + + def delete_item(self, template_id: int) -> None: + """delete template""" + + template = self.session.get(Playlists, template_id) + if not template: + raise ApplicationError( + f"manage_template.delete({template_id=}) can't load template" + ) + if helpers.ask_yes_no( + "Delete template", + f"Delete template '{template.name}': Are you sure?", + ): + # If template is currently open, re-check + open_idx = self.musicmuster.get_tab_index_for_playlist(template_id) + if open_idx: + if not helpers.ask_yes_no( + "Delete open template", + f"Template '{template.name}' is currently open. Really delete?", + ): + return + else: + self.musicmuster.playlist_section.tabPlaylist.removeTab(open_idx) + + log.debug(f"manage_templates: delete {template=}") + self.session.delete(template) + self.session.commit() + + def edit_item(self, template_id: int) -> None: + """Edit template""" + + template = self.session.get(Playlists, template_id) + if not template: + raise ApplicationError( + f"manage_template.edit({template_id=}) can't load template" + ) + # Simply load the template as a playlist. Any changes + # made will persist + self.musicmuster._open_playlist(template, is_template=True) + + def toggle_favourite(self, template_id: int, favourite: bool) -> None: + """Mark template as (not) favourite""" + + template = self.session.get(Playlists, template_id) + if not template: + return + template.favourite = favourite + self.session.commit() + + def new_item( + self, + ) -> None: + """Create new template""" + + # Get base template + template_id = self.musicmuster.solicit_template_to_use( + self.session, template_prompt="New template based upon:" + ) + if template_id is None: + return + + # Get new template name + name = self.musicmuster.get_playlist_name( + self.session, default="", prompt="New template name:" + ) + if not name: + return + + # Create playlist for template and mark is as a template + template = self.musicmuster._create_playlist(self.session, name, template_id) + template.is_template = True + self.session.commit() + + # Open it for editing + self.musicmuster._open_playlist(template, is_template=True) + + def rename_item(self, template_id: int) -> None: + """rename template""" + + template = self.session.get(Playlists, template_id) + if not template: + raise ApplicationError( + f"manage_template.delete({template_id=}) can't load template" + ) + new_name = self.musicmuster.get_playlist_name(self.session, template.name) + if not new_name: + return + + template.name = new_name + self.session.commit() + + self.change_text(template_id, new_name) @dataclass @@ -884,7 +1129,7 @@ class Window(QMainWindow): }, { "separator": True, - } + }, ] templates = Playlists.get_favourite_templates(session) for template in templates: @@ -965,7 +1210,7 @@ class Window(QMainWindow): else: template_id = selected_template_id - playlist_name = self.solicit_name(session) + playlist_name = self.get_playlist_name(session) if not playlist_name: return @@ -1030,10 +1275,10 @@ class Window(QMainWindow): session.commit() helpers.show_OK("Template", "Template saved", self) - def solicit_name( + def get_playlist_name( self, session: Session, default: str = "", prompt: str = "Playlist name:" ) -> Optional[str]: - """Get name of new playlist from user""" + """Get a name from the user""" dlg = QInputDialog(self) dlg.setInputMode(QInputDialog.InputMode.TextInput) @@ -1080,213 +1325,21 @@ class Window(QMainWindow): # # # # # # # # # # Manage templates and queries # # # # # # # # # # - def manage_queries(self) -> None: + def manage_queries_wrapper(self): """ - Delete / edit queries + Simply instantiate the manage_queries class """ - # Define callbacks to handle management options - def delete(query_id: int) -> None: - """delete query""" - - query = session.get(Queries, query_id) - if not query: - raise ApplicationError( - f"manage_template.delete({query_id=}) can't load query" - ) - if helpers.ask_yes_no( - "Delete query", - f"Delete query '{query.name}': " "Are you sure?", - ): - log.info(f"manage_queries: delete {query=}") - session.delete(query) - session.commit() - - def edit(query_id: int) -> None: - """Edit query""" - - query = session.get(Queries, query_id) - if not query: - raise ApplicationError( - f"manage_template.edit({query_id=}) can't load query" - ) - import pdb; pdb.set_trace() - dlg = FilterDialog(query.name, query.filter) - dlg.show() - - def favourite(query_id: int, favourite: bool) -> None: - """Mark query as (not) favourite""" - - query = session.get(Queries, query_id) - query.favourite = favourite - session.commit() - - def new_item() -> None: - """Create new query""" - - # TODO: create query - print("create query") - - def rename(query_id: int) -> Optional[str]: - """rename query""" - - query = session.get(Queries, query_id) - if not query: - raise ApplicationError( - f"manage_template.delete({query_id=}) can't load query" - ) - new_name = self.solicit_name(session, query.name, prompt="New query name") - if new_name: - query.rename(session, new_name) - idx = self.tabBar.currentIndex() - self.tabBar.setTabText(idx, new_name) - session.commit() - return new_name - - return None - - # Call listitem management dialog to manage queries - callbacks = ItemlistManagerCallbacks( - delete=delete, - edit=edit, - favourite=favourite, - new_item=new_item, - rename=rename, - ) - - # Build a list of queries - query_list: list[ItemlistItem] = [] - with db.Session() as session: - for query in Queries.get_all_queries(session): - query_list.append( - ItemlistItem( - name=query.name, id=query.id, favourite=query.favourite - ) - ) - # We need to retain a reference to the dialog box to stop it - # going out of scope and being garbage-collected. - self.dlg = ItemlistManager(query_list, callbacks) - self.dlg.show() + _ = ManageQueries(session, self) - def manage_templates(self) -> None: + def manage_templates_wrapper(self): """ - Delete / edit templates + Simply instantiate the manage_queries class """ - # Define callbacks to handle management options - def delete(template_id: int) -> None: - """delete template""" - - template = session.get(Playlists, template_id) - if not template: - raise ApplicationError( - f"manage_template.delete({template_id=}) can't load template" - ) - if helpers.ask_yes_no( - "Delete template", - f"Delete template '{template.name}': " "Are you sure?", - ): - # If template is currently open, re-check - open_idx = self.get_tab_index_for_playlist(template_id) - if open_idx: - if not helpers.ask_yes_no( - "Delete open template", - f"Template '{template.name}' is currently open. Really delete?", - ): - return - else: - self.playlist_section.tabPlaylist.removeTab(open_idx) - - log.info(f"manage_templates: delete {template=}") - session.delete(template) - session.commit() - - def edit(template_id: int) -> None: - """Edit template""" - - template = session.get(Playlists, template_id) - if not template: - raise ApplicationError( - f"manage_template.edit({template_id=}) can't load template" - ) - # Simply load the template as a playlist. Any changes - # made will persist - self._open_playlist(template, is_template=True) - - def favourite(template_id: int, favourite: bool) -> None: - """Mark template as (not) favourite""" - - template = session.get(Playlists, template_id) - template.favourite = favourite - session.commit() - - def new_item() -> None: - """Create new template""" - - # Get base template - template_id = self.solicit_template_to_use( - session, template_prompt="New template based upon:" - ) - if template_id is None: - return - - # Get new template name - name = self.solicit_name( - session, default="", prompt="New template name:" - ) - if not name: - return - - # Create playlist for template and mark is as a template - template = self._create_playlist(session, name, template_id) - template.is_template = True - session.commit() - - # Open it for editing - self._open_playlist(template, is_template=True) - - def rename(template_id: int) -> Optional[str]: - """rename template""" - - template = session.get(Playlists, template_id) - if not template: - raise ApplicationError( - f"manage_template.delete({template_id=}) can't load template" - ) - new_name = self.solicit_name(session, template.name) - if new_name: - template.rename(session, new_name) - idx = self.tabBar.currentIndex() - self.tabBar.setTabText(idx, new_name) - session.commit() - return new_name - - return None - - # Call listitem management dialog to manage templates - callbacks = ItemlistManagerCallbacks( - delete=delete, - edit=edit, - favourite=favourite, - new_item=new_item, - rename=rename, - ) - - # Build a list of templates - template_list: list[ItemlistItem] = [] - with db.Session() as session: - for template in Playlists.get_all_templates(session): - template_list.append( - ItemlistItem( - name=template.name, id=template.id, favourite=template.favourite - ) - ) - # We need to retain a reference to the dialog box to stop it - # going out of scope and being garbage-collected. - self.dlg = ItemlistManager(template_list, callbacks) - self.dlg.show() + _ = ManageTemplates(session, self) # # # # # # # # # # Miscellaneous functions # # # # # # # # # # @@ -1999,7 +2052,7 @@ class Window(QMainWindow): playlist_id = self.current.playlist_id playlist = session.get(Playlists, playlist_id) if playlist: - new_name = self.solicit_name(session, playlist.name) + new_name = self.get_playlist_name(session, playlist.name) if new_name: playlist.rename(session, new_name) idx = self.tabBar.currentIndex()