__init__.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. PLUGIN_NAME = "Ami (Columns)"
  2. PLUGIN_AUTHOR = "SuperSaltyGamer"
  3. PLUGIN_DESCRIPTION = "Custom computed columns in cluster and album panes."
  4. PLUGIN_VERSION = "1.0.0"
  5. PLUGIN_API_VERSIONS = ["2.10"]
  6. PLUGIN_LICENSE = "AGPL-3.0"
  7. from typing import Dict, Tuple
  8. from picard import log
  9. from picard.album import Album
  10. from picard.config import ListOption, IntOption, config
  11. from picard.file import File
  12. from picard.metadata import Metadata
  13. from picard.script import ScriptParser
  14. from picard.ui.itemviews import MainPanel
  15. from picard.ui.options import OptionsPage, register_options_page
  16. from picard.ui.util import qlistwidget_items
  17. from picard.ui.widgets.scriptlistwidget import ScriptListWidget, ScriptListWidgetItem
  18. from picard.ui.widgets.scripttextedit import ScriptTextEdit
  19. from PyQt5 import QtCore, QtWidgets
  20. OPTION_COLUMNS = "ami_columns_columns"
  21. OPTION_SELECTED_COLUMN = "ami_columns_selected_column"
  22. computed_columns: Dict[str, Tuple[bool, str]] = {}
  23. script_parser = ScriptParser()
  24. original_file_column = File.column
  25. original_album_column = Album.column
  26. def override_file_column(self: File, column_name: str):
  27. try:
  28. column = computed_columns.get(column_name, None)
  29. if column is None or not column[0]: # Handle built-in columns as usual.
  30. return original_file_column(self, column_name)
  31. new_metadata = Metadata()
  32. for name in self.metadata:
  33. new_metadata[name] = self.metadata.getall(name)
  34. value = script_parser.eval(column[1], new_metadata, self) or ""
  35. if not hasattr(self, "_computed_column_values"):
  36. self._computed_column_values = {}
  37. self._computed_column_values[column_name] = value
  38. return value
  39. except Exception:
  40. log.warning("Failed to compute column '%s' for %s", column_name, self, exc_info=True)
  41. def override_album_column(self: Album, column_name: str):
  42. try:
  43. column = computed_columns.get(column_name, None)
  44. if column is None or not column[0]: # Handle built-in columns as usual.
  45. return original_album_column(self, column_name)
  46. values = set()
  47. for track in self.tracks:
  48. for file in track.files:
  49. if hasattr(file, "_computed_column_values"):
  50. values.add(file._computed_column_values.get(column_name, ""))
  51. values_count = len(values)
  52. if values_count == 0:
  53. return ""
  54. elif values_count == 1:
  55. return values.pop()
  56. else:
  57. return f"(different across {len(values)} items)"
  58. except Exception:
  59. log.warning("Failed to compute column '%s' for %s", column_name, self, exc_info=True)
  60. File.column = override_file_column
  61. Album.column = override_album_column
  62. def update_columns():
  63. global computed_columns
  64. computed_columns = {name: (enabled, text) for name, enabled, text in config.setting[OPTION_COLUMNS]}
  65. MainPanel.columns = [column for column in MainPanel.columns if column[0] != column[1]]
  66. for name in computed_columns.keys():
  67. MainPanel.columns.append((name, name))
  68. MainPanel._column_indexes = {column[1]: i for i, column in enumerate(MainPanel.columns)}
  69. class PluginOptionsPage(OptionsPage):
  70. NAME = "ami_columns"
  71. TITLE = "Ami (Columns)"
  72. PARENT = "plugins"
  73. options = [
  74. ListOption("setting", OPTION_COLUMNS, []),
  75. IntOption("persist", OPTION_SELECTED_COLUMN, 0)
  76. ]
  77. def __init__(self, parent=None):
  78. super(PluginOptionsPage, self).__init__(parent)
  79. self.ui = Ui_PluginOptionsPage(self)
  80. def load(self):
  81. self.ui.script_list.clear()
  82. for name, enabled, text in config.setting[OPTION_COLUMNS]:
  83. self.ui.script_list.addItem(ScriptListWidgetItem(name, enabled, text))
  84. item = self.ui.script_list.item(config.persist[OPTION_SELECTED_COLUMN])
  85. if item:
  86. self.ui.script_list.setCurrentItem(item)
  87. item.setSelected(True)
  88. def save(self):
  89. scripts: list[(str, bool, str)] = []
  90. for item in qlistwidget_items(self.ui.script_list):
  91. pos, name, enabled, text = item.get_all()
  92. scripts.append((name, enabled, text))
  93. config.setting[OPTION_COLUMNS] = scripts
  94. config.persist[OPTION_SELECTED_COLUMN] = self.ui.script_list.currentRow()
  95. update_columns()
  96. def add_script(self):
  97. item = ScriptListWidgetItem(f"Column")
  98. item.setCheckState(QtCore.Qt.CheckState.Checked)
  99. self.ui.script_list.addItem(item)
  100. self.ui.script_list.setCurrentItem(item, QtCore.QItemSelectionModel.SelectionFlag.Clear | QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent)
  101. def remove_script(self):
  102. items = self.ui.script_list.selectedItems()
  103. if items:
  104. item = self.ui.script_list.takeItem(self.ui.script_list.row(items[0]))
  105. del item
  106. def script_selected(self):
  107. items = self.ui.script_list.selectedItems()
  108. if items:
  109. item = items[0]
  110. self.ui.script_edit.setEnabled(True)
  111. self.ui.script_edit.setText(item.script)
  112. self.ui.script_edit.setFocus(QtCore.Qt.FocusReason.OtherFocusReason)
  113. else:
  114. self.ui.script_edit.setEnabled(False)
  115. self.ui.script_edit.setText("")
  116. def live_update_and_check(self):
  117. items = self.ui.script_list.selectedItems()
  118. if not items:
  119. return
  120. item = items[0]
  121. item.script = self.ui.script_edit.toPlainText()
  122. self.ui.error_label.setStyleSheet("")
  123. self.ui.error_label.setText("")
  124. try:
  125. script_parser.eval(item.script)
  126. item.has_error = False
  127. except Exception as err:
  128. item.has_error = True
  129. self.ui.error_label.setStyleSheet(self.STYLESHEET_ERROR)
  130. self.ui.error_label.setText(str(err))
  131. return
  132. class Ui_PluginOptionsPage(object):
  133. def __init__(self, page: PluginOptionsPage):
  134. page.setObjectName("plugin_options_page")
  135. self.vertical_layout = QtWidgets.QVBoxLayout(page)
  136. self.vertical_layout.setObjectName("vertical_layout")
  137. self.warn_label = QtWidgets.QLabel(page)
  138. self.warn_label.setObjectName("warn_label")
  139. self.warn_label.setText("<font color='#ff3333'><b>Restart Picard before modifying visible columns in panes.</b></font>")
  140. self.vertical_layout.addWidget(self.warn_label)
  141. self.info_label = QtWidgets.QLabel(page)
  142. self.info_label.setObjectName("info_label")
  143. self.info_label.setText("Enabled columns will be computed in cluster and album panes.")
  144. self.vertical_layout.addWidget(self.info_label)
  145. self.splitter = QtWidgets.QSplitter(page)
  146. self.splitter.setObjectName("splitter")
  147. self.splitter.setOrientation(QtCore.Qt.Orientation.Horizontal)
  148. size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding)
  149. size_policy.setHorizontalStretch(0)
  150. size_policy.setVerticalStretch(0)
  151. size_policy.setHeightForWidth(self.splitter.sizePolicy().hasHeightForWidth())
  152. self.splitter.setSizePolicy(size_policy)
  153. self.script_list = ScriptListWidget(self.splitter)
  154. self.script_list.setObjectName("script_list")
  155. self.script_list.setMinimumSize(QtCore.QSize(120, 0))
  156. self.script_list.itemSelectionChanged.connect(page.script_selected)
  157. self.form_widget = QtWidgets.QWidget(self.splitter)
  158. self.form_widget.setObjectName("form_widget")
  159. self.vertical_layout_2 = QtWidgets.QVBoxLayout(self.form_widget)
  160. self.vertical_layout_2.setContentsMargins(0, 0, 0, 0)
  161. self.vertical_layout_2.setObjectName("vertical_layout_2")
  162. self.script_edit = ScriptTextEdit(self.form_widget)
  163. self.script_edit.setObjectName("script_edit")
  164. self.script_edit.setAcceptRichText(False)
  165. self.script_edit.textChanged.connect(page.live_update_and_check)
  166. self.vertical_layout_2.addWidget(self.script_edit)
  167. self.vertical_layout.addWidget(self.splitter)
  168. self.horizontal_layout = QtWidgets.QHBoxLayout()
  169. self.horizontal_layout.setObjectName("horizontal_layout")
  170. self.add_button = QtWidgets.QToolButton(page)
  171. self.add_button.setObjectName("add_button")
  172. self.add_button.setText("Add new column")
  173. self.add_button.clicked.connect(page.add_script)
  174. self.horizontal_layout.addWidget(self.add_button)
  175. self.remove_button = QtWidgets.QToolButton(page)
  176. self.remove_button.setObjectName("remove_button")
  177. self.remove_button.setText("Remove column")
  178. self.remove_button.clicked.connect(page.remove_script)
  179. self.horizontal_layout.addWidget(self.remove_button)
  180. self.horizontal_layout.addItem(QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum))
  181. self.vertical_layout.addLayout(self.horizontal_layout)
  182. self.error_label = QtWidgets.QLabel(page)
  183. self.error_label.setObjectName("script_error")
  184. self.error_label.setText("")
  185. self.error_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
  186. self.vertical_layout.addWidget(self.error_label)
  187. register_options_page(PluginOptionsPage)
  188. update_columns()