Compare commits
2 Commits
b4f5d92f5d
...
9e1995be68
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e1995be68 | ||
|
|
2abb672142 |
@ -83,7 +83,6 @@ class Filter:
|
|||||||
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
|
||||||
last_played_type: str = "before"
|
|
||||||
last_played_unit: str = "years"
|
last_played_unit: str = "years"
|
||||||
duration_type: str = "longer than"
|
duration_type: str = "longer than"
|
||||||
duration_number: int = 0
|
duration_number: int = 0
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
# Standard library imports
|
# Standard library imports
|
||||||
|
import sys
|
||||||
|
|
||||||
# PyQt imports
|
# PyQt imports
|
||||||
|
|
||||||
@ -8,6 +9,12 @@ from alchemical import Alchemical # type:ignore
|
|||||||
# App imports
|
# App imports
|
||||||
|
|
||||||
|
|
||||||
|
def is_alembic_command():
|
||||||
|
# Define keywords that indicate Alembic is being invoked.
|
||||||
|
alembic_keywords = {'alembic', 'revision', 'upgrade', 'downgrade', 'history', 'current'}
|
||||||
|
return any(arg in alembic_keywords for arg in sys.argv)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseManager:
|
class DatabaseManager:
|
||||||
"""
|
"""
|
||||||
Singleton class to ensure we only ever have one db object
|
Singleton class to ensure we only ever have one db object
|
||||||
@ -18,8 +25,8 @@ class DatabaseManager:
|
|||||||
def __init__(self, database_url: str, **kwargs: dict) -> None:
|
def __init__(self, database_url: str, **kwargs: dict) -> None:
|
||||||
if DatabaseManager.__instance is None:
|
if DatabaseManager.__instance is None:
|
||||||
self.db = Alchemical(database_url, **kwargs)
|
self.db = Alchemical(database_url, **kwargs)
|
||||||
# Database managed by Alembic so no create_all() required
|
if not is_alembic_command():
|
||||||
# self.db.create_all()
|
self.db.create_all()
|
||||||
DatabaseManager.__instance = self
|
DatabaseManager.__instance = self
|
||||||
else:
|
else:
|
||||||
raise Exception("Attempted to create a second DatabaseManager instance")
|
raise Exception("Attempted to create a second DatabaseManager instance")
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
# Standard library imports
|
# Standard library imports
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from dataclasses import asdict
|
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -15,8 +14,6 @@ from sqlalchemy import (
|
|||||||
String,
|
String,
|
||||||
)
|
)
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
|
||||||
from sqlalchemy.engine.interfaces import Dialect
|
|
||||||
from sqlalchemy.orm import (
|
from sqlalchemy.orm import (
|
||||||
Mapped,
|
Mapped,
|
||||||
mapped_column,
|
mapped_column,
|
||||||
@ -35,13 +32,13 @@ class JSONEncodedDict(TypeDecorator):
|
|||||||
|
|
||||||
impl = TEXT
|
impl = TEXT
|
||||||
|
|
||||||
def process_bind_param(self, value: dict | None, dialect: Dialect) -> str | None:
|
def process_bind_param(self, value, dialect):
|
||||||
"""Convert Python dictionary to JSON string before saving."""
|
"""Convert Python dictionary to JSON string before saving."""
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
return json.dumps(value, default=lambda o: o.__dict__)
|
return json.dumps(value)
|
||||||
|
|
||||||
def process_result_value(self, value: str | None, dialect: Dialect) -> dict | None:
|
def process_result_value(self, value, dialect):
|
||||||
"""Convert JSON string back to Python dictionary after retrieval."""
|
"""Convert JSON string back to Python dictionary after retrieval."""
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
@ -154,24 +151,13 @@ class QueriesTable(Model):
|
|||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
_filter_data: Mapped[dict | None] = mapped_column("filter_data", JSONEncodedDict, nullable=True)
|
filter: Mapped[Filter] = mapped_column(JSONEncodedDict, nullable=True)
|
||||||
favourite: Mapped[bool] = mapped_column(Boolean, nullable=False, index=False, default=False)
|
favourite: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, index=False, default=False
|
||||||
def _get_filter(self) -> Filter:
|
)
|
||||||
"""Convert stored JSON dictionary to a Filter object."""
|
|
||||||
if isinstance(self._filter_data, dict):
|
|
||||||
return Filter(**self._filter_data)
|
|
||||||
return Filter() # Default object if None or invalid data
|
|
||||||
|
|
||||||
def _set_filter(self, value: Filter | None) -> None:
|
|
||||||
"""Convert a Filter object to JSON before storing."""
|
|
||||||
self._filter_data = asdict(value) if isinstance(value, Filter) else None
|
|
||||||
|
|
||||||
# Single definition of `filter`
|
|
||||||
filter = property(_get_filter, _set_filter)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<QueriesTable(id={self.id}, name={self.name}, filter={self.filter})>"
|
return f"<Queries(id={self.id}, name={self.name}, filter={self.filter}>"
|
||||||
|
|
||||||
|
|
||||||
class SettingsTable(Model):
|
class SettingsTable(Model):
|
||||||
|
|||||||
@ -6,8 +6,6 @@ menus:
|
|||||||
- text: "Manage Templates"
|
- text: "Manage Templates"
|
||||||
handler: "manage_templates"
|
handler: "manage_templates"
|
||||||
- separator: true
|
- separator: true
|
||||||
- text: "Manage Queries"
|
|
||||||
handler: "manage_queries"
|
|
||||||
- separator: true
|
- separator: true
|
||||||
- text: "Exit"
|
- text: "Exit"
|
||||||
handler: "close"
|
handler: "close"
|
||||||
|
|||||||
@ -221,6 +221,14 @@ class Playlists(dbtables.PlaylistsTable):
|
|||||||
self.open = False
|
self.open = False
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
def delete(self, session: Session) -> None:
|
||||||
|
"""
|
||||||
|
Delete playlist
|
||||||
|
"""
|
||||||
|
|
||||||
|
session.execute(delete(Playlists).where(Playlists.id == self.id))
|
||||||
|
session.commit()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all(cls, session: Session) -> Sequence["Playlists"]:
|
def get_all(cls, session: Session) -> Sequence["Playlists"]:
|
||||||
"""Returns a list of all playlists ordered by last use"""
|
"""Returns a list of all playlists ordered by last use"""
|
||||||
@ -609,12 +617,6 @@ class Queries(dbtables.QueriesTable):
|
|||||||
session.add(self)
|
session.add(self)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_all_queries(cls, session: Session) -> Sequence["Queries"]:
|
|
||||||
"""Returns a list of all queries ordered by name"""
|
|
||||||
|
|
||||||
return session.scalars(select(cls).order_by(cls.name)).all()
|
|
||||||
|
|
||||||
|
|
||||||
class Settings(dbtables.SettingsTable):
|
class Settings(dbtables.SettingsTable):
|
||||||
def __init__(self, session: Session, name: str) -> None:
|
def __init__(self, session: Session, name: str) -> None:
|
||||||
|
|||||||
@ -46,7 +46,6 @@ from PyQt6.QtWidgets import (
|
|||||||
QMenu,
|
QMenu,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QSpinBox,
|
|
||||||
QTableWidget,
|
QTableWidget,
|
||||||
QTableWidgetItem,
|
QTableWidgetItem,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
@ -61,7 +60,6 @@ import stackprinter # type: ignore
|
|||||||
# App imports
|
# App imports
|
||||||
from classes import (
|
from classes import (
|
||||||
ApplicationError,
|
ApplicationError,
|
||||||
Filter,
|
|
||||||
MusicMusterSignals,
|
MusicMusterSignals,
|
||||||
TrackInfo,
|
TrackInfo,
|
||||||
)
|
)
|
||||||
@ -70,7 +68,7 @@ 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
|
||||||
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, Settings, Tracks
|
||||||
from music_manager import RowAndTrack, track_sequence
|
from music_manager import RowAndTrack, track_sequence
|
||||||
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
||||||
from playlists import PlaylistTab
|
from playlists import PlaylistTab
|
||||||
@ -168,126 +166,6 @@ class EditDeleteDialog(QDialog):
|
|||||||
self.reject()
|
self.reject()
|
||||||
|
|
||||||
|
|
||||||
class FilterDialog(QDialog):
|
|
||||||
def __init__(self, name: str, filter: Filter) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.filter = filter
|
|
||||||
|
|
||||||
self.setWindowTitle("Filter Settings")
|
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
|
|
||||||
# Name row
|
|
||||||
name_layout = QHBoxLayout()
|
|
||||||
name_label = QLabel("Name")
|
|
||||||
self.name_text = QLineEdit()
|
|
||||||
self.name_text.setText(name)
|
|
||||||
name_layout.addWidget(name_label)
|
|
||||||
name_layout.addWidget(self.name_text)
|
|
||||||
layout.addLayout(name_layout)
|
|
||||||
|
|
||||||
# Path row
|
|
||||||
path_layout = QHBoxLayout()
|
|
||||||
path_label = QLabel("Path")
|
|
||||||
self.path_combo = QComboBox()
|
|
||||||
self.path_combo.addItems(["contains", "excluding"])
|
|
||||||
for idx in range(self.path_combo.count()):
|
|
||||||
if self.path_combo.itemText(idx) == filter.path_type:
|
|
||||||
self.path_combo.setCurrentIndex(idx)
|
|
||||||
break
|
|
||||||
self.path_text = QLineEdit()
|
|
||||||
if filter.path:
|
|
||||||
self.path_text.setText(filter.path)
|
|
||||||
path_layout.addWidget(path_label)
|
|
||||||
path_layout.addWidget(self.path_combo)
|
|
||||||
path_layout.addWidget(self.path_text)
|
|
||||||
layout.addLayout(path_layout)
|
|
||||||
|
|
||||||
# Last played row
|
|
||||||
last_played_layout = QHBoxLayout()
|
|
||||||
last_played_label = QLabel("Last played")
|
|
||||||
self.last_played_combo = QComboBox()
|
|
||||||
self.last_played_combo.addItems(["before", "never"])
|
|
||||||
for idx in range(self.last_played_combo.count()):
|
|
||||||
if self.last_played_combo.itemText(idx) == filter.last_played_type:
|
|
||||||
self.last_played_combo.setCurrentIndex(idx)
|
|
||||||
break
|
|
||||||
|
|
||||||
self.last_played_spinbox = QSpinBox()
|
|
||||||
self.last_played_spinbox.setMinimum(0)
|
|
||||||
self.last_played_spinbox.setMaximum(100)
|
|
||||||
self.last_played_spinbox.setValue(filter.last_played_number or 0)
|
|
||||||
|
|
||||||
self.last_played_unit = QComboBox()
|
|
||||||
self.last_played_unit.addItems(["years", "months", "weeks", "days"])
|
|
||||||
for idx in range(self.last_played_unit.count()):
|
|
||||||
if self.last_played_unit.itemText(idx) == filter.last_played_unit:
|
|
||||||
self.last_played_unit.setCurrentIndex(idx)
|
|
||||||
break
|
|
||||||
|
|
||||||
last_played_ago_label = QLabel("ago")
|
|
||||||
|
|
||||||
last_played_layout.addWidget(last_played_label)
|
|
||||||
last_played_layout.addWidget(self.last_played_combo)
|
|
||||||
last_played_layout.addWidget(self.last_played_spinbox)
|
|
||||||
last_played_layout.addWidget(self.last_played_unit)
|
|
||||||
last_played_layout.addWidget(last_played_ago_label)
|
|
||||||
|
|
||||||
layout.addLayout(last_played_layout)
|
|
||||||
|
|
||||||
# Duration row
|
|
||||||
duration_layout = QHBoxLayout()
|
|
||||||
duration_label = QLabel("Duration")
|
|
||||||
self.duration_combo = QComboBox()
|
|
||||||
self.duration_combo.addItems(["longer than", "shorter than"])
|
|
||||||
for idx in range(self.duration_combo.count()):
|
|
||||||
if self.duration_combo.itemText(idx) == filter.duration_type:
|
|
||||||
self.duration_combo.setCurrentIndex(idx)
|
|
||||||
break
|
|
||||||
|
|
||||||
self.duration_spinbox = QSpinBox()
|
|
||||||
self.duration_spinbox.setMinimum(0)
|
|
||||||
self.duration_spinbox.setMaximum(1000)
|
|
||||||
self.duration_spinbox.setValue(filter.duration_number)
|
|
||||||
|
|
||||||
self.duration_unit = QComboBox()
|
|
||||||
self.duration_unit.addItems(["minutes", "seconds"])
|
|
||||||
self.duration_unit.setCurrentText("minutes")
|
|
||||||
for idx in range(self.duration_unit.count()):
|
|
||||||
if self.duration_unit.itemText(idx) == filter.duration_unit:
|
|
||||||
self.duration_unit.setCurrentIndex(idx)
|
|
||||||
break
|
|
||||||
|
|
||||||
duration_layout.addWidget(duration_label)
|
|
||||||
duration_layout.addWidget(self.duration_combo)
|
|
||||||
duration_layout.addWidget(self.duration_spinbox)
|
|
||||||
duration_layout.addWidget(self.duration_unit)
|
|
||||||
|
|
||||||
layout.addLayout(duration_layout)
|
|
||||||
|
|
||||||
# Buttons
|
|
||||||
button_layout = QHBoxLayout()
|
|
||||||
self.ok_button = QPushButton("OK")
|
|
||||||
self.cancel_button = QPushButton("Cancel")
|
|
||||||
self.cancel_button.clicked.connect(self.reject)
|
|
||||||
self.ok_button.clicked.connect(self.accept)
|
|
||||||
button_layout.addWidget(self.ok_button)
|
|
||||||
button_layout.addWidget(self.cancel_button)
|
|
||||||
layout.addLayout(button_layout)
|
|
||||||
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
# Connect signals
|
|
||||||
self.last_played_combo.currentIndexChanged.connect(self.toggle_last_played_controls)
|
|
||||||
|
|
||||||
self.toggle_last_played_controls()
|
|
||||||
|
|
||||||
def toggle_last_played_controls(self):
|
|
||||||
disabled = self.last_played_combo.currentText() == "never"
|
|
||||||
self.last_played_spinbox.setDisabled(disabled)
|
|
||||||
self.last_played_unit.setDisabled(disabled)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ItemlistItem:
|
class ItemlistItem:
|
||||||
id: int
|
id: int
|
||||||
@ -846,10 +724,6 @@ class Window(QMainWindow):
|
|||||||
# Dynamically call the correct function
|
# Dynamically call the correct function
|
||||||
items = getattr(self, f"get_{key}_items")()
|
items = getattr(self, f"get_{key}_items")()
|
||||||
for item in items:
|
for item in items:
|
||||||
# Check for separator
|
|
||||||
if "separator" in item and item["separator"] == "separator":
|
|
||||||
submenu.addSeparator()
|
|
||||||
continue
|
|
||||||
action = QAction(item["text"], self)
|
action = QAction(item["text"], self)
|
||||||
|
|
||||||
# Extract handler and arguments
|
# Extract handler and arguments
|
||||||
@ -881,9 +755,6 @@ class Window(QMainWindow):
|
|||||||
"text": "Show all",
|
"text": "Show all",
|
||||||
"handler": "create_playlist_from_template",
|
"handler": "create_playlist_from_template",
|
||||||
"args": (session, 0),
|
"args": (session, 0),
|
||||||
},
|
|
||||||
{
|
|
||||||
"separator": "separator",
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
templates = Playlists.get_favourite_templates(session)
|
templates = Playlists.get_favourite_templates(session)
|
||||||
@ -965,7 +836,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.solicit_playlist_name(session)
|
||||||
if not playlist_name:
|
if not playlist_name:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -987,7 +858,7 @@ class Window(QMainWindow):
|
|||||||
f"Delete playlist '{playlist.name}': " "Are you sure?",
|
f"Delete playlist '{playlist.name}': " "Are you sure?",
|
||||||
):
|
):
|
||||||
if self.close_playlist_tab():
|
if self.close_playlist_tab():
|
||||||
session.delete(playlist)
|
playlist.delete(session)
|
||||||
session.commit()
|
session.commit()
|
||||||
else:
|
else:
|
||||||
log.error("Failed to retrieve playlist")
|
log.error("Failed to retrieve playlist")
|
||||||
@ -1030,7 +901,7 @@ 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 solicit_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 name of new playlist from user"""
|
||||||
@ -1078,216 +949,6 @@ class Window(QMainWindow):
|
|||||||
|
|
||||||
return dlg.selected_id
|
return dlg.selected_id
|
||||||
|
|
||||||
# # # # # # # # # # Manage templates and queries # # # # # # # # # #
|
|
||||||
|
|
||||||
def manage_queries(self) -> None:
|
|
||||||
"""
|
|
||||||
Delete / edit queries
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
def manage_templates(self) -> None:
|
|
||||||
"""
|
|
||||||
Delete / edit templates
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
# # # # # # # # # # Miscellaneous functions # # # # # # # # # #
|
# # # # # # # # # # Miscellaneous functions # # # # # # # # # #
|
||||||
|
|
||||||
def select_duplicate_rows(self) -> None:
|
def select_duplicate_rows(self) -> None:
|
||||||
@ -1681,6 +1342,125 @@ class Window(QMainWindow):
|
|||||||
|
|
||||||
self.signals.search_wikipedia_signal.emit(track_info.title)
|
self.signals.search_wikipedia_signal.emit(track_info.title)
|
||||||
|
|
||||||
|
def manage_templates(self) -> None:
|
||||||
|
"""
|
||||||
|
Delete / edit templates
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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_templeate.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=}")
|
||||||
|
template.delete(session)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
def edit(template_id: int) -> None:
|
||||||
|
"""Edit template"""
|
||||||
|
|
||||||
|
template = session.get(Playlists, template_id)
|
||||||
|
if not template:
|
||||||
|
raise ApplicationError(
|
||||||
|
f"manage_templeate.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_playlist_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_templeate.delete({template_id=}) can't load template"
|
||||||
|
)
|
||||||
|
new_name = self.solicit_playlist_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()
|
||||||
|
|
||||||
def mark_rows_for_moving(self) -> None:
|
def mark_rows_for_moving(self) -> None:
|
||||||
"""
|
"""
|
||||||
Cut rows ready for pasting.
|
Cut rows ready for pasting.
|
||||||
@ -1999,7 +1779,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.solicit_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()
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
"""fix playlist cascades
|
"""create queries table
|
||||||
|
|
||||||
Revision ID: ab475332d873
|
Revision ID: 335cae31045f
|
||||||
Revises: 04df697e40cd
|
Revises: f14dd379850f
|
||||||
Create Date: 2025-02-26 13:11:15.417278
|
Create Date: 2025-02-24 20:43:41.534922
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
import dbtables
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = 'ab475332d873'
|
revision = '335cae31045f'
|
||||||
down_revision = '04df697e40cd'
|
down_revision = 'f14dd379850f'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
@ -29,6 +30,13 @@ def downgrade(engine_name: str) -> None:
|
|||||||
|
|
||||||
def upgrade_() -> None:
|
def upgrade_() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('queries',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('filter', dbtables.JSONEncodedDict(), nullable=True),
|
||||||
|
sa.Column('favourite', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
with op.batch_alter_table('playdates', schema=None) as batch_op:
|
with op.batch_alter_table('playdates', schema=None) as batch_op:
|
||||||
batch_op.drop_constraint('fk_playdates_track_id_tracks', type_='foreignkey')
|
batch_op.drop_constraint('fk_playdates_track_id_tracks', type_='foreignkey')
|
||||||
batch_op.create_foreign_key(None, 'tracks', ['track_id'], ['id'], ondelete='CASCADE')
|
batch_op.create_foreign_key(None, 'tracks', ['track_id'], ['id'], ondelete='CASCADE')
|
||||||
@ -42,5 +50,6 @@ def downgrade_() -> None:
|
|||||||
batch_op.drop_constraint(None, type_='foreignkey')
|
batch_op.drop_constraint(None, type_='foreignkey')
|
||||||
batch_op.create_foreign_key('fk_playdates_track_id_tracks', 'tracks', ['track_id'], ['id'])
|
batch_op.create_foreign_key('fk_playdates_track_id_tracks', 'tracks', ['track_id'], ['id'])
|
||||||
|
|
||||||
|
op.drop_table('queries')
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
"""create queries table
|
|
||||||
|
|
||||||
Revision ID: 4fc2a9a82ab0
|
|
||||||
Revises: ab475332d873
|
|
||||||
Create Date: 2025-02-26 13:13:25.118489
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
import dbtables
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '4fc2a9a82ab0'
|
|
||||||
down_revision = 'ab475332d873'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade(engine_name: str) -> None:
|
|
||||||
globals()["upgrade_%s" % engine_name]()
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade(engine_name: str) -> None:
|
|
||||||
globals()["downgrade_%s" % engine_name]()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade_() -> None:
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('queries',
|
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
||||||
sa.Column('name', sa.String(length=128), nullable=False),
|
|
||||||
sa.Column('filter_data', dbtables.JSONEncodedDict(), nullable=True),
|
|
||||||
sa.Column('favourite', sa.Boolean(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade_() -> None:
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_table('queries')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user