# Standard library imports from typing import Optional from dataclasses import asdict import datetime as dt import json # PyQt imports # Third party imports from alchemical import Model # type: ignore from sqlalchemy import ( Boolean, DateTime, ForeignKey, String, ) from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.engine.interfaces import Dialect from sqlalchemy.orm import ( Mapped, mapped_column, relationship, ) from sqlalchemy.types import TypeDecorator, TEXT # App imports from classes import Filter class JSONEncodedDict(TypeDecorator): """ Custom JSON Type for MariaDB (since native JSON type is just LONGTEXT) """ impl = TEXT def process_bind_param(self, value: dict | None, dialect: Dialect) -> str | None: """Convert Python dictionary to JSON string before saving.""" if value is None: return None return json.dumps(value, default=lambda o: o.__dict__) def process_result_value(self, value: str | None, dialect: Dialect) -> dict | None: """Convert JSON string back to Python dictionary after retrieval.""" if value is None: return None return json.loads(value) # Database classes class NoteColoursTable(Model): __tablename__ = "notecolours" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) substring: Mapped[str] = mapped_column(String(256), index=True) colour: Mapped[str] = mapped_column(String(21), index=False) enabled: Mapped[bool] = mapped_column(default=True, index=True) foreground: Mapped[Optional[str]] = mapped_column(String(21), index=False) 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) strip_substring: Mapped[bool] = mapped_column(default=True, index=False) 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", ondelete="CASCADE")) track: Mapped["TracksTable"] = relationship( "TracksTable", 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) rows: Mapped[list["PlaylistRowsTable"]] = relationship( "PlaylistRowsTable", back_populates="playlist", cascade="all, delete-orphan", order_by="PlaylistRowsTable.row_number", ) favourite: Mapped[bool] = mapped_column( Boolean, nullable=False, index=False, default=False ) def __repr__(self) -> str: return ( f"" ) class PlaylistRowsTable(Model): __tablename__ = "playlist_rows" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) row_number: Mapped[int] = mapped_column(index=True) note: Mapped[str] = mapped_column( String(2048), index=False, default="", nullable=False ) playlist_id: Mapped[int] = mapped_column( ForeignKey("playlists.id", ondelete="CASCADE"), index=True ) playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows") track_id: Mapped[Optional[int]] = mapped_column( ForeignKey("tracks.id", ondelete="CASCADE") ) track: Mapped["TracksTable"] = relationship( "TracksTable", back_populates="playlistrows", ) played: Mapped[bool] = mapped_column( Boolean, nullable=False, index=False, default=False ) def __repr__(self) -> str: return ( f"" ) class QueriesTable(Model): __tablename__ = "queries" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(128), nullable=False) _filter_data: Mapped[dict | None] = mapped_column("filter_data", JSONEncodedDict, nullable=False) favourite: Mapped[bool] = mapped_column(Boolean, nullable=False, index=False, default=False) def _get_filter(self) -> Filter: """Convert stored JSON dictionary to a Filter object.""" if isinstance(self._filter_data, dict): return Filter(**self._filter_data) return Filter() # Default object if None or invalid data def _set_filter(self, value: Filter | None) -> None: """Convert a Filter object to JSON before storing.""" self._filter_data = asdict(value) if isinstance(value, Filter) else None # Single definition of `filter` filter = property(_get_filter, _set_filter) 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) artist: Mapped[str] = mapped_column(String(256), index=True) bitrate: Mapped[int] = mapped_column(default=None) duration: Mapped[int] = mapped_column(index=True) fade_at: Mapped[int] = mapped_column(index=False) intro: Mapped[Optional[int]] = mapped_column(default=None) 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) title: Mapped[str] = mapped_column(String(256), index=True) playlistrows: Mapped[list[PlaylistRowsTable]] = relationship( "PlaylistRowsTable", back_populates="track", cascade="all, delete-orphan", ) playlists = association_proxy("playlistrows", "playlist") playdates: Mapped[list[PlaydatesTable]] = relationship( "PlaydatesTable", back_populates="track", cascade="all, delete-orphan", lazy="joined", ) def __repr__(self) -> str: return ( f"" )