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))
#
#
# def ms_to_mmss(ms: int, decimals: int = 0, negative: bool = False) -> str:
# """Convert milliseconds to mm:ss"""
#
# minutes: int
# remainder: int
# seconds: float
#
# if not ms:
# return "-"
# sign = ""
# if ms < 0:
# if negative:
# sign = "-"
# else:
# ms = 0
#
# minutes, remainder = divmod(ms, 60 * 1000)
# seconds = remainder / 1000
#
# # if seconds >= 59.5, it will be represented as 60, which looks odd.
# # So, fake it under those circumstances
# if seconds >= 59.5:
# seconds = 59.0
#
# return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}"
def ms_to_mmss(ms: int, decimals: int = 0, negative: bool = False) -> str:
"""Convert milliseconds to mm:ss"""
minutes: int
remainder: int
seconds: float
if not ms:
return "-"
sign = ""
if ms < 0:
if negative:
sign = "-"
else:
ms = 0
minutes, remainder = divmod(ms, 60 * 1000)
seconds = remainder / 1000
# if seconds >= 59.5, it will be represented as 60, which looks odd.
# So, fake it under those circumstances
if seconds >= 59.5:
seconds = 59.0
return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}"
#
#
# 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)
last_used = Column(DateTime, default=None, nullable=True)
loaded: bool = Column(Boolean, default=True, nullable=False)
rows = relationship("PlaylistRows", back_populates="playlist",
cascade="all, delete-orphan")
rows = relationship(
"PlaylistRows",
back_populates="playlist",
cascade="all, delete-orphan",
order_by="PlaylistRows.row_number"
)
def __repr__(self) -> str:
return f"<Playlists(id={self.id}, name={self.name}>"
@ -317,10 +321,6 @@ class Playlists(Base):
# ).all()
#
# @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"]:
# """Returns a list of all closed playlists ordered by last use"""
#
@ -529,7 +529,7 @@ class Tracks(Base):
silence_at = Column(Integer, index=False)
path = Column(String(2048), index=False, nullable=False)
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")
playlists = association_proxy("playlistrows", "playlist")
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 PyQt5 import QtCore
# from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt
# from PyQt5.Qt import QFont
# from PyQt5.QtGui import QColor, QDropEvent
# from PyQt5 import QtWidgets
@ -21,7 +21,7 @@ from PyQt5.QtWidgets import (
QTableWidgetItem,
)
#
# import helpers
import helpers
# import os
# import re
# import subprocess
@ -34,8 +34,8 @@ from config import Config
from models import (
# Notes,
# Playdates,
# Playlists,
# PlaylistTracks,
Playlists,
PlaylistRows,
Settings,
# Tracks,
# NoteColours
@ -55,16 +55,18 @@ from dbconfig import Session
# 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),
#
columns["userdata"] = Column(idx=0, heading=Config.COLUMN_NAME_AUTOPLAY)
columns["start_gap"] = 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["lastplayed"] = 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"""
#
@ -85,10 +87,10 @@ class PlaylistTab(QTableWidget):
# cellEditingStarted = QtCore.pyqtSignal(int, int)
# cellEditingEnded = QtCore.pyqtSignal()
# # Qt.UserRoles
# ROW_METADATA = Qt.UserRole
# Qt.UserRoles
ROW_METADATA = Qt.UserRole
# CONTENT_OBJECT = Qt.UserRole + 1
# ROW_DURATION = Qt.UserRole + 2
ROW_DURATION = Qt.UserRole + 2
def __init__(self, musicmuster: QMainWindow, session: Session,
playlist_id: int, *args, **kwargs):
@ -119,8 +121,8 @@ class PlaylistTab(QTableWidget):
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]))]
[a.heading for a in list(sorted(columns.values(),
key=lambda item: item.idx))]
)
# self.setDragEnabled(True)
@ -153,8 +155,8 @@ class PlaylistTab(QTableWidget):
# self.doubleClicked.connect(self._edit_cell)
self.horizontalHeader().sectionResized.connect(self._column_resize)
#
# # Now load our tracks and notes
# self.populate(session, self.playlist_id)
# Now load our tracks and notes
self.populate(session, self.playlist_id)
def _column_resize(self, idx, old, new):
"""
@ -165,7 +167,7 @@ class PlaylistTab(QTableWidget):
with Session() as session:
for column_name, data in columns.items():
idx = data[0].idx
idx = data.idx
width = self.columnWidth(idx)
attribute_name = f"playlist_{column_name}_col_width"
record = Settings.get_int_settings(session, attribute_name)
@ -350,6 +352,87 @@ class PlaylistTab(QTableWidget):
# return self.item(row, self.COL_TITLE).text()
# else:
# 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,
# repaint: bool = True) -> None:
@ -525,50 +608,37 @@ class PlaylistTab(QTableWidget):
#
# self._clear_current_track_row()
# self.current_track_start_time = None
#
# def populate(self, session: Session, playlist_id: int) -> None:
# """
# 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
# lower-numbered ones.
# """
#
def populate(self, session: Session, playlist_id: int) -> None:
"""
Populate from the associated playlist ID
"""
# data: List[Union[Tuple[List[int], Tracks], Tuple[List[int], Notes]]] \
# = []
# item: Union[Notes, Tracks]
# note: Notes
# row: int
# track: Tracks
#
# playlist = Playlists.get_by_id(session, playlist_id)
#
# for row, track in playlist.tracks.items():
# data.append(([row], track))
# for note in playlist.notes:
# data.append(([note.row], note))
#
# # Clear playlist
# self.setRowCount(0)
#
# # Now add data in row order
# for i in sorted(data, key=lambda x: x[0]):
# item = i[1]
# 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)
#
playlist = session.get(Playlists, playlist_id)
# Clear playlist
self.setRowCount(0)
# Add the rows
for row in playlist.rows:
self.insert_row(session, row, 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
# KAE self.save_playlist(session)
self.update_display(session)
# def save_playlist(self, session) -> None:
# """
# Save playlist to database.
@ -1614,7 +1684,7 @@ class PlaylistTab(QTableWidget):
"""Column widths from settings"""
for column_name, data in columns.items():
idx = data[0].idx
idx = data.idx
attr_name = f"playlist_{column_name}_col_width"
record: Settings = Settings.get_int_settings(session, attr_name)
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}")