WIP: queries management

Menus and management working. Wrong tracks showing up in queries.
This commit is contained in:
Keith Edmunds 2025-03-02 19:14:53 +00:00
parent aa6ab03555
commit 8e48d63ebb
4 changed files with 364 additions and 23 deletions

View File

@ -49,6 +49,18 @@ class Config(object):
FADEOUT_DB = -10 FADEOUT_DB = -10
FADEOUT_SECONDS = 5 FADEOUT_SECONDS = 5
FADEOUT_STEPS_PER_SECOND = 5 FADEOUT_STEPS_PER_SECOND = 5
FILTER_DURATION_LONGER = "longer than"
FILTER_DURATION_MINUTES = "minutes"
FILTER_DURATION_SECONDS = "seconds"
FILTER_DURATION_SHORTER = "shorter than"
FILTER_PATH_CONTAINS = "contains"
FILTER_PATH_EXCLUDING = "excluding"
FILTER_PLAYED_BEFORE = "before"
FILTER_PLAYED_DAYS = "days"
FILTER_PLAYED_MONTHS = "months"
FILTER_PLAYED_NEVER = "never"
FILTER_PLAYED_WEEKS = "weeks"
FILTER_PLAYED_YEARS = "years"
FUZZYMATCH_MINIMUM_LIST = 60.0 FUZZYMATCH_MINIMUM_LIST = 60.0
FUZZYMATCH_MINIMUM_SELECT_ARTIST = 80.0 FUZZYMATCH_MINIMUM_SELECT_ARTIST = 80.0
FUZZYMATCH_MINIMUM_SELECT_TITLE = 80.0 FUZZYMATCH_MINIMUM_SELECT_TITLE = 80.0

View File

@ -25,7 +25,7 @@ from sqlalchemy.orm.session import Session
from sqlalchemy.engine.row import RowMapping from sqlalchemy.engine.row import RowMapping
# App imports # App imports
from classes import ApplicationError from classes import ApplicationError, Filter
from config import Config from config import Config
from dbmanager import DatabaseManager from dbmanager import DatabaseManager
import dbtables import dbtables
@ -610,11 +610,21 @@ class Queries(dbtables.QueriesTable):
session.commit() session.commit()
@classmethod @classmethod
def get_all_queries(cls, session: Session) -> Sequence["Queries"]: def get_all(cls, session: Session) -> Sequence["Queries"]:
"""Returns a list of all queries ordered by name""" """Returns a list of all queries ordered by name"""
return session.scalars(select(cls).order_by(cls.name)).all() return session.scalars(select(cls).order_by(cls.name)).all()
@classmethod
def get_favourites(cls, session: Session) -> Sequence["Queries"]:
"""Returns a list of favourite queries ordered by name"""
return session.scalars(
select(cls)
.where(cls.favourite.is_(True))
.order_by(cls.name)
).all()
class Settings(dbtables.SettingsTable): class Settings(dbtables.SettingsTable):
def __init__(self, session: Session, name: str) -> None: def __init__(self, session: Session, name: str) -> None:
@ -700,6 +710,40 @@ class Tracks(dbtables.TracksTable):
.all() .all()
) )
@classmethod
def get_filtered_tracks(cls, session: Session, filter: Filter) -> Sequence["Tracks"]:
"""
Return tracks matching filter
"""
query = select(cls)
if filter.path:
if filter.path_type == "contains":
query = query.where(cls.path.ilike(f"%{filter.path}%"))
elif filter.path_type == "excluding":
query = query.where(cls.path.notilike(f"%{filter.path}%"))
else:
raise ApplicationError(f"Can't process filter path ({filter=})")
# TODO
# if last_played_number:
# need group_by track_id and having max/min lastplayed gt/lt, etc
seconds_duration = filter.duration_number
if filter.duration_unit == Config.FILTER_DURATION_MINUTES:
seconds_duration *= 60
elif filter.duration_unit != Config.FILTER_DURATION_SECONDS:
raise ApplicationError(f"Can't process filter duration ({filter=})")
if filter.duration_type == Config.FILTER_DURATION_LONGER:
query = query.where(cls.duration >= seconds_duration)
elif filter.duration_unit == Config.FILTER_DURATION_SHORTER:
query = query.where(cls.duration <= seconds_duration)
else:
raise ApplicationError(f"Can't process filter duration type ({filter=})")
records = session.scalars(
query).unique().all()
return records
@classmethod @classmethod
def get_by_path(cls, session: Session, path: str) -> Optional["Tracks"]: def get_by_path(cls, session: Session, path: str) -> Optional["Tracks"]:
""" """

View File

@ -20,6 +20,7 @@ from PyQt6.QtCore import (
Qt, Qt,
QTime, QTime,
QTimer, QTimer,
QVariant,
) )
from PyQt6.QtGui import ( from PyQt6.QtGui import (
QAction, QAction,
@ -32,6 +33,7 @@ from PyQt6.QtGui import (
QShortcut, QShortcut,
) )
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QAbstractItemView,
QApplication, QApplication,
QCheckBox, QCheckBox,
QComboBox, QComboBox,
@ -46,7 +48,9 @@ from PyQt6.QtWidgets import (
QMenu, QMenu,
QMessageBox, QMessageBox,
QPushButton, QPushButton,
QSizePolicy,
QSpinBox, QSpinBox,
QTableView,
QTableWidget, QTableWidget,
QTableWidgetItem, QTableWidgetItem,
QVBoxLayout, QVBoxLayout,
@ -74,6 +78,7 @@ from models import db, Playdates, PlaylistRows, Playlists, Queries, Settings, Tr
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 playlists import PlaylistTab from playlists import PlaylistTab
from querylistmodel import QuerylistModel
from ui import icons_rc # noqa F401 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
@ -190,7 +195,9 @@ class FilterDialog(QDialog):
path_layout = QHBoxLayout() path_layout = QHBoxLayout()
path_label = QLabel("Path") path_label = QLabel("Path")
self.path_combo = QComboBox() self.path_combo = QComboBox()
self.path_combo.addItems(["contains", "excluding"]) self.path_combo.addItems(
[Config.FILTER_PATH_CONTAINS, Config.FILTER_PATH_EXCLUDING]
)
for idx in range(self.path_combo.count()): for idx in range(self.path_combo.count()):
if self.path_combo.itemText(idx) == filter.path_type: if self.path_combo.itemText(idx) == filter.path_type:
self.path_combo.setCurrentIndex(idx) self.path_combo.setCurrentIndex(idx)
@ -207,7 +214,9 @@ class FilterDialog(QDialog):
last_played_layout = QHBoxLayout() last_played_layout = QHBoxLayout()
last_played_label = QLabel("Last played") last_played_label = QLabel("Last played")
self.last_played_combo = QComboBox() self.last_played_combo = QComboBox()
self.last_played_combo.addItems(["before", "never"]) self.last_played_combo.addItems(
[Config.FILTER_PLAYED_BEFORE, Config.FILTER_PLAYED_NEVER]
)
for idx in range(self.last_played_combo.count()): for idx in range(self.last_played_combo.count()):
if self.last_played_combo.itemText(idx) == filter.last_played_type: if self.last_played_combo.itemText(idx) == filter.last_played_type:
self.last_played_combo.setCurrentIndex(idx) self.last_played_combo.setCurrentIndex(idx)
@ -219,7 +228,14 @@ class FilterDialog(QDialog):
self.last_played_spinbox.setValue(filter.last_played_number or 0) self.last_played_spinbox.setValue(filter.last_played_number or 0)
self.last_played_unit = QComboBox() self.last_played_unit = QComboBox()
self.last_played_unit.addItems(["years", "months", "weeks", "days"]) self.last_played_unit.addItems(
[
Config.FILTER_PLAYED_YEARS,
Config.FILTER_PLAYED_MONTHS,
Config.FILTER_PLAYED_WEEKS,
Config.FILTER_PLAYED_DAYS,
]
)
for idx in range(self.last_played_unit.count()): for idx in range(self.last_played_unit.count()):
if self.last_played_unit.itemText(idx) == filter.last_played_unit: if self.last_played_unit.itemText(idx) == filter.last_played_unit:
self.last_played_unit.setCurrentIndex(idx) self.last_played_unit.setCurrentIndex(idx)
@ -239,7 +255,9 @@ class FilterDialog(QDialog):
duration_layout = QHBoxLayout() duration_layout = QHBoxLayout()
duration_label = QLabel("Duration") duration_label = QLabel("Duration")
self.duration_combo = QComboBox() self.duration_combo = QComboBox()
self.duration_combo.addItems(["longer than", "shorter than"]) self.duration_combo.addItems(
[Config.FILTER_DURATION_LONGER, Config.FILTER_DURATION_SHORTER]
)
for idx in range(self.duration_combo.count()): for idx in range(self.duration_combo.count()):
if self.duration_combo.itemText(idx) == filter.duration_type: if self.duration_combo.itemText(idx) == filter.duration_type:
self.duration_combo.setCurrentIndex(idx) self.duration_combo.setCurrentIndex(idx)
@ -251,8 +269,10 @@ class FilterDialog(QDialog):
self.duration_spinbox.setValue(filter.duration_number) self.duration_spinbox.setValue(filter.duration_number)
self.duration_unit = QComboBox() self.duration_unit = QComboBox()
self.duration_unit.addItems(["minutes", "seconds"]) self.duration_unit.addItems(
self.duration_unit.setCurrentText("minutes") [Config.FILTER_DURATION_MINUTES, Config.FILTER_DURATION_SECONDS]
)
self.duration_unit.setCurrentText(Config.FILTER_DURATION_MINUTES)
for idx in range(self.duration_unit.count()): for idx in range(self.duration_unit.count()):
if self.duration_unit.itemText(idx) == filter.duration_unit: if self.duration_unit.itemText(idx) == filter.duration_unit:
self.duration_unit.setCurrentIndex(idx) self.duration_unit.setCurrentIndex(idx)
@ -447,7 +467,7 @@ class ManageQueries(ItemlistManager):
# Build a list of queries # Build a list of queries
query_list: list[ItemlistItem] = [] query_list: list[ItemlistItem] = []
for query in Queries.get_all_queries(self.session): for query in Queries.get_all(self.session):
query_list.append( query_list.append(
ItemlistItem(name=query.name, id=query.id, favourite=query.favourite) ItemlistItem(name=query.name, id=query.id, favourite=query.favourite)
) )
@ -478,6 +498,7 @@ class ManageQueries(ItemlistManager):
dlg = FilterDialog(query.name, query.filter) dlg = FilterDialog(query.name, query.filter)
if dlg.exec(): if dlg.exec():
query.filter = dlg.filter query.filter = dlg.filter
query.name = dlg.name_text.text()
self.session.commit() self.session.commit()
def edit_item(self, query_id: int) -> None: def edit_item(self, query_id: int) -> None:
@ -771,6 +792,230 @@ class PreviewManager:
self.start_time = None self.start_time = None
class QueryDialog(QDialog):
"""Dialog box to handle selecting track from a query"""
def __init__(self, session: Session, default: int = 0) -> None:
super().__init__()
self.session = session
self.default = default
# 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)
# 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)
if self.default:
default_idx = self.combo_box.findData(QVariant(self.default))
self.combo_box.setCurrentIndex(default_idx)
self.path_text = QLineEdit()
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 range(model.rowCount()):
hint = self.table_view.sizeHintForRow(row)
header.resizeSection(row, hint)
def query_changed(self, idx: int) -> None:
"""
Called when user selects query
"""
# Get query id
query_id = self.combo_box.currentData()
query = self.session.get(Queries, query_id)
if not query:
return
# Create model
base_model = QuerylistModel(self.session, query.filter)
# Create table
self.table_view.setModel(base_model)
self.set_column_sizes()
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__()
@ -1146,12 +1391,52 @@ class Window(QMainWindow):
return submenu_items return submenu_items
def get_query_dynamic_submenu_items(self): def get_query_dynamic_submenu_items(
"""Returns dynamically generated menu items for Submenu 2.""" self,
return [ ) -> list[dict[str, str | tuple[Session, int] | bool]]:
{"text": "Action Xargs", "handler": "kae", "args": (21,)}, """
{"text": "Action Y", "handler": "action_y_handler"}, Return dynamically generated menu items, in this case
] templates marked as favourite from which to generate a
new playlist.
The handler is to call show_query with a session
and query_id.
"""
with db.Session() as session:
submenu_items: list[dict[str, str | tuple[Session, int] | bool]] = [
{
"text": "Show all",
"handler": "show_query",
"args": (session, 0),
},
{
"separator": True,
},
]
queries = Queries.get_favourites(session)
for query in queries:
submenu_items.append(
{
"text": query.name,
"handler": "show_query",
"args": (
session,
query.id,
),
}
)
return submenu_items
def show_query(self, session: Session, query_id: int) -> None:
"""
Show query dialog with query_id selected
"""
# Keep a reference else it will be gc'd
self.query_dialog = QueryDialog(session, query_id)
self.query_dialog.exec()
# # # # # # # # # # Playlist management functions # # # # # # # # # # # # # # # # # # # # Playlist management functions # # # # # # # # # #

View File

@ -38,7 +38,7 @@ from helpers import (
show_warning, show_warning,
) )
from log import log from log import log
from models import db, Playdates from models import db, Playdates, Tracks
from music_manager import RowAndTrack from music_manager import RowAndTrack
@ -228,20 +228,20 @@ class QuerylistModel(QAbstractTableModel):
row = 0 row = 0
try: try:
results = Tracks.get_filtered(self.session, self.filter) results = Tracks.get_filtered_tracks(self.session, self.filter)
for result in results: for result in results:
if hasattr(result, "lastplayed"): if hasattr(result, "lastplayed"):
lastplayed = result["lastplayed"] lastplayed = result["lastplayed"]
else: else:
lastplayed = None lastplayed = None
queryrow = QueryRow( queryrow = QueryRow(
artist=result["artist"], artist=result.artist,
bitrate=result["bitrate"], bitrate=result.bitrate or 0,
duration=result["duration"], duration=result.duration,
lastplayed=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