WIP: queries management
Menus and management working. Wrong tracks showing up in queries.
This commit is contained in:
parent
aa6ab03555
commit
8e48d63ebb
@ -49,6 +49,18 @@ class Config(object):
|
||||
FADEOUT_DB = -10
|
||||
FADEOUT_SECONDS = 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_SELECT_ARTIST = 80.0
|
||||
FUZZYMATCH_MINIMUM_SELECT_TITLE = 80.0
|
||||
|
||||
@ -25,7 +25,7 @@ from sqlalchemy.orm.session import Session
|
||||
from sqlalchemy.engine.row import RowMapping
|
||||
|
||||
# App imports
|
||||
from classes import ApplicationError
|
||||
from classes import ApplicationError, Filter
|
||||
from config import Config
|
||||
from dbmanager import DatabaseManager
|
||||
import dbtables
|
||||
@ -610,11 +610,21 @@ class Queries(dbtables.QueriesTable):
|
||||
session.commit()
|
||||
|
||||
@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"""
|
||||
|
||||
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):
|
||||
def __init__(self, session: Session, name: str) -> None:
|
||||
@ -700,6 +710,40 @@ class Tracks(dbtables.TracksTable):
|
||||
.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
|
||||
def get_by_path(cls, session: Session, path: str) -> Optional["Tracks"]:
|
||||
"""
|
||||
|
||||
@ -20,6 +20,7 @@ from PyQt6.QtCore import (
|
||||
Qt,
|
||||
QTime,
|
||||
QTimer,
|
||||
QVariant,
|
||||
)
|
||||
from PyQt6.QtGui import (
|
||||
QAction,
|
||||
@ -32,6 +33,7 @@ from PyQt6.QtGui import (
|
||||
QShortcut,
|
||||
)
|
||||
from PyQt6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
@ -46,7 +48,9 @@ from PyQt6.QtWidgets import (
|
||||
QMenu,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSpinBox,
|
||||
QTableView,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QVBoxLayout,
|
||||
@ -74,6 +78,7 @@ from models import db, Playdates, PlaylistRows, Playlists, Queries, Settings, Tr
|
||||
from music_manager import RowAndTrack, track_sequence
|
||||
from playlistmodel import PlaylistModel, PlaylistProxyModel
|
||||
from playlists import PlaylistTab
|
||||
from querylistmodel import QuerylistModel
|
||||
from ui import icons_rc # noqa F401
|
||||
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore
|
||||
from ui.downloadcsv_ui import Ui_DateSelect # type: ignore
|
||||
@ -190,7 +195,9 @@ class FilterDialog(QDialog):
|
||||
path_layout = QHBoxLayout()
|
||||
path_label = QLabel("Path")
|
||||
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()):
|
||||
if self.path_combo.itemText(idx) == filter.path_type:
|
||||
self.path_combo.setCurrentIndex(idx)
|
||||
@ -207,7 +214,9 @@ class FilterDialog(QDialog):
|
||||
last_played_layout = QHBoxLayout()
|
||||
last_played_label = QLabel("Last played")
|
||||
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()):
|
||||
if self.last_played_combo.itemText(idx) == filter.last_played_type:
|
||||
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_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()):
|
||||
if self.last_played_unit.itemText(idx) == filter.last_played_unit:
|
||||
self.last_played_unit.setCurrentIndex(idx)
|
||||
@ -239,7 +255,9 @@ class FilterDialog(QDialog):
|
||||
duration_layout = QHBoxLayout()
|
||||
duration_label = QLabel("Duration")
|
||||
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()):
|
||||
if self.duration_combo.itemText(idx) == filter.duration_type:
|
||||
self.duration_combo.setCurrentIndex(idx)
|
||||
@ -251,8 +269,10 @@ class FilterDialog(QDialog):
|
||||
self.duration_spinbox.setValue(filter.duration_number)
|
||||
|
||||
self.duration_unit = QComboBox()
|
||||
self.duration_unit.addItems(["minutes", "seconds"])
|
||||
self.duration_unit.setCurrentText("minutes")
|
||||
self.duration_unit.addItems(
|
||||
[Config.FILTER_DURATION_MINUTES, Config.FILTER_DURATION_SECONDS]
|
||||
)
|
||||
self.duration_unit.setCurrentText(Config.FILTER_DURATION_MINUTES)
|
||||
for idx in range(self.duration_unit.count()):
|
||||
if self.duration_unit.itemText(idx) == filter.duration_unit:
|
||||
self.duration_unit.setCurrentIndex(idx)
|
||||
@ -447,7 +467,7 @@ class ManageQueries(ItemlistManager):
|
||||
# Build a list of queries
|
||||
query_list: list[ItemlistItem] = []
|
||||
|
||||
for query in Queries.get_all_queries(self.session):
|
||||
for query in Queries.get_all(self.session):
|
||||
query_list.append(
|
||||
ItemlistItem(name=query.name, id=query.id, favourite=query.favourite)
|
||||
)
|
||||
@ -478,6 +498,7 @@ class ManageQueries(ItemlistManager):
|
||||
dlg = FilterDialog(query.name, query.filter)
|
||||
if dlg.exec():
|
||||
query.filter = dlg.filter
|
||||
query.name = dlg.name_text.text()
|
||||
self.session.commit()
|
||||
|
||||
def edit_item(self, query_id: int) -> None:
|
||||
@ -771,6 +792,230 @@ class PreviewManager:
|
||||
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):
|
||||
def __init__(self, parent=None, playlists=None, session=None):
|
||||
super().__init__()
|
||||
@ -1146,12 +1391,52 @@ class Window(QMainWindow):
|
||||
|
||||
return submenu_items
|
||||
|
||||
def get_query_dynamic_submenu_items(self):
|
||||
"""Returns dynamically generated menu items for Submenu 2."""
|
||||
return [
|
||||
{"text": "Action Xargs", "handler": "kae", "args": (21,)},
|
||||
{"text": "Action Y", "handler": "action_y_handler"},
|
||||
]
|
||||
def get_query_dynamic_submenu_items(
|
||||
self,
|
||||
) -> list[dict[str, str | tuple[Session, int] | bool]]:
|
||||
"""
|
||||
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 # # # # # # # # # #
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ from helpers import (
|
||||
show_warning,
|
||||
)
|
||||
from log import log
|
||||
from models import db, Playdates
|
||||
from models import db, Playdates, Tracks
|
||||
from music_manager import RowAndTrack
|
||||
|
||||
|
||||
@ -228,20 +228,20 @@ class QuerylistModel(QAbstractTableModel):
|
||||
row = 0
|
||||
|
||||
try:
|
||||
results = Tracks.get_filtered(self.session, self.filter)
|
||||
results = Tracks.get_filtered_tracks(self.session, self.filter)
|
||||
for result in results:
|
||||
if hasattr(result, "lastplayed"):
|
||||
lastplayed = result["lastplayed"]
|
||||
else:
|
||||
lastplayed = None
|
||||
queryrow = QueryRow(
|
||||
artist=result["artist"],
|
||||
bitrate=result["bitrate"],
|
||||
duration=result["duration"],
|
||||
artist=result.artist,
|
||||
bitrate=result.bitrate or 0,
|
||||
duration=result.duration,
|
||||
lastplayed=lastplayed,
|
||||
path=result["path"],
|
||||
title=result["title"],
|
||||
track_id=result["id"],
|
||||
path=result.path,
|
||||
title=result.title,
|
||||
track_id=result.id,
|
||||
)
|
||||
|
||||
self.querylist_rows[row] = queryrow
|
||||
|
||||
Loading…
Reference in New Issue
Block a user