Compare commits
2 Commits
b4f5d92f5d
...
9e1995be68
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e1995be68 | ||
|
|
2abb672142 |
@ -23,27 +23,7 @@ from PyQt6.QtWidgets import (
|
|||||||
# App imports
|
# App imports
|
||||||
|
|
||||||
|
|
||||||
class Col(Enum):
|
# Define singleton first as it's needed below
|
||||||
START_GAP = 0
|
|
||||||
TITLE = auto()
|
|
||||||
ARTIST = auto()
|
|
||||||
INTRO = auto()
|
|
||||||
DURATION = auto()
|
|
||||||
START_TIME = auto()
|
|
||||||
END_TIME = auto()
|
|
||||||
LAST_PLAYED = auto()
|
|
||||||
BITRATE = auto()
|
|
||||||
NOTE = auto()
|
|
||||||
|
|
||||||
|
|
||||||
class QueryCol(Enum):
|
|
||||||
TITLE = 0
|
|
||||||
ARTIST = auto()
|
|
||||||
DURATION = auto()
|
|
||||||
LAST_PLAYED = auto()
|
|
||||||
BITRATE = auto()
|
|
||||||
|
|
||||||
|
|
||||||
def singleton(cls):
|
def singleton(cls):
|
||||||
"""
|
"""
|
||||||
Make a class a Singleton class (see
|
Make a class a Singleton class (see
|
||||||
@ -66,6 +46,33 @@ def singleton(cls):
|
|||||||
return wrapper_singleton
|
return wrapper_singleton
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationError(Exception):
|
||||||
|
"""
|
||||||
|
Custom exception
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AudioMetadata(NamedTuple):
|
||||||
|
start_gap: int = 0
|
||||||
|
silence_at: int = 0
|
||||||
|
fade_at: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class Col(Enum):
|
||||||
|
START_GAP = 0
|
||||||
|
TITLE = auto()
|
||||||
|
ARTIST = auto()
|
||||||
|
INTRO = auto()
|
||||||
|
DURATION = auto()
|
||||||
|
START_TIME = auto()
|
||||||
|
END_TIME = auto()
|
||||||
|
LAST_PLAYED = auto()
|
||||||
|
BITRATE = auto()
|
||||||
|
NOTE = auto()
|
||||||
|
|
||||||
|
|
||||||
class FileErrors(NamedTuple):
|
class FileErrors(NamedTuple):
|
||||||
path: str
|
path: str
|
||||||
error: str
|
error: str
|
||||||
@ -82,20 +89,6 @@ class Filter:
|
|||||||
duration_unit: str = "minutes"
|
duration_unit: str = "minutes"
|
||||||
|
|
||||||
|
|
||||||
class ApplicationError(Exception):
|
|
||||||
"""
|
|
||||||
Custom exception
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AudioMetadata(NamedTuple):
|
|
||||||
start_gap: int = 0
|
|
||||||
silence_at: int = 0
|
|
||||||
fade_at: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
@singleton
|
@singleton
|
||||||
@dataclass
|
@dataclass
|
||||||
class MusicMusterSignals(QObject):
|
class MusicMusterSignals(QObject):
|
||||||
@ -142,6 +135,14 @@ class PlaylistStyle(QProxyStyle):
|
|||||||
super().drawPrimitive(element, option, painter, widget)
|
super().drawPrimitive(element, option, painter, widget)
|
||||||
|
|
||||||
|
|
||||||
|
class QueryCol(Enum):
|
||||||
|
TITLE = 0
|
||||||
|
ARTIST = auto()
|
||||||
|
DURATION = auto()
|
||||||
|
LAST_PLAYED = auto()
|
||||||
|
BITRATE = auto()
|
||||||
|
|
||||||
|
|
||||||
class Tags(NamedTuple):
|
class Tags(NamedTuple):
|
||||||
artist: str = ""
|
artist: str = ""
|
||||||
title: str = ""
|
title: str = ""
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
# Standard library imports
|
# Standard library imports
|
||||||
|
import sys
|
||||||
|
|
||||||
# PyQt imports
|
# PyQt imports
|
||||||
|
|
||||||
@ -8,6 +9,12 @@ from alchemical import Alchemical # type:ignore
|
|||||||
# App imports
|
# App imports
|
||||||
|
|
||||||
|
|
||||||
|
def is_alembic_command():
|
||||||
|
# Define keywords that indicate Alembic is being invoked.
|
||||||
|
alembic_keywords = {'alembic', 'revision', 'upgrade', 'downgrade', 'history', 'current'}
|
||||||
|
return any(arg in alembic_keywords for arg in sys.argv)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseManager:
|
class DatabaseManager:
|
||||||
"""
|
"""
|
||||||
Singleton class to ensure we only ever have one db object
|
Singleton class to ensure we only ever have one db object
|
||||||
@ -18,7 +25,8 @@ class DatabaseManager:
|
|||||||
def __init__(self, database_url: str, **kwargs: dict) -> None:
|
def __init__(self, database_url: str, **kwargs: dict) -> None:
|
||||||
if DatabaseManager.__instance is None:
|
if DatabaseManager.__instance is None:
|
||||||
self.db = Alchemical(database_url, **kwargs)
|
self.db = Alchemical(database_url, **kwargs)
|
||||||
self.db.create_all()
|
if not is_alembic_command():
|
||||||
|
self.db.create_all()
|
||||||
DatabaseManager.__instance = self
|
DatabaseManager.__instance = self
|
||||||
else:
|
else:
|
||||||
raise Exception("Attempted to create a second DatabaseManager instance")
|
raise Exception("Attempted to create a second DatabaseManager instance")
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
# Standard library imports
|
# Standard library imports
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
import json
|
||||||
|
|
||||||
# PyQt imports
|
# PyQt imports
|
||||||
|
|
||||||
@ -18,8 +19,30 @@ from sqlalchemy.orm import (
|
|||||||
mapped_column,
|
mapped_column,
|
||||||
relationship,
|
relationship,
|
||||||
)
|
)
|
||||||
|
from sqlalchemy.types import TypeDecorator, TEXT
|
||||||
|
|
||||||
# App imports
|
# 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, dialect):
|
||||||
|
"""Convert Python dictionary to JSON string before saving."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return json.dumps(value)
|
||||||
|
|
||||||
|
def process_result_value(self, value, dialect):
|
||||||
|
"""Convert JSON string back to Python dictionary after retrieval."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return json.loads(value)
|
||||||
|
|
||||||
|
|
||||||
# Database classes
|
# Database classes
|
||||||
@ -48,7 +71,7 @@ class PlaydatesTable(Model):
|
|||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
lastplayed: Mapped[dt.datetime] = mapped_column(index=True)
|
lastplayed: Mapped[dt.datetime] = mapped_column(index=True)
|
||||||
track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id"))
|
track_id: Mapped[int] = mapped_column(ForeignKey("tracks.id", ondelete="CASCADE"))
|
||||||
track: Mapped["TracksTable"] = relationship(
|
track: Mapped["TracksTable"] = relationship(
|
||||||
"TracksTable",
|
"TracksTable",
|
||||||
back_populates="playdates",
|
back_populates="playdates",
|
||||||
@ -104,7 +127,9 @@ class PlaylistRowsTable(Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows")
|
playlist: Mapped[PlaylistsTable] = relationship(back_populates="rows")
|
||||||
track_id: Mapped[Optional[int]] = mapped_column(ForeignKey("tracks.id", ondelete="CASCADE"))
|
track_id: Mapped[Optional[int]] = mapped_column(
|
||||||
|
ForeignKey("tracks.id", ondelete="CASCADE")
|
||||||
|
)
|
||||||
track: Mapped["TracksTable"] = relationship(
|
track: Mapped["TracksTable"] = relationship(
|
||||||
"TracksTable",
|
"TracksTable",
|
||||||
back_populates="playlistrows",
|
back_populates="playlistrows",
|
||||||
@ -126,15 +151,13 @@ class QueriesTable(Model):
|
|||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
sql: Mapped[str] = mapped_column(
|
filter: Mapped[Filter] = mapped_column(JSONEncodedDict, nullable=True)
|
||||||
String(2048), index=False, default="", nullable=False
|
|
||||||
)
|
|
||||||
favourite: Mapped[bool] = mapped_column(
|
favourite: Mapped[bool] = mapped_column(
|
||||||
Boolean, nullable=False, index=False, default=False
|
Boolean, nullable=False, index=False, default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Queries(id={self.id}, name={self.name}, sql={self.sql}>"
|
return f"<Queries(id={self.id}, name={self.name}, filter={self.filter}>"
|
||||||
|
|
||||||
|
|
||||||
class SettingsTable(Model):
|
class SettingsTable(Model):
|
||||||
|
|||||||
@ -253,10 +253,7 @@ class Playlists(dbtables.PlaylistsTable):
|
|||||||
|
|
||||||
return session.scalars(
|
return session.scalars(
|
||||||
select(cls)
|
select(cls)
|
||||||
.where(
|
.where(cls.is_template.is_(True), cls.favourite.is_(True))
|
||||||
cls.is_template.is_(True),
|
|
||||||
cls.favourite.is_(True)
|
|
||||||
)
|
|
||||||
.order_by(cls.name)
|
.order_by(cls.name)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
@ -608,6 +605,19 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
|
|||||||
session.connection().execute(stmt, sqla_map)
|
session.connection().execute(stmt, sqla_map)
|
||||||
|
|
||||||
|
|
||||||
|
class Queries(dbtables.QueriesTable):
|
||||||
|
def __init__(
|
||||||
|
self, session: Session, name: str, filter: dbtables.Filter, favourite: bool = False
|
||||||
|
) -> None:
|
||||||
|
"""Create new query"""
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
self.filter = filter
|
||||||
|
self.favourite = favourite
|
||||||
|
session.add(self)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
class Settings(dbtables.SettingsTable):
|
class Settings(dbtables.SettingsTable):
|
||||||
def __init__(self, session: Session, name: str) -> None:
|
def __init__(self, session: Session, name: str) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|||||||
@ -751,10 +751,11 @@ class Window(QMainWindow):
|
|||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
submenu_items: list[dict[str, str | tuple[Session, int]]] = [
|
submenu_items: list[dict[str, str | tuple[Session, int]]] = [
|
||||||
{"text": "Show all",
|
{
|
||||||
"handler": "create_playlist_from_template",
|
"text": "Show all",
|
||||||
"args": (session, 0)
|
"handler": "create_playlist_from_template",
|
||||||
}
|
"args": (session, 0),
|
||||||
|
}
|
||||||
]
|
]
|
||||||
templates = Playlists.get_favourite_templates(session)
|
templates = Playlists.get_favourite_templates(session)
|
||||||
for template in templates:
|
for template in templates:
|
||||||
@ -2199,7 +2200,12 @@ class Window(QMainWindow):
|
|||||||
self.playlist_section.tabPlaylist.setTabIcon(
|
self.playlist_section.tabPlaylist.setTabIcon(
|
||||||
idx, QIcon(Config.PLAYLIST_ICON_CURRENT)
|
idx, QIcon(Config.PLAYLIST_ICON_CURRENT)
|
||||||
)
|
)
|
||||||
elif self.playlist_section.tabPlaylist.widget(idx).model().sourceModel().is_template:
|
elif (
|
||||||
|
self.playlist_section.tabPlaylist.widget(idx)
|
||||||
|
.model()
|
||||||
|
.sourceModel()
|
||||||
|
.is_template
|
||||||
|
):
|
||||||
self.playlist_section.tabPlaylist.setTabIcon(
|
self.playlist_section.tabPlaylist.setTabIcon(
|
||||||
idx, QIcon(Config.PLAYLIST_ICON_TEMPLATE)
|
idx, QIcon(Config.PLAYLIST_ICON_TEMPLATE)
|
||||||
)
|
)
|
||||||
|
|||||||
55
migrations/versions/335cae31045f_create_queries_table.py
Normal file
55
migrations/versions/335cae31045f_create_queries_table.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
"""create queries table
|
||||||
|
|
||||||
|
Revision ID: 335cae31045f
|
||||||
|
Revises: f14dd379850f
|
||||||
|
Create Date: 2025-02-24 20:43:41.534922
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import dbtables
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '335cae31045f'
|
||||||
|
down_revision = 'f14dd379850f'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(engine_name: str) -> None:
|
||||||
|
globals()["upgrade_%s" % engine_name]()
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(engine_name: str) -> None:
|
||||||
|
globals()["downgrade_%s" % engine_name]()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade_() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('queries',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=128), nullable=False),
|
||||||
|
sa.Column('filter', dbtables.JSONEncodedDict(), nullable=True),
|
||||||
|
sa.Column('favourite', sa.Boolean(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('playdates', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint('fk_playdates_track_id_tracks', type_='foreignkey')
|
||||||
|
batch_op.create_foreign_key(None, 'tracks', ['track_id'], ['id'], ondelete='CASCADE')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade_() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('playdates', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint(None, type_='foreignkey')
|
||||||
|
batch_op.create_foreign_key('fk_playdates_track_id_tracks', 'tracks', ['track_id'], ['id'])
|
||||||
|
|
||||||
|
op.drop_table('queries')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user