rebase from dev
This commit is contained in:
parent
40756469ec
commit
3f248d363f
@ -97,6 +97,7 @@ class Config(object):
|
||||
PLAY_SETTLE = 500000
|
||||
PLAYLIST_ICON_CURRENT = ":/icons/green-circle.png"
|
||||
PLAYLIST_ICON_NEXT = ":/icons/yellow-circle.png"
|
||||
PLAYLIST_ICON_TEMPLATE = ":/icons/redstar.png"
|
||||
PREVIEW_ADVANCE_MS = 5000
|
||||
PREVIEW_BACK_MS = 5000
|
||||
PREVIEW_END_BUFFER_MS = 1000
|
||||
|
||||
@ -80,6 +80,9 @@ class PlaylistsTable(Model):
|
||||
cascade="all, delete-orphan",
|
||||
order_by="PlaylistRowsTable.row_number",
|
||||
)
|
||||
favourite: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, index=False, default=False
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
@ -101,9 +104,7 @@ class PlaylistRowsTable(Model):
|
||||
)
|
||||
|
||||
playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows")
|
||||
track_id: Mapped[Optional[int]] = mapped_column(
|
||||
ForeignKey("tracks.id", ondelete="CASCADE")
|
||||
)
|
||||
track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id", ondelete="CASCADE"))
|
||||
track: Mapped["TracksTable"] = relationship(
|
||||
"TracksTable",
|
||||
back_populates="playlistrows",
|
||||
|
||||
102
app/menu.yaml
Normal file
102
app/menu.yaml
Normal file
@ -0,0 +1,102 @@
|
||||
menus:
|
||||
- title: "&File"
|
||||
actions:
|
||||
- text: "Save as Template"
|
||||
handler: "save_as_template"
|
||||
- text: "Manage Templates"
|
||||
handler: "manage_templates"
|
||||
- separator: true
|
||||
- separator: true
|
||||
- text: "Exit"
|
||||
handler: "close"
|
||||
|
||||
- title: "&Playlist"
|
||||
actions:
|
||||
- text: "Open Playlist"
|
||||
handler: "open_existing_playlist"
|
||||
shortcut: "Ctrl+O"
|
||||
- text: "New Playlist"
|
||||
handler: "new_playlist_dynamic_submenu"
|
||||
submenu: true
|
||||
- text: "Close Playlist"
|
||||
handler: "close_playlist_tab"
|
||||
- text: "Rename Playlist"
|
||||
handler: "rename_playlist"
|
||||
- text: "Delete Playlist"
|
||||
handler: "delete_playlist"
|
||||
- separator: true
|
||||
- text: "Insert Track"
|
||||
handler: "insert_track"
|
||||
shortcut: "Ctrl+T"
|
||||
- text: "Select Track from Query"
|
||||
handler: "query_dynamic_submenu"
|
||||
submenu: true
|
||||
- text: "Insert Section Header"
|
||||
handler: "insert_header"
|
||||
shortcut: "Ctrl+H"
|
||||
- text: "Import Files"
|
||||
handler: "import_files_wrapper"
|
||||
shortcut: "Ctrl+Shift+I"
|
||||
- separator: true
|
||||
- text: "Mark for Moving"
|
||||
handler: "mark_rows_for_moving"
|
||||
shortcut: "Ctrl+C"
|
||||
- text: "Paste"
|
||||
handler: "paste_rows"
|
||||
shortcut: "Ctrl+V"
|
||||
- separator: true
|
||||
- text: "Export Playlist"
|
||||
handler: "export_playlist_tab"
|
||||
- text: "Download CSV of Played Tracks"
|
||||
handler: "download_played_tracks"
|
||||
- separator: true
|
||||
- text: "Select Duplicate Rows"
|
||||
handler: "select_duplicate_rows"
|
||||
- text: "Move Selected"
|
||||
handler: "move_selected"
|
||||
- text: "Move Unplayed"
|
||||
handler: "move_unplayed"
|
||||
- separator: true
|
||||
- text: "Clear Selection"
|
||||
handler: "clear_selection"
|
||||
shortcut: "Esc"
|
||||
store_reference: true # So we can enable/disable later
|
||||
|
||||
- title: "&Music"
|
||||
actions:
|
||||
- text: "Set Next"
|
||||
handler: "set_selected_track_next"
|
||||
shortcut: "Ctrl+N"
|
||||
- text: "Play Next"
|
||||
handler: "play_next"
|
||||
shortcut: "Return"
|
||||
- text: "Fade"
|
||||
handler: "fade"
|
||||
shortcut: "Ctrl+Z"
|
||||
- text: "Stop"
|
||||
handler: "stop"
|
||||
shortcut: "Ctrl+Alt+S"
|
||||
- text: "Resume"
|
||||
handler: "resume"
|
||||
shortcut: "Ctrl+R"
|
||||
- text: "Skip to Next"
|
||||
handler: "play_next"
|
||||
shortcut: "Ctrl+Alt+Return"
|
||||
- separator: true
|
||||
- text: "Search"
|
||||
handler: "search_playlist"
|
||||
shortcut: "/"
|
||||
- text: "Search Title in Wikipedia"
|
||||
handler: "lookup_row_in_wikipedia"
|
||||
shortcut: "Ctrl+W"
|
||||
- text: "Search Title in Songfacts"
|
||||
handler: "lookup_row_in_songfacts"
|
||||
shortcut: "Ctrl+S"
|
||||
|
||||
- title: "Help"
|
||||
actions:
|
||||
- text: "About"
|
||||
handler: "about"
|
||||
- text: "Debug"
|
||||
handler: "debug"
|
||||
|
||||
289
app/models.py
289
app/models.py
@ -192,6 +192,138 @@ class Playdates(dbtables.PlaydatesTable):
|
||||
).all()
|
||||
|
||||
|
||||
class Playlists(dbtables.PlaylistsTable):
|
||||
def __init__(self, session: Session, name: str, template_id: int) -> None:
|
||||
"""Create playlist with passed name"""
|
||||
|
||||
self.name = name
|
||||
self.last_used = dt.datetime.now()
|
||||
session.add(self)
|
||||
session.commit()
|
||||
|
||||
# If a template is specified, copy from it
|
||||
if template_id:
|
||||
PlaylistRows.copy_playlist(session, template_id, self.id)
|
||||
|
||||
@staticmethod
|
||||
def clear_tabs(session: Session, playlist_ids: list[int]) -> None:
|
||||
"""
|
||||
Make all tab records NULL
|
||||
"""
|
||||
|
||||
session.execute(
|
||||
update(Playlists).where((Playlists.id.in_(playlist_ids))).values(tab=None)
|
||||
)
|
||||
|
||||
def close(self, session: Session) -> None:
|
||||
"""Mark playlist as unloaded"""
|
||||
|
||||
self.open = False
|
||||
session.commit()
|
||||
|
||||
def delete(self, session: Session) -> None:
|
||||
"""
|
||||
Delete playlist
|
||||
"""
|
||||
|
||||
session.execute(delete(Playlists).where(Playlists.id == self.id))
|
||||
session.commit()
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, session: Session) -> Sequence["Playlists"]:
|
||||
"""Returns a list of all playlists ordered by last use"""
|
||||
|
||||
return session.scalars(
|
||||
select(cls)
|
||||
.filter(cls.is_template.is_(False))
|
||||
.order_by(cls.last_used.desc())
|
||||
).all()
|
||||
|
||||
@classmethod
|
||||
def get_all_templates(cls, session: Session) -> Sequence["Playlists"]:
|
||||
"""Returns a list of all templates ordered by name"""
|
||||
|
||||
return session.scalars(
|
||||
select(cls).where(cls.is_template.is_(True)).order_by(cls.name)
|
||||
).all()
|
||||
|
||||
@classmethod
|
||||
def get_favourite_templates(cls, session: Session) -> Sequence["Playlists"]:
|
||||
"""Returns a list of favourite templates ordered by name"""
|
||||
|
||||
return session.scalars(
|
||||
select(cls)
|
||||
.where(
|
||||
cls.is_template.is_(True),
|
||||
cls.favourite.is_(True)
|
||||
)
|
||||
.order_by(cls.name)
|
||||
).all()
|
||||
|
||||
@classmethod
|
||||
def get_closed(cls, session: Session) -> Sequence["Playlists"]:
|
||||
"""Returns a list of all closed playlists ordered by last use"""
|
||||
|
||||
return session.scalars(
|
||||
select(cls)
|
||||
.filter(
|
||||
cls.open.is_(False),
|
||||
cls.is_template.is_(False),
|
||||
)
|
||||
.order_by(cls.last_used.desc())
|
||||
).all()
|
||||
|
||||
@classmethod
|
||||
def get_open(cls, session: Session) -> Sequence[Optional["Playlists"]]:
|
||||
"""
|
||||
Return a list of loaded playlists ordered by tab.
|
||||
"""
|
||||
|
||||
return session.scalars(
|
||||
select(cls).where(cls.open.is_(True)).order_by(cls.tab)
|
||||
).all()
|
||||
|
||||
def mark_open(self) -> None:
|
||||
"""Mark playlist as loaded and used now"""
|
||||
|
||||
self.open = True
|
||||
self.last_used = dt.datetime.now()
|
||||
|
||||
@staticmethod
|
||||
def name_is_available(session: Session, name: str) -> bool:
|
||||
"""
|
||||
Return True if no playlist of this name exists else false.
|
||||
"""
|
||||
|
||||
return (
|
||||
session.execute(select(Playlists).where(Playlists.name == name)).first()
|
||||
is None
|
||||
)
|
||||
|
||||
def rename(self, session: Session, new_name: str) -> None:
|
||||
"""
|
||||
Rename playlist
|
||||
"""
|
||||
|
||||
self.name = new_name
|
||||
session.commit()
|
||||
|
||||
@staticmethod
|
||||
def save_as_template(
|
||||
session: Session, playlist_id: int, template_name: str
|
||||
) -> None:
|
||||
"""Save passed playlist as new template"""
|
||||
|
||||
template = Playlists(session, template_name, template_id=0)
|
||||
if not template or not template.id:
|
||||
return
|
||||
|
||||
template.is_template = True
|
||||
session.commit()
|
||||
|
||||
PlaylistRows.copy_playlist(session, playlist_id, template.id)
|
||||
|
||||
|
||||
class PlaylistRows(dbtables.PlaylistRowsTable):
|
||||
def __init__(
|
||||
self,
|
||||
@ -476,161 +608,8 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
|
||||
session.connection().execute(stmt, sqla_map)
|
||||
|
||||
|
||||
class Playlists(dbtables.PlaylistsTable):
|
||||
def __init__(self, session: Session, name: str):
|
||||
self.name = name
|
||||
self.last_used = dt.datetime.now()
|
||||
session.add(self)
|
||||
session.commit()
|
||||
|
||||
@staticmethod
|
||||
def clear_tabs(session: Session, playlist_ids: list[int]) -> None:
|
||||
"""
|
||||
Make all tab records NULL
|
||||
"""
|
||||
|
||||
session.execute(
|
||||
update(Playlists).where((Playlists.id.in_(playlist_ids))).values(tab=None)
|
||||
)
|
||||
|
||||
def close(self, session: Session) -> None:
|
||||
"""Mark playlist as unloaded"""
|
||||
|
||||
self.open = False
|
||||
session.commit()
|
||||
|
||||
@classmethod
|
||||
def create_playlist_from_template(
|
||||
cls, session: Session, template: "Playlists", playlist_name: str
|
||||
) -> Optional["Playlists"]:
|
||||
"""Create a new playlist from template"""
|
||||
|
||||
# Sanity check
|
||||
if not template.id:
|
||||
return None
|
||||
|
||||
playlist = cls(session, playlist_name)
|
||||
|
||||
# Sanity / mypy checks
|
||||
if not playlist or not playlist.id:
|
||||
return None
|
||||
|
||||
PlaylistRows.copy_playlist(session, template.id, playlist.id)
|
||||
|
||||
return playlist
|
||||
|
||||
def delete(self, session: Session) -> None:
|
||||
"""
|
||||
Delete playlist
|
||||
"""
|
||||
|
||||
session.execute(delete(Playlists).where(Playlists.id == self.id))
|
||||
session.commit()
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, session: Session) -> Sequence["Playlists"]:
|
||||
"""Returns a list of all playlists ordered by last use"""
|
||||
|
||||
return session.scalars(
|
||||
select(cls)
|
||||
.filter(cls.is_template.is_(False))
|
||||
.order_by(cls.last_used.desc())
|
||||
).all()
|
||||
|
||||
@classmethod
|
||||
def get_all_templates(cls, session: Session) -> Sequence["Playlists"]:
|
||||
"""Returns a list of all templates ordered by name"""
|
||||
|
||||
return session.scalars(
|
||||
select(cls).where(cls.is_template.is_(True)).order_by(cls.name)
|
||||
).all()
|
||||
|
||||
@classmethod
|
||||
def get_closed(cls, session: Session) -> Sequence["Playlists"]:
|
||||
"""Returns a list of all closed playlists ordered by last use"""
|
||||
|
||||
return session.scalars(
|
||||
select(cls)
|
||||
.filter(cls.open.is_(False), cls.is_template.is_(False))
|
||||
.order_by(cls.last_used.desc())
|
||||
).all()
|
||||
|
||||
@classmethod
|
||||
def get_open(cls, session: Session) -> Sequence[Optional["Playlists"]]:
|
||||
"""
|
||||
Return a list of loaded playlists ordered by tab.
|
||||
"""
|
||||
|
||||
return session.scalars(
|
||||
select(cls)
|
||||
.where(
|
||||
cls.open.is_(True),
|
||||
)
|
||||
.order_by(cls.tab)
|
||||
).all()
|
||||
|
||||
def mark_open(self) -> None:
|
||||
"""Mark playlist as loaded and used now"""
|
||||
|
||||
self.open = True
|
||||
self.last_used = dt.datetime.now()
|
||||
|
||||
@staticmethod
|
||||
def name_is_available(session: Session, name: str) -> bool:
|
||||
"""
|
||||
Return True if no playlist of this name exists else false.
|
||||
"""
|
||||
|
||||
return (
|
||||
session.execute(select(Playlists).where(Playlists.name == name)).first()
|
||||
is None
|
||||
)
|
||||
|
||||
def rename(self, session: Session, new_name: str) -> None:
|
||||
"""
|
||||
Rename playlist
|
||||
"""
|
||||
|
||||
self.name = new_name
|
||||
session.commit()
|
||||
|
||||
@staticmethod
|
||||
def save_as_template(
|
||||
session: Session, playlist_id: int, template_name: str
|
||||
) -> None:
|
||||
"""Save passed playlist as new template"""
|
||||
|
||||
template = Playlists(session, template_name)
|
||||
if not template or not template.id:
|
||||
return
|
||||
|
||||
template.is_template = True
|
||||
session.commit()
|
||||
|
||||
PlaylistRows.copy_playlist(session, playlist_id, template.id)
|
||||
|
||||
|
||||
class Queries(dbtables.QueriesTable):
|
||||
def __init__(
|
||||
self, session: Session, name: str, query: str, description: str = ""
|
||||
) -> None:
|
||||
self.query = query
|
||||
self.name = name
|
||||
self.description = description
|
||||
session.add(self)
|
||||
session.commit()
|
||||
|
||||
@classmethod
|
||||
def get_all(cls, session: Session) -> Sequence[Queries]:
|
||||
"""
|
||||
Return a list of all queries
|
||||
"""
|
||||
|
||||
return session.scalars(select(cls)).unique().all()
|
||||
|
||||
|
||||
class Settings(dbtables.SettingsTable):
|
||||
def __init__(self, session: Session, name: str):
|
||||
def __init__(self, session: Session, name: str) -> None:
|
||||
self.name = name
|
||||
session.add(self)
|
||||
session.commit()
|
||||
@ -658,7 +637,7 @@ class Tracks(dbtables.TracksTable):
|
||||
fade_at: int,
|
||||
silence_at: int,
|
||||
bitrate: int,
|
||||
):
|
||||
) -> None:
|
||||
self.path = path
|
||||
self.title = title
|
||||
self.artist = artist
|
||||
|
||||
@ -174,7 +174,9 @@ class ItemlistItem:
|
||||
|
||||
|
||||
class ItemlistManager(QDialog):
|
||||
def __init__(self, items: list[ItemlistItem], callbacks: ItemlistManagerCallbacks) -> None:
|
||||
def __init__(
|
||||
self, items: list[ItemlistItem], callbacks: ItemlistManagerCallbacks
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.setWindowTitle("Item Manager")
|
||||
self.setMinimumSize(600, 400)
|
||||
@ -189,7 +191,7 @@ class ItemlistManager(QDialog):
|
||||
if not hh:
|
||||
raise ApplicationError("ItemlistManager failed to create horizontalHeader")
|
||||
hh.setStretchLastSection(True)
|
||||
self.table.setColumnWidth(0, 200)
|
||||
self.table.setColumnWidth(0, 288)
|
||||
self.table.setColumnWidth(1, 300)
|
||||
|
||||
self.populate_table()
|
||||
@ -246,19 +248,25 @@ class ItemlistManager(QDialog):
|
||||
|
||||
self.table.setCellWidget(row, 1, widget)
|
||||
|
||||
def rename_item(self, item_id: int) -> None:
|
||||
print(f"Rename item {item_id}")
|
||||
|
||||
def edit_item(self, item_id: int) -> None:
|
||||
print(f"Edit item {item_id}")
|
||||
self.callbacks.edit(item_id)
|
||||
|
||||
def delete_item(self, item_id: int) -> None:
|
||||
print(f"Delete item {item_id}")
|
||||
self.callbacks.delete(item_id)
|
||||
|
||||
def edit_item(self, item_id: int) -> None:
|
||||
self.callbacks.edit(item_id)
|
||||
|
||||
def rename_item(self, item_id: int) -> None:
|
||||
new_name = self.callbacks.rename(item_id)
|
||||
if not new_name:
|
||||
return
|
||||
# Rename item in list
|
||||
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
|
||||
break
|
||||
|
||||
def toggle_favourite(self, item_id: int, checked: bool) -> None:
|
||||
print(f"Toggle favourite for item {item_id}: {checked}")
|
||||
self.callbacks.favourite(item_id, checked)
|
||||
|
||||
for row in range(self.table.rowCount()):
|
||||
@ -273,20 +281,16 @@ class ItemlistManager(QDialog):
|
||||
break
|
||||
|
||||
def new_item(self) -> None:
|
||||
print("New item")
|
||||
|
||||
# test_items = [
|
||||
# {"id": 1, "text": "Item 1", "favourite": False},
|
||||
# {"id": 2, "text": "Item 2", "favourite": True},
|
||||
# {"id": 3, "text": "Item 3", "favourite": False}
|
||||
# ]
|
||||
self.callbacks.new_item()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ItemlistManagerCallbacks:
|
||||
edit: Callable[[int], None]
|
||||
delete: Callable[[int], None]
|
||||
edit: Callable[[int], None]
|
||||
favourite: Callable[[int, bool], None]
|
||||
new_item: Callable[[], None]
|
||||
rename: Callable[[int], Optional[str]]
|
||||
|
||||
|
||||
class PreviewManager:
|
||||
@ -451,16 +455,21 @@ class TemplateSelectorDialog(QDialog):
|
||||
Class to manage user selection of template
|
||||
"""
|
||||
|
||||
def __init__(self, templates: list[tuple[str, int]]) -> None:
|
||||
def __init__(
|
||||
self, templates: list[tuple[str, int]], template_prompt: Optional[str]
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.templates = templates
|
||||
self.template_prompt = template_prompt
|
||||
self.selected_id = None
|
||||
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
# Create label
|
||||
label = QLabel("Select template:")
|
||||
if not self.template_prompt:
|
||||
self.template_prompt = "Select template:"
|
||||
label = QLabel(self.template_prompt)
|
||||
|
||||
# Create combo box
|
||||
self.combo_box = QComboBox()
|
||||
@ -592,6 +601,61 @@ class Window(QMainWindow):
|
||||
self.load_last_playlists()
|
||||
self.stop_autoplay = False
|
||||
|
||||
# # # # # # # # # # Overrides # # # # # # # # # #
|
||||
|
||||
def closeEvent(self, event: Optional[QCloseEvent]) -> None:
|
||||
"""Handle attempt to close main window"""
|
||||
|
||||
if not event:
|
||||
return
|
||||
|
||||
# Don't allow window to close when a track is playing
|
||||
if track_sequence.current and track_sequence.current.is_playing():
|
||||
event.ignore()
|
||||
helpers.show_warning(
|
||||
self, "Track playing", "Can't close application while track is playing"
|
||||
)
|
||||
else:
|
||||
with db.Session() as session:
|
||||
# Save tab number of open playlists
|
||||
open_playlist_ids: dict[int, int] = {}
|
||||
for idx in range(self.playlist_section.tabPlaylist.count()):
|
||||
open_playlist_ids[
|
||||
self.playlist_section.tabPlaylist.widget(idx).playlist_id
|
||||
] = idx
|
||||
Playlists.clear_tabs(session, list(open_playlist_ids.keys()))
|
||||
for playlist_id, idx in open_playlist_ids.items():
|
||||
playlist = session.get(Playlists, playlist_id)
|
||||
if playlist:
|
||||
log.debug(f"Set {playlist=} tab to {idx=}")
|
||||
playlist.tab = idx
|
||||
|
||||
# Save window attributes
|
||||
attributes_to_save = dict(
|
||||
mainwindow_height=self.height(),
|
||||
mainwindow_width=self.width(),
|
||||
mainwindow_x=self.x(),
|
||||
mainwindow_y=self.y(),
|
||||
active_tab=self.playlist_section.tabPlaylist.currentIndex(),
|
||||
)
|
||||
for name, value in attributes_to_save.items():
|
||||
record = Settings.get_setting(session, name)
|
||||
record.f_int = value
|
||||
|
||||
session.commit()
|
||||
|
||||
event.accept()
|
||||
|
||||
# # # # # # # # # # Internal utility functions # # # # # # # # # #
|
||||
|
||||
def active_base_model(self) -> PlaylistModel:
|
||||
return self.current.base_model
|
||||
|
||||
def active_tab(self) -> PlaylistTab:
|
||||
return self.playlist_section.tabPlaylist.currentWidget()
|
||||
|
||||
# # # # # # # # # # Menu functions # # # # # # # # # #
|
||||
|
||||
def create_action(
|
||||
self, text: str, handler: Callable, shortcut: Optional[str] = None
|
||||
) -> QAction:
|
||||
@ -610,7 +674,7 @@ class Window(QMainWindow):
|
||||
menu_bar = self.menuBar()
|
||||
|
||||
# Load menu structure from YAML file
|
||||
with open("menu.yaml", "r") as file:
|
||||
with open("app/menu.yaml", "r") as file:
|
||||
menu_data = yaml.safe_load(file)
|
||||
|
||||
self.menu_actions = {} # Store reference for enabling/disabling actions
|
||||
@ -661,26 +725,231 @@ class Window(QMainWindow):
|
||||
items = getattr(self, f"get_{key}_items")()
|
||||
for item in items:
|
||||
action = QAction(item["text"], self)
|
||||
action.triggered.connect(
|
||||
lambda _, i=item["handler"]: getattr(self, i)()
|
||||
)
|
||||
|
||||
# Extract handler and arguments
|
||||
handler = getattr(self, item["handler"], None)
|
||||
args = item.get("args", ())
|
||||
|
||||
if handler:
|
||||
# Use a lambda to pass arguments to the function
|
||||
action.triggered.connect(lambda _, h=handler, a=args: h(*a))
|
||||
|
||||
submenu.addAction(action)
|
||||
break
|
||||
|
||||
def get_new_playlist_dynamic_submenu_items(self):
|
||||
"""Returns dynamically generated menu items for Submenu 1."""
|
||||
return [
|
||||
{"text": "Option A", "handler": "option_a_handler"},
|
||||
{"text": "Option B", "handler": "option_b_handler"},
|
||||
]
|
||||
def get_new_playlist_dynamic_submenu_items(
|
||||
self,
|
||||
) -> list[dict[str, str | tuple[Session, int]]]:
|
||||
"""
|
||||
Return dynamically generated menu items, in this case
|
||||
templates marked as favourite from which to generate a
|
||||
new playlist.
|
||||
|
||||
The handler is to call create_playlist with a session
|
||||
and template_id.
|
||||
"""
|
||||
|
||||
with db.Session() as session:
|
||||
submenu_items: list[dict[str, str | tuple[Session, int]]] = [
|
||||
{"text": "Show all",
|
||||
"handler": "create_playlist_from_template",
|
||||
"args": (session, 0)
|
||||
}
|
||||
]
|
||||
templates = Playlists.get_favourite_templates(session)
|
||||
for template in templates:
|
||||
submenu_items.append(
|
||||
{
|
||||
"text": template.name,
|
||||
"handler": "create_playlist_from_template",
|
||||
"args": (
|
||||
session,
|
||||
template.id,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return submenu_items
|
||||
|
||||
def get_query_dynamic_submenu_items(self):
|
||||
"""Returns dynamically generated menu items for Submenu 2."""
|
||||
return [
|
||||
{"text": "Action X", "handler": "action_x_handler"},
|
||||
{"text": "Action Xargs", "handler": "kae", "args": (21,)},
|
||||
{"text": "Action Y", "handler": "action_y_handler"},
|
||||
]
|
||||
|
||||
# # # # # # # # # # Playlist management functions # # # # # # # # # #
|
||||
|
||||
def _create_playlist(
|
||||
self, session: Session, name: str, template_id: int
|
||||
) -> Playlists:
|
||||
"""
|
||||
Create a playlist in the database, populate it from the template
|
||||
if template_id > 0, and return the Playlists object.
|
||||
"""
|
||||
|
||||
log.debug(f" _create_playlist({name=}, {template_id=})")
|
||||
|
||||
return Playlists(session, name, template_id)
|
||||
|
||||
def _open_playlist(self, playlist: Playlists, is_template: bool = False) -> int:
|
||||
"""
|
||||
With passed playlist:
|
||||
- create models
|
||||
- create tab
|
||||
- switch to tab
|
||||
- mark playist as open
|
||||
return: tab index
|
||||
"""
|
||||
|
||||
log.debug(f" _open_playlist({playlist=}, {is_template=})")
|
||||
|
||||
# Create base model and proxy model
|
||||
base_model = PlaylistModel(playlist.id, is_template)
|
||||
proxy_model = PlaylistProxyModel()
|
||||
proxy_model.setSourceModel(base_model)
|
||||
|
||||
# Create tab
|
||||
playlist_tab = PlaylistTab(musicmuster=self, model=proxy_model)
|
||||
idx = self.playlist_section.tabPlaylist.addTab(playlist_tab, playlist.name)
|
||||
|
||||
# Mark playlist as open
|
||||
playlist.mark_open()
|
||||
|
||||
# Switch to new tab
|
||||
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
||||
self.update_playlist_icons()
|
||||
|
||||
return idx
|
||||
|
||||
def create_playlist_from_template(self, session: Session, template_id: int) -> None:
|
||||
"""
|
||||
Prompt for new playlist name and create from passed template_id
|
||||
"""
|
||||
|
||||
if template_id == 0:
|
||||
# Show all templates
|
||||
selected_template_id = self.solicit_template_to_use(session)
|
||||
if selected_template_id is None:
|
||||
return
|
||||
else:
|
||||
template_id = selected_template_id
|
||||
|
||||
playlist_name = self.solicit_playlist_name(session)
|
||||
if not playlist_name:
|
||||
return
|
||||
|
||||
playlist = self._create_playlist(session, playlist_name, template_id)
|
||||
self._open_playlist(playlist)
|
||||
session.commit()
|
||||
|
||||
def delete_playlist(self) -> None:
|
||||
"""
|
||||
Delete current playlist
|
||||
"""
|
||||
|
||||
with db.Session() as session:
|
||||
playlist_id = self.current.playlist_id
|
||||
playlist = session.get(Playlists, playlist_id)
|
||||
if playlist:
|
||||
if helpers.ask_yes_no(
|
||||
"Delete playlist",
|
||||
f"Delete playlist '{playlist.name}': " "Are you sure?",
|
||||
):
|
||||
if self.close_playlist_tab():
|
||||
playlist.delete(session)
|
||||
session.commit()
|
||||
else:
|
||||
log.error("Failed to retrieve playlist")
|
||||
|
||||
def open_existing_playlist(self) -> None:
|
||||
"""Open existing playlist"""
|
||||
|
||||
with db.Session() as session:
|
||||
playlists = Playlists.get_closed(session)
|
||||
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
|
||||
dlg.exec()
|
||||
playlist = dlg.playlist
|
||||
if playlist:
|
||||
self._open_playlist(playlist)
|
||||
session.commit()
|
||||
|
||||
def save_as_template(self) -> None:
|
||||
"""Save current playlist as template"""
|
||||
|
||||
with db.Session() as session:
|
||||
template_names = [a.name for a in Playlists.get_all_templates(session)]
|
||||
|
||||
while True:
|
||||
# Get name for new template
|
||||
dlg = QInputDialog(self)
|
||||
dlg.setInputMode(QInputDialog.InputMode.TextInput)
|
||||
dlg.setLabelText("Template name:")
|
||||
dlg.resize(500, 100)
|
||||
ok = dlg.exec()
|
||||
if not ok:
|
||||
return
|
||||
|
||||
template_name = dlg.textValue()
|
||||
if template_name not in template_names:
|
||||
break
|
||||
helpers.show_warning(
|
||||
self, "Duplicate template", "Template name already in use"
|
||||
)
|
||||
Playlists.save_as_template(session, self.current.playlist_id, template_name)
|
||||
session.commit()
|
||||
helpers.show_OK("Template", "Template saved", self)
|
||||
|
||||
def solicit_playlist_name(
|
||||
self, session: Session, default: str = "", prompt: str = "Playlist name:"
|
||||
) -> Optional[str]:
|
||||
"""Get name of new playlist from user"""
|
||||
|
||||
dlg = QInputDialog(self)
|
||||
dlg.setInputMode(QInputDialog.InputMode.TextInput)
|
||||
dlg.setLabelText(prompt)
|
||||
while True:
|
||||
if default:
|
||||
dlg.setTextValue(default)
|
||||
dlg.resize(500, 100)
|
||||
ok = dlg.exec()
|
||||
if ok:
|
||||
proposed_name = dlg.textValue()
|
||||
if Playlists.name_is_available(session, proposed_name):
|
||||
return proposed_name
|
||||
else:
|
||||
helpers.show_warning(
|
||||
self,
|
||||
"Name in use",
|
||||
f"There's already a playlist called '{proposed_name}'",
|
||||
)
|
||||
continue
|
||||
else:
|
||||
return None
|
||||
|
||||
def solicit_template_to_use(
|
||||
self, session: Session, template_prompt: Optional[str] = None
|
||||
) -> Optional[int]:
|
||||
"""
|
||||
Have user select a template. Return the template.id, or None if they cancel.
|
||||
template_id of zero means don't use a template.
|
||||
"""
|
||||
|
||||
template_name_id_list: list[tuple[str, int]] = []
|
||||
template_name_id_list.append((Config.NO_TEMPLATE_NAME, 0))
|
||||
|
||||
with db.Session() as session:
|
||||
for template in Playlists.get_all_templates(session):
|
||||
template_name_id_list.append((template.name, template.id))
|
||||
|
||||
dlg = TemplateSelectorDialog(template_name_id_list, template_prompt)
|
||||
if not dlg.exec() or dlg.selected_id is None:
|
||||
return None # User cancelled
|
||||
|
||||
return dlg.selected_id
|
||||
|
||||
# # # # # # # # # # Miscellaneous functions # # # # # # # # # #
|
||||
|
||||
def select_duplicate_rows(self) -> None:
|
||||
"""Call playlist to select duplicate rows"""
|
||||
|
||||
@ -707,12 +976,6 @@ class Window(QMainWindow):
|
||||
QMessageBox.StandardButton.Ok,
|
||||
)
|
||||
|
||||
def active_base_model(self) -> PlaylistModel:
|
||||
return self.current.base_model
|
||||
|
||||
def active_tab(self) -> PlaylistTab:
|
||||
return self.playlist_section.tabPlaylist.currentWidget()
|
||||
|
||||
def clear_next(self) -> None:
|
||||
"""
|
||||
Clear next track
|
||||
@ -731,49 +994,6 @@ class Window(QMainWindow):
|
||||
# Clear the search bar
|
||||
self.search_playlist_clear()
|
||||
|
||||
def closeEvent(self, event: Optional[QCloseEvent]) -> None:
|
||||
"""Handle attempt to close main window"""
|
||||
|
||||
if not event:
|
||||
return
|
||||
|
||||
# Don't allow window to close when a track is playing
|
||||
if track_sequence.current and track_sequence.current.is_playing():
|
||||
event.ignore()
|
||||
helpers.show_warning(
|
||||
self, "Track playing", "Can't close application while track is playing"
|
||||
)
|
||||
else:
|
||||
with db.Session() as session:
|
||||
# Save tab number of open playlists
|
||||
open_playlist_ids: dict[int, int] = {}
|
||||
for idx in range(self.playlist_section.tabPlaylist.count()):
|
||||
open_playlist_ids[
|
||||
self.playlist_section.tabPlaylist.widget(idx).playlist_id
|
||||
] = idx
|
||||
Playlists.clear_tabs(session, list(open_playlist_ids.keys()))
|
||||
for playlist_id, idx in open_playlist_ids.items():
|
||||
playlist = session.get(Playlists, playlist_id)
|
||||
if playlist:
|
||||
log.debug(f"Set {playlist=} tab to {idx=}")
|
||||
playlist.tab = idx
|
||||
|
||||
# Save window attributes
|
||||
attributes_to_save = dict(
|
||||
mainwindow_height=self.height(),
|
||||
mainwindow_width=self.width(),
|
||||
mainwindow_x=self.x(),
|
||||
mainwindow_y=self.y(),
|
||||
active_tab=self.playlist_section.tabPlaylist.currentIndex(),
|
||||
)
|
||||
for name, value in attributes_to_save.items():
|
||||
record = Settings.get_setting(session, name)
|
||||
record.f_int = value
|
||||
|
||||
session.commit()
|
||||
|
||||
event.accept()
|
||||
|
||||
def close_playlist_tab(self) -> bool:
|
||||
"""
|
||||
Close active playlist tab, called by menu item
|
||||
@ -860,43 +1080,6 @@ class Window(QMainWindow):
|
||||
self.signals.search_songfacts_signal.connect(self.open_songfacts_browser)
|
||||
self.signals.search_wikipedia_signal.connect(self.open_wikipedia_browser)
|
||||
|
||||
def create_playlist(
|
||||
self, session: Session, playlist_name: str
|
||||
) -> Optional[Playlists]:
|
||||
"""Create new playlist"""
|
||||
|
||||
log.debug(f"create_playlist({playlist_name=}")
|
||||
|
||||
playlist = Playlists(session, playlist_name)
|
||||
|
||||
if playlist:
|
||||
return playlist
|
||||
else:
|
||||
log.error(f"Failed to create playlist, {playlist_name=}")
|
||||
|
||||
return None
|
||||
|
||||
def create_playlist_tab(self, playlist: Playlists) -> int:
|
||||
"""
|
||||
Take the passed playlist, create a playlist tab and
|
||||
add tab to display. Return index number of tab.
|
||||
"""
|
||||
|
||||
log.debug(f"create_playlist_tab({playlist=})")
|
||||
|
||||
# Create model and proxy model
|
||||
base_model = PlaylistModel(playlist.id)
|
||||
proxy_model = PlaylistProxyModel()
|
||||
proxy_model.setSourceModel(base_model)
|
||||
|
||||
# Create tab
|
||||
playlist_tab = PlaylistTab(musicmuster=self, model=proxy_model)
|
||||
idx = self.playlist_section.tabPlaylist.addTab(playlist_tab, playlist.name)
|
||||
|
||||
log.debug(f"create_playlist_tab() returned: {idx=}")
|
||||
|
||||
return idx
|
||||
|
||||
def current_row_or_end(self) -> int:
|
||||
"""
|
||||
If a row or rows are selected, return the row number of the first
|
||||
@ -915,25 +1098,6 @@ class Window(QMainWindow):
|
||||
|
||||
ipdb.set_trace()
|
||||
|
||||
def delete_playlist(self) -> None:
|
||||
"""
|
||||
Delete current playlist
|
||||
"""
|
||||
|
||||
with db.Session() as session:
|
||||
playlist_id = self.current.playlist_id
|
||||
playlist = session.get(Playlists, playlist_id)
|
||||
if playlist:
|
||||
if helpers.ask_yes_no(
|
||||
"Delete playlist",
|
||||
f"Delete playlist '{playlist.name}': " "Are you sure?",
|
||||
):
|
||||
if self.close_playlist_tab():
|
||||
playlist.delete(session)
|
||||
session.commit()
|
||||
else:
|
||||
log.error("Failed to retrieve playlist")
|
||||
|
||||
def download_played_tracks(self) -> None:
|
||||
"""Download a CSV of played tracks"""
|
||||
|
||||
@ -1064,6 +1228,18 @@ class Window(QMainWindow):
|
||||
if track_sequence.current:
|
||||
track_sequence.current.fade()
|
||||
|
||||
def get_tab_index_for_playlist(self, playlist_id: int) -> Optional[int]:
|
||||
"""
|
||||
Return the tab index for the passed playlist_id if it is displayed,
|
||||
else return None.
|
||||
"""
|
||||
|
||||
for idx in range(self.playlist_section.tabPlaylist.count()):
|
||||
if self.playlist_section.tabPlaylist.widget(idx).playlist_id == playlist_id:
|
||||
return idx
|
||||
|
||||
return None
|
||||
|
||||
def hide_played(self):
|
||||
"""Toggle hide played tracks"""
|
||||
|
||||
@ -1129,7 +1305,7 @@ class Window(QMainWindow):
|
||||
if playlist:
|
||||
log.debug(f"load_last_playlists() loaded {playlist=}")
|
||||
# Create tab
|
||||
playlist_ids.append(self.create_playlist_tab(playlist))
|
||||
playlist_ids.append(self._open_playlist(playlist))
|
||||
|
||||
# Set active tab
|
||||
record = Settings.get_setting(session, "active_tab")
|
||||
@ -1170,60 +1346,119 @@ class Window(QMainWindow):
|
||||
Delete / edit templates
|
||||
"""
|
||||
|
||||
def edit(template_id: int) -> None:
|
||||
"""Edit template"""
|
||||
|
||||
print(f"manage_templates.edit({template_id=}")
|
||||
|
||||
# Define callbacks to handle management options
|
||||
def delete(template_id: int) -> None:
|
||||
"""delete template"""
|
||||
|
||||
print(f"manage_templates.delete({template_id=}")
|
||||
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:
|
||||
"""favourite template"""
|
||||
"""Mark template as (not) favourite"""
|
||||
|
||||
print(f"manage_templates.favourite({template_id=}")
|
||||
template = session.get(Playlists, template_id)
|
||||
template.favourite = favourite
|
||||
session.commit()
|
||||
|
||||
callbacks = ItemlistManagerCallbacks(edit, delete, favourite)
|
||||
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):
|
||||
# TODO: need to add in favourites
|
||||
template_list.append(ItemlistItem(name=template.name, id=template.id))
|
||||
|
||||
# # Get user's selection
|
||||
# dlg = EditDeleteDialog(template_list)
|
||||
# if not dlg.exec():
|
||||
# return # User cancelled
|
||||
|
||||
# action, template_id = dlg.selection
|
||||
|
||||
# playlist = session.get(Playlists, template_id)
|
||||
# if not playlist:
|
||||
# log.error(f"Error opening {template_id=}")
|
||||
|
||||
# if action == "Edit":
|
||||
# # Simply load the template as a playlist. Any changes
|
||||
# # made will persist
|
||||
# idx = self.create_playlist_tab(playlist)
|
||||
# self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
||||
|
||||
# elif action == "Delete":
|
||||
# if helpers.ask_yes_no(
|
||||
# "Delete template",
|
||||
# f"Delete template '{playlist.name}': " "Are you sure?",
|
||||
# ):
|
||||
# if self.close_playlist_tab():
|
||||
# playlist.delete(session)
|
||||
# session.commit()
|
||||
# else:
|
||||
# raise ApplicationError(
|
||||
# f"Unrecognised action from EditDeleteDialog: {action=}"
|
||||
# )
|
||||
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:
|
||||
"""
|
||||
@ -1308,63 +1543,6 @@ class Window(QMainWindow):
|
||||
self.move_playlist_rows(unplayed_rows)
|
||||
self.disable_selection_timing = False
|
||||
|
||||
def new_playlist(self) -> None:
|
||||
"""
|
||||
Create new playlist, optionally from template
|
||||
"""
|
||||
|
||||
# Build a list of (template-name, playlist-id) tuples starting
|
||||
# with the "no template" entry
|
||||
template_list: list[tuple[str, int]] = []
|
||||
template_list.append((Config.NO_TEMPLATE_NAME, 0))
|
||||
|
||||
with db.Session() as session:
|
||||
for template in Playlists.get_all_templates(session):
|
||||
template_list.append((template.name, template.id))
|
||||
|
||||
dlg = TemplateSelectorDialog(template_list)
|
||||
if not dlg.exec():
|
||||
return # User cancelled
|
||||
template_id = dlg.selected_id
|
||||
|
||||
# Get a name for this new playlist
|
||||
playlist_name = self.solicit_playlist_name(session)
|
||||
if not playlist_name:
|
||||
return
|
||||
|
||||
# If template_id == 0, user doesn't want a template
|
||||
if template_id == 0:
|
||||
playlist = self.create_playlist(session, playlist_name)
|
||||
else:
|
||||
playlist = Playlists.create_playlist_from_template(
|
||||
session, template, playlist_name
|
||||
)
|
||||
|
||||
if playlist:
|
||||
playlist.mark_open()
|
||||
# Need to ensure that the new playlist is committed to
|
||||
# the database before it is opened by the model.
|
||||
session.commit()
|
||||
idx = self.create_playlist_tab(playlist)
|
||||
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
||||
else:
|
||||
log.error("Playlist failed to create")
|
||||
|
||||
def open_playlist(self) -> None:
|
||||
"""Open existing playlist"""
|
||||
|
||||
with db.Session() as session:
|
||||
playlists = Playlists.get_closed(session)
|
||||
dlg = SelectPlaylistDialog(self, playlists=playlists, session=session)
|
||||
dlg.exec()
|
||||
playlist = dlg.playlist
|
||||
if playlist:
|
||||
idx = self.create_playlist_tab(playlist)
|
||||
playlist.mark_open()
|
||||
session.commit()
|
||||
|
||||
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
||||
|
||||
def open_songfacts_browser(self, title: str) -> None:
|
||||
"""Search Songfacts for title"""
|
||||
|
||||
@ -1688,32 +1866,6 @@ class Window(QMainWindow):
|
||||
)
|
||||
track_sequence.current.start_time -= dt.timedelta(milliseconds=elapsed_ms)
|
||||
|
||||
def save_as_template(self) -> None:
|
||||
"""Save current playlist as template"""
|
||||
|
||||
with db.Session() as session:
|
||||
template_names = [a.name for a in Playlists.get_all_templates(session)]
|
||||
|
||||
while True:
|
||||
# Get name for new template
|
||||
dlg = QInputDialog(self)
|
||||
dlg.setInputMode(QInputDialog.InputMode.TextInput)
|
||||
dlg.setLabelText("Template name:")
|
||||
dlg.resize(500, 100)
|
||||
ok = dlg.exec()
|
||||
if not ok:
|
||||
return
|
||||
|
||||
template_name = dlg.textValue()
|
||||
if template_name not in template_names:
|
||||
break
|
||||
helpers.show_warning(
|
||||
self, "Duplicate template", "Template name already in use"
|
||||
)
|
||||
Playlists.save_as_template(session, self.current.playlist_id, template_name)
|
||||
session.commit()
|
||||
helpers.show_OK("Template", "Template saved", self)
|
||||
|
||||
def search_playlist(self) -> None:
|
||||
"""Show text box to search playlist"""
|
||||
|
||||
@ -1830,43 +1982,16 @@ class Window(QMainWindow):
|
||||
|
||||
# Switch to correct tab
|
||||
if playlist_id != self.current.playlist_id:
|
||||
for idx in range(self.playlist_section.tabPlaylist.count()):
|
||||
if (
|
||||
self.playlist_section.tabPlaylist.widget(idx).playlist_id
|
||||
== playlist_id
|
||||
):
|
||||
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
||||
break
|
||||
open_idx = self.get_tab_index_for_playlist(playlist_id)
|
||||
if open_idx:
|
||||
self.playlist_section.tabPlaylist.setCurrentIndex(open_idx)
|
||||
else:
|
||||
raise ApplicationError(
|
||||
f"show_track() can't find current playlist tab {playlist_id=}"
|
||||
)
|
||||
|
||||
self.active_tab().scroll_to_top(playlist_track.row_number)
|
||||
|
||||
def solicit_playlist_name(
|
||||
self, session: Session, default: str = ""
|
||||
) -> Optional[str]:
|
||||
"""Get name of new playlist from user"""
|
||||
|
||||
dlg = QInputDialog(self)
|
||||
dlg.setInputMode(QInputDialog.InputMode.TextInput)
|
||||
dlg.setLabelText("Playlist name:")
|
||||
while True:
|
||||
if default:
|
||||
dlg.setTextValue(default)
|
||||
dlg.resize(500, 100)
|
||||
ok = dlg.exec()
|
||||
if ok:
|
||||
proposed_name = dlg.textValue()
|
||||
if Playlists.name_is_available(session, proposed_name):
|
||||
return proposed_name
|
||||
else:
|
||||
helpers.show_warning(
|
||||
self,
|
||||
"Name in use",
|
||||
f"There's already a playlist called '{proposed_name}'",
|
||||
)
|
||||
continue
|
||||
else:
|
||||
return None
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop playing immediately"""
|
||||
|
||||
@ -2074,6 +2199,10 @@ class Window(QMainWindow):
|
||||
self.playlist_section.tabPlaylist.setTabIcon(
|
||||
idx, QIcon(Config.PLAYLIST_ICON_CURRENT)
|
||||
)
|
||||
elif self.playlist_section.tabPlaylist.widget(idx).model().sourceModel().is_template:
|
||||
self.playlist_section.tabPlaylist.setTabIcon(
|
||||
idx, QIcon(Config.PLAYLIST_ICON_TEMPLATE)
|
||||
)
|
||||
else:
|
||||
self.playlist_section.tabPlaylist.setTabIcon(idx, QIcon())
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ from PyQt6.QtGui import (
|
||||
)
|
||||
|
||||
# Third party imports
|
||||
from sqlalchemy.orm.session import Session
|
||||
import obswebsocket # type: ignore
|
||||
|
||||
# import snoop # type: ignore
|
||||
@ -74,12 +75,14 @@ class PlaylistModel(QAbstractTableModel):
|
||||
def __init__(
|
||||
self,
|
||||
playlist_id: int,
|
||||
is_template: bool,
|
||||
*args: Optional[QObject],
|
||||
**kwargs: Optional[QObject],
|
||||
) -> None:
|
||||
log.debug("PlaylistModel.__init__()")
|
||||
|
||||
self.playlist_id = playlist_id
|
||||
self.is_template = is_template
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.playlist_rows: dict[int, RowAndTrack] = {}
|
||||
@ -498,7 +501,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
"""
|
||||
|
||||
if not index.isValid():
|
||||
return Qt.ItemFlag.NoItemFlags
|
||||
return Qt.ItemFlag.ItemIsDropEnabled
|
||||
|
||||
default = (
|
||||
Qt.ItemFlag.ItemIsEnabled
|
||||
@ -772,7 +775,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
|
||||
return None
|
||||
|
||||
def load_data(self, session: db.session) -> None:
|
||||
def load_data(self, session: Session) -> None:
|
||||
"""
|
||||
Same as refresh data, but only used when creating playslit.
|
||||
Distinguishes profile time between initial load and other
|
||||
@ -1061,7 +1064,7 @@ class PlaylistModel(QAbstractTableModel):
|
||||
# Update display
|
||||
self.invalidate_row(track_sequence.previous.row_number)
|
||||
|
||||
def refresh_data(self, session: db.session) -> None:
|
||||
def refresh_data(self, session: Session) -> None:
|
||||
"""
|
||||
Populate self.playlist_rows with playlist data
|
||||
|
||||
|
||||
@ -343,7 +343,7 @@ class PlaylistTab(QTableView):
|
||||
Override closeEditor to enable play controls and update display.
|
||||
"""
|
||||
|
||||
self.musicmuster.action_Clear_selection.setEnabled(True)
|
||||
self.musicmuster.enable_escape(True)
|
||||
|
||||
super(PlaylistTab, self).closeEditor(editor, hint)
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<RCC>
|
||||
<qresource prefix="icons">
|
||||
<file>yellow-circle.png</file>
|
||||
<file>redstar.png</file>
|
||||
<file>green-circle.png</file>
|
||||
<file>star.png</file>
|
||||
<file>star_empty.png</file>
|
||||
|
||||
1399
app/ui/icons_rc.py
1399
app/ui/icons_rc.py
File diff suppressed because it is too large
Load Diff
@ -1143,7 +1143,7 @@ padding-left: 8px;</string>
|
||||
</action>
|
||||
<action name="actionOpenPlaylist">
|
||||
<property name="text">
|
||||
<string>Open &playlist...</string>
|
||||
<string>O&pen...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionNewPlaylist">
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1249</width>
|
||||
<height>499</height>
|
||||
<height>538</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
|
||||
@ -764,7 +764,7 @@ class Ui_MainWindow(object):
|
||||
)
|
||||
self.actionE_xit.setText(_translate("MainWindow", "E&xit"))
|
||||
self.actionTest.setText(_translate("MainWindow", "&Test"))
|
||||
self.actionOpenPlaylist.setText(_translate("MainWindow", "Open &playlist..."))
|
||||
self.actionOpenPlaylist.setText(_translate("MainWindow", "O&pen..."))
|
||||
self.actionNewPlaylist.setText(_translate("MainWindow", "&New..."))
|
||||
self.actionTestFunction.setText(_translate("MainWindow", "&Test function"))
|
||||
self.actionSkipToFade.setText(
|
||||
@ -847,4 +847,4 @@ class Ui_MainWindow(object):
|
||||
|
||||
|
||||
from infotabs import InfoTabs
|
||||
from pyqtgraph import PlotWidget
|
||||
from pyqtgraph import PlotWidget # type: ignore
|
||||
|
||||
@ -1 +0,0 @@
|
||||
env.py.DEBUG
|
||||
27
migrations/env.py
Normal file
27
migrations/env.py
Normal file
@ -0,0 +1,27 @@
|
||||
from importlib import import_module
|
||||
from alembic import context
|
||||
from alchemical.alembic.env import run_migrations
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# import the application's Alchemical instance
|
||||
try:
|
||||
import_mod, db_name = config.get_main_option('alchemical_db', '').split(
|
||||
':')
|
||||
db = getattr(import_module(import_mod), db_name)
|
||||
except (ModuleNotFoundError, AttributeError):
|
||||
raise ValueError(
|
||||
'Could not import the Alchemical database instance. '
|
||||
'Ensure that the alchemical_db setting in alembic.ini is correct.'
|
||||
)
|
||||
|
||||
# run the migration engine
|
||||
# The dictionary provided as second argument includes options to pass to the
|
||||
# Alembic context. For details on what other options are available, see
|
||||
# https://alembic.sqlalchemy.org/en/latest/autogenerate.html
|
||||
run_migrations(db, {
|
||||
'render_as_batch': True,
|
||||
'compare_type': True,
|
||||
})
|
||||
@ -1,28 +0,0 @@
|
||||
from importlib import import_module
|
||||
from alembic import context
|
||||
from alchemical.alembic.env import run_migrations
|
||||
|
||||
# Load Alembic configuration
|
||||
config = context.config
|
||||
|
||||
try:
|
||||
# Import the Alchemical database instance as specified in alembic.ini
|
||||
import_mod, db_name = config.get_main_option('alchemical_db', '').split(':')
|
||||
db = getattr(import_module(import_mod), db_name)
|
||||
print(f"Successfully loaded Alchemical database instance: {db}")
|
||||
|
||||
# Use the metadata associated with the Alchemical instance
|
||||
metadata = db.Model.metadata
|
||||
print(f"Metadata tables detected: {metadata.tables.keys()}") # Debug output
|
||||
except (ModuleNotFoundError, AttributeError) as e:
|
||||
raise ValueError(
|
||||
'Could not import the Alchemical database instance or access metadata. '
|
||||
'Ensure that the alchemical_db setting in alembic.ini is correct and '
|
||||
'that the Alchemical instance is correctly configured.'
|
||||
) from e
|
||||
|
||||
# Run migrations with metadata
|
||||
run_migrations(db, {
|
||||
'render_as_batch': True,
|
||||
'compare_type': True,
|
||||
})
|
||||
@ -1,27 +0,0 @@
|
||||
from importlib import import_module
|
||||
from alembic import context
|
||||
from alchemical.alembic.env import run_migrations
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# import the application's Alchemical instance
|
||||
try:
|
||||
import_mod, db_name = config.get_main_option('alchemical_db', '').split(
|
||||
':')
|
||||
db = getattr(import_module(import_mod), db_name)
|
||||
except (ModuleNotFoundError, AttributeError):
|
||||
raise ValueError(
|
||||
'Could not import the Alchemical database instance. '
|
||||
'Ensure that the alchemical_db setting in alembic.ini is correct.'
|
||||
)
|
||||
|
||||
# run the migration engine
|
||||
# The dictionary provided as second argument includes options to pass to the
|
||||
# Alembic context. For details on what other options are available, see
|
||||
# https://alembic.sqlalchemy.org/en/latest/autogenerate.html
|
||||
run_migrations(db, {
|
||||
'render_as_batch': True,
|
||||
'compare_type': True,
|
||||
})
|
||||
@ -1,16 +1,16 @@
|
||||
"""Index for notesolours substring
|
||||
"""add favouirit to playlists
|
||||
|
||||
Revision ID: c76e865ccb85
|
||||
Revision ID: 04df697e40cd
|
||||
Revises: 33c04e3c12c8
|
||||
Create Date: 2025-02-07 18:21:01.760057
|
||||
Create Date: 2025-02-22 20:20:45.030024
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'c76e865ccb85'
|
||||
revision = '04df697e40cd'
|
||||
down_revision = '33c04e3c12c8'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
@ -30,39 +30,29 @@ def downgrade(engine_name: str) -> None:
|
||||
def upgrade_() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('notecolours', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('strip_substring', sa.Boolean(), nullable=False))
|
||||
batch_op.create_index(batch_op.f('ix_notecolours_substring'), ['substring'], unique=False)
|
||||
|
||||
with op.batch_alter_table('playdates', schema=None) as batch_op:
|
||||
batch_op.drop_constraint('fk_playdates_track_id_tracks', type_='foreignkey')
|
||||
batch_op.create_foreign_key(None, 'tracks', ['track_id'], ['id'], ondelete='CASCADE')
|
||||
|
||||
with op.batch_alter_table('playlist_rows', schema=None) as batch_op:
|
||||
batch_op.drop_constraint('playlist_rows_ibfk_1', type_='foreignkey')
|
||||
batch_op.create_foreign_key(None, 'tracks', ['track_id'], ['id'], ondelete='CASCADE')
|
||||
|
||||
with op.batch_alter_table('queries', schema=None) as batch_op:
|
||||
batch_op.drop_constraint('fk_queries_playlist_id_playlists', type_='foreignkey')
|
||||
batch_op.create_foreign_key(None, 'playlists', ['playlist_id'], ['id'], ondelete='CASCADE')
|
||||
with op.batch_alter_table('playlists', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('favourite', sa.Boolean(), nullable=False))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade_() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('queries', schema=None) as batch_op:
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.create_foreign_key('fk_queries_playlist_id_playlists', 'playlists', ['playlist_id'], ['id'])
|
||||
with op.batch_alter_table('playlists', schema=None) as batch_op:
|
||||
batch_op.drop_column('favourite')
|
||||
|
||||
with op.batch_alter_table('playlist_rows', schema=None) as batch_op:
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.create_foreign_key('playlist_rows_ibfk_1', 'tracks', ['track_id'], ['id'])
|
||||
|
||||
with op.batch_alter_table('playdates', schema=None) as batch_op:
|
||||
batch_op.drop_constraint(None, type_='foreignkey')
|
||||
batch_op.create_foreign_key('fk_playdates_track_id_tracks', 'tracks', ['track_id'], ['id'])
|
||||
|
||||
with op.batch_alter_table('notecolours', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_notecolours_substring'))
|
||||
batch_op.drop_column('strip_substring')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
"""Add queries table
|
||||
|
||||
Revision ID: 9c1254a8026d
|
||||
Revises: c76e865ccb85
|
||||
Create Date: 2025-02-14 16:32:37.064567
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '9c1254a8026d'
|
||||
down_revision = 'c76e865ccb85'
|
||||
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('sql', sa.String(length=2048), nullable=False),
|
||||
sa.Column('description', sa.String(length=512), 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