Merge pull request #2496 from zas/list_of_scripts

Introduce TaggingScriptSetting and few associated methods
This commit is contained in:
Laurent Monin
2024-05-27 12:32:34 +02:00
committed by GitHub
12 changed files with 192 additions and 137 deletions

View File

@@ -82,7 +82,7 @@ from picard.plugin import (
from picard.script import (
ScriptError,
ScriptParser,
enabled_tagger_scripts_texts,
iter_active_tagging_scripts,
)
from picard.track import Track
from picard.util import (
@@ -474,21 +474,21 @@ class Album(DataObject, MetadataItem):
track.metadata_images_changed.connect(self.update_metadata_images)
# Prepare parser for user's script
for s_name, s_text in enabled_tagger_scripts_texts():
for script in iter_active_tagging_scripts():
parser = ScriptParser()
for track in self._new_tracks:
# Run tagger script for each track
try:
parser.eval(s_text, track.metadata)
parser.eval(script.content, track.metadata)
except ScriptError:
log.exception("Failed to run tagger script %s on track", s_name)
log.exception("Failed to run tagger script %s on track", script.name)
track.metadata.strip_whitespace()
track.scripted_metadata.update(track.metadata)
# Run tagger script for the album itself
try:
parser.eval(s_text, self._new_metadata)
parser.eval(script.content, self._new_metadata)
except ScriptError:
log.exception("Failed to run tagger script %s on album", s_name)
log.exception("Failed to run tagger script %s on album", script.name)
self._new_metadata.strip_whitespace()
unmatched_files = [file for track in self.tracks for file in track.files]

View File

@@ -429,19 +429,19 @@ def upgrade_to_v2_7_0dev3(config):
"""
from picard.script import get_file_naming_script_presets
from picard.script.serializer import (
FileNamingScript,
ScriptImportError,
FileNamingScriptInfo,
ScriptSerializerFromFileError,
)
scripts = {}
for item in config.setting.raw_value('file_naming_scripts') or []:
try:
script_item = FileNamingScript().create_from_yaml(item, create_new_id=False)
script_item = FileNamingScriptInfo().create_from_yaml(item, create_new_id=False)
scripts[script_item['id']] = script_item.to_dict()
except ScriptImportError:
except ScriptSerializerFromFileError:
log.error("Error converting file naming script")
script_list = set(scripts.keys()) | set(map(lambda item: item['id'], get_file_naming_script_presets()))
if config.setting['selected_file_naming_script_id'] not in script_list:
script_item = FileNamingScript(
script_item = FileNamingScriptInfo(
script=config.setting.value('file_naming_format', TextOption),
title=_("Primary file naming script"),
readonly=False,

View File

@@ -63,7 +63,43 @@ from picard.script.parser import ( # noqa: F401 # pylint: disable=unused-import
ScriptUnknownFunction,
ScriptVariable,
)
from picard.script.serializer import FileNamingScript
from picard.script.serializer import FileNamingScriptInfo
class TaggingScriptSetting:
def __init__(self, pos=0, name="", enabled=False, content=""):
self.pos = pos
self.name = name
self.enabled = enabled
self.content = content
def iter_tagging_scripts_from_config(config=None):
if config is None:
config = get_config()
yield from iter_tagging_scripts_from_tuples(config.setting['list_of_scripts'])
def iter_tagging_scripts_from_tuples(tuples):
for pos, name, enabled, content in tuples:
yield TaggingScriptSetting(pos=pos, name=name, enabled=enabled, content=content)
def save_tagging_scripts_to_config(scripts, config=None):
if config is None:
config = get_config()
config.setting['list_of_scripts'] = [(s.pos, s.name, s.enabled, s.content) for s in scripts]
def iter_active_tagging_scripts(config=None):
"""Returns an iterator over the enabled and not empty tagging scripts."""
if config is None:
config = get_config()
if not config.setting['enable_tagger_scripts']:
return
for script in iter_tagging_scripts_from_config(config=config):
if script.enabled and script.content:
yield script
class ScriptFunctionDocError(Exception):
@@ -111,15 +147,6 @@ def script_function_documentation_all(fmt='markdown', pre='',
return "\n".join(doc_elements)
def enabled_tagger_scripts_texts():
"""Returns an iterator over the enabled tagger scripts.
For each script, you'll get a tuple consisting of the script name and text"""
config = get_config()
if not config.setting['enable_tagger_scripts']:
return []
return [(s_name, s_text) for _s_pos, s_name, s_enabled, s_text in config.setting['list_of_scripts'] if s_enabled and s_text]
def get_file_naming_script(settings):
"""Retrieve the file naming script.
@@ -146,7 +173,7 @@ def get_file_naming_script_presets():
"""Generator of preset example file naming script objects.
Yields:
FileNamingScript: the next example FileNamingScript object
FileNamingScriptInfo: the next example FileNamingScriptInfo object
"""
AUTHOR = "MusicBrainz Picard Development Team"
DESCRIPTION = _("This preset example file naming script does not require any special settings, tagging scripts or plugins.")
@@ -158,7 +185,7 @@ def get_file_naming_script_presets():
'title': _(title),
}
yield FileNamingScript(
yield FileNamingScriptInfo(
id=DEFAULT_NAMING_PRESET_ID,
title=preset_title(1, N_("Default file naming script")),
script=DEFAULT_FILE_NAMING_FORMAT,
@@ -170,7 +197,7 @@ def get_file_naming_script_presets():
script_language_version="1.0",
)
yield FileNamingScript(
yield FileNamingScriptInfo(
id="Preset 2",
title=preset_title(2, N_("[album artist]/[album]/[track #]. [title]")),
script="%albumartist%/\n"
@@ -184,7 +211,7 @@ def get_file_naming_script_presets():
script_language_version="1.0",
)
yield FileNamingScript(
yield FileNamingScriptInfo(
id="Preset 3",
title=preset_title(3, N_("[album artist]/[album]/[disc and track #] [artist] - [title]")),
script="$if2(%albumartist%,%artist%)/\n"

View File

@@ -49,7 +49,7 @@ from picard.util import make_filename_from_title
@unique
class PicardScriptType(IntEnum):
class ScriptSerializerType(IntEnum):
"""Picard Script object types
"""
BASE = 0
@@ -57,16 +57,28 @@ class PicardScriptType(IntEnum):
FILENAMING = 2
class ScriptImportExportError(Exception):
class ScriptSerializerError(Exception):
"""Base exception class for ScriptSerializer errors"""
class ScriptSerializerImportExportError(ScriptSerializerError):
def __init__(self, *args, format=None, filename=None, error_msg=None):
super().__init__(*args)
self.format = format
self.filename = filename
self.error_msg = error_msg
class ScriptImportError(Exception):
def __init__(self, *args):
super().__init__(*args)
class ScriptSerializerImportError(ScriptSerializerImportExportError):
"""Exception raised during script import"""
class ScriptSerializerExportError(ScriptSerializerImportExportError):
"""Exception raised during script export"""
class ScriptSerializerFromFileError(ScriptSerializerError):
"""Exception raised when converting a file to a ScriptSerializer"""
class MultilineLiteral(str):
@@ -80,12 +92,12 @@ class MultilineLiteral(str):
yaml.add_representer(MultilineLiteral, MultilineLiteral.yaml_presenter)
class PicardScript():
class ScriptSerializer():
"""Base class for Picard script objects.
"""
# Base class developed to support future tagging script class as possible replacement for currently used tuples in config.setting["list_of_scripts"].
TYPE = PicardScriptType.BASE
TYPE = ScriptSerializerType.BASE
OUTPUT_FIELDS = ('title', 'script_language_version', 'script', 'id')
# Don't automatically trigger changing the `script_last_updated` property when updating these properties.
@@ -226,7 +238,7 @@ class PicardScript():
with open(filename, 'w', encoding='utf-8') as o_file:
o_file.write(script_text)
except OSError as error:
raise ScriptImportExportError(format=FILE_ERROR_EXPORT, filename=filename, error_msg=error.strerror)
raise ScriptSerializerExportError(format=FILE_ERROR_EXPORT, filename=filename, error_msg=error.strerror)
dialog = QtWidgets.QMessageBox(
QtWidgets.QMessageBox.Icon.Information,
_("Export Script"),
@@ -255,14 +267,14 @@ class PicardScript():
with open(filename, 'r', encoding='utf-8') as i_file:
file_content = i_file.read()
except OSError as error:
raise ScriptImportExportError(format=FILE_ERROR_IMPORT, filename=filename, error_msg=error.strerror)
raise ScriptSerializerImportError(format=FILE_ERROR_IMPORT, filename=filename, error_msg=error.strerror)
if not file_content.strip():
raise ScriptImportExportError(format=FILE_ERROR_IMPORT, filename=filename, error_msg=N_("The file was empty"))
raise ScriptSerializerImportError(format=FILE_ERROR_IMPORT, filename=filename, error_msg=N_("The file was empty"))
if file_type == cls._file_types()['package']:
try:
return cls().create_from_yaml(file_content)
except ScriptImportError as error:
raise ScriptImportExportError(format=FILE_ERROR_DECODE, filename=filename, error_msg=error)
except ScriptSerializerFromFileError as error:
raise ScriptSerializerImportError(format=FILE_ERROR_DECODE, filename=filename, error_msg=error)
else:
return cls(
title=_("Imported from %s") % filename,
@@ -283,9 +295,9 @@ class PicardScript():
"""
new_object = cls()
if not isinstance(script_dict, Mapping):
raise ScriptImportError(N_("Argument is not a dictionary"))
raise ScriptSerializerFromFileError(N_("Argument is not a dictionary"))
if 'title' not in script_dict or 'script' not in script_dict:
raise ScriptImportError(N_("Invalid script package"))
raise ScriptSerializerFromFileError(N_("Invalid script package"))
new_object.update_from_dict(script_dict)
if create_new_id or not new_object['id']:
new_object._set_new_id()
@@ -327,9 +339,9 @@ class PicardScript():
new_object = cls()
yaml_dict = yaml.safe_load(yaml_string)
if not isinstance(yaml_dict, dict):
raise ScriptImportError(N_("File content not a dictionary"))
raise ScriptSerializerFromFileError(N_("File content not a dictionary"))
if 'title' not in yaml_dict or 'script' not in yaml_dict:
raise ScriptImportError(N_("Invalid script package"))
raise ScriptSerializerFromFileError(N_("Invalid script package"))
new_object.update_from_dict(yaml_dict)
if create_new_id or not new_object['id']:
new_object._set_new_id()
@@ -367,10 +379,10 @@ class PicardScript():
))
class TaggingScript(PicardScript):
class TaggingScriptInfo(ScriptSerializer):
"""Picard tagging script class
"""
TYPE = PicardScriptType.TAGGER
TYPE = ScriptSerializerType.TAGGER
OUTPUT_FIELDS = ('title', 'script_language_version', 'script', 'id')
def __init__(self, script='', title='', id=None, last_updated=None, script_language_version=None):
@@ -385,10 +397,10 @@ class TaggingScript(PicardScript):
super().__init__(script=script, title=title, id=id, last_updated=last_updated, script_language_version=script_language_version)
class FileNamingScript(PicardScript):
class FileNamingScriptInfo(ScriptSerializer):
"""Picard file naming script class
"""
TYPE = PicardScriptType.FILENAMING
TYPE = ScriptSerializerType.FILENAMING
OUTPUT_FIELDS = ('title', 'description', 'author', 'license', 'version', 'last_updated', 'script_language_version', 'script', 'id')
def __init__(

View File

@@ -71,7 +71,7 @@ from picard.metadata import (
from picard.script import (
ScriptError,
ScriptParser,
enabled_tagger_scripts_texts,
iter_active_tagging_scripts,
)
from picard.util import pattern_as_regex
from picard.util.imagelist import ImageList
@@ -201,12 +201,12 @@ class Track(DataObject, FileListItem):
@staticmethod
def run_scripts(metadata, strip_whitespace=False):
for s_name, s_text in enabled_tagger_scripts_texts():
for script in iter_active_tagging_scripts():
parser = ScriptParser()
try:
parser.eval(s_text, metadata)
parser.eval(script.content, metadata)
except ScriptError:
log.exception("Failed to run tagger script %s on track", s_name)
log.exception("Failed to run tagger script %s on track", script.name)
if strip_whitespace:
metadata.strip_whitespace()

View File

@@ -75,6 +75,7 @@ from picard.extension_points.item_actions import (
)
from picard.file import File
from picard.i18n import gettext as _
from picard.script import iter_tagging_scripts_from_tuples
from picard.track import (
NonAlbumTrack,
Track,
@@ -403,8 +404,6 @@ class BaseTreeView(QtWidgets.QTreeWidget):
CollectionMenu(selected_albums, _("Collections"), menu),
)
scripts = config.setting['list_of_scripts']
if plugin_actions:
plugin_menu = QtWidgets.QMenu(_("P&lugins"), menu)
plugin_menu.setIcon(self.icon_plugins)
@@ -424,8 +423,9 @@ class BaseTreeView(QtWidgets.QTreeWidget):
action_menu = plugin_menus[key] = action_menu.addMenu(key[-1])
action_menu.addAction(action)
scripts = config.setting['list_of_scripts']
if scripts:
scripts_menu = ScriptsMenu(scripts, _("&Run scripts"), menu)
scripts_menu = ScriptsMenu(iter_tagging_scripts_from_tuples(scripts), _("&Run scripts"), menu)
scripts_menu.setIcon(self.icon_plugins)
add_actions(
'-',

View File

@@ -44,7 +44,10 @@ from picard.i18n import (
gettext_constants,
)
from picard.profile import profile_groups_values
from picard.script import get_file_naming_script_presets
from picard.script import (
get_file_naming_script_presets,
iter_tagging_scripts_from_tuples,
)
from picard.util import get_base_title
from picard.ui.forms.ui_options_profiles import Ui_ProfileEditorDialog
@@ -249,7 +252,7 @@ class ProfilesOptionsPage(OptionsPage):
return _("Unknown script")
def _get_scripts_list(self, scripts):
enabled_scripts = ['<li>%s</li>' % name for (pos, name, enabled, script) in scripts if enabled]
enabled_scripts = ['<li>%s</li>' % s.name for s in iter_tagging_scripts_from_tuples(scripts) if s.enabled]
if not enabled_scripts:
return _("No enabled scripts")
return _("Enabled scripts:") + '<ul>' + "".join(enabled_scripts) + '</ul>'

View File

@@ -40,10 +40,15 @@ from picard.i18n import (
N_,
gettext as _,
)
from picard.script import ScriptParser
from picard.script import (
ScriptParser,
TaggingScriptSetting,
iter_tagging_scripts_from_config,
save_tagging_scripts_to_config,
)
from picard.script.serializer import (
ScriptImportExportError,
TaggingScript,
ScriptSerializerImportExportError,
TaggingScriptInfo,
)
from picard.ui import (
@@ -146,12 +151,12 @@ class ScriptingOptionsPage(OptionsPage):
error_message = _(fmt) % params
self.display_error(ScriptFileError(_(title), error_message))
def output_file_error(self, error: ScriptImportExportError):
def output_file_error(self, error: ScriptSerializerImportExportError):
"""Log file error and display error message dialog.
Args:
fmt (str): Format for the error type being displayed
error (ScriptImportExportError): The error as a ScriptImportExportError instance
error (ScriptSerializerImportExportError): The error as a ScriptSerializerImportExportError instance
"""
params = {
'filename': error.filename,
@@ -164,13 +169,14 @@ class ScriptingOptionsPage(OptionsPage):
a Picard script package.
"""
try:
script_item = TaggingScript().import_script(self)
except ScriptImportExportError as error:
tagging_script = TaggingScriptInfo().import_script(self)
except ScriptSerializerImportExportError as error:
self.output_file_error(error)
return
if script_item:
title = _("%s (imported)") % script_item['title']
list_item = ScriptListWidgetItem(title, False, script_item['script'])
if tagging_script:
title = _("%s (imported)") % tagging_script['title']
script = TaggingScriptSetting(name=title, enabled=False, content=tagging_script['script'])
list_item = ScriptListWidgetItem(script)
self.ui.script_list.addItem(list_item)
self.ui.script_list.setCurrentRow(self.ui.script_list.count() - 1)
@@ -178,19 +184,19 @@ class ScriptingOptionsPage(OptionsPage):
"""Export the current script to an external file. Export can be either as a plain text
script or a naming script package.
"""
items = self.ui.script_list.selectedItems()
if not items:
list_items = self.ui.script_list.selectedItems()
if not list_items:
return
item = items[0]
script_text = item.script
script_title = item.name if item.name.strip() else _("Unnamed Script")
if script_text:
script_item = TaggingScript(title=script_title, script=script_text)
list_item = list_items[0]
content = list_item.script.content
if content:
name = list_item.script.name.strip()
title = name or _("Unnamed Script")
tagging_script = TaggingScriptInfo(title=title, script=content)
try:
script_item.export_script(parent=self)
except ScriptImportExportError as error:
tagging_script.export_script(parent=self)
except ScriptSerializerImportExportError as error:
self.output_file_error(error)
def enable_tagger_scripts_toggled(self, on):
@@ -198,11 +204,11 @@ class ScriptingOptionsPage(OptionsPage):
self.ui.script_list.add_script()
def script_selected(self):
items = self.ui.script_list.selectedItems()
if items:
item = items[0]
list_items = self.ui.script_list.selectedItems()
if list_items:
list_item = list_items[0]
self.ui.tagger_script.setEnabled(True)
self.ui.tagger_script.setText(item.script)
self.ui.tagger_script.setText(list_item.script.content)
self.ui.tagger_script.setFocus(QtCore.Qt.FocusReason.OtherFocusReason)
self.ui.export_button.setEnabled(True)
else:
@@ -211,21 +217,21 @@ class ScriptingOptionsPage(OptionsPage):
self.ui.export_button.setEnabled(False)
def live_update_and_check(self):
items = self.ui.script_list.selectedItems()
if not items:
list_items = self.ui.script_list.selectedItems()
if not list_items:
return
script = items[0]
script.script = self.ui.tagger_script.toPlainText()
list_item = list_items[0]
list_item.script.content = self.ui.tagger_script.toPlainText()
self.ui.script_error.setStyleSheet("")
self.ui.script_error.setText("")
try:
self.check()
except OptionsCheckError as e:
script.has_error = True
list_item.has_error = True
self.ui.script_error.setStyleSheet(self.STYLESHEET_ERROR)
self.ui.script_error.setText(e.info)
return
script.has_error = False
list_item.has_error = False
def reset_selected_item(self):
widget = self.ui.script_list
@@ -248,8 +254,8 @@ class ScriptingOptionsPage(OptionsPage):
config = get_config()
self.ui.enable_tagger_scripts.setChecked(config.setting['enable_tagger_scripts'])
self.ui.script_list.clear()
for pos, name, enabled, text in config.setting['list_of_scripts']:
list_item = ScriptListWidgetItem(name, enabled, text)
for script in iter_tagging_scripts_from_config(config=config):
list_item = ScriptListWidgetItem(script)
self.ui.script_list.addItem(list_item)
# Select the last selected script item
@@ -261,12 +267,12 @@ class ScriptingOptionsPage(OptionsPage):
def _all_scripts(self):
for item in qlistwidget_items(self.ui.script_list):
yield item.get_all()
yield item.get_script()
def save(self):
config = get_config()
config.setting['enable_tagger_scripts'] = self.ui.enable_tagger_scripts.isChecked()
config.setting['list_of_scripts'] = list(self._all_scripts())
save_tagging_scripts_to_config(self._all_scripts())
config.persist['last_selected_script_pos'] = self.ui.script_list.currentRow()
def display_error(self, error):

View File

@@ -57,10 +57,11 @@ from picard.script import (
ScriptParser,
get_file_naming_script,
get_file_naming_script_presets,
iter_tagging_scripts_from_tuples,
)
from picard.script.serializer import (
FileNamingScript,
ScriptImportExportError,
FileNamingScriptInfo,
ScriptSerializerImportExportError,
)
from picard.util import (
get_base_title,
@@ -162,10 +163,10 @@ class ScriptEditorExamples():
try:
# Only apply scripts if the original file metadata has not been changed.
if self.settings['enable_tagger_scripts'] and not c_metadata.diff(file.orig_metadata):
for s_pos, s_name, s_enabled, s_text in self.settings['list_of_scripts']:
if s_enabled and s_text:
for s in iter_tagging_scripts_from_tuples(self.settings['list_of_scripts']):
if s.enabled and s.content:
parser = ScriptParser()
parser.eval(s_text, c_metadata)
parser.eval(s.content, c_metadata)
filename_before = file.filename
filename_after = file.make_filename(filename_before, c_metadata, self.settings, self.script_text)
if not self.settings['move_files']:
@@ -385,7 +386,7 @@ def populate_script_selection_combo_box(naming_scripts, selected_script_id, comb
"""Populate the specified script selection combo box and identify the selected script.
Args:
naming_scripts (dict): Dictionary of available user-defined naming scripts as script dictionaries as produced by FileNamingScript().to_dict()
naming_scripts (dict): Dictionary of available user-defined naming scripts as script dictionaries as produced by FileNamingScriptInfo().to_dict()
selected_script_id (str): ID code for the currently selected script
combo_box (QComboBox): Combo box object to populate
@@ -935,7 +936,7 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog):
"""Insert a new item into the script selection combo box and update the script list in the settings.
Args:
script_item (dict): File naming script to insert as produced by FileNamingScript().to_dict()
script_item (dict): File naming script to insert as produced by FileNamingScriptInfo().to_dict()
"""
self.selected_script_id = script_item['id']
self.naming_scripts[self.selected_script_id] = script_item
@@ -959,7 +960,7 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog):
def new_script(self, script):
"""Add a new script to the script selection combo box and script list.
"""
script_item = FileNamingScript(script=script)
script_item = FileNamingScriptInfo(script=script)
script_item.title = self.new_script_name()
self._insert_item(script_item.to_dict())
@@ -967,7 +968,7 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog):
"""Add a copy of the script as a new editable script to the script selection combo box.
"""
selected, script_item = self.get_selected_index_and_item()
new_item = FileNamingScript.create_from_dict(script_dict=script_item).copy()
new_item = FileNamingScriptInfo.create_from_dict(script_dict=script_item).copy()
base_title = "%s %s" % (get_base_title(script_item['title']), gettext_constants(DEFAULT_COPY_TEXT))
new_item.title = self.new_script_name(base_title)
@@ -1008,7 +1009,7 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog):
"""Get the specified item from the script selection combo box.
Returns:
dict: File naming script dictionary as produced by FileNamingScript().to_dict()
dict: File naming script dictionary as produced by FileNamingScriptInfo().to_dict()
"""
return self.get_script_item(self.ui.preset_naming_scripts.currentIndex())
@@ -1057,7 +1058,7 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog):
Args:
idx (int): Index of the item to update
script_item (dict): Updated file naming script information as produced by FileNamingScript().to_dict()
script_item (dict): Updated file naming script information as produced by FileNamingScriptInfo().to_dict()
"""
self.ui.preset_naming_scripts.setItemData(idx, script_item)
title = script_item['title']
@@ -1232,8 +1233,8 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog):
a naming script package.
"""
try:
script_item = FileNamingScript().import_script(self)
except ScriptImportExportError as error:
script_item = FileNamingScriptInfo().import_script(self)
except ScriptSerializerImportExportError as error:
self.output_file_error(error.format, error.filename, error.error_msg)
return
if script_item:
@@ -1276,11 +1277,11 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog):
script or a naming script package.
"""
selected = self.get_selected_item()
script_item = FileNamingScript.create_from_dict(script_dict=selected, create_new_id=False)
script_item = FileNamingScriptInfo.create_from_dict(script_dict=selected, create_new_id=False)
script_item.title = get_base_title(script_item.title)
try:
script_item.export_script(parent=self)
except ScriptImportExportError as error:
except ScriptSerializerImportExportError as error:
self.output_file_error(error.format, error.filename, error.error_msg)
def check_formats(self):
@@ -1401,7 +1402,7 @@ class ScriptDetailsEditor(PicardDialog):
def set_last_updated(self):
"""Set the last updated value to the current timestamp.
"""
self.ui.script_last_updated.setText(FileNamingScript.make_last_updated())
self.ui.script_last_updated.setText(FileNamingScriptInfo.make_last_updated())
self.ui.script_last_updated.setModified(True)
def save_changes(self):

View File

@@ -46,23 +46,21 @@ class ScriptsMenu(QtWidgets.QMenu):
super().__init__(*args)
for script in scripts:
action = self.addAction(script[1])
action = self.addAction(script.name)
action.triggered.connect(partial(self._run_script, script))
def _run_script(self, script):
s_name = script[1]
s_text = script[3]
parser = ScriptParser()
for obj in self._iter_unique_metadata_objects():
try:
parser.eval(s_text, obj.metadata)
parser.eval(script.content, obj.metadata)
obj.update()
except ScriptError as e:
log.exception('Error running tagger script "%s" on object %r', s_name, obj)
log.exception('Error running tagger script "%s" on object %r', script.name, obj)
msg = N_('Script error in "%(script)s": %(message)s')
mparms = {
'script': s_name,
'script': script.name,
'message': str(e),
}
self.tagger.window.set_statusbar_message(msg, mparms)

View File

@@ -35,6 +35,7 @@ from picard.i18n import (
gettext as _,
gettext_constants,
)
from picard.script import TaggingScriptSetting
from picard.util import unique_numbered_title
from picard.ui import HashableListWidgetItem
@@ -77,7 +78,7 @@ class ScriptListWidget(QtWidgets.QListWidget):
def add_script(self):
numbered_name = self.unique_script_name()
list_item = ScriptListWidgetItem(name=numbered_name)
list_item = ScriptListWidgetItem(TaggingScriptSetting(name=numbered_name, enabled=True))
list_item.setCheckState(QtCore.Qt.CheckState.Checked)
self.addItem(list_item)
self.setCurrentItem(list_item, QtCore.QItemSelectionModel.SelectionFlag.Clear
@@ -114,14 +115,15 @@ class ScriptListWidget(QtWidgets.QListWidget):
class ScriptListWidgetItem(HashableListWidgetItem):
"""Holds a script's list and text widget properties"""
def __init__(self, name=None, enabled=True, script=""):
super().__init__(name)
def __init__(self, script):
assert isinstance(script, TaggingScriptSetting)
super().__init__(script.name)
self.setFlags(self.flags() | QtCore.Qt.ItemFlag.ItemIsUserCheckable | QtCore.Qt.ItemFlag.ItemIsEditable)
if name is None:
name = gettext_constants(DEFAULT_SCRIPT_NAME)
self.setText(name)
self.setCheckState(QtCore.Qt.CheckState.Checked if enabled else QtCore.Qt.CheckState.Unchecked)
self.script = script
if not script.name:
script.name = gettext_constants(DEFAULT_SCRIPT_NAME)
self.setText(script.name)
self.setCheckState(QtCore.Qt.CheckState.Checked if script.enabled else QtCore.Qt.CheckState.Unchecked)
self._script = script
self.has_error = False
@property
@@ -136,6 +138,12 @@ class ScriptListWidgetItem(HashableListWidgetItem):
def enabled(self):
return self.checkState() == QtCore.Qt.CheckState.Checked
def get_all(self):
# tuples used to get pickle dump of settings to work
return (self.pos, self.name, self.enabled, self.script)
@property
def script(self):
return self._script
def get_script(self):
self._script.pos = self.pos
self._script.name = self.name
self._script.enabled = self.enabled
return self._script

View File

@@ -28,9 +28,9 @@ import yaml
from test.picardtestcase import PicardTestCase
from picard.script.serializer import (
FileNamingScript,
PicardScript,
ScriptImportError,
FileNamingScriptInfo,
ScriptSerializer,
ScriptSerializerFromFileError,
)
@@ -43,14 +43,14 @@ class MockDateTime(datetime.datetime):
raise Exception("Unexpected parameter tz=%r" % tz)
class PicardScriptTest(PicardTestCase):
class ScriptSerializerTest(PicardTestCase):
def assertYamlEquals(self, yaml_str, obj, msg=None):
self.assertEqual(obj, yaml.safe_load(yaml_str), msg)
def test_script_object_1(self):
# Check initial loaded values.
test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0')
test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0')
self.assertEqual(test_script.id, '12345')
self.assertEqual(test_script['id'], '12345')
self.assertEqual(test_script.last_updated, '2021-04-26')
@@ -59,7 +59,7 @@ class PicardScriptTest(PicardTestCase):
def test_script_object_2(self):
# Check updating values directly so as not to modify `last_updated`.
test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26')
test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26')
test_script.id = '54321'
self.assertEqual(test_script.id, '54321')
self.assertEqual(test_script['id'], '54321')
@@ -73,7 +73,7 @@ class PicardScriptTest(PicardTestCase):
def test_script_object_3(self):
# Check updating values that are ignored from modifying `last_updated`.
test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26')
test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26')
test_script.update_script_setting(id='54321')
self.assertEqual(test_script.id, '54321')
self.assertEqual(test_script['id'], '54321')
@@ -83,7 +83,7 @@ class PicardScriptTest(PicardTestCase):
@patch('datetime.datetime', MockDateTime)
def test_script_object_4(self):
# Check updating values that modify `last_updated`.
test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26')
test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26')
test_script.update_script_setting(title='Updated Script 1')
self.assertEqual(test_script.title, 'Updated Script 1')
self.assertEqual(test_script['title'], 'Updated Script 1')
@@ -93,7 +93,7 @@ class PicardScriptTest(PicardTestCase):
@patch('datetime.datetime', MockDateTime)
def test_script_object_5(self):
# Check updating values from dict that modify `last_updated`.
test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26')
test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26')
test_script.update_from_dict({"script": "Updated script"})
self.assertEqual(test_script.script, 'Updated script')
self.assertEqual(test_script['script'], 'Updated script')
@@ -102,7 +102,7 @@ class PicardScriptTest(PicardTestCase):
def test_script_object_6(self):
# Test that extra (unknown) settings are ignored during updating
test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0')
test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0')
test_script.update_script_setting(description='Updated description')
self.assertEqual(test_script['last_updated'], '2021-04-26')
self.assertYamlEquals(test_script.to_yaml(), {"id": "12345", "script": "Script text\n", "script_language_version": "1.0", "title": "Script 1"})
@@ -111,7 +111,7 @@ class PicardScriptTest(PicardTestCase):
def test_script_object_7(self):
# Test that extra (unknown) settings are ignored during updating from dict
test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0')
test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0')
test_script.update_from_dict({"description": "Updated description"})
self.assertEqual(test_script['last_updated'], '2021-04-26')
self.assertYamlEquals(test_script.to_yaml(), {"id": "12345", "script": "Script text\n", "script_language_version": "1.0", "title": "Script 1"})
@@ -120,18 +120,18 @@ class PicardScriptTest(PicardTestCase):
def test_script_object_8(self):
# Test that requested unknown settings return None
test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26')
test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26')
self.assertEqual(test_script['unknown_setting'], None)
def test_script_object_9(self):
# Test that an exception is raised when creating or updating using an invalid YAML string
with self.assertRaises(ScriptImportError):
PicardScript().create_from_yaml('Not a YAML string')
PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0')
with self.assertRaises(ScriptSerializerFromFileError):
ScriptSerializer().create_from_yaml('Not a YAML string')
ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0')
def test_naming_script_object_1(self):
# Check initial loaded values.
test_script = FileNamingScript(
test_script = FileNamingScriptInfo(
title='Script 1', script='Script text', id='12345', last_updated='2021-04-26',
description='Script description', author='Script author', script_language_version='1.0'
)