Implement templates

This commit is contained in:
Keith Edmunds 2022-10-01 14:14:26 +01:00
parent 9f32abc2ea
commit 00d3add0d3
8 changed files with 213 additions and 40 deletions

View File

@ -41,9 +41,7 @@ prepend_sys_path = .
sqlalchemy.url = SET
# sqlalchemy.url = mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod
# sqlalchemy.url = mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_dev
# sqlalchemy.url = mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_v2
# sqlalchemy.url = mysql+mysqldb://musicmusterv3:musicmusterv3@localhost/musicmuster_dev_v3
# sqlalchemy.url = mysql+mysqldb://dev_musicmuster:dev_musicmuster@localhost/dev_musicmuster
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run

View File

@ -325,6 +325,12 @@ def set_track_metadata(session, track):
session.commit()
def show_OK(title: str, msg: str) -> None:
"""Display a message to user"""
QMessageBox.information(None, title, msg, buttons=QMessageBox.Ok)
def show_warning(title: str, msg: str) -> None:
"""Display a warning to user"""

View File

@ -202,7 +202,8 @@ class Playlists(Base):
id: int = Column(Integer, primary_key=True, autoincrement=True)
name: str = Column(String(32), nullable=False, unique=True)
last_used = Column(DateTime, default=None, nullable=True)
loaded: bool = Column(Boolean, default=True, nullable=False)
loaded = Column(Boolean, default=True, nullable=False)
is_template = Column(Boolean, default=False, nullable=False)
rows = relationship(
"PlaylistRows",
back_populates="playlist",
@ -211,7 +212,10 @@ class Playlists(Base):
)
def __repr__(self) -> str:
return f"<Playlists(id={self.id}, name={self.name}>"
return (
f"<Playlists(id={self.id}, name={self.name}, "
f"is_templatee={self.is_template}>"
)
def __init__(self, session: Session, name: str) -> None:
self.name = name
@ -236,6 +240,19 @@ class Playlists(Base):
self.loaded = False
@classmethod
def create_playlist_from_template(cls,
session: Session,
template: "Playlists",
playlist_name: str) \
-> "Playlists":
"""Create a new playlist from template"""
playlist = cls(session, playlist_name)
PlaylistRows.copy_playlist(session, template.id, playlist.id)
return playlist
@classmethod
def get_all(cls, session: Session) -> List["Playlists"]:
"""Returns a list of all playlists ordered by last use"""
@ -243,12 +260,27 @@ class Playlists(Base):
return (
session.execute(
select(cls)
.filter(cls.is_template.is_(False))
.order_by(cls.loaded.desc(), cls.last_used.desc())
)
.scalars()
.all()
)
@classmethod
def get_all_templates(cls, session: Session) -> List["Playlists"]:
"""Returns a list of all templates ordered by name"""
return (
session.execute(
select(cls)
.filter(cls.is_template.is_(True))
.order_by(cls.name)
)
.scalars()
.all()
)
@classmethod
def get_closed(cls, session: Session) -> List["Playlists"]:
"""Returns a list of all closed playlists ordered by last use"""
@ -256,7 +288,10 @@ class Playlists(Base):
return (
session.execute(
select(cls)
.filter(cls.loaded.is_(False))
.filter(
cls.loaded.is_(False),
cls.is_template.is_(False)
)
.order_by(cls.last_used.desc())
)
.scalars()
@ -285,20 +320,16 @@ class Playlists(Base):
self.loaded = True
self.last_used = datetime.now()
# def remove_track(self, session: Session, row: int) -> None:
# log.debug(f"Playlist.remove_track({self.id=}, {row=})")
#
# # Refresh self first (this is necessary when calling
# remove_track
# # multiple times before session.commit())
# session.refresh(self)
# # Get tracks collection for this playlist
# # Tracks are a dictionary of tracks keyed on row
# # number. Remove the relevant row.
# del self.tracks[row]
# # Save the new tracks collection
# session.flush()
#
@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.is_template = True
session.commit()
PlaylistRows.copy_playlist(session, playlist_id, template.id)
class PlaylistRows(Base):
@ -320,17 +351,37 @@ class PlaylistRows(Base):
f"note={self.note} row_number={self.row_number}>"
)
def __init__(
self, session: Session, playlist_id: int, track_id: int,
row_number: int) -> None:
def __init__(self,
session: Session,
playlist_id: int,
track_id: int,
row_number: int,
note: Optional[str] = None
) -> None:
"""Create PlaylistRows object"""
self.playlist_id = playlist_id
self.track_id = track_id
self.row_number = row_number
self.note = note
session.add(self)
session.flush()
@staticmethod
def copy_playlist(session: Session,
src_id: int,
dst_id: int) -> None:
"""Copy playlist entries"""
src_rows = session.execute(
select(PlaylistRows)
.filter(PlaylistRows.playlist_id == src_id)
).scalars().all()
for plr in src_rows:
PlaylistRows(session, dst_id, plr.track_id, plr.row_number,
plr.note)
@staticmethod
def delete_higher_rows(session: Session, playlist_id: int, row: int) \
-> None:

View File

@ -199,9 +199,11 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionInsertSectionHeader.triggered.connect(self.insert_header)
self.actionInsertTrack.triggered.connect(self.insert_track)
self.actionMoveSelected.triggered.connect(self.move_selected)
self.actionNewPlaylist.triggered.connect(self.create_playlist)
self.actionNew_from_template.triggered.connect(self.new_from_template)
self.actionNewPlaylist.triggered.connect(self.create_and_show_playlist)
self.actionOpenPlaylist.triggered.connect(self.open_playlist)
self.actionPlay_next.triggered.connect(self.play_next)
self.actionSave_as_template.triggered.connect(self.save_as_template)
self.actionSearch.triggered.connect(self.search_playlist)
self.actionSelect_next_track.triggered.connect(self.select_next_row)
self.actionSelect_previous_track.triggered.connect(
@ -224,18 +226,23 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer.timeout.connect(self.tick)
def create_playlist(self) -> None:
def create_playlist(self,
session: Session,
playlist_name: Optional[str] = None) -> Playlists:
"""Create new playlist"""
dlg = QInputDialog(self)
dlg.setInputMode(QInputDialog.TextInput)
dlg.setLabelText("Playlist name:")
dlg.resize(500, 100)
ok = dlg.exec()
if ok:
with Session() as session:
playlist = Playlists(session, dlg.textValue())
self.create_playlist_tab(session, playlist)
if not playlist_name:
playlist_name = self.solicit_playlist_name()
playlist = Playlists(session, playlist_name)
return playlist
def create_and_show_playlist(self) -> None:
"""Create new playlist and display it"""
with Session() as session:
playlist = self.create_playlist(session)
self.create_playlist_tab(session, playlist)
def create_playlist_tab(self, session: Session,
playlist: Playlists) -> None:
@ -609,6 +616,24 @@ class Window(QMainWindow, Ui_MainWindow):
session, playlist_id)
self.move_playlist_rows(session, unplayed_playlist_rows)
def new_from_template(self) -> None:
"""Create new playlist from template"""
with Session() as session:
templates = Playlists.get_all_templates(session)
dlg = SelectPlaylistDialog(self, playlists=templates,
session=session)
dlg.exec()
template = dlg.playlist
if template:
playlist_name = self.solicit_playlist_name()
if not playlist_name:
return
playlist = Playlists.create_playlist_from_template(
session, template, playlist_name)
playlist.mark_open(session)
self.create_playlist_tab(session, playlist)
def open_playlist(self):
"""Open existing playlist"""
@ -706,6 +731,35 @@ class Window(QMainWindow, Ui_MainWindow):
self.label_end_time.setText(
self.current_track_end_time.strftime(Config.TRACK_TIME_FORMAT))
def save_as_template(self) -> None:
"""Save current playlist as template"""
with 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.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("Duplicate template",
"Template name already in use"
)
Playlists.save_as_template(session,
self.visible_playlist_tab().playlist_id,
template_name)
helpers.show_OK("Template", "Template saved")
def search_playlist(self) -> None:
"""Show text box to search playlist"""
@ -784,6 +838,19 @@ class Window(QMainWindow, Ui_MainWindow):
self.tabPlaylist.setCurrentWidget(self.next_track_playlist_tab)
self.tabPlaylist.currentWidget().scroll_next_to_top()
def solicit_playlist_name(self) -> Optional[str]:
"""Get name of playlist from user"""
dlg = QInputDialog(self)
dlg.setInputMode(QInputDialog.TextInput)
dlg.setLabelText("Playlist name:")
dlg.resize(500, 100)
ok = dlg.exec()
if ok:
return dlg.textValue()
else:
return None
def stop(self) -> None:
"""Stop playing immediately"""

View File

@ -572,8 +572,7 @@ class PlaylistTab(QTableWidget):
# can be reset by calling PlaylistRows.fixup_rownumbers() later,
# so just fudge a row number for now.
row_number = 0
plr = PlaylistRows(session, self.playlist_id, None, row_number)
plr.note = note
plr = PlaylistRows(session, self.playlist_id, None, row_number, note)
self.insert_row(session, plr)
PlaylistRows.fixup_rownumbers(session, self.playlist_id)
if repaint:

View File

@ -761,7 +761,7 @@ text-align: left;</string>
<x>0</x>
<y>0</y>
<width>1280</width>
<height>24</height>
<height>26</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
@ -775,12 +775,14 @@ text-align: left;</string>
<addaction name="actionExport_playlist"/>
<addaction name="actionDeletePlaylist"/>
<addaction name="separator"/>
<addaction name="actionNew_from_template"/>
<addaction name="actionSave_as_template"/>
<addaction name="separator"/>
<addaction name="actionMoveSelected"/>
<addaction name="actionMoveUnplayed"/>
<addaction name="actionDownload_CSV_of_played_tracks"/>
<addaction name="separator"/>
<addaction name="actionE_xit"/>
<addaction name="separator"/>
</widget>
<widget class="QMenu" name="menuPlaylist">
<property name="title">
@ -1093,6 +1095,16 @@ text-align: left;</string>
<string>&amp;About</string>
</property>
</action>
<action name="actionSave_as_template">
<property name="text">
<string>Save as template...</string>
</property>
</action>
<action name="actionNew_from_template">
<property name="text">
<string>New from template...</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@ -348,7 +348,7 @@ class Ui_MainWindow(object):
self.gridLayout_4.addWidget(self.frame_5, 3, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1280, 24))
self.menubar.setGeometry(QtCore.QRect(0, 0, 1280, 26))
self.menubar.setObjectName("menubar")
self.menuFile = QtWidgets.QMenu(self.menubar)
self.menuFile.setObjectName("menuFile")
@ -458,6 +458,10 @@ class Ui_MainWindow(object):
self.actionFind_previous.setObjectName("actionFind_previous")
self.action_About = QtWidgets.QAction(MainWindow)
self.action_About.setObjectName("action_About")
self.actionSave_as_template = QtWidgets.QAction(MainWindow)
self.actionSave_as_template.setObjectName("actionSave_as_template")
self.actionNew_from_template = QtWidgets.QAction(MainWindow)
self.actionNew_from_template.setObjectName("actionNew_from_template")
self.menuFile.addAction(self.actionNewPlaylist)
self.menuFile.addAction(self.actionOpenPlaylist)
self.menuFile.addAction(self.actionClosePlaylist)
@ -465,12 +469,14 @@ class Ui_MainWindow(object):
self.menuFile.addAction(self.actionExport_playlist)
self.menuFile.addAction(self.actionDeletePlaylist)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionNew_from_template)
self.menuFile.addAction(self.actionSave_as_template)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionMoveSelected)
self.menuFile.addAction(self.actionMoveUnplayed)
self.menuFile.addAction(self.actionDownload_CSV_of_played_tracks)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionE_xit)
self.menuFile.addSeparator()
self.menuPlaylist.addSeparator()
self.menuPlaylist.addAction(self.actionPlay_next)
self.menuPlaylist.addAction(self.actionFade)
@ -587,5 +593,7 @@ class Ui_MainWindow(object):
self.actionFind_previous.setText(_translate("MainWindow", "Find previous"))
self.actionFind_previous.setShortcut(_translate("MainWindow", "P"))
self.action_About.setText(_translate("MainWindow", "&About"))
self.actionSave_as_template.setText(_translate("MainWindow", "Save as template..."))
self.actionNew_from_template.setText(_translate("MainWindow", "New from template..."))
from infotabs import InfoTabs
import icons_rc

View File

@ -0,0 +1,32 @@
"""Add templates
Revision ID: b4f524e2140c
Revises: ed3100326c38
Create Date: 2022-10-01 13:30:21.663287
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b4f524e2140c'
down_revision = 'ed3100326c38'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_foreign_key(None, 'playlist_rows', 'tracks', ['track_id'], ['id'])
op.create_foreign_key(None, 'playlist_rows', 'playlists', ['playlist_id'], ['id'])
op.add_column('playlists', sa.Column('is_template', sa.Boolean(), nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('playlists', 'is_template')
op.drop_constraint(None, 'playlist_rows', type_='foreignkey')
op.drop_constraint(None, 'playlist_rows', type_='foreignkey')
# ### end Alembic commands ###