Query tabs WIP
This commit is contained in:
parent
5ed7b822e1
commit
955bea2037
@ -14,6 +14,11 @@ from PyQt6.QtCore import (
|
|||||||
pyqtSignal,
|
pyqtSignal,
|
||||||
QObject,
|
QObject,
|
||||||
)
|
)
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QProxyStyle,
|
||||||
|
QStyle,
|
||||||
|
QStyleOption,
|
||||||
|
)
|
||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
|
|
||||||
@ -31,6 +36,14 @@ class Col(Enum):
|
|||||||
NOTE = 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
|
||||||
@ -100,6 +113,24 @@ class MusicMusterSignals(QObject):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistStyle(QProxyStyle):
|
||||||
|
def drawPrimitive(self, element, option, painter, widget=None):
|
||||||
|
"""
|
||||||
|
Draw a line across the entire row rather than just the column
|
||||||
|
we're hovering over.
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
element == QStyle.PrimitiveElement.PE_IndicatorItemViewItemDrop
|
||||||
|
and not option.rect.isNull()
|
||||||
|
):
|
||||||
|
option_new = QStyleOption(option)
|
||||||
|
option_new.rect.setLeft(0)
|
||||||
|
if widget:
|
||||||
|
option_new.rect.setRight(widget.width())
|
||||||
|
option = option_new
|
||||||
|
super().drawPrimitive(element, option, painter, widget)
|
||||||
|
|
||||||
|
|
||||||
class Tags(NamedTuple):
|
class Tags(NamedTuple):
|
||||||
artist: str = ""
|
artist: str = ""
|
||||||
title: str = ""
|
title: str = ""
|
||||||
|
|||||||
@ -31,6 +31,7 @@ class Config(object):
|
|||||||
COLOUR_NORMAL_TAB = "#000000"
|
COLOUR_NORMAL_TAB = "#000000"
|
||||||
COLOUR_NOTES_PLAYLIST = "#b8daff"
|
COLOUR_NOTES_PLAYLIST = "#b8daff"
|
||||||
COLOUR_ODD_PLAYLIST = "#f2f2f2"
|
COLOUR_ODD_PLAYLIST = "#f2f2f2"
|
||||||
|
COLOUR_QUERYLIST_SELECTED = "#d3ffd3"
|
||||||
COLOUR_UNREADABLE = "#dc3545"
|
COLOUR_UNREADABLE = "#dc3545"
|
||||||
COLOUR_WARNING_TIMER = "#ffc107"
|
COLOUR_WARNING_TIMER = "#ffc107"
|
||||||
DBFS_SILENCE = -50
|
DBFS_SILENCE = -50
|
||||||
|
|||||||
@ -18,7 +18,6 @@ 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()
|
|
||||||
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")
|
||||||
|
|||||||
@ -50,7 +50,8 @@ class PlaydatesTable(Model):
|
|||||||
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"))
|
||||||
track: Mapped["TracksTable"] = relationship(
|
track: Mapped["TracksTable"] = relationship(
|
||||||
"TracksTable", back_populates="playdates"
|
"TracksTable",
|
||||||
|
back_populates="playdates",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
@ -103,7 +104,7 @@ 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"))
|
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",
|
||||||
@ -127,7 +128,9 @@ class QueriesTable(Model):
|
|||||||
query: Mapped[str] = mapped_column(
|
query: Mapped[str] = mapped_column(
|
||||||
String(2048), index=False, default="", nullable=False
|
String(2048), index=False, default="", nullable=False
|
||||||
)
|
)
|
||||||
playlist_id: Mapped[int] = mapped_column(ForeignKey("playlists.id"), index=True)
|
playlist_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("playlists.id", ondelete="CASCADE"), index=True
|
||||||
|
)
|
||||||
playlist: Mapped[PlaylistsTable] = relationship(back_populates="query")
|
playlist: Mapped[PlaylistsTable] = relationship(back_populates="query")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
|||||||
@ -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
|
from sqlalchemy.orm import joinedload, selectinload
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
@ -36,7 +36,6 @@ if DATABASE_URL is None:
|
|||||||
if "unittest" in sys.modules and "sqlite" not in DATABASE_URL:
|
if "unittest" in sys.modules and "sqlite" not in DATABASE_URL:
|
||||||
raise ValueError("Unit tests running on non-Sqlite database")
|
raise ValueError("Unit tests running on non-Sqlite database")
|
||||||
db = DatabaseManager.get_instance(DATABASE_URL, engine_options=Config.ENGINE_OPTIONS).db
|
db = DatabaseManager.get_instance(DATABASE_URL, engine_options=Config.ENGINE_OPTIONS).db
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
|
|
||||||
# Database classes
|
# Database classes
|
||||||
@ -236,10 +235,23 @@ class Playlists(dbtables.PlaylistsTable):
|
|||||||
|
|
||||||
return session.scalars(
|
return session.scalars(
|
||||||
select(cls)
|
select(cls)
|
||||||
.filter(cls.is_template.is_(False))
|
.filter(
|
||||||
|
cls.is_template.is_(False),
|
||||||
|
~cls.query.has()
|
||||||
|
)
|
||||||
.order_by(cls.last_used.desc())
|
.order_by(cls.last_used.desc())
|
||||||
).all()
|
).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
|
@classmethod
|
||||||
def get_all_templates(cls, session: Session) -> Sequence["Playlists"]:
|
def get_all_templates(cls, session: Session) -> Sequence["Playlists"]:
|
||||||
"""Returns a list of all templates ordered by name"""
|
"""Returns a list of all templates ordered by name"""
|
||||||
@ -257,6 +269,7 @@ class Playlists(dbtables.PlaylistsTable):
|
|||||||
.filter(
|
.filter(
|
||||||
cls.open.is_(False),
|
cls.open.is_(False),
|
||||||
cls.is_template.is_(False),
|
cls.is_template.is_(False),
|
||||||
|
~cls.query.has()
|
||||||
)
|
)
|
||||||
.order_by(cls.last_used.desc())
|
.order_by(cls.last_used.desc())
|
||||||
).all()
|
).all()
|
||||||
@ -268,7 +281,13 @@ class Playlists(dbtables.PlaylistsTable):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
return session.scalars(
|
return session.scalars(
|
||||||
select(cls).where(cls.open.is_(True)).order_by(cls.tab)
|
select(cls)
|
||||||
|
.where(
|
||||||
|
cls.open.is_(True),
|
||||||
|
~cls.query.has()
|
||||||
|
)
|
||||||
|
.order_by(cls.tab)
|
||||||
|
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
def mark_open(self) -> None:
|
def mark_open(self) -> None:
|
||||||
@ -312,6 +331,26 @@ class Playlists(dbtables.PlaylistsTable):
|
|||||||
PlaylistRows.copy_playlist(session, playlist_id, template.id)
|
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,
|
||||||
|
|||||||
@ -63,7 +63,9 @@ from log import log
|
|||||||
from models import db, Playdates, PlaylistRows, Playlists, Settings, Tracks
|
from models import db, Playdates, PlaylistRows, Playlists, 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 playlists import PlaylistTab
|
from playlists import PlaylistTab
|
||||||
|
from querylists import QuerylistTab
|
||||||
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
|
||||||
@ -768,7 +770,7 @@ class Window(QMainWindow):
|
|||||||
|
|
||||||
def create_playlist_tab(self, playlist: Playlists) -> int:
|
def create_playlist_tab(self, playlist: Playlists) -> int:
|
||||||
"""
|
"""
|
||||||
Take the passed proxy model, create a playlist tab and
|
Take the passed playlist, create a playlist tab and
|
||||||
add tab to display. Return index number of tab.
|
add tab to display. Return index number of tab.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -787,6 +789,25 @@ 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
|
||||||
@ -1236,6 +1257,19 @@ class Window(QMainWindow):
|
|||||||
|
|
||||||
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
self.playlist_section.tabPlaylist.setCurrentIndex(idx)
|
||||||
|
|
||||||
|
def open_querylist(self) -> None:
|
||||||
|
"""Open existing querylist"""
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
querylists = Playlists.get_all_queries(session)
|
||||||
|
dlg = SelectPlaylistDialog(self, playlists=querylists, session=session)
|
||||||
|
dlg.exec()
|
||||||
|
querylist = dlg.playlist
|
||||||
|
if querylist:
|
||||||
|
idx = self.create_querylist_tab(querylist)
|
||||||
|
|
||||||
|
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"""
|
||||||
|
|
||||||
|
|||||||
@ -497,7 +497,7 @@ class PlaylistModel(QAbstractTableModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return Qt.ItemFlag.ItemIsDropEnabled
|
return Qt.ItemFlag.NoItemFlags
|
||||||
|
|
||||||
default = (
|
default = (
|
||||||
Qt.ItemFlag.ItemIsEnabled
|
Qt.ItemFlag.ItemIsEnabled
|
||||||
|
|||||||
@ -22,10 +22,7 @@ from PyQt6.QtWidgets import (
|
|||||||
QFrame,
|
QFrame,
|
||||||
QMenu,
|
QMenu,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
QProxyStyle,
|
|
||||||
QStyle,
|
|
||||||
QStyledItemDelegate,
|
QStyledItemDelegate,
|
||||||
QStyleOption,
|
|
||||||
QStyleOptionViewItem,
|
QStyleOptionViewItem,
|
||||||
QTableView,
|
QTableView,
|
||||||
QTableWidgetItem,
|
QTableWidgetItem,
|
||||||
@ -37,7 +34,7 @@ from PyQt6.QtWidgets import (
|
|||||||
|
|
||||||
# App imports
|
# App imports
|
||||||
from audacity_controller import AudacityController
|
from audacity_controller import AudacityController
|
||||||
from classes import ApplicationError, Col, MusicMusterSignals, TrackInfo
|
from classes import ApplicationError, Col, MusicMusterSignals, PlaylistStyle, TrackInfo
|
||||||
from config import Config
|
from config import Config
|
||||||
from dialogs import TrackSelectDialog
|
from dialogs import TrackSelectDialog
|
||||||
from helpers import (
|
from helpers import (
|
||||||
@ -112,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() == Col.INTRO.value:
|
if index.column() == QueryCol.INTRO.value:
|
||||||
editor = QDoubleSpinBox(parent)
|
editor = QDoubleSpinBox(parent)
|
||||||
editor.setDecimals(1)
|
editor.setDecimals(1)
|
||||||
editor.setSingleStep(0.1)
|
editor.setSingleStep(0.1)
|
||||||
@ -248,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() == Col.INTRO.value:
|
if index.column() == QueryCol.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:
|
||||||
@ -268,24 +265,6 @@ class PlaylistDelegate(QStyledItemDelegate):
|
|||||||
editor.setGeometry(option.rect)
|
editor.setGeometry(option.rect)
|
||||||
|
|
||||||
|
|
||||||
class PlaylistStyle(QProxyStyle):
|
|
||||||
def drawPrimitive(self, element, option, painter, widget=None):
|
|
||||||
"""
|
|
||||||
Draw a line across the entire row rather than just the column
|
|
||||||
we're hovering over.
|
|
||||||
"""
|
|
||||||
if (
|
|
||||||
element == QStyle.PrimitiveElement.PE_IndicatorItemViewItemDrop
|
|
||||||
and not option.rect.isNull()
|
|
||||||
):
|
|
||||||
option_new = QStyleOption(option)
|
|
||||||
option_new.rect.setLeft(0)
|
|
||||||
if widget:
|
|
||||||
option_new.rect.setRight(widget.width())
|
|
||||||
option = option_new
|
|
||||||
super().drawPrimitive(element, option, painter, widget)
|
|
||||||
|
|
||||||
|
|
||||||
class PlaylistTab(QTableView):
|
class PlaylistTab(QTableView):
|
||||||
"""
|
"""
|
||||||
The playlist view
|
The playlist view
|
||||||
|
|||||||
304
app/querylistmodel.py
Normal file
304
app/querylistmodel.py
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
# Standard library imports
|
||||||
|
# Allow forward reference to PlaylistModel
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import cast
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
# PyQt imports
|
||||||
|
from PyQt6.QtCore import (
|
||||||
|
QAbstractTableModel,
|
||||||
|
QModelIndex,
|
||||||
|
QRegularExpression,
|
||||||
|
QSortFilterProxyModel,
|
||||||
|
Qt,
|
||||||
|
QVariant,
|
||||||
|
)
|
||||||
|
from PyQt6.QtGui import (
|
||||||
|
QBrush,
|
||||||
|
QColor,
|
||||||
|
QFont,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
|
||||||
|
# import snoop # type: ignore
|
||||||
|
|
||||||
|
# App imports
|
||||||
|
from classes import (
|
||||||
|
QueryCol,
|
||||||
|
)
|
||||||
|
from config import Config
|
||||||
|
from helpers import (
|
||||||
|
file_is_unreadable,
|
||||||
|
get_relative_date,
|
||||||
|
ms_to_mmss,
|
||||||
|
)
|
||||||
|
from log import log
|
||||||
|
from models import db, Playdates
|
||||||
|
from music_manager import RowAndTrack
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class QueryRow:
|
||||||
|
artist: str
|
||||||
|
bitrate: int
|
||||||
|
duration: int
|
||||||
|
lastplayed: dt.datetime
|
||||||
|
path: str
|
||||||
|
title: str
|
||||||
|
track_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class QuerylistModel(QAbstractTableModel):
|
||||||
|
"""
|
||||||
|
The Querylist Model
|
||||||
|
|
||||||
|
Used to support query lists. The underlying database is never
|
||||||
|
updated. We just present tracks that match a query and allow the user
|
||||||
|
to copy those to a playlist.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
playlist_id: int,
|
||||||
|
) -> None:
|
||||||
|
log.debug("QuerylistModel.__init__()")
|
||||||
|
|
||||||
|
self.playlist_id = playlist_id
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.querylist_rows: dict[int, QueryRow] = {}
|
||||||
|
self._selected_rows: set[int] = set()
|
||||||
|
|
||||||
|
with db.Session() as session:
|
||||||
|
# Populate self.playlist_rows
|
||||||
|
self.load_data(session)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"<QuerylistModel: playlist_id={self.playlist_id}, {self.rowCount()} rows>"
|
||||||
|
)
|
||||||
|
|
||||||
|
def background_role(self, row: int, column: int, qrow: QueryRow) -> QVariant:
|
||||||
|
"""Return background setting"""
|
||||||
|
|
||||||
|
# Unreadable track file
|
||||||
|
if file_is_unreadable(qrow.path):
|
||||||
|
return QVariant(QColor(Config.COLOUR_UNREADABLE))
|
||||||
|
|
||||||
|
# Selected row
|
||||||
|
if row in self._selected_rows:
|
||||||
|
return QVariant(QColor(Config.COLOUR_QUERYLIST_SELECTED))
|
||||||
|
|
||||||
|
# Individual cell colouring
|
||||||
|
if column == QueryCol.BITRATE.value:
|
||||||
|
if not qrow.bitrate or qrow.bitrate < Config.BITRATE_LOW_THRESHOLD:
|
||||||
|
return QVariant(QColor(Config.COLOUR_BITRATE_LOW))
|
||||||
|
elif qrow.bitrate < Config.BITRATE_OK_THRESHOLD:
|
||||||
|
return QVariant(QColor(Config.COLOUR_BITRATE_MEDIUM))
|
||||||
|
else:
|
||||||
|
return QVariant(QColor(Config.COLOUR_BITRATE_OK))
|
||||||
|
|
||||||
|
return QVariant()
|
||||||
|
|
||||||
|
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||||
|
"""Standard function for view"""
|
||||||
|
|
||||||
|
return len(QueryCol)
|
||||||
|
|
||||||
|
def data(
|
||||||
|
self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole
|
||||||
|
) -> QVariant:
|
||||||
|
"""Return data to view"""
|
||||||
|
|
||||||
|
if not index.isValid() or not (0 <= index.row() < len(self.querylist_rows)):
|
||||||
|
return QVariant()
|
||||||
|
|
||||||
|
row = index.row()
|
||||||
|
column = index.column()
|
||||||
|
# rat for playlist row data as it's used a lot
|
||||||
|
qrow = self.querylist_rows[row]
|
||||||
|
|
||||||
|
# Dispatch to role-specific functions
|
||||||
|
dispatch_table: dict[int, Callable] = {
|
||||||
|
int(Qt.ItemDataRole.BackgroundRole): self.background_role,
|
||||||
|
int(Qt.ItemDataRole.DisplayRole): self.display_role,
|
||||||
|
int(Qt.ItemDataRole.ToolTipRole): self.tooltip_role,
|
||||||
|
}
|
||||||
|
|
||||||
|
if role in dispatch_table:
|
||||||
|
return QVariant(dispatch_table[role](row, column, qrow))
|
||||||
|
|
||||||
|
# Document other roles but don't use them
|
||||||
|
if role in [
|
||||||
|
Qt.ItemDataRole.DecorationRole,
|
||||||
|
Qt.ItemDataRole.EditRole,
|
||||||
|
Qt.ItemDataRole.FontRole,
|
||||||
|
Qt.ItemDataRole.ForegroundRole,
|
||||||
|
Qt.ItemDataRole.InitialSortOrderRole,
|
||||||
|
Qt.ItemDataRole.SizeHintRole,
|
||||||
|
Qt.ItemDataRole.StatusTipRole,
|
||||||
|
Qt.ItemDataRole.TextAlignmentRole,
|
||||||
|
Qt.ItemDataRole.ToolTipRole,
|
||||||
|
Qt.ItemDataRole.WhatsThisRole,
|
||||||
|
]:
|
||||||
|
return QVariant()
|
||||||
|
|
||||||
|
# Fall through to no-op
|
||||||
|
return QVariant()
|
||||||
|
|
||||||
|
def display_role(self, row: int, column: int, qrow: QueryRow) -> QVariant:
|
||||||
|
"""
|
||||||
|
Return text for display
|
||||||
|
"""
|
||||||
|
|
||||||
|
dispatch_table = {
|
||||||
|
QueryCol.ARTIST.value: QVariant(qrow.artist),
|
||||||
|
QueryCol.BITRATE.value: QVariant(qrow.bitrate),
|
||||||
|
QueryCol.DURATION.value: QVariant(ms_to_mmss(qrow.duration)),
|
||||||
|
QueryCol.LAST_PLAYED.value: QVariant(get_relative_date(qrow.lastplayed)),
|
||||||
|
QueryCol.TITLE.value: QVariant(qrow.title),
|
||||||
|
}
|
||||||
|
if column in dispatch_table:
|
||||||
|
return dispatch_table[column]
|
||||||
|
|
||||||
|
return QVariant()
|
||||||
|
|
||||||
|
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
|
||||||
|
"""
|
||||||
|
Standard model flags
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not index.isValid():
|
||||||
|
return Qt.ItemFlag.NoItemFlags
|
||||||
|
|
||||||
|
return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
|
||||||
|
|
||||||
|
def get_selected_track_ids(self) -> list[int]:
|
||||||
|
"""
|
||||||
|
Return a list of track_ids from selected tracks
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [self.querylist_rows[row].track_id for row in self._selected_rows]
|
||||||
|
|
||||||
|
def headerData(
|
||||||
|
self,
|
||||||
|
section: int,
|
||||||
|
orientation: Qt.Orientation,
|
||||||
|
role: int = Qt.ItemDataRole.DisplayRole,
|
||||||
|
) -> QVariant:
|
||||||
|
"""
|
||||||
|
Return text for headers
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_dispatch_table = {
|
||||||
|
QueryCol.TITLE.value: QVariant(Config.HEADER_TITLE),
|
||||||
|
QueryCol.ARTIST.value: QVariant(Config.HEADER_ARTIST),
|
||||||
|
QueryCol.DURATION.value: QVariant(Config.HEADER_DURATION),
|
||||||
|
QueryCol.LAST_PLAYED.value: QVariant(Config.HEADER_LAST_PLAYED),
|
||||||
|
QueryCol.BITRATE.value: QVariant(Config.HEADER_BITRATE),
|
||||||
|
}
|
||||||
|
|
||||||
|
if role == Qt.ItemDataRole.DisplayRole:
|
||||||
|
if orientation == Qt.Orientation.Horizontal:
|
||||||
|
return display_dispatch_table[section]
|
||||||
|
else:
|
||||||
|
if Config.ROWS_FROM_ZERO:
|
||||||
|
return QVariant(str(section))
|
||||||
|
else:
|
||||||
|
return QVariant(str(section + 1))
|
||||||
|
|
||||||
|
elif role == Qt.ItemDataRole.FontRole:
|
||||||
|
boldfont = QFont()
|
||||||
|
boldfont.setBold(True)
|
||||||
|
return QVariant(boldfont)
|
||||||
|
|
||||||
|
return QVariant()
|
||||||
|
|
||||||
|
def load_data(
|
||||||
|
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
|
||||||
|
to ensure the required fields are returned.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO: Move the SQLAlchemy parts to models later, but for now as proof
|
||||||
|
# of concept we'll keep it here.
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
# Clear any exsiting rows
|
||||||
|
self.querylist_rows = {}
|
||||||
|
row = 0
|
||||||
|
|
||||||
|
results = session.execute(text(sql)).mappings().all()
|
||||||
|
for result in results:
|
||||||
|
queryrow = QueryRow(
|
||||||
|
artist=result["artist"],
|
||||||
|
bitrate=result["bitrate"],
|
||||||
|
duration=result["duration"],
|
||||||
|
lastplayed=result["lastplayed"],
|
||||||
|
path=result["path"],
|
||||||
|
title=result["title"],
|
||||||
|
track_id=result["id"],
|
||||||
|
)
|
||||||
|
self.querylist_rows[row] = queryrow
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
|
||||||
|
"""Standard function for view"""
|
||||||
|
|
||||||
|
return len(self.querylist_rows)
|
||||||
|
|
||||||
|
def toggle_row_selection(self, row: int) -> None:
|
||||||
|
if row in self._selected_rows:
|
||||||
|
self._selected_rows.discard(row)
|
||||||
|
else:
|
||||||
|
self._selected_rows.add(row)
|
||||||
|
|
||||||
|
# Emit dataChanged for the entire row
|
||||||
|
top_left = self.index(row, 0)
|
||||||
|
bottom_right = self.index(row, self.columnCount() - 1)
|
||||||
|
self.dataChanged.emit(top_left, bottom_right, [Qt.ItemDataRole.BackgroundRole])
|
||||||
|
|
||||||
|
def tooltip_role(self, row: int, column: int, rat: RowAndTrack) -> QVariant:
|
||||||
|
"""
|
||||||
|
Return tooltip. Currently only used for last_played column.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if column != QueryCol.LAST_PLAYED.value:
|
||||||
|
return QVariant()
|
||||||
|
with db.Session() as session:
|
||||||
|
track_id = self.querylist_rows[row].track_id
|
||||||
|
if not track_id:
|
||||||
|
return QVariant()
|
||||||
|
playdates = Playdates.last_playdates(session, track_id)
|
||||||
|
return QVariant(
|
||||||
|
"<br>".join(
|
||||||
|
[
|
||||||
|
a.lastplayed.strftime(Config.LAST_PLAYED_TOOLTIP_DATE_FORMAT)
|
||||||
|
for a in reversed(playdates)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
193
app/querylists.py
Normal file
193
app/querylists.py
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
# 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
|
||||||
@ -997,6 +997,9 @@ padding-left: 8px;</string>
|
|||||||
<addaction name="actionRenamePlaylist"/>
|
<addaction name="actionRenamePlaylist"/>
|
||||||
<addaction name="actionDeletePlaylist"/>
|
<addaction name="actionDeletePlaylist"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
|
<addaction name="actionOpenQuerylist"/>
|
||||||
|
<addaction name="actionManage_querylists"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
<addaction name="actionSave_as_template"/>
|
<addaction name="actionSave_as_template"/>
|
||||||
<addaction name="actionManage_templates"/>
|
<addaction name="actionManage_templates"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
@ -1140,7 +1143,7 @@ padding-left: 8px;</string>
|
|||||||
</action>
|
</action>
|
||||||
<action name="actionOpenPlaylist">
|
<action name="actionOpenPlaylist">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>O&pen...</string>
|
<string>Open &playlist...</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionNewPlaylist">
|
<action name="actionNewPlaylist">
|
||||||
@ -1369,6 +1372,16 @@ padding-left: 8px;</string>
|
|||||||
<string>Import files...</string>
|
<string>Import files...</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionOpenQuerylist">
|
||||||
|
<property name="text">
|
||||||
|
<string>Open &querylist...</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionManage_querylists">
|
||||||
|
<property name="text">
|
||||||
|
<string>Manage querylists...</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Form implementation generated from reading ui file 'app/ui/main_window.ui'
|
# Form implementation generated from reading ui file 'app/ui/main_window.ui'
|
||||||
#
|
#
|
||||||
# Created by: PyQt6 UI code generator 6.8.0
|
# Created by: PyQt6 UI code generator 6.8.1
|
||||||
#
|
#
|
||||||
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
|
# 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.
|
# run again. Do not edit this file unless you know what you are doing.
|
||||||
@ -657,6 +657,10 @@ class Ui_MainWindow(object):
|
|||||||
self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows")
|
self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows")
|
||||||
self.actionImport_files = QtGui.QAction(parent=MainWindow)
|
self.actionImport_files = QtGui.QAction(parent=MainWindow)
|
||||||
self.actionImport_files.setObjectName("actionImport_files")
|
self.actionImport_files.setObjectName("actionImport_files")
|
||||||
|
self.actionOpenQuerylist = QtGui.QAction(parent=MainWindow)
|
||||||
|
self.actionOpenQuerylist.setObjectName("actionOpenQuerylist")
|
||||||
|
self.actionManage_querylists = QtGui.QAction(parent=MainWindow)
|
||||||
|
self.actionManage_querylists.setObjectName("actionManage_querylists")
|
||||||
self.menuFile.addSeparator()
|
self.menuFile.addSeparator()
|
||||||
self.menuFile.addAction(self.actionInsertTrack)
|
self.menuFile.addAction(self.actionInsertTrack)
|
||||||
self.menuFile.addAction(self.actionRemove)
|
self.menuFile.addAction(self.actionRemove)
|
||||||
@ -680,6 +684,9 @@ class Ui_MainWindow(object):
|
|||||||
self.menuPlaylist.addAction(self.actionRenamePlaylist)
|
self.menuPlaylist.addAction(self.actionRenamePlaylist)
|
||||||
self.menuPlaylist.addAction(self.actionDeletePlaylist)
|
self.menuPlaylist.addAction(self.actionDeletePlaylist)
|
||||||
self.menuPlaylist.addSeparator()
|
self.menuPlaylist.addSeparator()
|
||||||
|
self.menuPlaylist.addAction(self.actionOpenQuerylist)
|
||||||
|
self.menuPlaylist.addAction(self.actionManage_querylists)
|
||||||
|
self.menuPlaylist.addSeparator()
|
||||||
self.menuPlaylist.addAction(self.actionSave_as_template)
|
self.menuPlaylist.addAction(self.actionSave_as_template)
|
||||||
self.menuPlaylist.addAction(self.actionManage_templates)
|
self.menuPlaylist.addAction(self.actionManage_templates)
|
||||||
self.menuPlaylist.addSeparator()
|
self.menuPlaylist.addSeparator()
|
||||||
@ -757,7 +764,7 @@ class Ui_MainWindow(object):
|
|||||||
)
|
)
|
||||||
self.actionE_xit.setText(_translate("MainWindow", "E&xit"))
|
self.actionE_xit.setText(_translate("MainWindow", "E&xit"))
|
||||||
self.actionTest.setText(_translate("MainWindow", "&Test"))
|
self.actionTest.setText(_translate("MainWindow", "&Test"))
|
||||||
self.actionOpenPlaylist.setText(_translate("MainWindow", "O&pen..."))
|
self.actionOpenPlaylist.setText(_translate("MainWindow", "Open &playlist..."))
|
||||||
self.actionNewPlaylist.setText(_translate("MainWindow", "&New..."))
|
self.actionNewPlaylist.setText(_translate("MainWindow", "&New..."))
|
||||||
self.actionTestFunction.setText(_translate("MainWindow", "&Test function"))
|
self.actionTestFunction.setText(_translate("MainWindow", "&Test function"))
|
||||||
self.actionSkipToFade.setText(
|
self.actionSkipToFade.setText(
|
||||||
@ -840,4 +847,4 @@ class Ui_MainWindow(object):
|
|||||||
|
|
||||||
|
|
||||||
from infotabs import InfoTabs
|
from infotabs import InfoTabs
|
||||||
from pyqtgraph import PlotWidget # type: ignore
|
from pyqtgraph import PlotWidget
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
from importlib import import_module
|
|
||||||
from alembic import context
|
|
||||||
from alchemical.alembic.env import run_migrations
|
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
|
||||||
# access to the values within the .ini file in use.
|
|
||||||
config = context.config
|
|
||||||
|
|
||||||
# import the application's Alchemical instance
|
|
||||||
try:
|
|
||||||
import_mod, db_name = config.get_main_option('alchemical_db', '').split(
|
|
||||||
':')
|
|
||||||
db = getattr(import_module(import_mod), db_name)
|
|
||||||
except (ModuleNotFoundError, AttributeError):
|
|
||||||
raise ValueError(
|
|
||||||
'Could not import the Alchemical database instance. '
|
|
||||||
'Ensure that the alchemical_db setting in alembic.ini is correct.'
|
|
||||||
)
|
|
||||||
|
|
||||||
# run the migration engine
|
|
||||||
# The dictionary provided as second argument includes options to pass to the
|
|
||||||
# Alembic context. For details on what other options are available, see
|
|
||||||
# https://alembic.sqlalchemy.org/en/latest/autogenerate.html
|
|
||||||
run_migrations(db, {
|
|
||||||
'render_as_batch': True,
|
|
||||||
'compare_type': True,
|
|
||||||
})
|
|
||||||
1
migrations/env.py
Symbolic link
1
migrations/env.py
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
env.py.DEBUG
|
||||||
28
migrations/env.py.DEBUG
Normal file
28
migrations/env.py.DEBUG
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from importlib import import_module
|
||||||
|
from alembic import context
|
||||||
|
from alchemical.alembic.env import run_migrations
|
||||||
|
|
||||||
|
# Load Alembic configuration
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Import the Alchemical database instance as specified in alembic.ini
|
||||||
|
import_mod, db_name = config.get_main_option('alchemical_db', '').split(':')
|
||||||
|
db = getattr(import_module(import_mod), db_name)
|
||||||
|
print(f"Successfully loaded Alchemical database instance: {db}")
|
||||||
|
|
||||||
|
# Use the metadata associated with the Alchemical instance
|
||||||
|
metadata = db.Model.metadata
|
||||||
|
print(f"Metadata tables detected: {metadata.tables.keys()}") # Debug output
|
||||||
|
except (ModuleNotFoundError, AttributeError) as e:
|
||||||
|
raise ValueError(
|
||||||
|
'Could not import the Alchemical database instance or access metadata. '
|
||||||
|
'Ensure that the alchemical_db setting in alembic.ini is correct and '
|
||||||
|
'that the Alchemical instance is correctly configured.'
|
||||||
|
) from e
|
||||||
|
|
||||||
|
# Run migrations with metadata
|
||||||
|
run_migrations(db, {
|
||||||
|
'render_as_batch': True,
|
||||||
|
'compare_type': True,
|
||||||
|
})
|
||||||
27
migrations/env.py.NODEBUG
Normal file
27
migrations/env.py.NODEBUG
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from importlib import import_module
|
||||||
|
from alembic import context
|
||||||
|
from alchemical.alembic.env import run_migrations
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# import the application's Alchemical instance
|
||||||
|
try:
|
||||||
|
import_mod, db_name = config.get_main_option('alchemical_db', '').split(
|
||||||
|
':')
|
||||||
|
db = getattr(import_module(import_mod), db_name)
|
||||||
|
except (ModuleNotFoundError, AttributeError):
|
||||||
|
raise ValueError(
|
||||||
|
'Could not import the Alchemical database instance. '
|
||||||
|
'Ensure that the alchemical_db setting in alembic.ini is correct.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# run the migration engine
|
||||||
|
# The dictionary provided as second argument includes options to pass to the
|
||||||
|
# Alembic context. For details on what other options are available, see
|
||||||
|
# https://alembic.sqlalchemy.org/en/latest/autogenerate.html
|
||||||
|
run_migrations(db, {
|
||||||
|
'render_as_batch': True,
|
||||||
|
'compare_type': True,
|
||||||
|
})
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
"""Add data for query playlists
|
||||||
|
|
||||||
|
Revision ID: 014f2d4c88a5
|
||||||
|
Revises: 33c04e3c12c8
|
||||||
|
Create Date: 2024-12-30 14:23:36.924478
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '014f2d4c88a5'
|
||||||
|
down_revision = '33c04e3c12c8'
|
||||||
|
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('query', sa.String(length=2048), nullable=False),
|
||||||
|
sa.Column('playlist_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['playlist_id'], ['playlists.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 ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade_() -> None:
|
||||||
|
# ### 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')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
"""Index for notesolours substring
|
||||||
|
|
||||||
|
Revision ID: c76e865ccb85
|
||||||
|
Revises: 33c04e3c12c8
|
||||||
|
Create Date: 2025-02-07 18:21:01.760057
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'c76e865ccb85'
|
||||||
|
down_revision = '33c04e3c12c8'
|
||||||
|
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! ###
|
||||||
|
with op.batch_alter_table('notecolours', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_notecolours_substring'), ['substring'], unique=False)
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
with op.batch_alter_table('playlist_rows', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint('playlist_rows_ibfk_1', type_='foreignkey')
|
||||||
|
batch_op.create_foreign_key(None, 'tracks', ['track_id'], ['id'], ondelete='CASCADE')
|
||||||
|
|
||||||
|
with op.batch_alter_table('queries', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint('fk_queries_playlist_id_playlists', type_='foreignkey')
|
||||||
|
batch_op.create_foreign_key(None, 'playlists', ['playlist_id'], ['id'], ondelete='CASCADE')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade_() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('queries', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint(None, type_='foreignkey')
|
||||||
|
batch_op.create_foreign_key('fk_queries_playlist_id_playlists', 'playlists', ['playlist_id'], ['id'])
|
||||||
|
|
||||||
|
with op.batch_alter_table('playlist_rows', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint(None, type_='foreignkey')
|
||||||
|
batch_op.create_foreign_key('playlist_rows_ibfk_1', 'tracks', ['track_id'], ['id'])
|
||||||
|
|
||||||
|
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'])
|
||||||
|
|
||||||
|
with op.batch_alter_table('notecolours', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_notecolours_substring'))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user