--- /dev/null
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2019 Nick Bowler
+#
+# License GPLv3+: GNU General Public License version 3 or any later version.
+# This is free software: you are free to change and redistribute it.
+# There is NO WARRANTY, to the extent permitted by law.
+
+PLUGIN_NAME = u"External Script Manager"
+PLUGIN_AUTHOR = u"Nick Bowler"
+PLUGIN_DESCRIPTION = u'''<p>Synchronizes scripts between Picard and an
+external directory. This is primarily intended to allow scripts to be
+edited using normal text editors and managed in version control.</p>
+
+<p>Scripts are located under the Picard user directory, (usually
+<code>~/.config/MusicBrainz/Picard/scriptmanager</code>. This directory
+will be created the first time the plugin is loaded. Files in this directory
+are loaded based on their extension; tagger scripts have the extension
+<code>.tag</code>.</p>
+
+<p>Any scripts managed by this plugin can still be created, edited or deleted
+using the built-in interface within Picard. Such changes will be reflected on
+disk. Scripts with names that begin with <code>//scriptmanager/</code> are
+managed by this plugin.</p>
+'''
+
+PLUGIN_VERSION = "0"
+PLUGIN_API_VERSIONS = ["2.0"]
+PLUGIN_LICENSE = "GPL-3.0-or-later"
+
+from picard import (config, log)
+from picard.const import (USER_DIR)
+from pathlib import Path
+
+from picard.ui.options.scripting import ScriptingOptionsPage
+
+import os
+
+def modulename():
+ return modulename.__module__[len("picard.plugins."):]
+
+# Remove at most one trailing newline from a string
+def read_script(file):
+ with open(file) as f:
+ s = f.read()
+ if (s[-1:] == '\n'):
+ return s[:-1]
+ return s
+
+def write_script(file, text):
+ try:
+ old_text = read_script(file)
+ except FileNotFoundError:
+ old_text = None
+
+ if (old_text != text):
+ with open(file + ".tmp", "w") as f:
+ f.write(text + '\n')
+ os.replace(file + ".tmp", file)
+ return True
+ return False
+
+def script_files(dir = os.path.join(USER_DIR, "scriptmanager")):
+ try:
+ Path(dir).mkdir(exist_ok=True)
+ except FileExistsError:
+ log.warning("%s is not a directory" % dir)
+ return []
+
+ tagger_scripts = {}
+ for f in os.listdir(dir):
+ if os.path.splitext(f)[1] == ".tag":
+ tagger_scripts["//scriptmanager/%s" % f] = os.path.join(dir, f)
+ return tagger_scripts
+
+# When saving scripts in the UI, write out changes to the filesystem
+def install_ui_save_hook():
+ script_save = ScriptingOptionsPage.save
+ def save_hook(self):
+ script_save(self)
+
+ dir = os.path.join(USER_DIR, "scriptmanager")
+ tagger_scripts = script_files(dir)
+ for pos, name, enabled, text in config.setting["list_of_scripts"]:
+ if name in tagger_scripts:
+ del tagger_scripts[name]
+
+ if (name.startswith("//scriptmanager/")):
+ (basename, ext) = os.path.splitext(os.path.basename(name))
+ if (ext == ".tag"):
+ file = os.path.join(dir, basename + ext)
+ if write_script(file, text):
+ log.info("wrote script %s" % name)
+ else:
+ log.warning("invalid managed script name: %s" % name)
+
+ # Remove all remaining scripts
+ for name in tagger_scripts:
+ log.info("deleted script %s" % name)
+ try:
+ os.remove(tagger_scripts[name])
+ except FileNotFoundError:
+ pass
+ ScriptingOptionsPage.save = save_hook
+
+# Refresh the script list when visiting the scripting UI
+def install_ui_load_hook():
+ script_load = ScriptingOptionsPage.load
+ def load_hook(self):
+ refresh_scripts()
+ script_load(self)
+ ScriptingOptionsPage.load = load_hook
+
+# When the plugin is loaded, load scripts from the filesystem into Picard.
+def refresh_scripts():
+ tagger_scripts = script_files()
+
+ # Update existing script list...
+ list_of_scripts = config.setting["list_of_scripts"]
+ for i, (pos, name, enabled, text) in enumerate(list_of_scripts[:]):
+ if name in tagger_scripts:
+ text_update = read_script(tagger_scripts[name])
+ if text != text_update:
+ log.info("refreshed script %s" % name)
+ list_of_scripts[i] = (pos, name, enabled, text_update)
+ del tagger_scripts[name]
+ elif name.startswith("//scriptmanager/"):
+ log.info("removed script %s" % name)
+ del list_of_scripts[i]
+
+ # Add any new scripts that were found...
+ for name in tagger_scripts:
+ log.info("imported script %s" % name)
+ text = read_script(tagger_scripts[name])
+ list_of_scripts.append((0, name, False, text))
+
+ # Fixup position entries and write out updated script list
+ for i, (pos, name, enabled, text) in enumerate(list_of_scripts):
+ list_of_scripts[i] = (i, name, enabled, text)
+ config.setting["list_of_scripts"] = list_of_scripts
+
+if modulename() in config.setting["enabled_plugins"]:
+ install_ui_save_hook()
+ install_ui_load_hook()
+ refresh_scripts()
+else:
+ log.debug("%s disabled in configuration" % (modulename()))