From ab47bb0ab4bf5c5979db02a91d7769ff0bf01a55 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sun, 3 Jul 2022 20:59:10 +0100 Subject: [PATCH] SQLA2.0 playlist column headers display --- .envrc | 2 +- app/config.py | 1 + app/dbconfig.py | 1 + app/models.py | 103 +++++++++++++++-------------- app/musicmuster.py | 51 +++++++-------- app/playlists.py | 160 ++++++++++++++++++--------------------------- 6 files changed, 145 insertions(+), 173 deletions(-) diff --git a/.envrc b/.envrc index 8a10a42..3c7ce0f 100644 --- a/.envrc +++ b/.envrc @@ -5,5 +5,5 @@ if on_git_branch master; then export MM_DB="mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_prod" else MYSQL_DATABASE="musicmuster_dev" export MM_ENV="DEVELOPMENT" - export MM_DB="mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_dev" + export MM_DB="mysql+mysqldb://musicmusterv3:musicmusterv3@localhost/musicmuster_dev_v3" fi diff --git a/app/config.py b/app/config.py index be36d5b..4825275 100644 --- a/app/config.py +++ b/app/config.py @@ -28,6 +28,7 @@ class Config(object): COLUMN_NAME_LAST_PLAYED = "Last played" COLUMN_NAME_LEADING_SILENCE = "Gap" COLUMN_NAME_LENGTH = "Length" + COLUMN_NAME_NOTES = "Notes" COLUMN_NAME_START_TIME = "Start" COLUMN_NAME_TITLE = "Title" DBFS_FADE = -12 diff --git a/app/dbconfig.py b/app/dbconfig.py index e860e81..a19b623 100644 --- a/app/dbconfig.py +++ b/app/dbconfig.py @@ -1,3 +1,4 @@ +import inspect import logging import os from config import Config diff --git a/app/models.py b/app/models.py index a5db814..ba9a5a6 100644 --- a/app/models.py +++ b/app/models.py @@ -3,25 +3,26 @@ # import os.path # import re # -# from dbconfig import Session +from dbconfig import Session # -# from datetime import datetime -# from typing import List, Optional +from datetime import datetime +from typing import List, Optional # # from pydub import AudioSegment # from sqlalchemy.ext.associationproxy import association_proxy # from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta -# from sqlalchemy import ( -# Boolean, -# Column, -# DateTime, -# Float, -# ForeignKey, -# func, -# Integer, -# String, -# UniqueConstraint, -# ) +from sqlalchemy import ( + Boolean, + Column, + DateTime, + # Float, + # ForeignKey, + # func, + Integer, + String, + # UniqueConstraint, + select, +) # from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import ( # backref, @@ -253,19 +254,19 @@ Base = declarative_base() # session.query(Playdates).filter( # Playdates.track_id == track_id).delete() # session.flush() -# -# -# class Playlists(Base): -# """ -# Manage playlists -# """ -# -# __tablename__ = "playlists" -# -# id: int = Column(Integer, primary_key=True, autoincrement=True) -# name: str = Column(String(32), nullable=False, unique=True) -# last_used: datetime = Column(DateTime, default=None, nullable=True) -# loaded: bool = Column(Boolean, default=True, nullable=False) + + +class Playlists(Base): + """ + Manage playlists + """ + + __tablename__ = "playlists" + + id: int = Column(Integer, primary_key=True, autoincrement=True) + name: str = Column(String(32), nullable=False, unique=True) + last_used: datetime = Column(DateTime, default=None, nullable=True) + loaded: bool = Column(Boolean, default=True, nullable=False) # notes = relationship( # "Notes", order_by="Notes.row", # back_populates="playlist", lazy="select" @@ -278,9 +279,9 @@ Base = declarative_base() # self.name = name # session.add(self) # session.flush() -# -# def __repr__(self) -> str: -# return f"" + + def __repr__(self) -> str: + return f"" # # def add_track( # self, session: Session, track_id: int, @@ -323,25 +324,29 @@ Base = declarative_base() # .filter(cls.loaded.is_(False)) # .order_by(cls.last_used.desc()) # ).all() -# -# @classmethod -# def get_open(cls, session: Session) -> List["Playlists"]: -# """ -# Return a list of playlists marked "loaded", ordered by loaded date. -# """ -# -# return ( -# session.query(cls) -# .filter(cls.loaded.is_(True)) -# .order_by(cls.last_used.desc()) -# ).all() -# -# def mark_open(self, session: Session) -> None: -# """Mark playlist as loaded and used now""" -# -# self.loaded = True -# self.last_used = datetime.now() -# session.flush() + + @classmethod + def get_open(cls, session: Session) -> List[Optional["Playlists"]]: + """ + Return a list of playlists marked "loaded", ordered by loaded date. + """ + + return ( + session.execute( + select(cls) + .where(cls.loaded.is_(True)) + .order_by(cls.last_used.desc()) + ) + .scalars() + .all() + ) + + def mark_open(self, session: Session) -> None: + """Mark playlist as loaded and used now""" + + self.loaded = True + self.last_used = datetime.now() + # session.flush() # # @staticmethod # def next_free_row(session: Session, playlist_id: int) -> int: diff --git a/app/musicmuster.py b/app/musicmuster.py index 6c5a602..d4b6db8 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -27,8 +27,7 @@ from PyQt5.QtWidgets import ( # QMessageBox, ) # -import dbconfig -# from dbconfig import Session +from dbconfig import engine, Session # import helpers # import music # @@ -36,11 +35,11 @@ import dbconfig from models import ( Base, # Playdates, - # Playlists, + Playlists, # Settings, # Tracks ) -# from playlists import PlaylistTab +from playlists import PlaylistTab # from sqlalchemy.orm.exc import DetachedInstanceError # from ui.dlg_search_database_ui import Ui_Dialog # from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist @@ -95,7 +94,7 @@ class Window(QMainWindow, Ui_MainWindow): # self.visible_playlist_tab: Callable[[], PlaylistTab] = \ # self.tabPlaylist.currentWidget # - #self.load_last_playlists() + self._load_last_playlists() # self.enable_play_next_controls() # self.check_audacity() # self.timer.start(Config.TIMER_MS) @@ -270,18 +269,18 @@ class Window(QMainWindow, Ui_MainWindow): # except AttributeError: # # Just return if there's no visible playlist tab # return -# -# def create_playlist_tab(self, session: Session, -# playlist: Playlists) -> None: -# """ -# Take the passed playlist database object, create a playlist tab and -# add tab to display. -# """ -# -# playlist_tab: PlaylistTab = PlaylistTab( -# musicmuster=self, session=session, playlist_id=playlist.id) -# idx: int = self.tabPlaylist.addTab(playlist_tab, playlist.name) -# self.tabPlaylist.setCurrentIndex(idx) + + def create_playlist_tab(self, session: Session, + playlist: Playlists) -> None: + """ + Take the passed playlist database object, create a playlist tab and + add tab to display. + """ + + playlist_tab: PlaylistTab = PlaylistTab( + musicmuster=self, session=session, playlist_id=playlist.id) + idx: int = self.tabPlaylist.addTab(playlist_tab, playlist.name) + self.tabPlaylist.setCurrentIndex(idx) # # def disable_play_next_controls(self) -> None: # """ @@ -494,14 +493,14 @@ class Window(QMainWindow, Ui_MainWindow): # # If we don't specify "repaint=False", playlist will # # also be saved to database # self.visible_playlist_tab().insert_track(session, track) -# -# def load_last_playlists(self): -# """Load the playlists that were loaded at end of last session""" -# -# with Session() as session: -# for playlist in Playlists.get_open(session): -# self.create_playlist_tab(session, playlist) -# playlist.mark_open(session) + + def _load_last_playlists(self): + """Load the playlists that were open when the last session closed""" + + with Session() as session: + for playlist in Playlists.get_open(session): + self.create_playlist_tab(session, playlist) + playlist.mark_open(session) # # def move_selected(self) -> None: # """Move selected rows to another playlist""" @@ -1168,7 +1167,7 @@ if __name__ == "__main__": # else: # # Normal run try: - Base.metadata.create_all(dbconfig.engine) + Base.metadata.create_all(engine) app = QApplication(sys.argv) win = Window() win.show() diff --git a/app/playlists.py b/app/playlists.py index 8d9a317..582a0de 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -1,3 +1,5 @@ +from collections import namedtuple + # from enum import Enum, auto # from typing import Dict, List, Optional, Set, Tuple, Union # @@ -6,18 +8,18 @@ # from PyQt5.Qt import QFont # from PyQt5.QtGui import QColor, QDropEvent # from PyQt5 import QtWidgets -# from PyQt5.QtWidgets import ( -# QAbstractItemView, -# QApplication, -# QInputDialog, -# QLineEdit, -# QMainWindow, -# QMenu, -# QStyledItemDelegate, -# QMessageBox, -# QTableWidget, -# QTableWidgetItem, -# ) +from PyQt5.QtWidgets import ( + # QAbstractItemView, + # QApplication, + # QInputDialog, + # QLineEdit, + QMainWindow, + # QMenu, + # QStyledItemDelegate, + # QMessageBox, + QTableWidget, + QTableWidgetItem, +) # # import helpers # import os @@ -25,7 +27,7 @@ # import subprocess # import threading # -# from config import Config +from config import Config # from datetime import datetime, timedelta # from helpers import get_relative_date, open_in_audacity # from log import log.debug, log.error @@ -38,8 +40,8 @@ # Tracks, # NoteColours # ) -# from dbconfig import Session -# +from dbconfig import Session +#from collections import namedtuple # start_time_re = re.compile(r"@\d\d:\d\d:\d\d") # # @@ -50,20 +52,19 @@ # NEXT = 3 # CURRENT = 4 # PLAYED = 5 -# -# -# class Columns(Enum): -# AUTOPLAY = COL_USERDATA = auto() -# MSS = auto() -# NOTE = TITLE = auto() -# ARTIST = auto() -# DURATION = auto() -# START_TIME = auto() -# END_TIME = auto() -# LAST_PLAYED = auto() -# ROW_NOTES = auto() -# LAST = ROW_NOTES -# + +# Columns +Column = namedtuple("Column", ['idx', 'heading']) +columns = {} +columns["userdata"] = Column(idx=0, heading=Config.COLUMN_NAME_AUTOPLAY), +columns["mss"] = Column(idx=1, heading=Config.COLUMN_NAME_LEADING_SILENCE), +columns["title"] = Column(idx=2, heading=Config.COLUMN_NAME_TITLE), +columns["artist"] = Column(idx=3, heading=Config.COLUMN_NAME_ARTIST), +columns["duration"] = Column(idx=4, heading=Config.COLUMN_NAME_LENGTH), +columns["start_time"] = Column(idx=5, heading=Config.COLUMN_NAME_START_TIME), +columns["end_time"] = Column(idx=6, heading=Config.COLUMN_NAME_END_TIME), +columns["last_played"] = Column(idx=7, heading=Config.COLUMN_NAME_LAST_PLAYED), +columns["row_notes"] = Column(idx=8, heading=Config.COLUMN_NAME_NOTES), # # class NoSelectDelegate(QStyledItemDelegate): # """https://stackoverflow.com/questions/72790705/dont-select-text-in-qtablewidget-cell-when-editing/72792962#72792962""" @@ -79,84 +80,49 @@ # editor.deselect() # editor.selectionChanged.connect(deselect) # return editor -# -# class PlaylistTab(QTableWidget): -# cellEditingStarted = QtCore.pyqtSignal(int, int) -# cellEditingEnded = QtCore.pyqtSignal() -# -# # Column names -# COL_AUTOPLAY = COL_USERDATA = 0 -# COL_MSS = 1 -# COL_NOTE = COL_TITLE = 2 -# COL_ARTIST = 3 -# COL_DURATION = 4 -# COL_START_TIME = 5 -# COL_END_TIME = 6 -# COL_LAST_PLAYED = 7 -# COL_ROW_NOTES = 8 -# COL_LAST = COL_ROW_NOTES -# -# NOTE_COL_SPAN = COL_LAST - COL_NOTE + 1 -# NOTE_ROW_SPAN = 1 -# -# # Qt.UserRoles -# ROW_METADATA = Qt.UserRole -# CONTENT_OBJECT = Qt.UserRole + 1 -# ROW_DURATION = Qt.UserRole + 2 -# -# def __init__(self, musicmuster: QMainWindow, session: Session, -# playlist_id: int, *args, **kwargs): -# super().__init__(*args, **kwargs) -# -# self.musicmuster: QMainWindow = musicmuster -# self.playlist_id: int = playlist_id + + +class PlaylistTab(QTableWidget): + # cellEditingStarted = QtCore.pyqtSignal(int, int) + # cellEditingEnded = QtCore.pyqtSignal() + + # # Qt.UserRoles + # ROW_METADATA = Qt.UserRole + # CONTENT_OBJECT = Qt.UserRole + 1 + # ROW_DURATION = Qt.UserRole + 2 + + def __init__(self, musicmuster: QMainWindow, session: Session, + playlist_id: int, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.musicmuster: QMainWindow = musicmuster + self.playlist_id: int = playlist_id # self.menu: Optional[QMenu] = None # self.current_track_start_time: Optional[datetime] = None # # # Don't select text on edit # self.setItemDelegate(NoSelectDelegate(self)) # -# # Set up widget + # Set up widget # self.setEditTriggers(QtWidgets.QAbstractItemView.AllEditTriggers) -# self.setAlternatingRowColors(True) + self.setAlternatingRowColors(True) # self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) # self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) # self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) -# self.setRowCount(0) -# self.setColumnCount(9) -# # Add header row -# item: QTableWidgetItem = QtWidgets.QTableWidgetItem() -# self.setHorizontalHeaderItem(0, item) -# item = QtWidgets.QTableWidgetItem() -# self.setHorizontalHeaderItem(1, item) -# item = QtWidgets.QTableWidgetItem() -# self.setHorizontalHeaderItem(2, item) -# item = QtWidgets.QTableWidgetItem() -# self.setHorizontalHeaderItem(3, item) -# item = QtWidgets.QTableWidgetItem() -# self.setHorizontalHeaderItem(4, item) -# item = QtWidgets.QTableWidgetItem() -# self.setHorizontalHeaderItem(5, item) -# item = QtWidgets.QTableWidgetItem() -# self.setHorizontalHeaderItem(6, item) -# item = QtWidgets.QTableWidgetItem() -# self.setHorizontalHeaderItem(7, item) -# item = QtWidgets.QTableWidgetItem() -# self.setHorizontalHeaderItem(8, item) -# self.horizontalHeader().setMinimumSectionSize(0) -# -# self._set_column_widths(session) -# self.setHorizontalHeaderLabels([ -# Config.COLUMN_NAME_AUTOPLAY, -# Config.COLUMN_NAME_LEADING_SILENCE, -# Config.COLUMN_NAME_TITLE, -# Config.COLUMN_NAME_ARTIST, -# Config.COLUMN_NAME_LENGTH, -# Config.COLUMN_NAME_START_TIME, -# Config.COLUMN_NAME_END_TIME, -# Config.COLUMN_NAME_LAST_PLAYED, -# "Row notes", -# ]) + self.setRowCount(0) + self.setColumnCount(len(columns)) + # Add header row + for idx in [a for a in range(len(columns))]: + item: QTableWidgetItem = QTableWidgetItem() + self.setHorizontalHeaderItem(idx, item) + + self.horizontalHeader().setMinimumSectionSize(0) + # self._set_column_widths(session) + # Set column headings sorted by idx + self.setHorizontalHeaderLabels( + [a[0].heading for a in list(sorted(columns.values(), + key=lambda item: item[0][0]))] + ) # # self.setDragEnabled(True) # self.setAcceptDrops(True)