rebase from dev

This commit is contained in:
Keith Edmunds 2025-02-23 21:05:21 +00:00
parent 40756469ec
commit 3f248d363f
17 changed files with 2091 additions and 609 deletions

View File

@ -97,6 +97,7 @@ class Config(object):
PLAY_SETTLE = 500000 PLAY_SETTLE = 500000
PLAYLIST_ICON_CURRENT = ":/icons/green-circle.png" PLAYLIST_ICON_CURRENT = ":/icons/green-circle.png"
PLAYLIST_ICON_NEXT = ":/icons/yellow-circle.png" PLAYLIST_ICON_NEXT = ":/icons/yellow-circle.png"
PLAYLIST_ICON_TEMPLATE = ":/icons/redstar.png"
PREVIEW_ADVANCE_MS = 5000 PREVIEW_ADVANCE_MS = 5000
PREVIEW_BACK_MS = 5000 PREVIEW_BACK_MS = 5000
PREVIEW_END_BUFFER_MS = 1000 PREVIEW_END_BUFFER_MS = 1000

View File

@ -80,6 +80,9 @@ 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(
Boolean, nullable=False, index=False, default=False
)
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -101,9 +104,7 @@ class PlaylistRowsTable(Model):
) )
playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows") playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows")
track_id: Mapped[Optional[int]] = mapped_column( track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id", ondelete="CASCADE"))
ForeignKey("tracks.id", ondelete="CASCADE")
)
track: Mapped["TracksTable"] = relationship( track: Mapped["TracksTable"] = relationship(
"TracksTable", "TracksTable",
back_populates="playlistrows", back_populates="playlistrows",

102
app/menu.yaml Normal file
View 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"

View File

@ -192,6 +192,138 @@ class Playdates(dbtables.PlaydatesTable):
).all() ).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): class PlaylistRows(dbtables.PlaylistRowsTable):
def __init__( def __init__(
self, self,
@ -476,161 +608,8 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
session.connection().execute(stmt, sqla_map) 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): class Settings(dbtables.SettingsTable):
def __init__(self, session: Session, name: str): def __init__(self, session: Session, name: str) -> None:
self.name = name self.name = name
session.add(self) session.add(self)
session.commit() session.commit()
@ -658,7 +637,7 @@ class Tracks(dbtables.TracksTable):
fade_at: int, fade_at: int,
silence_at: int, silence_at: int,
bitrate: int, bitrate: int,
): ) -> None:
self.path = path self.path = path
self.title = title self.title = title
self.artist = artist self.artist = artist

View File

@ -174,7 +174,9 @@ class ItemlistItem:
class ItemlistManager(QDialog): class ItemlistManager(QDialog):
def __init__(self, items: list[ItemlistItem], callbacks: ItemlistManagerCallbacks) -> None: def __init__(
self, items: list[ItemlistItem], callbacks: ItemlistManagerCallbacks
) -> None:
super().__init__() super().__init__()
self.setWindowTitle("Item Manager") self.setWindowTitle("Item Manager")
self.setMinimumSize(600, 400) self.setMinimumSize(600, 400)
@ -189,7 +191,7 @@ class ItemlistManager(QDialog):
if not hh: if not hh:
raise ApplicationError("ItemlistManager failed to create horizontalHeader") raise ApplicationError("ItemlistManager failed to create horizontalHeader")
hh.setStretchLastSection(True) hh.setStretchLastSection(True)
self.table.setColumnWidth(0, 200) self.table.setColumnWidth(0, 288)
self.table.setColumnWidth(1, 300) self.table.setColumnWidth(1, 300)
self.populate_table() self.populate_table()
@ -246,19 +248,25 @@ class ItemlistManager(QDialog):
self.table.setCellWidget(row, 1, widget) 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: def delete_item(self, item_id: int) -> None:
print(f"Delete item {item_id}")
self.callbacks.delete(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: 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) self.callbacks.favourite(item_id, checked)
for row in range(self.table.rowCount()): for row in range(self.table.rowCount()):
@ -273,20 +281,16 @@ class ItemlistManager(QDialog):
break break
def new_item(self) -> None: def new_item(self) -> None:
print("New item") self.callbacks.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}
# ]
@dataclass @dataclass
class ItemlistManagerCallbacks: class ItemlistManagerCallbacks:
edit: Callable[[int], None]
delete: Callable[[int], None] delete: Callable[[int], None]
edit: Callable[[int], None]
favourite: Callable[[int, bool], None] favourite: Callable[[int, bool], None]
new_item: Callable[[], None]
rename: Callable[[int], Optional[str]]
class PreviewManager: class PreviewManager:
@ -451,16 +455,21 @@ class TemplateSelectorDialog(QDialog):
Class to manage user selection of template 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__() 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
label = QLabel("Select template:") if not self.template_prompt:
self.template_prompt = "Select template:"
label = QLabel(self.template_prompt)
# Create combo box # Create combo box
self.combo_box = QComboBox() self.combo_box = QComboBox()
@ -592,6 +601,61 @@ class Window(QMainWindow):
self.load_last_playlists() self.load_last_playlists()
self.stop_autoplay = False 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( def create_action(
self, text: str, handler: Callable, shortcut: Optional[str] = None self, text: str, handler: Callable, shortcut: Optional[str] = None
) -> QAction: ) -> QAction:
@ -610,7 +674,7 @@ class Window(QMainWindow):
menu_bar = self.menuBar() menu_bar = self.menuBar()
# Load menu structure from YAML file # 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) menu_data = yaml.safe_load(file)
self.menu_actions = {} # Store reference for enabling/disabling actions self.menu_actions = {} # Store reference for enabling/disabling actions
@ -661,26 +725,231 @@ class Window(QMainWindow):
items = getattr(self, f"get_{key}_items")() items = getattr(self, f"get_{key}_items")()
for item in items: for item in items:
action = QAction(item["text"], self) 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) submenu.addAction(action)
break break
def get_new_playlist_dynamic_submenu_items(self): def get_new_playlist_dynamic_submenu_items(
"""Returns dynamically generated menu items for Submenu 1.""" self,
return [ ) -> list[dict[str, str | tuple[Session, int]]]:
{"text": "Option A", "handler": "option_a_handler"}, """
{"text": "Option B", "handler": "option_b_handler"}, 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): def get_query_dynamic_submenu_items(self):
"""Returns dynamically generated menu items for Submenu 2.""" """Returns dynamically generated menu items for Submenu 2."""
return [ return [
{"text": "Action X", "handler": "action_x_handler"}, {"text": "Action Xargs", "handler": "kae", "args": (21,)},
{"text": "Action Y", "handler": "action_y_handler"}, {"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: def select_duplicate_rows(self) -> None:
"""Call playlist to select duplicate rows""" """Call playlist to select duplicate rows"""
@ -707,12 +976,6 @@ class Window(QMainWindow):
QMessageBox.StandardButton.Ok, 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: def clear_next(self) -> None:
""" """
Clear next track Clear next track
@ -731,49 +994,6 @@ class Window(QMainWindow):
# Clear the search bar # Clear the search bar
self.search_playlist_clear() 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: def close_playlist_tab(self) -> bool:
""" """
Close active playlist tab, called by menu item 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_songfacts_signal.connect(self.open_songfacts_browser)
self.signals.search_wikipedia_signal.connect(self.open_wikipedia_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: def current_row_or_end(self) -> int:
""" """
If a row or rows are selected, return the row number of the first If a row or rows are selected, return the row number of the first
@ -915,25 +1098,6 @@ class Window(QMainWindow):
ipdb.set_trace() 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: def download_played_tracks(self) -> None:
"""Download a CSV of played tracks""" """Download a CSV of played tracks"""
@ -1064,6 +1228,18 @@ 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"""
@ -1129,7 +1305,7 @@ class Window(QMainWindow):
if playlist: if playlist:
log.debug(f"load_last_playlists() loaded {playlist=}") log.debug(f"load_last_playlists() loaded {playlist=}")
# Create tab # Create tab
playlist_ids.append(self.create_playlist_tab(playlist)) playlist_ids.append(self._open_playlist(playlist))
# Set active tab # Set active tab
record = Settings.get_setting(session, "active_tab") record = Settings.get_setting(session, "active_tab")
@ -1170,60 +1346,119 @@ class Window(QMainWindow):
Delete / edit templates Delete / edit templates
""" """
def edit(template_id: int) -> None: # Define callbacks to handle management options
"""Edit template"""
print(f"manage_templates.edit({template_id=}")
def delete(template_id: int) -> None: def delete(template_id: int) -> None:
"""delete template""" """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: 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 # Build a list of templates
template_list: list[ItemlistItem] = [] 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_list.append(ItemlistItem(name=template.name, id=template.id)) ItemlistItem(
name=template.name, id=template.id, favourite=template.favourite
# # Get user's selection )
# dlg = EditDeleteDialog(template_list) )
# if not dlg.exec(): # We need to retain a reference to the dialog box to stop it
# return # User cancelled # going out of scope and being garbage-collected.
self.dlg = ItemlistManager(template_list, callbacks)
# action, template_id = dlg.selection self.dlg.show()
# 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:
""" """
@ -1308,63 +1543,6 @@ 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) -> 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: def open_songfacts_browser(self, title: str) -> None:
"""Search Songfacts for title""" """Search Songfacts for title"""
@ -1688,32 +1866,6 @@ class Window(QMainWindow):
) )
track_sequence.current.start_time -= dt.timedelta(milliseconds=elapsed_ms) 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: def search_playlist(self) -> None:
"""Show text box to search playlist""" """Show text box to search playlist"""
@ -1830,43 +1982,16 @@ 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:
for idx in range(self.playlist_section.tabPlaylist.count()): open_idx = self.get_tab_index_for_playlist(playlist_id)
if ( if open_idx:
self.playlist_section.tabPlaylist.widget(idx).playlist_id self.playlist_section.tabPlaylist.setCurrentIndex(open_idx)
== playlist_id else:
): raise ApplicationError(
self.playlist_section.tabPlaylist.setCurrentIndex(idx) f"show_track() can't find current playlist tab {playlist_id=}"
break )
self.active_tab().scroll_to_top(playlist_track.row_number) 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: def stop(self) -> None:
"""Stop playing immediately""" """Stop playing immediately"""
@ -2074,6 +2199,10 @@ class Window(QMainWindow):
self.playlist_section.tabPlaylist.setTabIcon( self.playlist_section.tabPlaylist.setTabIcon(
idx, QIcon(Config.PLAYLIST_ICON_CURRENT) 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: else:
self.playlist_section.tabPlaylist.setTabIcon(idx, QIcon()) self.playlist_section.tabPlaylist.setTabIcon(idx, QIcon())

View File

@ -26,6 +26,7 @@ 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
@ -74,12 +75,14 @@ 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] = {}
@ -498,7 +501,7 @@ class PlaylistModel(QAbstractTableModel):
""" """
if not index.isValid(): if not index.isValid():
return Qt.ItemFlag.NoItemFlags return Qt.ItemFlag.ItemIsDropEnabled
default = ( default = (
Qt.ItemFlag.ItemIsEnabled Qt.ItemFlag.ItemIsEnabled
@ -772,7 +775,7 @@ class PlaylistModel(QAbstractTableModel):
return None 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. 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
@ -1061,7 +1064,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: db.session) -> None: def refresh_data(self, session: Session) -> None:
""" """
Populate self.playlist_rows with playlist data Populate self.playlist_rows with playlist data

View File

@ -343,7 +343,7 @@ class PlaylistTab(QTableView):
Override closeEditor to enable play controls and update display. 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) super(PlaylistTab, self).closeEditor(editor, hint)

View File

@ -1,6 +1,7 @@
<RCC> <RCC>
<qresource prefix="icons"> <qresource prefix="icons">
<file>yellow-circle.png</file> <file>yellow-circle.png</file>
<file>redstar.png</file>
<file>green-circle.png</file> <file>green-circle.png</file>
<file>star.png</file> <file>star.png</file>
<file>star_empty.png</file> <file>star_empty.png</file>

File diff suppressed because it is too large Load Diff

View File

@ -1143,7 +1143,7 @@ padding-left: 8px;</string>
</action> </action>
<action name="actionOpenPlaylist"> <action name="actionOpenPlaylist">
<property name="text"> <property name="text">
<string>Open &amp;playlist...</string> <string>O&amp;pen...</string>
</property> </property>
</action> </action>
<action name="actionNewPlaylist"> <action name="actionNewPlaylist">

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1249</width> <width>1249</width>
<height>499</height> <height>538</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">

View File

@ -764,7 +764,7 @@ class Ui_MainWindow(object):
) )
self.actionE_xit.setText(_translate("MainWindow", "E&xit")) self.actionE_xit.setText(_translate("MainWindow", "E&xit"))
self.actionTest.setText(_translate("MainWindow", "&Test")) 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.actionNewPlaylist.setText(_translate("MainWindow", "&New..."))
self.actionTestFunction.setText(_translate("MainWindow", "&Test function")) self.actionTestFunction.setText(_translate("MainWindow", "&Test function"))
self.actionSkipToFade.setText( self.actionSkipToFade.setText(
@ -847,4 +847,4 @@ class Ui_MainWindow(object):
from infotabs import InfoTabs from infotabs import InfoTabs
from pyqtgraph import PlotWidget from pyqtgraph import PlotWidget # type: ignore

View File

@ -1 +0,0 @@
env.py.DEBUG

27
migrations/env.py Normal file
View 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,
})

View File

@ -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,
})

View File

@ -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,
})

View File

@ -1,16 +1,16 @@
"""Index for notesolours substring """add favouirit to playlists
Revision ID: c76e865ccb85 Revision ID: 04df697e40cd
Revises: 33c04e3c12c8 Revises: 33c04e3c12c8
Create Date: 2025-02-07 18:21:01.760057 Create Date: 2025-02-22 20:20:45.030024
""" """
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'c76e865ccb85' revision = '04df697e40cd'
down_revision = '33c04e3c12c8' down_revision = '33c04e3c12c8'
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -30,39 +30,29 @@ def downgrade(engine_name: str) -> None:
def upgrade_() -> None: def upgrade_() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('notecolours', schema=None) as batch_op: 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) 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: with op.batch_alter_table('playlist_rows', schema=None) as batch_op:
batch_op.drop_constraint('playlist_rows_ibfk_1', type_='foreignkey') 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: with op.batch_alter_table('playlists', schema=None) as batch_op:
batch_op.drop_constraint('fk_queries_playlist_id_playlists', type_='foreignkey') batch_op.add_column(sa.Column('favourite', sa.Boolean(), nullable=False))
batch_op.create_foreign_key(None, 'playlists', ['playlist_id'], ['id'], ondelete='CASCADE')
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade_() -> None: def downgrade_() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('queries', schema=None) as batch_op: with op.batch_alter_table('playlists', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='foreignkey') batch_op.drop_column('favourite')
batch_op.create_foreign_key('fk_queries_playlist_id_playlists', 'playlists', ['playlist_id'], ['id'])
with op.batch_alter_table('playlist_rows', schema=None) as batch_op: 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']) 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: with op.batch_alter_table('notecolours', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_notecolours_substring')) batch_op.drop_index(batch_op.f('ix_notecolours_substring'))
batch_op.drop_column('strip_substring')
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@ -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 ###