Make manage queries and manage templates into classes

This commit is contained in:
Keith Edmunds 2025-02-28 11:25:29 +00:00
parent 90d72464cb
commit aa6ab03555
4 changed files with 300 additions and 229 deletions

View File

@ -80,6 +80,7 @@ class FileErrors(NamedTuple):
@dataclass @dataclass
class Filter: class Filter:
version: int = 1
path_type: str = "contains" path_type: str = "contains"
path: Optional[str] = None path: Optional[str] = None
last_played_number: Optional[int] = None last_played_number: Optional[int] = None

View File

@ -10,7 +10,7 @@ import ssl
import tempfile import tempfile
# PyQt imports # PyQt imports
from PyQt6.QtWidgets import QMainWindow, QMessageBox, QWidget from PyQt6.QtWidgets import QInputDialog, QMainWindow, QMessageBox, QWidget
# Third party imports # Third party imports
from mutagen.flac import FLAC # type: ignore 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( def get_relative_date(
past_date: Optional[dt.datetime], reference_date: Optional[dt.datetime] = None past_date: Optional[dt.datetime], reference_date: Optional[dt.datetime] = None
) -> str: ) -> str:

View File

@ -4,10 +4,10 @@ menus:
- text: "Save as Template" - text: "Save as Template"
handler: "save_as_template" handler: "save_as_template"
- text: "Manage Templates" - text: "Manage Templates"
handler: "manage_templates" handler: "manage_templates_wrapper"
- separator: true - separator: true
- text: "Manage Queries" - text: "Manage Queries"
handler: "manage_queries" handler: "manage_queries_wrapper"
- separator: true - separator: true
- text: "Exit" - text: "Exit"
handler: "close" handler: "close"

View File

@ -68,7 +68,7 @@ from classes import (
from config import Config from config import Config
from dialogs import TrackSelectDialog from dialogs import TrackSelectDialog
from file_importer import FileImporter from file_importer import FileImporter
from helpers import file_is_unreadable from helpers import file_is_unreadable, get_name
from log import log from log import log
from models import db, Playdates, PlaylistRows, Playlists, Queries, Settings, Tracks from models import db, Playdates, PlaylistRows, Playlists, Queries, Settings, Tracks
from music_manager import RowAndTrack, track_sequence from music_manager import RowAndTrack, track_sequence
@ -270,7 +270,7 @@ class FilterDialog(QDialog):
self.ok_button = QPushButton("OK") self.ok_button = QPushButton("OK")
self.cancel_button = QPushButton("Cancel") self.cancel_button = QPushButton("Cancel")
self.cancel_button.clicked.connect(self.reject) 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.ok_button)
button_layout.addWidget(self.cancel_button) button_layout.addWidget(self.cancel_button)
layout.addLayout(button_layout) layout.addLayout(button_layout)
@ -278,7 +278,9 @@ class FilterDialog(QDialog):
self.setLayout(layout) self.setLayout(layout)
# Connect signals # 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() self.toggle_last_played_controls()
@ -287,6 +289,21 @@ class FilterDialog(QDialog):
self.last_played_spinbox.setDisabled(disabled) self.last_played_spinbox.setDisabled(disabled)
self.last_played_unit.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 @dataclass
class ItemlistItem: class ItemlistItem:
@ -296,18 +313,14 @@ class ItemlistItem:
class ItemlistManager(QDialog): class ItemlistManager(QDialog):
def __init__( def __init__(self) -> None:
self, items: list[ItemlistItem], callbacks: ItemlistManagerCallbacks
) -> None:
super().__init__() super().__init__()
self.setWindowTitle("Item Manager") self.setWindowTitle("Item Manager")
self.setMinimumSize(600, 400) self.setMinimumSize(600, 400)
self.items = items
self.callbacks = callbacks
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
self.table = QTableWidget(len(items), 2, self) self.table = QTableWidget(self)
self.table.setColumnCount(2)
self.table.setHorizontalHeaderLabels(["Item", "Actions"]) self.table.setHorizontalHeaderLabels(["Item", "Actions"])
hh = self.table.horizontalHeader() hh = self.table.horizontalHeader()
if not hh: if not hh:
@ -316,8 +329,6 @@ class ItemlistManager(QDialog):
self.table.setColumnWidth(0, 288) self.table.setColumnWidth(0, 288)
self.table.setColumnWidth(1, 300) self.table.setColumnWidth(1, 300)
self.populate_table()
layout.addWidget(self.table) layout.addWidget(self.table)
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
@ -331,8 +342,10 @@ class ItemlistManager(QDialog):
layout.addLayout(button_layout) 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.""" """Populates the table with items and action buttons."""
self.items = items
self.table.setRowCount(len(self.items)) self.table.setRowCount(len(self.items))
for row, item in enumerate(self.items): for row, item in enumerate(self.items):
@ -371,25 +384,35 @@ class ItemlistManager(QDialog):
self.table.setCellWidget(row, 1, widget) self.table.setCellWidget(row, 1, widget)
def delete_item(self, item_id: int) -> None: 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: 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: def rename_item(self, item_id: int) -> None:
new_name = self.callbacks.rename(item_id) """Subclass must implement this method"""
if not new_name: raise NotImplementedError
return
# Rename item in list def change_text(self, item_id: int, new_text: str) -> None:
"""
Update text for one row
"""
for row in range(self.table.rowCount()): for row in range(self.table.rowCount()):
item = self.table.item(row, 0) item = self.table.item(row, 0)
if item and self.items[row].id == item_id: if item and self.items[row].id == item_id:
item.setText(new_name) item.setText(new_text)
self.items[row].name = new_name self.items[row].name = new_text
break break
def toggle_favourite(self, item_id: int, checked: bool) -> None: 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()): for row in range(self.table.rowCount()):
item = self.table.item(row, 0) item = self.table.item(row, 0)
@ -402,8 +425,230 @@ class ItemlistManager(QDialog):
self.items[row].favourite = checked self.items[row].favourite = checked
break 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: 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 @dataclass
@ -884,7 +1129,7 @@ class Window(QMainWindow):
}, },
{ {
"separator": True, "separator": True,
} },
] ]
templates = Playlists.get_favourite_templates(session) templates = Playlists.get_favourite_templates(session)
for template in templates: for template in templates:
@ -965,7 +1210,7 @@ class Window(QMainWindow):
else: else:
template_id = selected_template_id template_id = selected_template_id
playlist_name = self.solicit_name(session) playlist_name = self.get_playlist_name(session)
if not playlist_name: if not playlist_name:
return return
@ -1030,10 +1275,10 @@ class Window(QMainWindow):
session.commit() session.commit()
helpers.show_OK("Template", "Template saved", self) helpers.show_OK("Template", "Template saved", self)
def solicit_name( def get_playlist_name(
self, session: Session, default: str = "", prompt: str = "Playlist name:" self, session: Session, default: str = "", prompt: str = "Playlist name:"
) -> Optional[str]: ) -> Optional[str]:
"""Get name of new playlist from user""" """Get a name from the user"""
dlg = QInputDialog(self) dlg = QInputDialog(self)
dlg.setInputMode(QInputDialog.InputMode.TextInput) dlg.setInputMode(QInputDialog.InputMode.TextInput)
@ -1080,213 +1325,21 @@ class Window(QMainWindow):
# # # # # # # # # # Manage templates and queries # # # # # # # # # # # # # # # # # # # # 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: with db.Session() as session:
for query in Queries.get_all_queries(session): _ = ManageQueries(session, self)
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()
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: with db.Session() as session:
for template in Playlists.get_all_templates(session): _ = ManageTemplates(session, self)
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()
# # # # # # # # # # Miscellaneous functions # # # # # # # # # # # # # # # # # # # # Miscellaneous functions # # # # # # # # # #
@ -1999,7 +2052,7 @@ class Window(QMainWindow):
playlist_id = self.current.playlist_id playlist_id = self.current.playlist_id
playlist = session.get(Playlists, playlist_id) playlist = session.get(Playlists, playlist_id)
if playlist: if playlist:
new_name = self.solicit_name(session, playlist.name) new_name = self.get_playlist_name(session, playlist.name)
if new_name: if new_name:
playlist.rename(session, new_name) playlist.rename(session, new_name)
idx = self.tabBar.currentIndex() idx = self.tabBar.currentIndex()