From 9d44642fea687da27831a71da7744f16b4adb4c6 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Tue, 2 Apr 2024 21:42:22 +0100 Subject: [PATCH] Migrate to Alchemical --- app/classes.py | 9 ++- app/dbconfig.py | 38 --------- app/dbtables.py | 187 ++++++++++++++++++++++++++++++++++++++++++ app/dialogs.py | 9 ++- app/models.py | 189 ++++++------------------------------------- app/musicmuster.py | 79 +++++++++--------- app/playlistmodel.py | 48 ++++++----- app/playlists.py | 21 ++--- poetry.lock | 20 ++++- pyproject.toml | 1 + 10 files changed, 321 insertions(+), 280 deletions(-) delete mode 100644 app/dbconfig.py create mode 100644 app/dbtables.py diff --git a/app/classes.py b/app/classes.py index 851500f..83ead0d 100644 --- a/app/classes.py +++ b/app/classes.py @@ -1,13 +1,18 @@ +# Standard library imports from dataclasses import dataclass -import datetime as dt from typing import Optional +import datetime as dt +# PyQt imports from PyQt6.QtCore import pyqtSignal, QObject, QThread + +# Third party imports import numpy as np import pyqtgraph as pg # type: ignore +from sqlalchemy.orm import scoped_session +# App imports from config import Config -from dbconfig import scoped_session from models import PlaylistRows import helpers diff --git a/app/dbconfig.py b/app/dbconfig.py deleted file mode 100644 index 30dc603..0000000 --- a/app/dbconfig.py +++ /dev/null @@ -1,38 +0,0 @@ -import inspect -import os -from config import Config -from contextlib import contextmanager -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker, scoped_session -from typing import Generator - -from log import log - -MYSQL_CONNECT = os.environ.get("MM_DB") -if MYSQL_CONNECT is None: - raise ValueError("MYSQL_CONNECT is undefined") -else: - dbname = MYSQL_CONNECT.split("/")[-1] - log.debug(f"Database: {dbname}") - -engine = create_engine( - MYSQL_CONNECT, - echo=Config.DISPLAY_SQL, - pool_pre_ping=True, - future=True, - connect_args={"charset": "utf8mb4"}, -) - - -@contextmanager -def Session() -> Generator[scoped_session, None, None]: - frame = inspect.stack()[2] - file = frame.filename - function = frame.function - lineno = frame.lineno - Session = scoped_session(sessionmaker(bind=engine)) - log.debug(f"Session acquired: {file}:{function}:{lineno} " f"[{hex(id(Session))}]") - yield Session - log.debug(f" Session released [{hex(id(Session))}]") - Session.commit() - Session.close() diff --git a/app/dbtables.py b/app/dbtables.py new file mode 100644 index 0000000..93b8e24 --- /dev/null +++ b/app/dbtables.py @@ -0,0 +1,187 @@ +# Standard library imports +from typing import List, Optional +import datetime as dt +import os + +# PyQt imports + +# Third party imports +from alchemical import Alchemical, Model # type: ignore +from sqlalchemy import ( + Boolean, + DateTime, + ForeignKey, + String, +) +from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.orm import ( + Mapped, + mapped_column, + relationship, +) + +# App imports + + +# Database classes +# Note: initialisation of the 'db' variable is at the foot of this +# module. +class CartsTable(Model): + __tablename__ = "carts" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + cart_number: Mapped[int] = mapped_column(unique=True) + name: Mapped[str] = mapped_column(String(256), index=True) + duration: Mapped[Optional[int]] = mapped_column(index=True) + path: Mapped[Optional[str]] = mapped_column(String(2048), index=False) + enabled: Mapped[Optional[bool]] = mapped_column(default=False) + + def __repr__(self) -> str: + return ( + f"" + ) + + +class NoteColoursTable(Model): + __tablename__ = "notecolours" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + substring: Mapped[str] = mapped_column(String(256), index=False) + colour: Mapped[str] = mapped_column(String(21), index=False) + enabled: Mapped[bool] = mapped_column(default=True, index=True) + is_regex: Mapped[bool] = mapped_column(default=False, index=False) + is_casesensitive: Mapped[bool] = mapped_column(default=False, index=False) + order: Mapped[Optional[int]] = mapped_column(index=True) + + def __repr__(self) -> str: + return ( + f"" + ) + + +class PlaydatesTable(Model): + __tablename__ = "playdates" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + lastplayed: Mapped[dt.datetime] = mapped_column(index=True) + track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id")) + track: Mapped["TracksTable"] = relationship("Tracks", back_populates="playdates") + + def __repr__(self) -> str: + return ( + f"" + ) + + +class PlaylistsTable(Model): + """ + Manage playlists + """ + + __tablename__ = "playlists" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(32), unique=True) + last_used: Mapped[Optional[dt.datetime]] = mapped_column(DateTime, default=None) + tab: Mapped[Optional[int]] = mapped_column(default=None) + open: Mapped[bool] = mapped_column(default=False) + is_template: Mapped[bool] = mapped_column(default=False) + deleted: Mapped[bool] = mapped_column(default=False) + rows: Mapped[List["PlaylistRowsTable"]] = relationship( + "PlaylistRows", + back_populates="playlist", + cascade="all, delete-orphan", + order_by="PlaylistRows.plr_rownum", + ) + + def __repr__(self) -> str: + return ( + f"" + ) + + +class PlaylistRowsTable(Model): + __tablename__ = "playlist_rows" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + plr_rownum: Mapped[int] + note: Mapped[str] = mapped_column( + String(2048), index=False, default="", nullable=False + ) + playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id")) + playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows") + track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id")) + track: Mapped["TracksTable"] = relationship( + "Tracks", + back_populates="playlistrows", + ) + played: Mapped[bool] = mapped_column( + Boolean, nullable=False, index=False, default=False + ) + + def __repr__(self) -> str: + return ( + f"" + ) + + +class SettingsTable(Model): + """Manage settings""" + + __tablename__ = "settings" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(64), unique=True) + f_datetime: Mapped[Optional[dt.datetime]] = mapped_column(default=None) + f_int: Mapped[Optional[int]] = mapped_column(default=None) + f_string: Mapped[Optional[str]] = mapped_column(String(128), default=None) + + def __repr__(self) -> str: + return ( + f"" + ) + + +class TracksTable(Model): + __tablename__ = "tracks" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + title: Mapped[str] = mapped_column(String(256), index=True) + artist: Mapped[str] = mapped_column(String(256), index=True) + bitrate: Mapped[Optional[int]] = mapped_column(default=None) + duration: Mapped[int] = mapped_column(index=True) + fade_at: Mapped[int] = mapped_column(index=False) + mtime: Mapped[float] = mapped_column(index=True) + path: Mapped[str] = mapped_column(String(2048), index=False, unique=True) + silence_at: Mapped[int] = mapped_column(index=False) + start_gap: Mapped[int] = mapped_column(index=False) + playlistrows: Mapped[List[PlaylistRowsTable]] = relationship( + "PlaylistRows", back_populates="track" + ) + playlists = association_proxy("playlistrows", "playlist") + playdates: Mapped[List[PlaydatesTable]] = relationship( + "Playdates", + back_populates="track", + lazy="joined", + ) + + def __repr__(self) -> str: + return ( + f"" + ) + + +MYSQL_CONNECT = os.environ.get("MM_DB") +if MYSQL_CONNECT is None: + raise ValueError("MYSQL_CONNECT is undefined") +else: + dbname = MYSQL_CONNECT.split("/")[-1] +db = Alchemical(MYSQL_CONNECT) diff --git a/app/dialogs.py b/app/dialogs.py index 31e015a..ab5465e 100644 --- a/app/dialogs.py +++ b/app/dialogs.py @@ -1,10 +1,17 @@ +# Standard library imports + +# PyQt imports + +# Third party imports + +# App imports from typing import Optional from PyQt6.QtCore import QEvent, Qt from PyQt6.QtWidgets import QDialog, QListWidgetItem from classes import MusicMusterSignals -from dbconfig import scoped_session +from sqlalchemy.orm import scoped_session from helpers import ( ask_yes_no, get_relative_date, diff --git a/app/models.py b/app/models.py index 56d186d..3b15c20 100644 --- a/app/models.py +++ b/app/models.py @@ -1,63 +1,37 @@ -#!/usr/bin/python3 - +# Standard library imports +from typing import List, Optional, Sequence +import datetime as dt import re -from config import Config -from dbconfig import scoped_session - -import datetime as dt -from typing import List, Optional, Sequence - -from sqlalchemy.ext.associationproxy import association_proxy +# PyQt imports +# Third party imports from sqlalchemy import ( bindparam, - Boolean, - DateTime, delete, - ForeignKey, func, select, - String, update, ) - -from sqlalchemy.orm import ( - DeclarativeBase, - joinedload, - Mapped, - mapped_column, - relationship, -) -from sqlalchemy.orm.exc import ( - NoResultFound, -) from sqlalchemy.exc import ( IntegrityError, ) +from sqlalchemy.orm import ( + joinedload, + scoped_session, +) +from sqlalchemy.orm.exc import ( + NoResultFound, +) + +# App imports +import dbtables +from config import Config from log import log -class Base(DeclarativeBase): - pass - - # Database classes -class Carts(Base): - __tablename__ = "carts" - - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - cart_number: Mapped[int] = mapped_column(unique=True) - name: Mapped[str] = mapped_column(String(256), index=True) - duration: Mapped[Optional[int]] = mapped_column(index=True) - path: Mapped[Optional[str]] = mapped_column(String(2048), index=False) - enabled: Mapped[Optional[bool]] = mapped_column(default=False) - - def __repr__(self) -> str: - return ( - f"" - ) +class Carts(dbtables.CartsTable): def __init__( self, @@ -80,22 +54,7 @@ class Carts(Base): session.commit() -class NoteColours(Base): - __tablename__ = "notecolours" - - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - substring: Mapped[str] = mapped_column(String(256), index=False) - colour: Mapped[str] = mapped_column(String(21), index=False) - enabled: Mapped[bool] = mapped_column(default=True, index=True) - is_regex: Mapped[bool] = mapped_column(default=False, index=False) - is_casesensitive: Mapped[bool] = mapped_column(default=False, index=False) - order: Mapped[Optional[int]] = mapped_column(index=True) - - def __repr__(self) -> str: - return ( - f"" - ) +class NoteColours(dbtables.NoteColoursTable): def __init__( self, @@ -157,19 +116,7 @@ class NoteColours(Base): return None -class Playdates(Base): - __tablename__ = "playdates" - - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - lastplayed: Mapped[dt.datetime] = mapped_column(index=True) - track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id")) - track: Mapped["Tracks"] = relationship("Tracks", back_populates="playdates") - - def __repr__(self) -> str: - return ( - f"" - ) +class Playdates(dbtables.PlaydatesTable): def __init__(self, session: scoped_session, track_id: int) -> None: """Record that track was played""" @@ -208,32 +155,7 @@ class Playdates(Base): ).all() -class Playlists(Base): - """ - Manage playlists - """ - - __tablename__ = "playlists" - - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - name: Mapped[str] = mapped_column(String(32), unique=True) - last_used: Mapped[Optional[dt.datetime]] = mapped_column(DateTime, default=None) - tab: Mapped[Optional[int]] = mapped_column(default=None) - open: Mapped[bool] = mapped_column(default=False) - is_template: Mapped[bool] = mapped_column(default=False) - deleted: Mapped[bool] = mapped_column(default=False) - rows: Mapped[List["PlaylistRows"]] = relationship( - "PlaylistRows", - back_populates="playlist", - cascade="all, delete-orphan", - order_by="PlaylistRows.plr_rownum", - ) - - def __repr__(self) -> str: - return ( - f"" - ) +class Playlists(dbtables.PlaylistsTable): def __init__(self, session: scoped_session, name: str): self.name = name @@ -366,31 +288,7 @@ class Playlists(Base): PlaylistRows.copy_playlist(session, playlist_id, template.id) -class PlaylistRows(Base): - __tablename__ = "playlist_rows" - - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - plr_rownum: Mapped[int] - note: Mapped[str] = mapped_column( - String(2048), index=False, default="", nullable=False - ) - playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id")) - playlist: Mapped[Playlists] = relationship(back_populates="rows") - track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id")) - track: Mapped["Tracks"] = relationship( - "Tracks", - back_populates="playlistrows", - ) - played: Mapped[bool] = mapped_column( - Boolean, nullable=False, index=False, default=False - ) - - def __repr__(self) -> str: - return ( - f"" - ) +class PlaylistRows(dbtables.PlaylistRowsTable): def __init__( self, @@ -669,27 +567,7 @@ class PlaylistRows(Base): session.connection().execute(stmt, sqla_map) -class Settings(Base): - """Manage settings""" - - __tablename__ = "settings" - - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - name: Mapped[str] = mapped_column(String(64), unique=True) - f_datetime: Mapped[Optional[dt.datetime]] = mapped_column(default=None) - f_int: Mapped[Optional[int]] = mapped_column(default=None) - f_string: Mapped[Optional[str]] = mapped_column(String(128), default=None) - - def __repr__(self) -> str: - return ( - f"" - ) - - def __init__(self, session: scoped_session, name: str): - self.name = name - session.add(self) - session.flush() +class Settings(dbtables.SettingsTable): @classmethod def all_as_dict(cls, session): @@ -722,28 +600,7 @@ class Settings(Base): session.flush() -class Tracks(Base): - __tablename__ = "tracks" - - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - title: Mapped[str] = mapped_column(String(256), index=True) - artist: Mapped[str] = mapped_column(String(256), index=True) - bitrate: Mapped[Optional[int]] = mapped_column(default=None) - duration: Mapped[int] = mapped_column(index=True) - fade_at: Mapped[int] = mapped_column(index=False) - mtime: Mapped[float] = mapped_column(index=True) - path: Mapped[str] = mapped_column(String(2048), index=False, unique=True) - silence_at: Mapped[int] = mapped_column(index=False) - start_gap: Mapped[int] = mapped_column(index=False) - playlistrows: Mapped[List[PlaylistRows]] = relationship( - "PlaylistRows", back_populates="track" - ) - playlists = association_proxy("playlistrows", "playlist") - playdates: Mapped[List[Playdates]] = relationship( - "Playdates", - back_populates="track", - lazy="joined", - ) +class Tracks(dbtables.TracksTable): def __repr__(self) -> str: return ( diff --git a/app/musicmuster.py b/app/musicmuster.py index c0ced6e..73b42d6 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -1,22 +1,17 @@ #!/usr/bin/env python3 -import datetime as dt -from time import sleep -from typing import ( - cast, - List, - Optional, -) +# Standard library imports from os.path import basename - +from time import sleep +from typing import cast, List, Optional import argparse +import datetime as dt import os import subprocess import sys import threading -import pipeclient -from pygame import mixer +# PyQt imports from PyQt6.QtCore import ( pyqtSignal, QDate, @@ -49,8 +44,14 @@ from PyQt6.QtWidgets import ( QProgressBar, QPushButton, ) + +# Third party imports +from pygame import mixer +import pipeclient +from sqlalchemy.orm import scoped_session import stackprinter # type: ignore +# App imports from classes import ( track_sequence, FadeCurve, @@ -58,23 +59,19 @@ from classes import ( PlaylistTrack, ) from config import Config -from dbconfig import ( - engine, - scoped_session, - Session, -) +from dbtables import db from dialogs import TrackSelectDialog from log import log -from models import Base, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks +from models import Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks from playlistmodel import PlaylistModel, PlaylistProxyModel from playlists import PlaylistTab +from ui import icons_rc # noqa F401 from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore from ui.downloadcsv_ui import Ui_DateSelect # type: ignore from ui.main_window_ui import Ui_MainWindow # type: ignore from utilities import check_db, update_bitrates import helpers -from ui import icons_rc # noqa F401 import music @@ -168,7 +165,7 @@ class ImportTrack(QObject): Create track objects from passed files and add to visible playlist """ - with Session() as session: + with db.Session() as session: for fname in self.filenames: self.signals.status_message_signal.emit( f"Importing {basename(fname)}", 5000 @@ -255,7 +252,7 @@ class Window(QMainWindow, Ui_MainWindow): except subprocess.CalledProcessError as exc_info: git_tag = str(exc_info.output) - with Session() as session: + with db.Session() as session: if session.bind: dbname = session.bind.engine.url.database @@ -317,7 +314,7 @@ class Window(QMainWindow, Ui_MainWindow): def cart_edit(self, btn: CartButton, event: QEvent): """Handle context menu for cart button""" - with Session() as session: + with db.Session() as session: cart = session.query(Carts).get(btn.cart_id) if cart is None: log.error("cart_edit: cart not found") @@ -349,7 +346,7 @@ class Window(QMainWindow, Ui_MainWindow): def carts_init(self) -> None: """Initialse carts data structures""" - with Session() as session: + with db.Session() as session: # Number carts from 1 for humanity for cart_number in range(1, Config.CARTS_COUNT + 1): cart = session.query(Carts).get(cart_number) @@ -426,7 +423,7 @@ class Window(QMainWindow, Ui_MainWindow): self, "Track playing", "Can't close application while track is playing" ) else: - with Session() as session: + with db.Session() as session: settings = Settings.all_as_dict(session) record = settings["mainwindow_height"] if record.f_int != self.height(): @@ -495,7 +492,7 @@ class Window(QMainWindow, Ui_MainWindow): return False # Record playlist as closed and update remaining playlist tabs - with Session() as session: + with db.Session() as session: playlist = session.get(Playlists, closing_tab_playlist_id) if playlist: playlist.close() @@ -588,7 +585,7 @@ class Window(QMainWindow, Ui_MainWindow): def create_and_show_playlist(self) -> None: """Create new playlist and display it""" - with Session() as session: + with db.Session() as session: playlist = self.create_playlist(session) if playlist: self.create_playlist_tab(playlist) @@ -636,7 +633,7 @@ class Window(QMainWindow, Ui_MainWindow): Delete current playlist """ - with Session() as session: + with db.Session() as session: playlist_id = self.active_tab().playlist_id playlist = session.get(Playlists, playlist_id) if playlist: @@ -670,7 +667,7 @@ class Window(QMainWindow, Ui_MainWindow): path += ".csv" with open(path, "w") as f: - with Session() as session: + with db.Session() as session: for playdate in Playdates.played_after(session, start_dt): f.write(f"{playdate.track.artist},{playdate.track.title}\n") @@ -702,7 +699,7 @@ class Window(QMainWindow, Ui_MainWindow): playlist_id = self.active_tab().playlist_id - with Session() as session: + with db.Session() as session: # Get output filename playlist = session.get(Playlists, playlist_id) if not playlist: @@ -784,7 +781,7 @@ class Window(QMainWindow, Ui_MainWindow): if not dlg.exec(): return - with Session() as session: + with db.Session() as session: new_tracks = [] for fname in dlg.selectedFiles(): txt = "" @@ -883,7 +880,7 @@ class Window(QMainWindow, Ui_MainWindow): self.active_tab().source_model_selected_row_number() or self.active_proxy_model().rowCount() ) - with Session() as session: + with db.Session() as session: dlg = TrackSelectDialog( session=session, new_row_number=new_row_number, @@ -895,7 +892,7 @@ class Window(QMainWindow, Ui_MainWindow): """Load the playlists that were open when the last session closed""" playlist_ids = [] - with Session() as session: + with db.Session() as session: for playlist in Playlists.get_open(session): if playlist: _ = self.create_playlist_tab(playlist) @@ -952,7 +949,7 @@ class Window(QMainWindow, Ui_MainWindow): visible_tab = self.active_tab() source_playlist_id = visible_tab.playlist_id - with Session() as session: + with db.Session() as session: for playlist in Playlists.get_all(session): if playlist.id == source_playlist_id: continue @@ -1005,7 +1002,7 @@ class Window(QMainWindow, Ui_MainWindow): def new_from_template(self) -> None: """Create new playlist from template""" - with Session() as session: + with db.Session() as session: templates = Playlists.get_all_templates(session) dlg = SelectPlaylistDialog(self, playlists=templates, session=session) dlg.exec() @@ -1031,7 +1028,7 @@ class Window(QMainWindow, Ui_MainWindow): def open_playlist(self) -> None: """Open existing playlist""" - with Session() as session: + with db.Session() as session: playlists = Playlists.get_closed(session) dlg = SelectPlaylistDialog(self, playlists=playlists, session=session) dlg.exec() @@ -1193,7 +1190,7 @@ class Window(QMainWindow, Ui_MainWindow): Rename current playlist """ - with Session() as session: + with db.Session() as session: playlist_id = self.active_tab().playlist_id playlist = session.get(Playlists, playlist_id) if playlist: @@ -1242,7 +1239,7 @@ class Window(QMainWindow, Ui_MainWindow): def save_as_template(self) -> None: """Save current playlist as template""" - with Session() as session: + with db.Session() as session: template_names = [a.name for a in Playlists.get_all_templates(session)] while True: @@ -1304,7 +1301,7 @@ class Window(QMainWindow, Ui_MainWindow): def set_main_window_size(self) -> None: """Set size of window from database""" - with Session() as session: + with db.Session() as session: settings = Settings.all_as_dict(session) record = settings["mainwindow_x"] x = record.f_int or 1 @@ -1751,18 +1748,15 @@ if __name__ == "__main__": # Run as required if args.check_db: - log.debug("Updating database") - with Session() as session: + log.debug("Checking database") + with db.Session() as session: check_db(session) - engine.dispose() elif args.update_bitrates: log.debug("Update bitrates") - with Session() as session: + with db.Session() as session: update_bitrates(session) - engine.dispose() else: try: - Base.metadata.create_all(engine) app = QApplication(sys.argv) # PyQt6 defaults to a grey for labels palette = app.palette() @@ -1780,7 +1774,6 @@ if __name__ == "__main__": win = Window() win.show() status = app.exec() - engine.dispose() sys.exit(status) except Exception as exc: if os.environ["MM_ENV"] == "PRODUCTION": diff --git a/app/playlistmodel.py b/app/playlistmodel.py index a6467b8..e73715c 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -1,5 +1,14 @@ +# Standard library imports # Allow forward reference to PlaylistModel from __future__ import annotations + +# PyQt imports + +# Third party imports + +# App imports +from dbtables import db + import obsws_python as obs # type: ignore import re import datetime as dt @@ -25,7 +34,6 @@ from PyQt6.QtGui import ( from classes import track_sequence, MusicMusterSignals, PlaylistTrack from config import Config -from dbconfig import scoped_session, Session from helpers import ( file_is_unreadable, get_embedded_time, @@ -129,7 +137,7 @@ class PlaylistModel(QAbstractTableModel): self.signals.end_reset_model_signal.connect(self.end_reset_model) self.signals.row_order_changed_signal.connect(self.row_order_changed) - with Session() as session: + with db.Session() as session: # Ensure row numbers in playlist are contiguous PlaylistRows.fixup_rownumbers(session, playlist_id) # Populate self.playlist_rows @@ -165,7 +173,7 @@ class PlaylistModel(QAbstractTableModel): "Header row already has track associated" ) return - with Session() as session: + with db.Session() as session: plr = session.get(PlaylistRows, prd.plrid) if plr: # Add track to PlaylistRows @@ -187,7 +195,7 @@ class PlaylistModel(QAbstractTableModel): # Header row if self.is_header_row(row): # Check for specific header colouring - with Session() as session: + with db.Session() as session: note_colour = NoteColours.get_colour(session, prd.note) if note_colour: return QBrush(QColor(note_colour)) @@ -216,7 +224,7 @@ class PlaylistModel(QAbstractTableModel): return QBrush(QColor(Config.COLOUR_BITRATE_OK)) if column == Col.NOTE.value: if prd.note: - with Session() as session: + with db.Session() as session: note_colour = NoteColours.get_colour(session, prd.note) if note_colour: return QBrush(QColor(note_colour)) @@ -275,7 +283,7 @@ class PlaylistModel(QAbstractTableModel): log.debug("Call OBS scene change") self.obs_scene_change(row_number) - with Session() as session: + with db.Session() as session: # Update Playdates in database log.debug("update playdates") Playdates(session, track_sequence.now.track_id) @@ -377,7 +385,7 @@ class PlaylistModel(QAbstractTableModel): Delete from highest row back so that not yet deleted row numbers don't change. """ - with Session() as session: + with db.Session() as session: for row_number in sorted(row_numbers, reverse=True): log.info(f"delete_rows(), {row_number=}") super().beginRemoveRows(QModelIndex(), row_number, row_number) @@ -454,7 +462,7 @@ class PlaylistModel(QAbstractTableModel): if playlist_id != self.playlist_id: log.debug(f"end_reset_model: not us ({self.playlist_id=})") return - with Session() as session: + with db.Session() as session: self.refresh_data(session) super().endResetModel() self.reset_track_sequence_row_numbers() @@ -754,7 +762,7 @@ class PlaylistModel(QAbstractTableModel): new_row_number = self._get_new_row_number(proposed_row_number) - with Session() as session: + with db.Session() as session: super().beginInsertRows(QModelIndex(), new_row_number, new_row_number) plr = PlaylistRows.insert_row(session, self.playlist_id, new_row_number) @@ -820,7 +828,7 @@ class PlaylistModel(QAbstractTableModel): Mark row as unplayed """ - with Session() as session: + with db.Session() as session: for row_number in row_numbers: plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid) if not plr: @@ -883,7 +891,7 @@ class PlaylistModel(QAbstractTableModel): plrid = self.playlist_rows[oldrow].plrid sqla_map.append({"plrid": plrid, "plr_rownum": newrow}) - with Session() as session: + with db.Session() as session: PlaylistRows.update_plr_rownumbers(session, self.playlist_id, sqla_map) # Update playlist_rows self.refresh_data(session) @@ -912,7 +920,7 @@ class PlaylistModel(QAbstractTableModel): # Prepare destination playlist for a reset self.signals.begin_reset_model_signal.emit(to_playlist_id) - with Session() as session: + with db.Session() as session: # Make room in destination playlist max_destination_row_number = PlaylistRows.get_last_used_row( session, to_playlist_id @@ -962,7 +970,7 @@ class PlaylistModel(QAbstractTableModel): log.info(f"move_track_add_note({new_row_number=}, {existing_prd=}, {note=}") if note: - with Session() as session: + with db.Session() as session: plr = session.get(PlaylistRows, existing_prd.plrid) if plr: if plr.note: @@ -1050,7 +1058,7 @@ class PlaylistModel(QAbstractTableModel): # Update display self.invalidate_row(track_sequence.previous.plr_rownum) - def refresh_data(self, session: scoped_session): + def refresh_data(self, session: db.session): """Populate dicts for data calls""" # Populate self.playlist_rows with playlist data @@ -1071,7 +1079,7 @@ class PlaylistModel(QAbstractTableModel): log.info(f"remove_track({row_number=})") - with Session() as session: + with db.Session() as session: plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid) if plr: plr.track_id = None @@ -1085,7 +1093,7 @@ class PlaylistModel(QAbstractTableModel): track_id = self.playlist_rows[row_number].track_id if track_id: - with Session() as session: + with db.Session() as session: track = session.get(Tracks, track_id) set_track_metadata(track) self.refresh_row(session, row_number) @@ -1101,7 +1109,7 @@ class PlaylistModel(QAbstractTableModel): # Check the track_sequence next, now and previous plrs and # update the row number - with Session() as session: + with db.Session() as session: if track_sequence.next.plr_rownum: next_plr = session.get(PlaylistRows, track_sequence.next.plr_id) if next_plr: @@ -1206,7 +1214,7 @@ class PlaylistModel(QAbstractTableModel): return # Update track_sequence - with Session() as session: + with db.Session() as session: track_sequence.next = PlaylistTrack() try: plrid = self.playlist_rows[row_number].plrid @@ -1246,7 +1254,7 @@ class PlaylistModel(QAbstractTableModel): row_number = index.row() column = index.column() - with Session() as session: + with db.Session() as session: plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid) if not plr: print( @@ -1455,7 +1463,7 @@ class PlaylistProxyModel(QSortFilterProxyModel): if self.source_model.played_tracks_hidden: if self.source_model.is_played_row(source_row): # Don't hide current or next track - with Session() as session: + with db.Session() as session: if track_sequence.next.plr_id: next_plr = session.get(PlaylistRows, track_sequence.next.plr_id) if ( diff --git a/app/playlists.py b/app/playlists.py index 170fc27..d9d25b2 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -1,8 +1,9 @@ +# Standard library imports +from typing import Callable, cast, List, Optional, TYPE_CHECKING import psutil import time -from pprint import pprint -from typing import Callable, cast, List, Optional, TYPE_CHECKING +# PyQt imports from PyQt6.QtCore import ( QEvent, QModelIndex, @@ -30,10 +31,13 @@ from PyQt6.QtWidgets import ( QStyleOption, ) -from dbconfig import Session -from dialogs import TrackSelectDialog +# Third party imports + +# App imports from classes import MusicMusterSignals, track_sequence from config import Config +from dbtables import db +from dialogs import TrackSelectDialog from helpers import ( ask_yes_no, ms_to_mmss, @@ -42,10 +46,9 @@ from helpers import ( ) from log import log from models import Settings - +from playlistmodel import PlaylistModel, PlaylistProxyModel if TYPE_CHECKING: from musicmuster import Window -from playlistmodel import PlaylistModel, PlaylistProxyModel class EscapeDelegate(QStyledItemDelegate): @@ -335,7 +338,7 @@ class PlaylistTab(QTableView): if model_row_number is None: return - with Session() as session: + with db.Session() as session: dlg = TrackSelectDialog( session=session, new_row_number=model_row_number, @@ -536,7 +539,7 @@ class PlaylistTab(QTableView): # Resize rows if necessary self.resizeRowsToContents() - with Session() as session: + with db.Session() as session: attr_name = f"playlist_col_{column_number}_width" record = Settings.get_int_settings(session, attr_name) record.f_int = self.columnWidth(column_number) @@ -830,7 +833,7 @@ class PlaylistTab(QTableView): return # Last column is set to stretch so ignore it here - with Session() as session: + with db.Session() as session: for column_number in range(header.count() - 1): attr_name = f"playlist_col_{column_number}_width" record = Settings.get_int_settings(session, attr_name) diff --git a/poetry.lock b/poetry.lock index 6a5cd67..23d7a09 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,6 +12,24 @@ files = [ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +[[package]] +name = "alchemical" +version = "1.0.1" +description = "Modern SQLAlchemy simplified" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "alchemical-1.0.1-py3-none-any.whl", hash = "sha256:79a98d459ed4f8afa8ed44b21d4d2ccf3586c76d73f53f647a9338aaba2bb33c"}, + {file = "alchemical-1.0.1.tar.gz", hash = "sha256:cff882cfef9533a56c53aa6bb38d5fa19939ea283226017c7c9369b73422200a"}, +] + +[package.dependencies] +sqlalchemy = ">=1.4.24" + +[package.extras] +docs = ["sphinx"] + [[package]] name = "alembic" version = "1.13.1" @@ -2273,4 +2291,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "500cefc31e30cba9ae917cc51b7407961d69825d1fcae53515ed1fa12f4ab171" +content-hash = "f4fb2696ae984283c4c0d7816ba7cbd7be714695d6eb3c84b5da62b3809f9c82" diff --git a/pyproject.toml b/pyproject.toml index d8840a4..3283311 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ pyqt6-webengine = "^6.5.0" pygame = "^2.4.0" pyqtgraph = "^0.13.3" colorlog = "^6.8.0" +alchemical = "^1.0.1" [tool.poetry.dev-dependencies] ipdb = "^0.13.9"