Compare commits

..

No commits in common. "639f006a100ad0b995484fbdd9de7ac5932a43c6" and "aef8cb5cb5b011c67e063b9f7565c4796c374834" have entirely different histories.

8 changed files with 208 additions and 580 deletions

View File

@ -31,7 +31,6 @@ class Config(object):
COLOUR_NORMAL_TAB = "#000000" COLOUR_NORMAL_TAB = "#000000"
COLOUR_NOTES_PLAYLIST = "#b8daff" COLOUR_NOTES_PLAYLIST = "#b8daff"
COLOUR_ODD_PLAYLIST = "#f2f2f2" COLOUR_ODD_PLAYLIST = "#f2f2f2"
COLOUR_TEMPLATE_ROW = "#FFAF68"
COLOUR_UNREADABLE = "#dc3545" COLOUR_UNREADABLE = "#dc3545"
COLOUR_WARNING_TIMER = "#ffc107" COLOUR_WARNING_TIMER = "#ffc107"
DBFS_SILENCE = -50 DBFS_SILENCE = -50

View File

@ -80,8 +80,8 @@ class PlaylistsTable(Model):
cascade="all, delete-orphan", cascade="all, delete-orphan",
order_by="PlaylistRowsTable.row_number", order_by="PlaylistRowsTable.row_number",
) )
favourite: Mapped[bool] = mapped_column( query: Mapped["QueriesTable"] = relationship(
Boolean, nullable=False, index=False, default=False back_populates="playlist", cascade="all, delete-orphan"
) )
def __repr__(self) -> str: def __repr__(self) -> str:
@ -121,6 +121,20 @@ class PlaylistRowsTable(Model):
) )
class QueriesTable(Model):
__tablename__ = "queries"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
query: Mapped[str] = mapped_column(
String(2048), index=False, default="", nullable=False
)
playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id"), index=True)
playlist: Mapped[PlaylistsTable] = relationship(back_populates="query")
def __repr__(self) -> str:
return f"<Queries(id={self.id}, playlist={self.playlist}, query={self.query}>"
class SettingsTable(Model): class SettingsTable(Model):
"""Manage settings""" """Manage settings"""

View File

@ -203,12 +203,12 @@ class Playlists(dbtables.PlaylistsTable):
@classmethod @classmethod
def create_playlist_from_template( def create_playlist_from_template(
cls, session: Session, template_id: int, playlist_name: str cls, session: Session, template: "Playlists", playlist_name: str
) -> Optional["Playlists"]: ) -> Optional["Playlists"]:
"""Create a new playlist from template""" """Create a new playlist from template"""
# Sanity check # Sanity check
if not template_id: if not template.id:
return None return None
playlist = cls(session, playlist_name) playlist = cls(session, playlist_name)
@ -217,7 +217,7 @@ class Playlists(dbtables.PlaylistsTable):
if not playlist or not playlist.id: if not playlist or not playlist.id:
return None return None
PlaylistRows.copy_playlist(session, template_id, playlist.id) PlaylistRows.copy_playlist(session, template.id, playlist.id)
return playlist return playlist

View File

@ -1,18 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Standard library imports # Standard library imports
from __future__ import annotations
from slugify import slugify # type: ignore from slugify import slugify # type: ignore
from typing import Callable, Optional from typing import Callable, Optional
import argparse import argparse
from dataclasses import dataclass
import datetime as dt import datetime as dt
import os import os
import subprocess import subprocess
import sys import sys
import urllib.parse import urllib.parse
import webbrowser import webbrowser
import yaml
# PyQt imports # PyQt imports
from PyQt6.QtCore import ( from PyQt6.QtCore import (
@ -25,7 +22,6 @@ from PyQt6.QtGui import (
QAction, QAction,
QCloseEvent, QCloseEvent,
QColor, QColor,
QFont,
QIcon, QIcon,
QKeySequence, QKeySequence,
QPalette, QPalette,
@ -33,7 +29,6 @@ from PyQt6.QtGui import (
) )
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QApplication,
QCheckBox,
QComboBox, QComboBox,
QDialog, QDialog,
QFileDialog, QFileDialog,
@ -43,11 +38,8 @@ from PyQt6.QtWidgets import (
QLineEdit, QLineEdit,
QListWidgetItem, QListWidgetItem,
QMainWindow, QMainWindow,
QMenu,
QMessageBox, QMessageBox,
QPushButton, QPushButton,
QTableWidget,
QTableWidgetItem,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
) )
@ -83,6 +75,18 @@ from utilities import check_db, update_bitrates
import helpers import helpers
class DownloadCSV(QDialog):
def __init__(self, parent=None):
super().__init__()
self.ui = Ui_DateSelect()
self.ui.setupUi(self)
self.ui.dateTimeEdit.setDate(QDate.currentDate())
self.ui.dateTimeEdit.setTime(QTime(19, 59, 0))
self.ui.buttonBox.accepted.connect(self.accept)
self.ui.buttonBox.rejected.connect(self.reject)
class Current: class Current:
base_model: PlaylistModel base_model: PlaylistModel
proxy_model: PlaylistProxyModel proxy_model: PlaylistProxyModel
@ -96,18 +100,6 @@ class Current:
) )
class DownloadCSV(QDialog):
def __init__(self, parent=None):
super().__init__()
self.ui = Ui_DateSelect()
self.ui.setupUi(self)
self.ui.dateTimeEdit.setDate(QDate.currentDate())
self.ui.dateTimeEdit.setTime(QTime(19, 59, 0))
self.ui.buttonBox.accepted.connect(self.accept)
self.ui.buttonBox.rejected.connect(self.reject)
class EditDeleteDialog(QDialog): class EditDeleteDialog(QDialog):
def __init__(self, templates: list[tuple[str, int]]) -> None: def __init__(self, templates: list[tuple[str, int]]) -> None:
super().__init__() super().__init__()
@ -166,134 +158,6 @@ class EditDeleteDialog(QDialog):
self.reject() self.reject()
@dataclass
class ItemlistItem:
id: int
name: str
favourite: bool = False
class ItemlistManager(QDialog):
def __init__(
self, items: list[ItemlistItem], callbacks: ItemlistManagerCallbacks
) -> None:
super().__init__()
self.setWindowTitle("Item Manager")
self.setMinimumSize(600, 400)
self.items = items
self.callbacks = callbacks
layout = QVBoxLayout(self)
self.table = QTableWidget(len(items), 2, self)
self.table.setHorizontalHeaderLabels(["Item", "Actions"])
hh = self.table.horizontalHeader()
if not hh:
raise ApplicationError("ItemlistManager failed to create horizontalHeader")
hh.setStretchLastSection(True)
self.table.setColumnWidth(0, 200)
self.table.setColumnWidth(1, 300)
self.populate_table()
layout.addWidget(self.table)
button_layout = QHBoxLayout()
self.new_button = QPushButton("New")
self.new_button.clicked.connect(self.new_item)
button_layout.addWidget(self.new_button)
self.close_button = QPushButton("Close")
self.close_button.clicked.connect(self.close)
button_layout.addWidget(self.close_button)
layout.addLayout(button_layout)
def populate_table(self) -> None:
"""Populates the table with items and action buttons."""
self.table.setRowCount(len(self.items))
for row, item in enumerate(self.items):
item_text = QTableWidgetItem(item.name)
if item.favourite:
item_text.setFont(QFont("Arial", weight=QFont.Weight.Bold))
self.table.setItem(row, 0, item_text)
# Action Buttons and Checkbox in a widget
widget = QWidget()
h_layout = QHBoxLayout(widget)
h_layout.setContentsMargins(0, 0, 0, 0)
h_layout.setSpacing(5)
rename_button = QPushButton("Rename")
rename_button.clicked.connect(lambda _, i=item.id: self.rename_item(i))
h_layout.addWidget(rename_button)
edit_button = QPushButton("Edit")
edit_button.clicked.connect(lambda _, i=item.id: self.edit_item(i))
h_layout.addWidget(edit_button)
delete_button = QPushButton("Delete")
delete_button.clicked.connect(lambda _, i=item.id: self.delete_item(i))
h_layout.addWidget(delete_button)
fav_checkbox = QCheckBox()
fav_checkbox.setChecked(item.favourite)
fav_checkbox.stateChanged.connect(
lambda state, cb=fav_checkbox, i=item.id: self.toggle_favourite(
i, cb.isChecked()
)
)
h_layout.addWidget(fav_checkbox)
self.table.setCellWidget(row, 1, widget)
def delete_item(self, item_id: int) -> None:
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()):
item = self.table.item(row, 0)
if item and self.items[row].id == item_id:
font = QFont(
"Arial",
weight=QFont.Weight.Bold if checked else QFont.Weight.Normal,
)
item.setFont(font)
self.items[row].favourite = checked
break
def new_item(self) -> None:
self.callbacks.new_item()
@dataclass
class ItemlistManagerCallbacks:
delete: Callable[[int], None]
edit: Callable[[int], None]
favourite: Callable[[int, bool], None]
new_item: Callable[[], None]
rename: Callable[[int], Optional[str]]
class PreviewManager: class PreviewManager:
""" """
Manage track preview player Manage track preview player
@ -456,21 +320,16 @@ class TemplateSelectorDialog(QDialog):
Class to manage user selection of template Class to manage user selection of template
""" """
def __init__( def __init__(self, templates: list[tuple[str, int]]) -> None:
self, templates: list[tuple[str, int]], template_prompt: Optional[str]
) -> None:
super().__init__() super().__init__()
self.templates = templates self.templates = templates
self.template_prompt = template_prompt
self.selected_id = None self.selected_id = None
self.init_ui() self.init_ui()
def init_ui(self): def init_ui(self):
# Create label # Create label
if not self.template_prompt: label = QLabel("Select template:")
self.template_prompt = "Select template:"
label = QLabel(self.template_prompt)
# Create combo box # Create combo box
self.combo_box = QComboBox() self.combo_box = QComboBox()
@ -616,85 +475,104 @@ class Window(QMainWindow):
return action return action
def create_menu_bar(self): def create_menu_bar(self):
"""Dynamically creates the menu bar from a YAML file."""
menu_bar = self.menuBar() menu_bar = self.menuBar()
# Load menu structure from YAML file # File Menu
with open("menu.yaml", "r") as file: file_menu = menu_bar.addMenu("&File")
menu_data = yaml.safe_load(file) file_menu.addAction(
self.create_action("Open Playlist", self.open_playlist, "Ctrl+O")
)
file_menu.addAction(self.create_action("New Playlist", self.new_playlist))
file_menu.addAction(
self.create_action("Close Playlist", self.close_playlist_tab)
)
file_menu.addAction(self.create_action("Rename Playlist", self.rename_playlist))
file_menu.addAction(self.create_action("Delete Playlist", self.delete_playlist))
file_menu.addSeparator()
file_menu.addAction(
self.create_action("Save as Template", self.save_as_template)
)
file_menu.addAction(
self.create_action("Manage Templates", self.manage_templates)
)
file_menu.addSeparator()
file_menu.addAction(
self.create_action(
"Import Files", self.import_files_wrapper, "Ctrl+Shift+I"
)
)
file_menu.addSeparator()
file_menu.addAction(self.create_action("Exit", self.close))
self.menu_actions = {} # Store reference for enabling/disabling actions # Playlist Menu
self.dynamic_submenus = {} # Store submenus for dynamic population playlist_menu = menu_bar.addMenu("&Playlist")
playlist_menu.addSeparator()
playlist_menu.addAction(
self.create_action("Insert Track", self.insert_track, "Ctrl+T")
)
playlist_menu.addAction(
self.create_action("Insert Section Header", self.insert_header, "Ctrl+H")
)
playlist_menu.addSeparator()
playlist_menu.addAction(
self.create_action("Mark for Moving", self.mark_rows_for_moving, "Ctrl+C")
)
playlist_menu.addAction(self.create_action("Paste", self.paste_rows, "Ctrl+V"))
playlist_menu.addSeparator()
playlist_menu.addAction(
self.create_action("Export Playlist", self.export_playlist_tab)
)
playlist_menu.addAction(
self.create_action(
"Download CSV of Played Tracks", self.download_played_tracks
)
)
playlist_menu.addSeparator()
playlist_menu.addAction(
self.create_action(
"Select Duplicate Rows",
lambda: self.active_tab().select_duplicate_rows(),
)
)
playlist_menu.addAction(self.create_action("Move Selected", self.move_selected))
playlist_menu.addAction(self.create_action("Move Unplayed", self.move_unplayed))
for menu_item in menu_data["menus"]: # Clear Selection with Escape key. Save in module so we can
menu = menu_bar.addMenu(menu_item["title"]) # enable/disable it later
self.action_Clear_selection = self.create_action(
"Clear Selection", self.clear_selection, "Esc"
)
playlist_menu.addAction(self.action_Clear_selection)
for action_item in menu_item["actions"]: # Music Menu
if "separator" in action_item and action_item["separator"]: music_menu = menu_bar.addMenu("&Music")
menu.addSeparator() music_menu.addAction(
continue self.create_action("Set Next", self.set_selected_track_next, "Ctrl+N")
)
music_menu.addAction(self.create_action("Play Next", self.play_next, "Return"))
music_menu.addAction(self.create_action("Fade", self.fade, "Ctrl+Z"))
music_menu.addAction(self.create_action("Stop", self.stop, "Ctrl+Alt+S"))
music_menu.addAction(self.create_action("Resume", self.resume, "Ctrl+R"))
music_menu.addAction(
self.create_action("Skip to Next", self.play_next, "Ctrl+Alt+Return")
)
music_menu.addSeparator()
music_menu.addAction(self.create_action("Search", self.search_playlist, "/"))
music_menu.addAction(
self.create_action(
"Search Title in Wikipedia", self.lookup_row_in_wikipedia, "Ctrl+W"
)
)
music_menu.addAction(
self.create_action(
"Search Title in Songfacts", self.lookup_row_in_songfacts, "Ctrl+S"
)
)
# Check whether this is a submenu first # Help Menu
if action_item.get("submenu"): help_menu = menu_bar.addMenu("Help")
submenu = QMenu(action_item["text"], self) help_menu.addAction(self.create_action("About", self.about))
menu.addMenu(submenu) help_menu.addAction(self.create_action("Debug", self.debug))
# Store submenu reference for dynamic population
self.dynamic_submenus[action_item["handler"]] = submenu
submenu.aboutToShow.connect(self.populate_dynamic_submenu)
continue # Skip the rest of the loop (no handler needed)
# Now check for a normal menu action
handler = getattr(self, action_item["handler"], None)
if handler is None:
print(f"Warning: No handler found for {action_item['text']}")
continue
action = self.create_action(
action_item["text"], handler, action_item.get("shortcut")
)
# Store reference to "Clear Selection" so we can enable/disable it
if action_item.get("store_reference"):
self.menu_actions[action_item["handler"]] = action
menu.addAction(action)
def populate_dynamic_submenu(self):
"""Dynamically populates submenus when they are selected."""
submenu = self.sender() # Get the submenu that triggered the event
# Find which submenu it is
for key, stored_submenu in self.dynamic_submenus.items():
if submenu == stored_submenu:
submenu.clear()
# Dynamically call the correct function
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)()
)
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_query_dynamic_submenu_items(self):
"""Returns dynamically generated menu items for Submenu 2."""
return [
{"text": "Action X", "handler": "action_x_handler"},
{"text": "Action Y", "handler": "action_y_handler"},
]
def select_duplicate_rows(self) -> None:
"""Call playlist to select duplicate rows"""
self.active_tab().select_duplicate_rows()
def about(self) -> None: def about(self) -> None:
"""Get git tag and database name""" """Get git tag and database name"""
@ -871,23 +749,13 @@ class Window(QMainWindow):
self.signals.search_wikipedia_signal.connect(self.open_wikipedia_browser) self.signals.search_wikipedia_signal.connect(self.open_wikipedia_browser)
def create_playlist( def create_playlist(
self, session: Session, template_id: int self, session: Session, playlist_name: str
) -> Optional[Playlists]: ) -> Optional[Playlists]:
"""Create new playlist""" """Create new playlist"""
# Get a name for this new playlist log.debug(f"create_playlist({playlist_name=}")
playlist_name = self.solicit_playlist_name(session)
if not playlist_name:
return None
# If template.id == 0, user doesn't want a template playlist = Playlists(session, playlist_name)
playlist: Optional[Playlists]
if template_id == 0:
playlist = Playlists(session, playlist_name)
else:
playlist = Playlists.create_playlist_from_template(
session, template_id, playlist_name
)
if playlist: if playlist:
return playlist return playlist
@ -896,7 +764,7 @@ class Window(QMainWindow):
return None return None
def create_playlist_tab(self, playlist: Playlists, is_template: bool = False) -> int: def create_playlist_tab(self, playlist: Playlists) -> int:
""" """
Take the passed playlist, create a playlist tab and Take the passed playlist, create a playlist tab and
add tab to display. Return index number of tab. add tab to display. Return index number of tab.
@ -905,7 +773,7 @@ class Window(QMainWindow):
log.debug(f"create_playlist_tab({playlist=})") log.debug(f"create_playlist_tab({playlist=})")
# Create model and proxy model # Create model and proxy model
base_model = PlaylistModel(playlist.id, is_template) base_model = PlaylistModel(playlist.id)
proxy_model = PlaylistProxyModel() proxy_model = PlaylistProxyModel()
proxy_model.setSourceModel(base_model) proxy_model.setSourceModel(base_model)
@ -995,8 +863,7 @@ class Window(QMainWindow):
log.debug(f"enable_escape({enabled=})") log.debug(f"enable_escape({enabled=})")
if "clear_selection" in self.menu_actions: self.action_Clear_selection.setEnabled(enabled)
self.menu_actions["clear_selection"].setEnabled(enabled)
def end_of_track_actions(self) -> None: def end_of_track_actions(self) -> None:
""" """
@ -1084,18 +951,6 @@ class Window(QMainWindow):
if track_sequence.current: if track_sequence.current:
track_sequence.current.fade() 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): def hide_played(self):
"""Toggle hide played tracks""" """Toggle hide played tracks"""
@ -1202,105 +1057,42 @@ class Window(QMainWindow):
Delete / edit templates Delete / edit templates
""" """
# Define callbacks to handle management options # Build a list of (template-name, playlist-id) tuples
def delete(template_id: int) -> None: template_list: list[tuple[str, int]] = []
"""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
idx = self.create_playlist_tab(template, is_template=True)
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
def favourite(template_id: int, favourite: bool) -> None:
"""Mark template as (not) favourite"""
print(f"manage_templates.favourite({template_id=}")
print(f"{session=}")
def new_item() -> None:
"""Create new template"""
# Get base template
template_id = self.solicit_template_to_use(
session, template_prmompt="New template based upon:"
)
if template_id is None:
return
new_template = self.create_playlist(session, template_id)
if new_template:
self.open_playlist(session, new_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: with db.Session() as session:
for template in Playlists.get_all_templates(session): for template in Playlists.get_all_templates(session):
# TODO: need to add in favourites template_list.append((template.name, template.id))
template_list.append(ItemlistItem(name=template.name, id=template.id))
# We need to retain a reference to the dialog box to stop it # Get user's selection
# going out of scope and being garbage-collected. dlg = EditDeleteDialog(template_list)
self.dlg = ItemlistManager(template_list, callbacks) if not dlg.exec():
self.dlg.show() 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=}"
)
def mark_rows_for_moving(self) -> None: def mark_rows_for_moving(self) -> None:
""" """
@ -1385,17 +1177,37 @@ class Window(QMainWindow):
self.move_playlist_rows(unplayed_rows) self.move_playlist_rows(unplayed_rows)
self.disable_selection_timing = False self.disable_selection_timing = False
def new_playlist(self) -> Optional[Playlists]: def new_playlist(self) -> None:
""" """
Create new playlist, optionally from template Create new playlist, optionally from template
""" """
with db.Session() as session: # Build a list of (template-name, playlist-id) tuples starting
template_id = self.solicit_template_to_use(session) # with the "no template" entry
if not template_id: template_list: list[tuple[str, int]] = []
return None # User cancelled template_list.append((Config.NO_TEMPLATE_NAME, 0))
playlist = self.create_playlist(session, template_id) 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: if playlist:
playlist.mark_open() playlist.mark_open()
@ -1404,13 +1216,10 @@ class Window(QMainWindow):
session.commit() session.commit()
idx = self.create_playlist_tab(playlist) idx = self.create_playlist_tab(playlist)
self.playlist_section.tabPlaylist.setCurrentIndex(idx) self.playlist_section.tabPlaylist.setCurrentIndex(idx)
return playlist
else: else:
ApplicationError("new_playlist: Playlist failed to create") log.error("Playlist failed to create")
return None def open_playlist(self) -> None:
def open_existing_playlist(self) -> None:
"""Open existing playlist""" """Open existing playlist"""
with db.Session() as session: with db.Session() as session:
@ -1419,18 +1228,11 @@ class Window(QMainWindow):
dlg.exec() dlg.exec()
playlist = dlg.playlist playlist = dlg.playlist
if playlist: if playlist:
self.open_playlist(session, playlist) idx = self.create_playlist_tab(playlist)
playlist.mark_open()
session.commit()
def open_playlist( self.playlist_section.tabPlaylist.setCurrentIndex(idx)
self, session: Session, playlist: Playlists, is_template: bool = False
) -> None:
"""Open passed playlist"""
idx = self.create_playlist_tab(playlist, is_template)
playlist.mark_open()
session.commit()
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
def open_songfacts_browser(self, title: str) -> None: def open_songfacts_browser(self, title: str) -> None:
"""Search Songfacts for title""" """Search Songfacts for title"""
@ -1897,45 +1699,24 @@ class Window(QMainWindow):
# Switch to correct tab # Switch to correct tab
if playlist_id != self.current.playlist_id: if playlist_id != self.current.playlist_id:
open_idx = self.get_tab_index_for_playlist(playlist_id) for idx in range(self.playlist_section.tabPlaylist.count()):
if open_idx: if (
self.playlist_section.tabPlaylist.setCurrentIndex(open_idx) self.playlist_section.tabPlaylist.widget(idx).playlist_id
else: == playlist_id
raise ApplicationError( ):
f"show_track() can't find current playlist tab {playlist_id=}" self.playlist_section.tabPlaylist.setCurrentIndex(idx)
) break
self.active_tab().scroll_to_top(playlist_track.row_number) self.active_tab().scroll_to_top(playlist_track.row_number)
def solicit_template_to_use(
self, session: Session, template_prmompt: 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_prmompt)
if not dlg.exec() or dlg.selected_id is None:
return None # User cancelled
return dlg.selected_id
def solicit_playlist_name( def solicit_playlist_name(
self, session: Session, default: str = "", prompt: str = "Playlist name:" self, session: Session, default: str = ""
) -> Optional[str]: ) -> Optional[str]:
"""Get name of new playlist from user""" """Get name of new playlist from user"""
dlg = QInputDialog(self) dlg = QInputDialog(self)
dlg.setInputMode(QInputDialog.InputMode.TextInput) dlg.setInputMode(QInputDialog.InputMode.TextInput)
dlg.setLabelText(prompt) dlg.setLabelText("Playlist name:")
while True: while True:
if default: if default:
dlg.setTextValue(default) dlg.setTextValue(default)

View File

@ -26,7 +26,6 @@ from PyQt6.QtGui import (
) )
# Third party imports # Third party imports
from sqlalchemy.orm.session import Session
import obswebsocket # type: ignore import obswebsocket # type: ignore
# import snoop # type: ignore # import snoop # type: ignore
@ -75,14 +74,12 @@ class PlaylistModel(QAbstractTableModel):
def __init__( def __init__(
self, self,
playlist_id: int, playlist_id: int,
is_template: bool,
*args: Optional[QObject], *args: Optional[QObject],
**kwargs: Optional[QObject], **kwargs: Optional[QObject],
) -> None: ) -> None:
log.debug("PlaylistModel.__init__()") log.debug("PlaylistModel.__init__()")
self.playlist_id = playlist_id self.playlist_id = playlist_id
self.is_template = is_template
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.playlist_rows: dict[int, RowAndTrack] = {} self.playlist_rows: dict[int, RowAndTrack] = {}
@ -226,9 +223,6 @@ class PlaylistModel(QAbstractTableModel):
if note_background: if note_background:
return QBrush(QColor(note_background)) return QBrush(QColor(note_background))
if self.is_template:
return QBrush(QColor(Config.COLOUR_TEMPLATE_ROW))
return QBrush() return QBrush()
def begin_reset_model(self, playlist_id: int) -> None: def begin_reset_model(self, playlist_id: int) -> None:
@ -778,7 +772,7 @@ class PlaylistModel(QAbstractTableModel):
return None return None
def load_data(self, session: Session) -> None: def load_data(self, session: db.session) -> None:
""" """
Same as refresh data, but only used when creating playslit. Same as refresh data, but only used when creating playslit.
Distinguishes profile time between initial load and other Distinguishes profile time between initial load and other
@ -1067,7 +1061,7 @@ class PlaylistModel(QAbstractTableModel):
# Update display # Update display
self.invalidate_row(track_sequence.previous.row_number) self.invalidate_row(track_sequence.previous.row_number)
def refresh_data(self, session: Session) -> None: def refresh_data(self, session: db.session) -> None:
""" """
Populate self.playlist_rows with playlist data Populate self.playlist_rows with playlist data

View File

@ -364,7 +364,7 @@ class PlaylistTab(QTableView):
Override closeEditor to enable play controls and update display. Override closeEditor to enable play controls and update display.
""" """
self.musicmuster.enable_escape(True) self.musicmuster.action_Clear_selection.setEnabled(True)
super(PlaylistTab, self).closeEditor(editor, hint) super(PlaylistTab, self).closeEditor(editor, hint)

102
menu.yaml
View File

@ -1,102 +0,0 @@
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"

View File

@ -1,58 +0,0 @@
"""add favouirit to playlists
Revision ID: 04df697e40cd
Revises: 33c04e3c12c8
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 = '04df697e40cd'
down_revision = '33c04e3c12c8'
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! ###
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('playlist_rows', schema=None) as batch_op:
batch_op.drop_constraint('playlist_rows_ibfk_1', type_='foreignkey')
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('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.create_foreign_key('playlist_rows_ibfk_1', '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 ###