227 lines
7.7 KiB
Python
227 lines
7.7 KiB
Python
# 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, unique=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"<NoteColours(id={self.id}, substring={self.substring}, "
|
|
f"colour={self.colour}>"
|
|
)
|
|
|
|
|
|
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"<Playdates(id={self.id}, track_id={self.track_id} "
|
|
f"lastplayed={self.lastplayed}>"
|
|
)
|
|
|
|
|
|
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"<Playlists(id={self.id}, name={self.name}, "
|
|
f"is_templatee={self.is_template}, open={self.open}>"
|
|
)
|
|
|
|
|
|
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"<PlaylistRows(id={self.id}, playlist_id={self.playlist_id}, "
|
|
f"track_id={self.track_id}, "
|
|
f"note={self.note}, row_number={self.row_number}>"
|
|
)
|
|
|
|
|
|
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"<QueriesTable(id={self.id}, name={self.name}, filter={self.filter})>"
|
|
|
|
|
|
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"<Settings(id={self.id}, name={self.name}, "
|
|
f"f_datetime={self.f_datetime}, f_int={self.f_int}, f_string={self.f_string}>"
|
|
)
|
|
|
|
|
|
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"<Track(id={self.id}, title={self.title}, "
|
|
f"artist={self.artist}, path={self.path}>"
|
|
)
|