123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- PLUGIN_NAME = "Ami (Columns)"
- PLUGIN_AUTHOR = "SuperSaltyGamer"
- PLUGIN_DESCRIPTION = "Custom computed columns in cluster and album panes."
- PLUGIN_VERSION = "1.0.0"
- PLUGIN_API_VERSIONS = ["2.10"]
- PLUGIN_LICENSE = "AGPL-3.0"
- from typing import Dict, Tuple
- from picard import log
- from picard.album import Album
- from picard.config import ListOption, IntOption, config
- from picard.file import File
- from picard.metadata import Metadata
- from picard.script import ScriptParser
- from picard.ui.itemviews import MainPanel
- from picard.ui.options import OptionsPage, register_options_page
- from picard.ui.util import qlistwidget_items
- from picard.ui.widgets.scriptlistwidget import ScriptListWidget, ScriptListWidgetItem
- from picard.ui.widgets.scripttextedit import ScriptTextEdit
- from PyQt5 import QtCore, QtWidgets
- OPTION_COLUMNS = "ami_columns_columns"
- OPTION_SELECTED_COLUMN = "ami_columns_selected_column"
- computed_columns: Dict[str, Tuple[bool, str]] = {}
- script_parser = ScriptParser()
- original_file_column = File.column
- original_album_column = Album.column
- def override_file_column(self: File, column_name: str):
- try:
- column = computed_columns.get(column_name, None)
- if column is None or not column[0]: # Handle built-in columns as usual.
- return original_file_column(self, column_name)
- new_metadata = Metadata()
- for name in self.metadata:
- new_metadata[name] = self.metadata.getall(name)
- value = script_parser.eval(column[1], new_metadata, self) or ""
- if not hasattr(self, "_computed_column_values"):
- self._computed_column_values = {}
- self._computed_column_values[column_name] = value
- return value
- except Exception:
- log.warning("Failed to compute column '%s' for %s", column_name, self, exc_info=True)
- def override_album_column(self: Album, column_name: str):
- try:
- column = computed_columns.get(column_name, None)
- if column is None or not column[0]: # Handle built-in columns as usual.
- return original_album_column(self, column_name)
- values = set()
- for track in self.tracks:
- for file in track.files:
- if hasattr(file, "_computed_column_values"):
- values.add(file._computed_column_values.get(column_name, ""))
- values_count = len(values)
- if values_count == 0:
- return ""
- elif values_count == 1:
- return values.pop()
- else:
- return f"(different across {len(values)} items)"
- except Exception:
- log.warning("Failed to compute column '%s' for %s", column_name, self, exc_info=True)
- File.column = override_file_column
- Album.column = override_album_column
- def update_columns():
- global computed_columns
- computed_columns = {name: (enabled, text) for name, enabled, text in config.setting[OPTION_COLUMNS]}
- MainPanel.columns = [column for column in MainPanel.columns if column[0] != column[1]]
- for name in computed_columns.keys():
- MainPanel.columns.append((name, name))
- MainPanel._column_indexes = {column[1]: i for i, column in enumerate(MainPanel.columns)}
- class PluginOptionsPage(OptionsPage):
- NAME = "ami_columns"
- TITLE = "Ami (Columns)"
- PARENT = "plugins"
- options = [
- ListOption("setting", OPTION_COLUMNS, []),
- IntOption("persist", OPTION_SELECTED_COLUMN, 0)
- ]
- def __init__(self, parent=None):
- super(PluginOptionsPage, self).__init__(parent)
- self.ui = Ui_PluginOptionsPage(self)
- def load(self):
- self.ui.script_list.clear()
- for name, enabled, text in config.setting[OPTION_COLUMNS]:
- self.ui.script_list.addItem(ScriptListWidgetItem(name, enabled, text))
- item = self.ui.script_list.item(config.persist[OPTION_SELECTED_COLUMN])
- if item:
- self.ui.script_list.setCurrentItem(item)
- item.setSelected(True)
- def save(self):
- scripts: list[(str, bool, str)] = []
- for item in qlistwidget_items(self.ui.script_list):
- pos, name, enabled, text = item.get_all()
- scripts.append((name, enabled, text))
- config.setting[OPTION_COLUMNS] = scripts
- config.persist[OPTION_SELECTED_COLUMN] = self.ui.script_list.currentRow()
- update_columns()
- def add_script(self):
- item = ScriptListWidgetItem(f"Column")
- item.setCheckState(QtCore.Qt.CheckState.Checked)
- self.ui.script_list.addItem(item)
- self.ui.script_list.setCurrentItem(item, QtCore.QItemSelectionModel.SelectionFlag.Clear | QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent)
- def remove_script(self):
- items = self.ui.script_list.selectedItems()
- if items:
- item = self.ui.script_list.takeItem(self.ui.script_list.row(items[0]))
- del item
- def script_selected(self):
- items = self.ui.script_list.selectedItems()
- if items:
- item = items[0]
- self.ui.script_edit.setEnabled(True)
- self.ui.script_edit.setText(item.script)
- self.ui.script_edit.setFocus(QtCore.Qt.FocusReason.OtherFocusReason)
- else:
- self.ui.script_edit.setEnabled(False)
- self.ui.script_edit.setText("")
- def live_update_and_check(self):
- items = self.ui.script_list.selectedItems()
- if not items:
- return
- item = items[0]
- item.script = self.ui.script_edit.toPlainText()
- self.ui.error_label.setStyleSheet("")
- self.ui.error_label.setText("")
- try:
- script_parser.eval(item.script)
- item.has_error = False
- except Exception as err:
- item.has_error = True
- self.ui.error_label.setStyleSheet(self.STYLESHEET_ERROR)
- self.ui.error_label.setText(str(err))
- return
- class Ui_PluginOptionsPage(object):
- def __init__(self, page: PluginOptionsPage):
- page.setObjectName("plugin_options_page")
- self.vertical_layout = QtWidgets.QVBoxLayout(page)
- self.vertical_layout.setObjectName("vertical_layout")
- self.warn_label = QtWidgets.QLabel(page)
- self.warn_label.setObjectName("warn_label")
- self.warn_label.setText("<font color='#ff3333'><b>Restart Picard before modifying visible columns in panes.</b></font>")
- self.vertical_layout.addWidget(self.warn_label)
- self.info_label = QtWidgets.QLabel(page)
- self.info_label.setObjectName("info_label")
- self.info_label.setText("Enabled columns will be computed in cluster and album panes.")
- self.vertical_layout.addWidget(self.info_label)
- self.splitter = QtWidgets.QSplitter(page)
- self.splitter.setObjectName("splitter")
- self.splitter.setOrientation(QtCore.Qt.Orientation.Horizontal)
- size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
- size_policy.setHorizontalStretch(0)
- size_policy.setVerticalStretch(0)
- size_policy.setHeightForWidth(self.splitter.sizePolicy().hasHeightForWidth())
- self.splitter.setSizePolicy(size_policy)
- self.script_list = ScriptListWidget(self.splitter)
- self.script_list.setObjectName("script_list")
- self.script_list.setMinimumSize(QtCore.QSize(120, 0))
- self.script_list.itemSelectionChanged.connect(page.script_selected)
- self.form_widget = QtWidgets.QWidget(self.splitter)
- self.form_widget.setObjectName("form_widget")
- self.vertical_layout_2 = QtWidgets.QVBoxLayout(self.form_widget)
- self.vertical_layout_2.setContentsMargins(0, 0, 0, 0)
- self.vertical_layout_2.setObjectName("vertical_layout_2")
- self.script_edit = ScriptTextEdit(self.form_widget)
- self.script_edit.setObjectName("script_edit")
- self.script_edit.setAcceptRichText(False)
- self.script_edit.textChanged.connect(page.live_update_and_check)
- self.vertical_layout_2.addWidget(self.script_edit)
- self.vertical_layout.addWidget(self.splitter)
- self.horizontal_layout = QtWidgets.QHBoxLayout()
- self.horizontal_layout.setObjectName("horizontal_layout")
- self.add_button = QtWidgets.QToolButton(page)
- self.add_button.setObjectName("add_button")
- self.add_button.setText("Add new column")
- self.add_button.clicked.connect(page.add_script)
- self.horizontal_layout.addWidget(self.add_button)
- self.remove_button = QtWidgets.QToolButton(page)
- self.remove_button.setObjectName("remove_button")
- self.remove_button.setText("Remove column")
- self.remove_button.clicked.connect(page.remove_script)
- self.horizontal_layout.addWidget(self.remove_button)
- self.horizontal_layout.addItem(QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum))
- self.vertical_layout.addLayout(self.horizontal_layout)
- self.error_label = QtWidgets.QLabel(page)
- self.error_label.setObjectName("script_error")
- self.error_label.setText("")
- self.error_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
- self.vertical_layout.addWidget(self.error_label)
- register_options_page(PluginOptionsPage)
- update_columns()
|