mirror of
https://github.com/fergalmoran/picard.git
synced 2026-01-03 23:23:59 +00:00
Merge pull request #2496 from zas/list_of_scripts
Introduce TaggingScriptSetting and few associated methods
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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__(
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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(
|
||||
'-',
|
||||
|
||||
@@ -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>'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user