Compare commits
No commits in common. "639f006a100ad0b995484fbdd9de7ac5932a43c6" and "aef8cb5cb5b011c67e063b9f7565c4796c374834" have entirely different histories.
639f006a10
...
aef8cb5cb5
@ -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
|
||||||
|
|||||||
@ -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"""
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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")
|
||||||
self.menu_actions = {} # Store reference for enabling/disabling actions
|
|
||||||
self.dynamic_submenus = {} # Store submenus for dynamic population
|
|
||||||
|
|
||||||
for menu_item in menu_data["menus"]:
|
|
||||||
menu = menu_bar.addMenu(menu_item["title"])
|
|
||||||
|
|
||||||
for action_item in menu_item["actions"]:
|
|
||||||
if "separator" in action_item and action_item["separator"]:
|
|
||||||
menu.addSeparator()
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check whether this is a submenu first
|
|
||||||
if action_item.get("submenu"):
|
|
||||||
submenu = QMenu(action_item["text"], self)
|
|
||||||
menu.addMenu(submenu)
|
|
||||||
|
|
||||||
# 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
|
file_menu.addAction(self.create_action("New Playlist", self.new_playlist))
|
||||||
if action_item.get("store_reference"):
|
file_menu.addAction(
|
||||||
self.menu_actions[action_item["handler"]] = action
|
self.create_action("Close Playlist", self.close_playlist_tab)
|
||||||
|
|
||||||
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)
|
file_menu.addAction(self.create_action("Rename Playlist", self.rename_playlist))
|
||||||
break
|
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))
|
||||||
|
|
||||||
def get_new_playlist_dynamic_submenu_items(self):
|
# Playlist Menu
|
||||||
"""Returns dynamically generated menu items for Submenu 1."""
|
playlist_menu = menu_bar.addMenu("&Playlist")
|
||||||
return [
|
playlist_menu.addSeparator()
|
||||||
{"text": "Option A", "handler": "option_a_handler"},
|
playlist_menu.addAction(
|
||||||
{"text": "Option B", "handler": "option_b_handler"},
|
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))
|
||||||
|
|
||||||
def get_query_dynamic_submenu_items(self):
|
# Clear Selection with Escape key. Save in module so we can
|
||||||
"""Returns dynamically generated menu items for Submenu 2."""
|
# enable/disable it later
|
||||||
return [
|
self.action_Clear_selection = self.create_action(
|
||||||
{"text": "Action X", "handler": "action_x_handler"},
|
"Clear Selection", self.clear_selection, "Esc"
|
||||||
{"text": "Action Y", "handler": "action_y_handler"},
|
)
|
||||||
]
|
playlist_menu.addAction(self.action_Clear_selection)
|
||||||
|
|
||||||
def select_duplicate_rows(self) -> None:
|
# Music Menu
|
||||||
"""Call playlist to select duplicate rows"""
|
music_menu = menu_bar.addMenu("&Music")
|
||||||
|
music_menu.addAction(
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.active_tab().select_duplicate_rows()
|
# Help Menu
|
||||||
|
help_menu = menu_bar.addMenu("Help")
|
||||||
|
help_menu.addAction(self.create_action("About", self.about))
|
||||||
|
help_menu.addAction(self.create_action("Debug", self.debug))
|
||||||
|
|
||||||
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: Optional[Playlists]
|
|
||||||
if template_id == 0:
|
|
||||||
playlist = Playlists(session, playlist_name)
|
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,14 +1228,7 @@ 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)
|
||||||
|
|
||||||
def open_playlist(
|
|
||||||
self, session: Session, playlist: Playlists, is_template: bool = False
|
|
||||||
) -> None:
|
|
||||||
"""Open passed playlist"""
|
|
||||||
|
|
||||||
idx = self.create_playlist_tab(playlist, is_template)
|
|
||||||
playlist.mark_open()
|
playlist.mark_open()
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
102
menu.yaml
@ -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"
|
|
||||||
|
|
||||||
@ -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 ###
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user