SQLA2: WIP

This commit is contained in:
Keith Edmunds 2022-07-31 21:11:34 +01:00
parent 64799ccc61
commit b7111d8a3b
6 changed files with 327 additions and 100 deletions

View File

@ -137,31 +137,31 @@
# return min(trim_ms, len(audio_segment)) # return min(trim_ms, len(audio_segment))
# #
# #
# def ms_to_mmss(ms: int, decimals: int = 0, negative: bool = False) -> str: def ms_to_mmss(ms: int, decimals: int = 0, negative: bool = False) -> str:
# """Convert milliseconds to mm:ss""" """Convert milliseconds to mm:ss"""
#
# minutes: int minutes: int
# remainder: int remainder: int
# seconds: float seconds: float
#
# if not ms: if not ms:
# return "-" return "-"
# sign = "" sign = ""
# if ms < 0: if ms < 0:
# if negative: if negative:
# sign = "-" sign = "-"
# else: else:
# ms = 0 ms = 0
#
# minutes, remainder = divmod(ms, 60 * 1000) minutes, remainder = divmod(ms, 60 * 1000)
# seconds = remainder / 1000 seconds = remainder / 1000
#
# # if seconds >= 59.5, it will be represented as 60, which looks odd. # if seconds >= 59.5, it will be represented as 60, which looks odd.
# # So, fake it under those circumstances # So, fake it under those circumstances
# if seconds >= 59.5: if seconds >= 59.5:
# seconds = 59.0 seconds = 59.0
#
# return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}" return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}"
# #
# #
# def open_in_audacity(path: str) -> Optional[bool]: # def open_in_audacity(path: str) -> Optional[bool]:

View File

@ -275,8 +275,12 @@ class Playlists(Base):
name: str = Column(String(32), nullable=False, unique=True) name: str = Column(String(32), nullable=False, unique=True)
last_used = Column(DateTime, default=None, nullable=True) last_used = Column(DateTime, default=None, nullable=True)
loaded: bool = Column(Boolean, default=True, nullable=False) loaded: bool = Column(Boolean, default=True, nullable=False)
rows = relationship("PlaylistRows", back_populates="playlist", rows = relationship(
cascade="all, delete-orphan") "PlaylistRows",
back_populates="playlist",
cascade="all, delete-orphan",
order_by="PlaylistRows.row_number"
)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<Playlists(id={self.id}, name={self.name}>" return f"<Playlists(id={self.id}, name={self.name}>"
@ -317,10 +321,6 @@ class Playlists(Base):
# ).all() # ).all()
# #
# @classmethod # @classmethod
# def get_by_id(cls, session: Session, playlist_id: int) -> "Playlists":
# return (session.query(cls).filter(cls.id == playlist_id)).one()
#
# @classmethod
# def get_closed(cls, session: Session) -> List["Playlists"]: # def get_closed(cls, session: Session) -> List["Playlists"]:
# """Returns a list of all closed playlists ordered by last use""" # """Returns a list of all closed playlists ordered by last use"""
# #
@ -529,7 +529,7 @@ class Tracks(Base):
silence_at = Column(Integer, index=False) silence_at = Column(Integer, index=False)
path = Column(String(2048), index=False, nullable=False) path = Column(String(2048), index=False, nullable=False)
mtime = Column(Float, index=True) mtime = Column(Float, index=True)
lastplayed = Column(DateTime, index=True, default=None) # lastplayed = Column(DateTime, index=True, default=None)
playlistrows = relationship("PlaylistRows", back_populates="track") playlistrows = relationship("PlaylistRows", back_populates="track")
playlists = association_proxy("playlistrows", "playlist") playlists = association_proxy("playlistrows", "playlist")
playdates = relationship("Playdates", back_populates="track") playdates = relationship("Playdates", back_populates="track")

View File

@ -4,7 +4,7 @@ from collections import namedtuple
# from typing import Dict, List, Optional, Set, Tuple, Union # from typing import Dict, List, Optional, Set, Tuple, Union
# #
# from PyQt5 import QtCore # from PyQt5 import QtCore
# from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
# from PyQt5.Qt import QFont # from PyQt5.Qt import QFont
# from PyQt5.QtGui import QColor, QDropEvent # from PyQt5.QtGui import QColor, QDropEvent
# from PyQt5 import QtWidgets # from PyQt5 import QtWidgets
@ -21,7 +21,7 @@ from PyQt5.QtWidgets import (
QTableWidgetItem, QTableWidgetItem,
) )
# #
# import helpers import helpers
# import os # import os
# import re # import re
# import subprocess # import subprocess
@ -34,8 +34,8 @@ from config import Config
from models import ( from models import (
# Notes, # Notes,
# Playdates, # Playdates,
# Playlists, Playlists,
# PlaylistTracks, PlaylistRows,
Settings, Settings,
# Tracks, # Tracks,
# NoteColours # NoteColours
@ -55,16 +55,18 @@ from dbconfig import Session
# Columns # Columns
Column = namedtuple("Column", ['idx', 'heading']) Column = namedtuple("Column", ['idx', 'heading'])
columns = {} columns = {}
columns["userdata"] = Column(idx=0, heading=Config.COLUMN_NAME_AUTOPLAY), columns["userdata"] = Column(idx=0, heading=Config.COLUMN_NAME_AUTOPLAY)
columns["mss"] = Column(idx=1, heading=Config.COLUMN_NAME_LEADING_SILENCE), columns["start_gap"] = Column(
columns["title"] = Column(idx=2, heading=Config.COLUMN_NAME_TITLE), idx=1, heading=Config.COLUMN_NAME_LEADING_SILENCE)
columns["artist"] = Column(idx=3, heading=Config.COLUMN_NAME_ARTIST), columns["title"] = Column(idx=2, heading=Config.COLUMN_NAME_TITLE)
columns["duration"] = Column(idx=4, heading=Config.COLUMN_NAME_LENGTH), columns["artist"] = Column(idx=3, heading=Config.COLUMN_NAME_ARTIST)
columns["start_time"] = Column(idx=5, heading=Config.COLUMN_NAME_START_TIME), columns["duration"] = Column(idx=4, heading=Config.COLUMN_NAME_LENGTH)
columns["end_time"] = Column(idx=6, heading=Config.COLUMN_NAME_END_TIME), columns["start_time"] = Column(idx=5, heading=Config.COLUMN_NAME_START_TIME)
columns["last_played"] = Column(idx=7, heading=Config.COLUMN_NAME_LAST_PLAYED), columns["end_time"] = Column(idx=6, heading=Config.COLUMN_NAME_END_TIME)
columns["row_notes"] = Column(idx=8, heading=Config.COLUMN_NAME_NOTES), columns["lastplayed"] = Column(idx=7, heading=Config.COLUMN_NAME_LAST_PLAYED)
# columns["row_notes"] = Column(idx=8, heading=Config.COLUMN_NAME_NOTES)
# class NoSelectDelegate(QStyledItemDelegate): # class NoSelectDelegate(QStyledItemDelegate):
# """https://stackoverflow.com/questions/72790705/dont-select-text-in-qtablewidget-cell-when-editing/72792962#72792962""" # """https://stackoverflow.com/questions/72790705/dont-select-text-in-qtablewidget-cell-when-editing/72792962#72792962"""
# #
@ -85,10 +87,10 @@ class PlaylistTab(QTableWidget):
# cellEditingStarted = QtCore.pyqtSignal(int, int) # cellEditingStarted = QtCore.pyqtSignal(int, int)
# cellEditingEnded = QtCore.pyqtSignal() # cellEditingEnded = QtCore.pyqtSignal()
# # Qt.UserRoles # Qt.UserRoles
# ROW_METADATA = Qt.UserRole ROW_METADATA = Qt.UserRole
# CONTENT_OBJECT = Qt.UserRole + 1 # CONTENT_OBJECT = Qt.UserRole + 1
# ROW_DURATION = Qt.UserRole + 2 ROW_DURATION = Qt.UserRole + 2
def __init__(self, musicmuster: QMainWindow, session: Session, def __init__(self, musicmuster: QMainWindow, session: Session,
playlist_id: int, *args, **kwargs): playlist_id: int, *args, **kwargs):
@ -119,8 +121,8 @@ class PlaylistTab(QTableWidget):
self._set_column_widths(session) self._set_column_widths(session)
# Set column headings sorted by idx # Set column headings sorted by idx
self.setHorizontalHeaderLabels( self.setHorizontalHeaderLabels(
[a[0].heading for a in list(sorted(columns.values(), [a.heading for a in list(sorted(columns.values(),
key=lambda item: item[0][0]))] key=lambda item: item.idx))]
) )
# self.setDragEnabled(True) # self.setDragEnabled(True)
@ -153,8 +155,8 @@ class PlaylistTab(QTableWidget):
# self.doubleClicked.connect(self._edit_cell) # self.doubleClicked.connect(self._edit_cell)
self.horizontalHeader().sectionResized.connect(self._column_resize) self.horizontalHeader().sectionResized.connect(self._column_resize)
# #
# # Now load our tracks and notes # Now load our tracks and notes
# self.populate(session, self.playlist_id) self.populate(session, self.playlist_id)
def _column_resize(self, idx, old, new): def _column_resize(self, idx, old, new):
""" """
@ -165,7 +167,7 @@ class PlaylistTab(QTableWidget):
with Session() as session: with Session() as session:
for column_name, data in columns.items(): for column_name, data in columns.items():
idx = data[0].idx idx = data.idx
width = self.columnWidth(idx) width = self.columnWidth(idx)
attribute_name = f"playlist_{column_name}_col_width" attribute_name = f"playlist_{column_name}_col_width"
record = Settings.get_int_settings(session, attribute_name) record = Settings.get_int_settings(session, attribute_name)
@ -350,6 +352,87 @@ class PlaylistTab(QTableWidget):
# return self.item(row, self.COL_TITLE).text() # return self.item(row, self.COL_TITLE).text()
# else: # else:
# return None # return None
def insert_row(self, session: Session, row_data: PlaylistRows,
repaint: bool = True) -> None:
"""
Insert a row into playlist tab.
If playlist has a row selected, add new row above. Otherwise,
add to end of playlist.
Note: we ignore the row number in the PlaylistRows record. That is
used only to order the query that generates the records.
"""
if self.selectionModel().hasSelection():
row = self.currentRow()
else:
row = self.rowCount()
self.insertRow(row)
# Add row metadata to userdata column
item: QTableWidgetItem = QTableWidgetItem()
item.setData(self.ROW_METADATA, 0)
self.setItem(row, columns['userdata'].idx, item)
# Prepare notes, start and end items for later
notes_item = QTableWidgetItem(row_data.note)
start_item = QTableWidgetItem()
end_item = QTableWidgetItem()
if row_data.track_id:
# Add track details to items
start_gap = row_data.track.start_gap
start_gap_item = QTableWidgetItem(str(start_gap))
if start_gap and start_gap >= 500:
start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START))
title_item = QTableWidgetItem(row_data.track.title)
artist_item = QTableWidgetItem(row_data.track.artist)
duration_item = QTableWidgetItem(
helpers.ms_to_mmss(row_data.track.duration))
self._set_row_duration(row, row_data.track.duration)
last_playtime = Playdates.last_played(session, row_data.track.id)
last_played_str = get_relative_date(last_playtime)
last_played_item = QTableWidgetItem(last_played_str)
self.setItem(row, columns['lastplayed'], last_played_item)
# Mark track if file is unreadable
if not self._file_is_readable(row_data.track.path):
self._set_unreadable_row(row)
else:
# This is a note row so make empty items (row background
# won't be coloured without items present)
start_gap_item = QTableWidgetItem()
title_item = QTableWidgetItem()
artist_item = QTableWidgetItem()
duration_item = QTableWidgetItem()
last_played_item = QTableWidgetItem()
# Add items to table
self.setItem(row, columns['start_gap'].idx, start_gap_item)
self.setItem(row, columns['title'].idx, title_item)
self.setItem(row, columns['artist'].idx, artist_item)
self.setItem(row, columns['duration'].idx, duration_item)
self.setItem(row, columns['start_time'].idx, start_item)
self.setItem(row, columns['end_time'].idx, end_item)
self.setItem(row, columns['row_notes'].idx, notes_item)
if not row_data.track_id:
# Span note across table
self.setSpan(row, 0, len(columns), 1)
# Scroll to new row
self.scrollToItem(title_item, QAbstractItemView.PositionAtCenter)
if repaint:
self.save_playlist(session)
self.update_display(session, clear_selection=False)
# #
# def insert_track(self, session: Session, track: Tracks, # def insert_track(self, session: Session, track: Tracks,
# repaint: bool = True) -> None: # repaint: bool = True) -> None:
@ -525,50 +608,37 @@ class PlaylistTab(QTableWidget):
# #
# self._clear_current_track_row() # self._clear_current_track_row()
# self.current_track_start_time = None # self.current_track_start_time = None
#
# def populate(self, session: Session, playlist_id: int) -> None: def populate(self, session: Session, playlist_id: int) -> None:
# """ """
# Populate from the associated playlist ID Populate from the associated playlist ID
# """
# We don't mandate that an item will be on its specified row, only
# that it will be above larger-numbered row items, and below # data: List[Union[Tuple[List[int], Tracks], Tuple[List[int], Notes]]] \
# lower-numbered ones. # = []
# """ # item: Union[Notes, Tracks]
# # note: Notes
# data: List[Union[Tuple[List[int], Tracks], Tuple[List[int], Notes]]] \ # row: int
# = [] # track: Tracks
# item: Union[Notes, Tracks]
# note: Notes playlist = session.get(Playlists, playlist_id)
# row: int
# track: Tracks # Clear playlist
# self.setRowCount(0)
# playlist = Playlists.get_by_id(session, playlist_id)
# # Add the rows
# for row, track in playlist.tracks.items(): for row in playlist.rows:
# data.append(([row], track)) self.insert_row(session, row, repaint=False)
# for note in playlist.notes:
# data.append(([note.row], note)) # Scroll to top
# scroll_to: QTableWidgetItem = self.item(0, 0)
# # Clear playlist self.scrollToItem(scroll_to, QAbstractItemView.PositionAtTop)
# self.setRowCount(0)
# # We possibly don't need to save the playlist here, but row
# # Now add data in row order # numbers may have changed during population, and it's cheap to do
# for i in sorted(data, key=lambda x: x[0]): # KAE self.save_playlist(session)
# item = i[1] self.update_display(session)
# if isinstance(item, Tracks):
# self.insert_track(session, item, repaint=False)
# elif isinstance(item, Notes):
# self._insert_note(session, item, repaint=False)
#
# # Scroll to top
# scroll_to: QTableWidgetItem = self.item(0, 0)
# self.scrollToItem(scroll_to, QAbstractItemView.PositionAtTop)
#
# # We possibly don't need to save the playlist here, but row
# # numbers may have changed during population, and it's cheap to do
# self.save_playlist(session)
# self.update_display(session)
#
# def save_playlist(self, session) -> None: # def save_playlist(self, session) -> None:
# """ # """
# Save playlist to database. # Save playlist to database.
@ -1614,7 +1684,7 @@ class PlaylistTab(QTableWidget):
"""Column widths from settings""" """Column widths from settings"""
for column_name, data in columns.items(): for column_name, data in columns.items():
idx = data[0].idx idx = data.idx
attr_name = f"playlist_{column_name}_col_width" attr_name = f"playlist_{column_name}_col_width"
record: Settings = Settings.get_int_settings(session, attr_name) record: Settings = Settings.get_int_settings(session, attr_name)
if record and record.f_int is not None: if record and record.f_int is not None:

9
ipython_commands.txt Normal file
View File

@ -0,0 +1,9 @@
from sqlalchemy.orm import (sessionmaker, scoped_session)
s = sessionmaker(bind=engine)
from dbconfig import engine
s = sessionmaker(bind=engine)
s
playlist in s
s = scoped_session(sessionmaker(bind=engine))
playlist_id = 3
playlist = Playlists.get_by_id(session, playlist_id)

View File

@ -0,0 +1,54 @@
"""schema changes for row notes
Revision ID: 3b063011ed67
Revises: 51f61433256f
Create Date: 2022-07-06 19:48:23.960471
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '3b063011ed67'
down_revision = '51f61433256f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('notes')
op.add_column('playlist_rows', sa.Column('note', sa.String(length=2048), nullable=True))
op.alter_column('playlist_rows', 'track_id',
existing_type=mysql.INTEGER(display_width=11),
nullable=True)
op.drop_index('uniquerow', table_name='playlist_rows')
op.drop_column('playlist_rows', 'text')
op.alter_column('playlist_rows', 'row', new_column_name='row_number',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
op.create_index('uniquerow', 'playlist_rows', ['row_number', 'playlist_id'], unique=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('playlist_rows', 'row_number', new_column_name='row',
existing_type=mysql.INTEGER(display_width=11),
nullable=False)
op.add_column('playlist_rows', sa.Column('text', mysql.VARCHAR(length=2048), nullable=True))
op.drop_index('uniquerow', table_name='playlist_rows')
op.create_index('uniquerow', 'playlist_rows', ['row', 'playlist_id'], unique=False)
op.drop_column('playlist_rows', 'note')
op.create_table('notes',
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False),
sa.Column('playlist_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True),
sa.Column('row', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False),
sa.Column('note', mysql.VARCHAR(length=256), nullable=True),
sa.ForeignKeyConstraint(['playlist_id'], ['playlists.id'], name='notes_ibfk_1'),
sa.PrimaryKeyConstraint('id'),
mysql_default_charset='utf8mb4',
mysql_engine='InnoDB'
)
# ### end Alembic commands ###

94
play.py Executable file
View File

@ -0,0 +1,94 @@
#!/usr/bin/env python
from sqlalchemy import create_engine
from sqlalchemy import text
from sqlalchemy import Table, Column, Integer, String
from sqlalchemy import ForeignKey
from sqlalchemy import select
from sqlalchemy import insert
from sqlalchemy.orm import Session
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
engine = create_engine("sqlite+pysqlite:///:memory:", echo=True, future=True)
class User(Base):
__tablename__ = 'user_account'
id = Column(Integer, primary_key=True)
name = Column(String(30))
fullname = Column(String)
addresses = relationship("Address", back_populates="user")
def __repr__(self):
return (
f"User(id={self.id!r}, name={self.name!r}, "
f"fullname={self.fullname!r})"
)
class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
email_address = Column(String, nullable=False)
user_id = Column(Integer, ForeignKey('user_account.id'))
user = relationship("User", back_populates="addresses")
def __repr__(self):
return f"Address(id={self.id!r}, email_address={self.email_address!r})"
Base.metadata.create_all(engine)
squidward = User(name="squidward", fullname="Squidward Tentacles")
krabs = User(name="ehkrabs", fullname="Eugene H. Krabs")
session = Session(engine)
session.add(squidward)
session.add(krabs)
session.commit()
u1 = User(name='pkrabs', fullname='Pearl Krabs')
a1 = Address(email_address="pearl.krabs@gmail.com")
u1.addresses.append(a1)
a2 = Address(email_address="pearl@aol.com", user=u1)
session.add(u1)
session.add(a1)
session.add(a2)
session.commit()
# with engine.connect() as conn:
# conn.execute(text("CREATE TABLE some_table (x int, y int)"))
# conn.execute(
# text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
# [{"x": 1, "y": 1}, {"x": 2, "y": 4}]
# )
# conn.commit()
#
# with engine.begin() as conn:
# conn.execute(
# text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
# [{"x": 6, "y": 8}, {"x": 9, "y": 10}]
# )
#
# # with engine.connect() as conn:
# # result = conn.execute(text("SELECT x, y FROM some_table"))
# # for row in result:
# # print(f"x: {row.x} y: {row.y}")
#
#
# stmt = text(
# "SELECT x, y FROM some_table WHERE y > :y ORDER BY x, y").bindparams(y=6)
#
# with Session(engine) as session:
# result = session.execute(stmt)
# for row in result:
# print(f"x: {row.x} y: {row.y}")