Query searches working
More UI needed
This commit is contained in:
parent
7c0db00b75
commit
e6404d075e
@ -87,6 +87,7 @@ class Config(object):
|
|||||||
MAX_MISSING_FILES_TO_REPORT = 10
|
MAX_MISSING_FILES_TO_REPORT = 10
|
||||||
MILLISECOND_SIGFIGS = 0
|
MILLISECOND_SIGFIGS = 0
|
||||||
MINIMUM_ROW_HEIGHT = 30
|
MINIMUM_ROW_HEIGHT = 30
|
||||||
|
NO_QUERY_NAME = "Select query"
|
||||||
NO_TEMPLATE_NAME = "None"
|
NO_TEMPLATE_NAME = "None"
|
||||||
NOTE_TIME_FORMAT = "%H:%M"
|
NOTE_TIME_FORMAT = "%H:%M"
|
||||||
OBS_HOST = "localhost"
|
OBS_HOST = "localhost"
|
||||||
|
|||||||
@ -80,9 +80,6 @@ class PlaylistsTable(Model):
|
|||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
order_by="PlaylistRowsTable.row_number",
|
order_by="PlaylistRowsTable.row_number",
|
||||||
)
|
)
|
||||||
query: Mapped["QueriesTable"] = relationship(
|
|
||||||
back_populates="playlist", cascade="all, delete-orphan"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return (
|
||||||
@ -104,7 +101,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",
|
||||||
@ -125,16 +124,16 @@ class QueriesTable(Model):
|
|||||||
__tablename__ = "queries"
|
__tablename__ = "queries"
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
query: Mapped[str] = mapped_column(
|
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
sql: Mapped[str] = mapped_column(
|
||||||
String(2048), index=False, default="", nullable=False
|
String(2048), index=False, default="", nullable=False
|
||||||
)
|
)
|
||||||
playlist_id: Mapped[int] = mapped_column(
|
description: Mapped[str] = mapped_column(
|
||||||
ForeignKey("playlists.id", ondelete="CASCADE"), index=True
|
String(512), index=False, default="", nullable=False
|
||||||
)
|
)
|
||||||
playlist: Mapped[PlaylistsTable] = relationship(back_populates="query")
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Queries(id={self.id}, playlist={self.playlist}, query={self.query}>"
|
return f"<Queries(id={self.id}, name={self.name}, sql={self.sql}>"
|
||||||
|
|
||||||
|
|
||||||
class SettingsTable(Model):
|
class SettingsTable(Model):
|
||||||
|
|||||||
328
app/models.py
328
app/models.py
@ -19,7 +19,7 @@ from sqlalchemy import (
|
|||||||
)
|
)
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm.exc import NoResultFound
|
from sqlalchemy.orm.exc import NoResultFound
|
||||||
from sqlalchemy.orm import joinedload, selectinload
|
from sqlalchemy.orm import joinedload
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
@ -178,179 +178,6 @@ class Playdates(dbtables.PlaydatesTable):
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
|
||||||
class Playlists(dbtables.PlaylistsTable):
|
|
||||||
def __init__(self, session: Session, name: str):
|
|
||||||
self.name = name
|
|
||||||
self.last_used = dt.datetime.now()
|
|
||||||
session.add(self)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def clear_tabs(session: Session, playlist_ids: list[int]) -> None:
|
|
||||||
"""
|
|
||||||
Make all tab records NULL
|
|
||||||
"""
|
|
||||||
|
|
||||||
session.execute(
|
|
||||||
update(Playlists).where((Playlists.id.in_(playlist_ids))).values(tab=None)
|
|
||||||
)
|
|
||||||
|
|
||||||
def close(self, session: Session) -> None:
|
|
||||||
"""Mark playlist as unloaded"""
|
|
||||||
|
|
||||||
self.open = False
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_playlist_from_template(
|
|
||||||
cls, session: Session, template: "Playlists", playlist_name: str
|
|
||||||
) -> Optional["Playlists"]:
|
|
||||||
"""Create a new playlist from template"""
|
|
||||||
|
|
||||||
# Sanity check
|
|
||||||
if not template.id:
|
|
||||||
return None
|
|
||||||
|
|
||||||
playlist = cls(session, playlist_name)
|
|
||||||
|
|
||||||
# Sanity / mypy checks
|
|
||||||
if not playlist or not playlist.id:
|
|
||||||
return None
|
|
||||||
|
|
||||||
PlaylistRows.copy_playlist(session, template.id, playlist.id)
|
|
||||||
|
|
||||||
return playlist
|
|
||||||
|
|
||||||
def delete(self, session: Session) -> None:
|
|
||||||
"""
|
|
||||||
Delete playlist
|
|
||||||
"""
|
|
||||||
|
|
||||||
session.execute(delete(Playlists).where(Playlists.id == self.id))
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_all(cls, session: Session) -> Sequence["Playlists"]:
|
|
||||||
"""Returns a list of all playlists ordered by last use"""
|
|
||||||
|
|
||||||
return session.scalars(
|
|
||||||
select(cls)
|
|
||||||
.filter(
|
|
||||||
cls.is_template.is_(False),
|
|
||||||
~cls.query.has()
|
|
||||||
)
|
|
||||||
.order_by(cls.last_used.desc())
|
|
||||||
).all()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_all_queries(cls, session: Session) -> Sequence["Playlists"]:
|
|
||||||
"""Returns a list of all query lists ordered by name"""
|
|
||||||
|
|
||||||
return session.scalars(
|
|
||||||
select(cls)
|
|
||||||
.where(cls.query.has())
|
|
||||||
.order_by(cls.name)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_all_templates(cls, session: Session) -> Sequence["Playlists"]:
|
|
||||||
"""Returns a list of all templates ordered by name"""
|
|
||||||
|
|
||||||
return session.scalars(
|
|
||||||
select(cls).where(cls.is_template.is_(True)).order_by(cls.name)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_closed(cls, session: Session) -> Sequence["Playlists"]:
|
|
||||||
"""Returns a list of all closed playlists ordered by last use"""
|
|
||||||
|
|
||||||
return session.scalars(
|
|
||||||
select(cls)
|
|
||||||
.filter(
|
|
||||||
cls.open.is_(False),
|
|
||||||
cls.is_template.is_(False),
|
|
||||||
~cls.query.has()
|
|
||||||
)
|
|
||||||
.order_by(cls.last_used.desc())
|
|
||||||
).all()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_open(cls, session: Session) -> Sequence[Optional["Playlists"]]:
|
|
||||||
"""
|
|
||||||
Return a list of loaded playlists ordered by tab.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return session.scalars(
|
|
||||||
select(cls)
|
|
||||||
.where(
|
|
||||||
cls.open.is_(True),
|
|
||||||
~cls.query.has()
|
|
||||||
)
|
|
||||||
.order_by(cls.tab)
|
|
||||||
|
|
||||||
).all()
|
|
||||||
|
|
||||||
def mark_open(self) -> None:
|
|
||||||
"""Mark playlist as loaded and used now"""
|
|
||||||
|
|
||||||
self.open = True
|
|
||||||
self.last_used = dt.datetime.now()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def name_is_available(session: Session, name: str) -> bool:
|
|
||||||
"""
|
|
||||||
Return True if no playlist of this name exists else false.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return (
|
|
||||||
session.execute(select(Playlists).where(Playlists.name == name)).first()
|
|
||||||
is None
|
|
||||||
)
|
|
||||||
|
|
||||||
def rename(self, session: Session, new_name: str) -> None:
|
|
||||||
"""
|
|
||||||
Rename playlist
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.name = new_name
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def save_as_template(
|
|
||||||
session: Session, playlist_id: int, template_name: str
|
|
||||||
) -> None:
|
|
||||||
"""Save passed playlist as new template"""
|
|
||||||
|
|
||||||
template = Playlists(session, template_name)
|
|
||||||
if not template or not template.id:
|
|
||||||
return
|
|
||||||
|
|
||||||
template.is_template = True
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
PlaylistRows.copy_playlist(session, playlist_id, template.id)
|
|
||||||
|
|
||||||
|
|
||||||
class Queries(dbtables.QueriesTable):
|
|
||||||
def __init__(self, session: Session, playlist_id: int, query: str = "") -> None:
|
|
||||||
self.playlist_id = playlist_id
|
|
||||||
self.query = query
|
|
||||||
session.add(self)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_query(session: Session, playlist_id: int) -> str:
|
|
||||||
"""
|
|
||||||
Return query associated with playlist or null string if none
|
|
||||||
"""
|
|
||||||
|
|
||||||
return session.execute(
|
|
||||||
select(Queries.query).where(
|
|
||||||
Queries.playlist_id == playlist_id
|
|
||||||
)
|
|
||||||
).scalar_one()
|
|
||||||
|
|
||||||
|
|
||||||
class PlaylistRows(dbtables.PlaylistRowsTable):
|
class PlaylistRows(dbtables.PlaylistRowsTable):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -635,6 +462,159 @@ class PlaylistRows(dbtables.PlaylistRowsTable):
|
|||||||
session.connection().execute(stmt, sqla_map)
|
session.connection().execute(stmt, sqla_map)
|
||||||
|
|
||||||
|
|
||||||
|
class Playlists(dbtables.PlaylistsTable):
|
||||||
|
def __init__(self, session: Session, name: str):
|
||||||
|
self.name = name
|
||||||
|
self.last_used = dt.datetime.now()
|
||||||
|
session.add(self)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clear_tabs(session: Session, playlist_ids: list[int]) -> None:
|
||||||
|
"""
|
||||||
|
Make all tab records NULL
|
||||||
|
"""
|
||||||
|
|
||||||
|
session.execute(
|
||||||
|
update(Playlists).where((Playlists.id.in_(playlist_ids))).values(tab=None)
|
||||||
|
)
|
||||||
|
|
||||||
|
def close(self, session: Session) -> None:
|
||||||
|
"""Mark playlist as unloaded"""
|
||||||
|
|
||||||
|
self.open = False
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_playlist_from_template(
|
||||||
|
cls, session: Session, template: "Playlists", playlist_name: str
|
||||||
|
) -> Optional["Playlists"]:
|
||||||
|
"""Create a new playlist from template"""
|
||||||
|
|
||||||
|
# Sanity check
|
||||||
|
if not template.id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
playlist = cls(session, playlist_name)
|
||||||
|
|
||||||
|
# Sanity / mypy checks
|
||||||
|
if not playlist or not playlist.id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
PlaylistRows.copy_playlist(session, template.id, playlist.id)
|
||||||
|
|
||||||
|
return playlist
|
||||||
|
|
||||||
|
def delete(self, session: Session) -> None:
|
||||||
|
"""
|
||||||
|
Delete playlist
|
||||||
|
"""
|
||||||
|
|
||||||
|
session.execute(delete(Playlists).where(Playlists.id == self.id))
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all(cls, session: Session) -> Sequence["Playlists"]:
|
||||||
|
"""Returns a list of all playlists ordered by last use"""
|
||||||
|
|
||||||
|
return session.scalars(
|
||||||
|
select(cls)
|
||||||
|
.filter(cls.is_template.is_(False))
|
||||||
|
.order_by(cls.last_used.desc())
|
||||||
|
).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all_templates(cls, session: Session) -> Sequence["Playlists"]:
|
||||||
|
"""Returns a list of all templates ordered by name"""
|
||||||
|
|
||||||
|
return session.scalars(
|
||||||
|
select(cls).where(cls.is_template.is_(True)).order_by(cls.name)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_closed(cls, session: Session) -> Sequence["Playlists"]:
|
||||||
|
"""Returns a list of all closed playlists ordered by last use"""
|
||||||
|
|
||||||
|
return session.scalars(
|
||||||
|
select(cls)
|
||||||
|
.filter(cls.open.is_(False), cls.is_template.is_(False))
|
||||||
|
.order_by(cls.last_used.desc())
|
||||||
|
).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_open(cls, session: Session) -> Sequence[Optional["Playlists"]]:
|
||||||
|
"""
|
||||||
|
Return a list of loaded playlists ordered by tab.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return session.scalars(
|
||||||
|
select(cls)
|
||||||
|
.where(
|
||||||
|
cls.open.is_(True),
|
||||||
|
)
|
||||||
|
.order_by(cls.tab)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
def mark_open(self) -> None:
|
||||||
|
"""Mark playlist as loaded and used now"""
|
||||||
|
|
||||||
|
self.open = True
|
||||||
|
self.last_used = dt.datetime.now()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def name_is_available(session: Session, name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Return True if no playlist of this name exists else false.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
session.execute(select(Playlists).where(Playlists.name == name)).first()
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
|
||||||
|
def rename(self, session: Session, new_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Rename playlist
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.name = new_name
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_as_template(
|
||||||
|
session: Session, playlist_id: int, template_name: str
|
||||||
|
) -> None:
|
||||||
|
"""Save passed playlist as new template"""
|
||||||
|
|
||||||
|
template = Playlists(session, template_name)
|
||||||
|
if not template or not template.id:
|
||||||
|
return
|
||||||
|
|
||||||
|
template.is_template = True
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
PlaylistRows.copy_playlist(session, playlist_id, template.id)
|
||||||
|
|
||||||
|
|
||||||
|
class Queries(dbtables.QueriesTable):
|
||||||
|
def __init__(
|
||||||
|
self, session: Session, name: str, query: str, description: str = ""
|
||||||
|
) -> None:
|
||||||
|
self.query = query
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
session.add(self)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all(cls, session: Session) -> Sequence[Queries]:
|
||||||
|
"""
|
||||||
|
Return a list of all queries
|
||||||
|
"""
|
||||||
|
|
||||||
|
return session.scalars(select(cls)).unique().all()
|
||||||
|
|
||||||
|
|
||||||
class Settings(dbtables.SettingsTable):
|
class Settings(dbtables.SettingsTable):
|
||||||
def __init__(self, session: Session, name: str):
|
def __init__(self, session: Session, name: str):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|||||||
@ -28,6 +28,7 @@ from PyQt6.QtGui import (
|
|||||||
QShortcut,
|
QShortcut,
|
||||||
)
|
)
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
|
QAbstractItemView,
|
||||||
QApplication,
|
QApplication,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
QDialog,
|
QDialog,
|
||||||
@ -40,6 +41,8 @@ from PyQt6.QtWidgets import (
|
|||||||
QMainWindow,
|
QMainWindow,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
|
QSizePolicy,
|
||||||
|
QTableView,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
@ -60,13 +63,19 @@ from dialogs import TrackSelectDialog
|
|||||||
from file_importer import FileImporter
|
from file_importer import FileImporter
|
||||||
from helpers import file_is_unreadable
|
from helpers import file_is_unreadable
|
||||||
from log import log
|
from log import log
|
||||||
from models import db, Playdates, PlaylistRows, Playlists, Settings, Tracks
|
from models import (
|
||||||
|
db,
|
||||||
|
Playdates,
|
||||||
|
PlaylistRows,
|
||||||
|
Playlists,
|
||||||
|
Queries,
|
||||||
|
Settings,
|
||||||
|
Tracks,
|
||||||
|
)
|
||||||
from music_manager import RowAndTrack, track_sequence
|
from music_manager import RowAndTrack, track_sequence
|
||||||
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
||||||
from querylistmodel import QuerylistModel
|
from querylistmodel import QuerylistModel
|
||||||
from playlists import PlaylistTab
|
from playlists import PlaylistTab
|
||||||
from querylists import QuerylistTab
|
|
||||||
from ui import icons_rc # noqa F401
|
|
||||||
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore
|
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore
|
||||||
from ui.downloadcsv_ui import Ui_DateSelect # type: ignore
|
from ui.downloadcsv_ui import Ui_DateSelect # type: ignore
|
||||||
from ui.main_window_header_ui import Ui_HeaderSection # type: ignore
|
from ui.main_window_header_ui import Ui_HeaderSection # type: ignore
|
||||||
@ -271,6 +280,234 @@ class PreviewManager:
|
|||||||
self.start_time = None
|
self.start_time = None
|
||||||
|
|
||||||
|
|
||||||
|
class QueryDialog(QDialog):
|
||||||
|
"""Dialog box to handle selecting track from a SQL query"""
|
||||||
|
|
||||||
|
def __init__(self, session: Session) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
# Build a list of (query-name, playlist-id) tuples
|
||||||
|
self.selected_tracks: list[int] = []
|
||||||
|
|
||||||
|
self.query_list: list[tuple[str, int]] = []
|
||||||
|
self.query_list.append((Config.NO_QUERY_NAME, 0))
|
||||||
|
for query in Queries.get_all(self.session):
|
||||||
|
self.query_list.append((query.name, query.id))
|
||||||
|
|
||||||
|
self.setWindowTitle("Query Selector")
|
||||||
|
|
||||||
|
# Create label
|
||||||
|
query_label = QLabel("Query:")
|
||||||
|
|
||||||
|
# Top layout (Query label, combo box, and info label)
|
||||||
|
top_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
# Query label
|
||||||
|
query_label = QLabel("Query:")
|
||||||
|
top_layout.addWidget(query_label)
|
||||||
|
|
||||||
|
# Combo Box with fixed width
|
||||||
|
self.combo_box = QComboBox()
|
||||||
|
self.combo_box.setFixedWidth(150) # Adjust as necessary for 20 characters
|
||||||
|
for text, id_ in self.query_list:
|
||||||
|
self.combo_box.addItem(text, id_)
|
||||||
|
top_layout.addWidget(self.combo_box)
|
||||||
|
|
||||||
|
# Information label (two-row height, wrapping)
|
||||||
|
self.description_label = QLabel("")
|
||||||
|
self.description_label.setWordWrap(True)
|
||||||
|
self.description_label.setMinimumHeight(40) # Approximate height for two rows
|
||||||
|
self.description_label.setSizePolicy(
|
||||||
|
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
|
||||||
|
)
|
||||||
|
top_layout.addWidget(self.description_label)
|
||||||
|
|
||||||
|
# Table (middle part)
|
||||||
|
self.table_view = QTableView()
|
||||||
|
self.table_view.setSelectionMode(
|
||||||
|
QAbstractItemView.SelectionMode.ExtendedSelection
|
||||||
|
)
|
||||||
|
self.table_view.setSelectionBehavior(
|
||||||
|
QAbstractItemView.SelectionBehavior.SelectRows
|
||||||
|
)
|
||||||
|
self.table_view.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||||
|
self.table_view.setAlternatingRowColors(True)
|
||||||
|
self.table_view.setVerticalScrollMode(
|
||||||
|
QAbstractItemView.ScrollMode.ScrollPerPixel
|
||||||
|
)
|
||||||
|
self.table_view.clicked.connect(self.handle_row_click)
|
||||||
|
|
||||||
|
# Bottom layout (buttons)
|
||||||
|
bottom_layout = QHBoxLayout()
|
||||||
|
bottom_layout.addStretch() # Push buttons to the right
|
||||||
|
|
||||||
|
self.add_tracks_button = QPushButton("Add tracks")
|
||||||
|
self.add_tracks_button.setEnabled(False) # Disabled by default
|
||||||
|
self.add_tracks_button.clicked.connect(self.add_tracks_clicked)
|
||||||
|
bottom_layout.addWidget(self.add_tracks_button)
|
||||||
|
|
||||||
|
self.cancel_button = QPushButton("Cancel")
|
||||||
|
self.cancel_button.clicked.connect(self.cancel_clicked)
|
||||||
|
bottom_layout.addWidget(self.cancel_button)
|
||||||
|
|
||||||
|
# Main layout
|
||||||
|
main_layout = QVBoxLayout()
|
||||||
|
main_layout.addLayout(top_layout)
|
||||||
|
main_layout.addWidget(self.table_view)
|
||||||
|
main_layout.addLayout(bottom_layout)
|
||||||
|
|
||||||
|
self.combo_box.currentIndexChanged.connect(self.query_changed)
|
||||||
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
|
# Stretch last column *after* setting column widths which is
|
||||||
|
# *much* faster
|
||||||
|
h_header = self.table_view.horizontalHeader()
|
||||||
|
if h_header:
|
||||||
|
h_header.sectionResized.connect(self._column_resize)
|
||||||
|
h_header.setStretchLastSection(True)
|
||||||
|
# Resize on vertical header click
|
||||||
|
v_header = self.table_view.verticalHeader()
|
||||||
|
if v_header:
|
||||||
|
v_header.setMinimumSectionSize(5)
|
||||||
|
v_header.sectionHandleDoubleClicked.disconnect()
|
||||||
|
v_header.sectionHandleDoubleClicked.connect(
|
||||||
|
self.table_view.resizeRowToContents
|
||||||
|
)
|
||||||
|
|
||||||
|
self.set_window_size()
|
||||||
|
self.resizeRowsToContents()
|
||||||
|
|
||||||
|
def add_tracks_clicked(self):
|
||||||
|
self.selected_tracks = self.table_view.model().get_selected_track_ids()
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def cancel_clicked(self):
|
||||||
|
self.selected_tracks = []
|
||||||
|
self.reject()
|
||||||
|
|
||||||
|
def closeEvent(self, event: QCloseEvent | None) -> None:
|
||||||
|
"""
|
||||||
|
Record size and columns
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.save_sizes()
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
def accept(self) -> None:
|
||||||
|
self.save_sizes()
|
||||||
|
super().accept()
|
||||||
|
|
||||||
|
def reject(self) -> None:
|
||||||
|
self.save_sizes()
|
||||||
|
super().reject()
|
||||||
|
|
||||||
|
def save_sizes(self) -> None:
|
||||||
|
"""
|
||||||
|
Save window size
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Save dialog box attributes
|
||||||
|
attributes_to_save = dict(
|
||||||
|
querylist_height=self.height(),
|
||||||
|
querylist_width=self.width(),
|
||||||
|
querylist_x=self.x(),
|
||||||
|
querylist_y=self.y(),
|
||||||
|
)
|
||||||
|
for name, value in attributes_to_save.items():
|
||||||
|
record = Settings.get_setting(self.session, name)
|
||||||
|
record.f_int = value
|
||||||
|
|
||||||
|
header = self.table_view.horizontalHeader()
|
||||||
|
if header is None:
|
||||||
|
return
|
||||||
|
column_count = header.count()
|
||||||
|
if column_count < 2:
|
||||||
|
return
|
||||||
|
for column_number in range(column_count - 1):
|
||||||
|
attr_name = f"querylist_col_{column_number}_width"
|
||||||
|
record = Settings.get_setting(self.session, attr_name)
|
||||||
|
record.f_int = self.table_view.columnWidth(column_number)
|
||||||
|
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
def _column_resize(self, column_number: int, _old: int, _new: int) -> None:
|
||||||
|
"""
|
||||||
|
Called when column width changes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
header = self.table_view.horizontalHeader()
|
||||||
|
if not header:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Resize rows if necessary
|
||||||
|
self.resizeRowsToContents()
|
||||||
|
|
||||||
|
def resizeRowsToContents(self):
|
||||||
|
header = self.table_view.verticalHeader()
|
||||||
|
model = self.table_view.model()
|
||||||
|
if model:
|
||||||
|
for row in model.rowCount():
|
||||||
|
hint = self.sizeHintForRow(row)
|
||||||
|
header.resizeSection(row, hint)
|
||||||
|
|
||||||
|
def query_changed(self, idx: int) -> None:
|
||||||
|
"""
|
||||||
|
Called when user selects query
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get query
|
||||||
|
query = self.session.get(Queries, idx)
|
||||||
|
if not query:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create model
|
||||||
|
base_model = QuerylistModel(self.session, query.sql)
|
||||||
|
|
||||||
|
# Create table
|
||||||
|
self.table_view.setModel(base_model)
|
||||||
|
self.set_column_sizes()
|
||||||
|
self.description_label.setText(query.description)
|
||||||
|
|
||||||
|
def handle_row_click(self, index):
|
||||||
|
self.table_view.model().toggle_row_selection(index.row())
|
||||||
|
self.table_view.clearSelection()
|
||||||
|
|
||||||
|
# Enable 'Add tracks' button only when a row is selected
|
||||||
|
selected = self.table_view.model().get_selected_track_ids()
|
||||||
|
self.add_tracks_button.setEnabled(selected != [])
|
||||||
|
|
||||||
|
def set_window_size(self) -> None:
|
||||||
|
"""Set window sizes"""
|
||||||
|
|
||||||
|
x = Settings.get_setting(self.session, "querylist_x").f_int or 100
|
||||||
|
y = Settings.get_setting(self.session, "querylist_y").f_int or 100
|
||||||
|
width = Settings.get_setting(self.session, "querylist_width").f_int or 100
|
||||||
|
height = Settings.get_setting(self.session, "querylist_height").f_int or 100
|
||||||
|
self.setGeometry(x, y, width, height)
|
||||||
|
|
||||||
|
def set_column_sizes(self) -> None:
|
||||||
|
"""Set column sizes"""
|
||||||
|
|
||||||
|
header = self.table_view.horizontalHeader()
|
||||||
|
if header is None:
|
||||||
|
return
|
||||||
|
column_count = header.count()
|
||||||
|
if column_count < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Last column is set to stretch so ignore it here
|
||||||
|
for column_number in range(column_count - 1):
|
||||||
|
attr_name = f"querylist_col_{column_number}_width"
|
||||||
|
record = Settings.get_setting(self.session, attr_name)
|
||||||
|
if record.f_int is not None:
|
||||||
|
self.table_view.setColumnWidth(column_number, record.f_int)
|
||||||
|
else:
|
||||||
|
self.table_view.setColumnWidth(
|
||||||
|
column_number, Config.DEFAULT_COLUMN_WIDTH
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SelectPlaylistDialog(QDialog):
|
class SelectPlaylistDialog(QDialog):
|
||||||
def __init__(self, parent=None, playlists=None, session=None):
|
def __init__(self, parent=None, playlists=None, session=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -789,25 +1026,6 @@ class Window(QMainWindow):
|
|||||||
|
|
||||||
return idx
|
return idx
|
||||||
|
|
||||||
def create_querylist_tab(self, querylist: Playlists) -> int:
|
|
||||||
"""
|
|
||||||
Take the passed querylist, create a querylist tab and
|
|
||||||
add tab to display. Return index number of tab.
|
|
||||||
"""
|
|
||||||
|
|
||||||
log.debug(f"create_querylist_tab({querylist=})")
|
|
||||||
|
|
||||||
# Create model and proxy model
|
|
||||||
base_model = QuerylistModel(querylist.id)
|
|
||||||
|
|
||||||
# Create tab
|
|
||||||
querylist_tab = QuerylistTab(musicmuster=self, model=base_model)
|
|
||||||
idx = self.playlist_section.tabPlaylist.addTab(querylist_tab, querylist.name)
|
|
||||||
|
|
||||||
log.debug(f"create_querylist_tab() returned: {idx=}")
|
|
||||||
|
|
||||||
return idx
|
|
||||||
|
|
||||||
def current_row_or_end(self) -> int:
|
def current_row_or_end(self) -> int:
|
||||||
"""
|
"""
|
||||||
If a row or rows are selected, return the row number of the first
|
If a row or rows are selected, return the row number of the first
|
||||||
@ -1261,14 +1479,13 @@ class Window(QMainWindow):
|
|||||||
"""Open existing querylist"""
|
"""Open existing querylist"""
|
||||||
|
|
||||||
with db.Session() as session:
|
with db.Session() as session:
|
||||||
querylists = Playlists.get_all_queries(session)
|
dlg = QueryDialog(session)
|
||||||
dlg = SelectPlaylistDialog(self, playlists=querylists, session=session)
|
if dlg.exec():
|
||||||
dlg.exec()
|
new_row_number = self.current_row_or_end()
|
||||||
querylist = dlg.playlist
|
for track_id in dlg.selected_tracks:
|
||||||
if querylist:
|
self.current.base_model.insert_row(new_row_number, track_id)
|
||||||
idx = self.create_querylist_tab(querylist)
|
else:
|
||||||
|
return # User cancelled
|
||||||
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
|
||||||
|
|
||||||
def open_songfacts_browser(self, title: str) -> None:
|
def open_songfacts_browser(self, title: str) -> None:
|
||||||
"""Search Songfacts for title"""
|
"""Search Songfacts for title"""
|
||||||
|
|||||||
@ -109,7 +109,7 @@ class PlaylistDelegate(QStyledItemDelegate):
|
|||||||
if self.current_editor:
|
if self.current_editor:
|
||||||
editor = self.current_editor
|
editor = self.current_editor
|
||||||
else:
|
else:
|
||||||
if index.column() == QueryCol.INTRO.value:
|
if index.column() == Col.INTRO.value:
|
||||||
editor = QDoubleSpinBox(parent)
|
editor = QDoubleSpinBox(parent)
|
||||||
editor.setDecimals(1)
|
editor.setDecimals(1)
|
||||||
editor.setSingleStep(0.1)
|
editor.setSingleStep(0.1)
|
||||||
@ -245,7 +245,7 @@ class PlaylistDelegate(QStyledItemDelegate):
|
|||||||
self.original_model_data = self.base_model.data(
|
self.original_model_data = self.base_model.data(
|
||||||
edit_index, Qt.ItemDataRole.EditRole
|
edit_index, Qt.ItemDataRole.EditRole
|
||||||
)
|
)
|
||||||
if index.column() == QueryCol.INTRO.value:
|
if index.column() == Col.INTRO.value:
|
||||||
if self.original_model_data.value():
|
if self.original_model_data.value():
|
||||||
editor.setValue(self.original_model_data.value() / 1000)
|
editor.setValue(self.original_model_data.value() / 1000)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -4,25 +4,23 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import cast
|
from typing import Optional
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
|
||||||
# PyQt imports
|
# PyQt imports
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
QAbstractTableModel,
|
QAbstractTableModel,
|
||||||
QModelIndex,
|
QModelIndex,
|
||||||
QRegularExpression,
|
|
||||||
QSortFilterProxyModel,
|
|
||||||
Qt,
|
Qt,
|
||||||
QVariant,
|
QVariant,
|
||||||
)
|
)
|
||||||
from PyQt6.QtGui import (
|
from PyQt6.QtGui import (
|
||||||
QBrush,
|
|
||||||
QColor,
|
QColor,
|
||||||
QFont,
|
QFont,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
# import snoop # type: ignore
|
# import snoop # type: ignore
|
||||||
|
|
||||||
@ -46,7 +44,7 @@ class QueryRow:
|
|||||||
artist: str
|
artist: str
|
||||||
bitrate: int
|
bitrate: int
|
||||||
duration: int
|
duration: int
|
||||||
lastplayed: dt.datetime
|
lastplayed: Optional[dt.datetime]
|
||||||
path: str
|
path: str
|
||||||
title: str
|
title: str
|
||||||
track_id: int
|
track_id: int
|
||||||
@ -62,26 +60,24 @@ class QuerylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, session: Session, sql: str) -> None:
|
||||||
self,
|
"""
|
||||||
playlist_id: int,
|
Load query
|
||||||
) -> None:
|
"""
|
||||||
log.debug("QuerylistModel.__init__()")
|
|
||||||
|
log.debug(f"QuerylistModel.__init__({sql=})")
|
||||||
|
|
||||||
self.playlist_id = playlist_id
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.session = session
|
||||||
|
self.sql = sql
|
||||||
|
|
||||||
self.querylist_rows: dict[int, QueryRow] = {}
|
self.querylist_rows: dict[int, QueryRow] = {}
|
||||||
self._selected_rows: set[int] = set()
|
self._selected_rows: set[int] = set()
|
||||||
|
|
||||||
with db.Session() as session:
|
self.load_data()
|
||||||
# Populate self.playlist_rows
|
|
||||||
self.load_data(session)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return (
|
return f"<QuerylistModel: sql={self.sql}, {self.rowCount()} rows>"
|
||||||
f"<QuerylistModel: playlist_id={self.playlist_id}, {self.rowCount()} rows>"
|
|
||||||
)
|
|
||||||
|
|
||||||
def background_role(self, row: int, column: int, qrow: QueryRow) -> QVariant:
|
def background_role(self, row: int, column: int, qrow: QueryRow) -> QVariant:
|
||||||
"""Return background setting"""
|
"""Return background setting"""
|
||||||
@ -219,28 +215,9 @@ class QuerylistModel(QAbstractTableModel):
|
|||||||
|
|
||||||
return QVariant()
|
return QVariant()
|
||||||
|
|
||||||
def load_data(
|
def load_data(self) -> None:
|
||||||
self,
|
|
||||||
session: db.session,
|
|
||||||
sql: str = """
|
|
||||||
SELECT
|
|
||||||
tracks.*,playdates.lastplayed
|
|
||||||
FROM
|
|
||||||
tracks,playdates
|
|
||||||
WHERE
|
|
||||||
playdates.track_id=tracks.id
|
|
||||||
AND tracks.path LIKE '%/Singles/p%'
|
|
||||||
GROUP BY
|
|
||||||
tracks.id
|
|
||||||
HAVING
|
|
||||||
MAX(playdates.lastplayed) < DATE_SUB(NOW(), INTERVAL 1 YEAR)
|
|
||||||
ORDER BY tracks.title
|
|
||||||
;
|
|
||||||
""",
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Load data from user-defined query. Can probably hard-code the SELECT part
|
Populate self.querylist_rows
|
||||||
to ensure the required fields are returned.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO: Move the SQLAlchemy parts to models later, but for now as proof
|
# TODO: Move the SQLAlchemy parts to models later, but for now as proof
|
||||||
@ -252,17 +229,22 @@ class QuerylistModel(QAbstractTableModel):
|
|||||||
self.querylist_rows = {}
|
self.querylist_rows = {}
|
||||||
row = 0
|
row = 0
|
||||||
|
|
||||||
results = session.execute(text(sql)).mappings().all()
|
results = self.session.execute(text(self.sql)).mappings().all()
|
||||||
for result in results:
|
for result in results:
|
||||||
|
if hasattr(result, "lastplayed"):
|
||||||
|
lastplayed = result["lastplayed"]
|
||||||
|
else:
|
||||||
|
lastplayed = None
|
||||||
queryrow = QueryRow(
|
queryrow = QueryRow(
|
||||||
artist=result["artist"],
|
artist=result["artist"],
|
||||||
bitrate=result["bitrate"],
|
bitrate=result["bitrate"],
|
||||||
duration=result["duration"],
|
duration=result["duration"],
|
||||||
lastplayed=result["lastplayed"],
|
lastplayed=lastplayed,
|
||||||
path=result["path"],
|
path=result["path"],
|
||||||
title=result["title"],
|
title=result["title"],
|
||||||
track_id=result["id"],
|
track_id=result["id"],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.querylist_rows[row] = queryrow
|
self.querylist_rows[row] = queryrow
|
||||||
row += 1
|
row += 1
|
||||||
|
|
||||||
|
|||||||
@ -1,193 +0,0 @@
|
|||||||
# Standard library imports
|
|
||||||
from typing import cast, Optional, TYPE_CHECKING
|
|
||||||
|
|
||||||
# PyQt imports
|
|
||||||
from PyQt6.QtCore import (
|
|
||||||
QTimer,
|
|
||||||
)
|
|
||||||
from PyQt6.QtWidgets import (
|
|
||||||
QAbstractItemView,
|
|
||||||
QTableView,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Third party imports
|
|
||||||
# import line_profiler
|
|
||||||
|
|
||||||
# App imports
|
|
||||||
from audacity_controller import AudacityController
|
|
||||||
from classes import ApplicationError, MusicMusterSignals, PlaylistStyle
|
|
||||||
from config import Config
|
|
||||||
from helpers import (
|
|
||||||
show_warning,
|
|
||||||
)
|
|
||||||
from log import log
|
|
||||||
from models import db, Settings
|
|
||||||
from querylistmodel import QuerylistModel
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from musicmuster import Window
|
|
||||||
|
|
||||||
|
|
||||||
class QuerylistTab(QTableView):
|
|
||||||
"""
|
|
||||||
The querylist view
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, musicmuster: "Window", model: QuerylistModel) -> None:
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# Save passed settings
|
|
||||||
self.musicmuster = musicmuster
|
|
||||||
|
|
||||||
self.playlist_id = model.playlist_id
|
|
||||||
|
|
||||||
# Set up widget
|
|
||||||
self.setAlternatingRowColors(True)
|
|
||||||
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
|
||||||
|
|
||||||
# Set our custom style - this draws the drop indicator across the whole row
|
|
||||||
self.setStyle(PlaylistStyle())
|
|
||||||
|
|
||||||
# We will enable dragging when rows are selected. Disabling it
|
|
||||||
# here means we can click and drag to select rows.
|
|
||||||
self.setDragEnabled(False)
|
|
||||||
|
|
||||||
# Connect signals
|
|
||||||
self.signals = MusicMusterSignals()
|
|
||||||
self.signals.resize_rows_signal.connect(self.resize_rows)
|
|
||||||
|
|
||||||
# Selection model
|
|
||||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
||||||
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
||||||
|
|
||||||
# Enable item editing for checkboxes
|
|
||||||
self.clicked.connect(self.handle_row_click)
|
|
||||||
|
|
||||||
# Set up for Audacity
|
|
||||||
try:
|
|
||||||
self.ac: Optional[AudacityController] = AudacityController()
|
|
||||||
except ApplicationError as e:
|
|
||||||
self.ac = None
|
|
||||||
show_warning(self.musicmuster, "Audacity error", str(e))
|
|
||||||
|
|
||||||
# Load model, set column widths
|
|
||||||
self.setModel(model)
|
|
||||||
self._set_column_widths()
|
|
||||||
|
|
||||||
# Stretch last column *after* setting column widths which is
|
|
||||||
# *much* faster
|
|
||||||
h_header = self.horizontalHeader()
|
|
||||||
if h_header:
|
|
||||||
h_header.sectionResized.connect(self._column_resize)
|
|
||||||
h_header.setStretchLastSection(True)
|
|
||||||
# Resize on vertical header click
|
|
||||||
v_header = self.verticalHeader()
|
|
||||||
if v_header:
|
|
||||||
v_header.setMinimumSectionSize(5)
|
|
||||||
v_header.sectionHandleDoubleClicked.disconnect()
|
|
||||||
v_header.sectionHandleDoubleClicked.connect(self.resizeRowToContents)
|
|
||||||
|
|
||||||
# Setting ResizeToContents causes screen flash on load
|
|
||||||
self.resize_rows()
|
|
||||||
|
|
||||||
# ########## Overridden class functions ##########
|
|
||||||
|
|
||||||
def resizeRowToContents(self, row):
|
|
||||||
super().resizeRowToContents(row)
|
|
||||||
self.verticalHeader().resizeSection(row, self.sizeHintForRow(row))
|
|
||||||
|
|
||||||
def resizeRowsToContents(self):
|
|
||||||
header = self.verticalHeader()
|
|
||||||
for row in range(self.model().rowCount()):
|
|
||||||
hint = self.sizeHintForRow(row)
|
|
||||||
header.resizeSection(row, hint)
|
|
||||||
|
|
||||||
# ########## Custom functions ##########
|
|
||||||
def clear_selection(self) -> None:
|
|
||||||
"""Unselect all tracks and reset drag mode"""
|
|
||||||
|
|
||||||
self.clearSelection()
|
|
||||||
# We want to remove the focus from any widget otherwise keyboard
|
|
||||||
# activity may edit a cell.
|
|
||||||
fw = self.musicmuster.focusWidget()
|
|
||||||
if fw:
|
|
||||||
fw.clearFocus()
|
|
||||||
self.setDragEnabled(False)
|
|
||||||
|
|
||||||
def _column_resize(self, column_number: int, _old: int, _new: int) -> None:
|
|
||||||
"""
|
|
||||||
Called when column width changes. Save new width to database.
|
|
||||||
"""
|
|
||||||
|
|
||||||
log.debug(f"_column_resize({column_number=}, {_old=}, {_new=}")
|
|
||||||
|
|
||||||
header = self.horizontalHeader()
|
|
||||||
if not header:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Resize rows if necessary
|
|
||||||
self.resizeRowsToContents()
|
|
||||||
|
|
||||||
with db.Session() as session:
|
|
||||||
attr_name = f"querylist_col_{column_number}_width"
|
|
||||||
record = Settings.get_setting(session, attr_name)
|
|
||||||
record.f_int = self.columnWidth(column_number)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
def handle_row_click(self, index):
|
|
||||||
self.model().toggle_row_selection(index.row())
|
|
||||||
self.clearSelection()
|
|
||||||
|
|
||||||
def model(self) -> QuerylistModel:
|
|
||||||
"""
|
|
||||||
Override return type to keep mypy happy in this module
|
|
||||||
"""
|
|
||||||
|
|
||||||
return cast(QuerylistModel, super().model())
|
|
||||||
|
|
||||||
def resize_rows(self, playlist_id: Optional[int] = None) -> None:
|
|
||||||
"""
|
|
||||||
If playlist_id is us, resize rows
|
|
||||||
"""
|
|
||||||
|
|
||||||
log.debug(f"resize_rows({playlist_id=}) {self.playlist_id=}")
|
|
||||||
|
|
||||||
if playlist_id and playlist_id != self.playlist_id:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Suggestion from phind.com
|
|
||||||
def resize_row(row, count=1):
|
|
||||||
row_count = self.model().rowCount()
|
|
||||||
for todo in range(count):
|
|
||||||
if row < row_count:
|
|
||||||
self.resizeRowToContents(row)
|
|
||||||
row += 1
|
|
||||||
if row < row_count:
|
|
||||||
QTimer.singleShot(0, lambda: resize_row(row, count))
|
|
||||||
|
|
||||||
# Start resizing from row 0, 10 rows at a time
|
|
||||||
QTimer.singleShot(0, lambda: resize_row(0, Config.RESIZE_ROW_CHUNK_SIZE))
|
|
||||||
|
|
||||||
def _set_column_widths(self) -> None:
|
|
||||||
"""Column widths from settings"""
|
|
||||||
|
|
||||||
log.debug("_set_column_widths()")
|
|
||||||
|
|
||||||
header = self.horizontalHeader()
|
|
||||||
if not header:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Last column is set to stretch so ignore it here
|
|
||||||
with db.Session() as session:
|
|
||||||
for column_number in range(header.count() - 1):
|
|
||||||
attr_name = f"querylist_col_{column_number}_width"
|
|
||||||
record = Settings.get_setting(session, attr_name)
|
|
||||||
if record.f_int is not None:
|
|
||||||
self.setColumnWidth(column_number, record.f_int)
|
|
||||||
else:
|
|
||||||
self.setColumnWidth(column_number, Config.DEFAULT_COLUMN_WIDTH)
|
|
||||||
|
|
||||||
def tab_live(self) -> None:
|
|
||||||
"""Noop for query tabs"""
|
|
||||||
|
|
||||||
return
|
|
||||||
94
app/ui/dlgQuery.ui
Normal file
94
app/ui/dlgQuery.ui
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>queryDialog</class>
|
||||||
|
<widget class="QDialog" name="queryDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>762</width>
|
||||||
|
<height>686</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Query</string>
|
||||||
|
</property>
|
||||||
|
<widget class="QTableView" name="tableView">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>10</x>
|
||||||
|
<y>65</y>
|
||||||
|
<width>741</width>
|
||||||
|
<height>561</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>20</x>
|
||||||
|
<y>10</y>
|
||||||
|
<width>61</width>
|
||||||
|
<height>24</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Query:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QComboBox" name="cboQuery">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>80</x>
|
||||||
|
<y>10</y>
|
||||||
|
<width>221</width>
|
||||||
|
<height>32</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QPushButton" name="btnAddTracks">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>530</x>
|
||||||
|
<y>640</y>
|
||||||
|
<width>102</width>
|
||||||
|
<height>36</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Add &tracks</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QLabel" name="lblDescription">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>330</x>
|
||||||
|
<y>10</y>
|
||||||
|
<width>401</width>
|
||||||
|
<height>46</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>TextLabel</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QPushButton" name="pushButton">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>650</x>
|
||||||
|
<y>640</y>
|
||||||
|
<width>102</width>
|
||||||
|
<height>36</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Close</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
45
app/ui/dlgQuery_ui.py
Normal file
45
app/ui/dlgQuery_ui.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Form implementation generated from reading ui file 'app/ui/dlgQuery.ui'
|
||||||
|
#
|
||||||
|
# Created by: PyQt6 UI code generator 6.8.1
|
||||||
|
#
|
||||||
|
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
|
||||||
|
# run again. Do not edit this file unless you know what you are doing.
|
||||||
|
|
||||||
|
|
||||||
|
from PyQt6 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
|
||||||
|
class Ui_queryDialog(object):
|
||||||
|
def setupUi(self, queryDialog):
|
||||||
|
queryDialog.setObjectName("queryDialog")
|
||||||
|
queryDialog.resize(762, 686)
|
||||||
|
self.tableView = QtWidgets.QTableView(parent=queryDialog)
|
||||||
|
self.tableView.setGeometry(QtCore.QRect(10, 65, 741, 561))
|
||||||
|
self.tableView.setObjectName("tableView")
|
||||||
|
self.label = QtWidgets.QLabel(parent=queryDialog)
|
||||||
|
self.label.setGeometry(QtCore.QRect(20, 10, 61, 24))
|
||||||
|
self.label.setObjectName("label")
|
||||||
|
self.cboQuery = QtWidgets.QComboBox(parent=queryDialog)
|
||||||
|
self.cboQuery.setGeometry(QtCore.QRect(80, 10, 221, 32))
|
||||||
|
self.cboQuery.setObjectName("cboQuery")
|
||||||
|
self.btnAddTracks = QtWidgets.QPushButton(parent=queryDialog)
|
||||||
|
self.btnAddTracks.setGeometry(QtCore.QRect(530, 640, 102, 36))
|
||||||
|
self.btnAddTracks.setObjectName("btnAddTracks")
|
||||||
|
self.lblDescription = QtWidgets.QLabel(parent=queryDialog)
|
||||||
|
self.lblDescription.setGeometry(QtCore.QRect(330, 10, 401, 46))
|
||||||
|
self.lblDescription.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop)
|
||||||
|
self.lblDescription.setObjectName("lblDescription")
|
||||||
|
self.pushButton = QtWidgets.QPushButton(parent=queryDialog)
|
||||||
|
self.pushButton.setGeometry(QtCore.QRect(650, 640, 102, 36))
|
||||||
|
self.pushButton.setObjectName("pushButton")
|
||||||
|
|
||||||
|
self.retranslateUi(queryDialog)
|
||||||
|
QtCore.QMetaObject.connectSlotsByName(queryDialog)
|
||||||
|
|
||||||
|
def retranslateUi(self, queryDialog):
|
||||||
|
_translate = QtCore.QCoreApplication.translate
|
||||||
|
queryDialog.setWindowTitle(_translate("queryDialog", "Query"))
|
||||||
|
self.label.setText(_translate("queryDialog", "Query:"))
|
||||||
|
self.btnAddTracks.setText(_translate("queryDialog", "Add &tracks"))
|
||||||
|
self.lblDescription.setText(_translate("queryDialog", "TextLabel"))
|
||||||
|
self.pushButton.setText(_translate("queryDialog", "Close"))
|
||||||
@ -1,8 +1,8 @@
|
|||||||
"""Add data for query playlists
|
"""Add queries table
|
||||||
|
|
||||||
Revision ID: 014f2d4c88a5
|
Revision ID: 9c1254a8026d
|
||||||
Revises: 33c04e3c12c8
|
Revises: c76e865ccb85
|
||||||
Create Date: 2024-12-30 14:23:36.924478
|
Create Date: 2025-02-14 16:32:37.064567
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
@ -10,8 +10,8 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = '014f2d4c88a5'
|
revision = '9c1254a8026d'
|
||||||
down_revision = '33c04e3c12c8'
|
down_revision = 'c76e865ccb85'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
@ -31,22 +31,16 @@ def upgrade_() -> None:
|
|||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('queries',
|
op.create_table('queries',
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
sa.Column('query', sa.String(length=2048), nullable=False),
|
sa.Column('name', sa.String(length=128), nullable=False),
|
||||||
sa.Column('playlist_id', sa.Integer(), nullable=False),
|
sa.Column('sql', sa.String(length=2048), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['playlist_id'], ['playlists.id'], ),
|
sa.Column('description', sa.String(length=512), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id')
|
||||||
)
|
)
|
||||||
with op.batch_alter_table('queries', schema=None) as batch_op:
|
|
||||||
batch_op.create_index(batch_op.f('ix_queries_playlist_id'), ['playlist_id'], unique=False)
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade_() -> None:
|
def downgrade_() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
with op.batch_alter_table('queries', schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f('ix_queries_playlist_id'))
|
|
||||||
|
|
||||||
op.drop_table('queries')
|
op.drop_table('queries')
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user