[
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non: [push, pull_request]\n\njobs:\n  unittest:\n\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']\n\n    steps:\n    - uses: actions/checkout@v1\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v1\n      with:\n        python-version: ${{ matrix.python-version }}\n    - name: Install gettext\n      run: sudo apt-get install gettext\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install picard\n    - name: Test plugins\n      run: python -m unittest discover -v\n"
  },
  {
    "path": ".gitignore",
    "content": "# Generated by the script\nplugins.json\nplugins/*.zip\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# Distribution / packaging / development\n.venv/\n.vscode/\n.Python\nenv/\nbuild/\n"
  },
  {
    "path": ".prospector.yml",
    "content": "# Configuration for prospector, mainly used by Codacy.\n\npep8:\n  # Please see comments in setup.cfg as to why we disable the below checks.\n  disable:\n    - E127\n    - E128\n    - E129\n    - E226\n    - E241\n    - E501\n    - W503\n\n\npyflakes:\n  disable:\n    # Undefined name. Ignore this since it otherwise detects the gettext\n    # helpers (_, ngettext, N_) as undefined. There seems to be no clean way\n    # to specify additional builtins for pyflakes with prospector.\n    # We also test for this with flake8 in a saner way.\n    - F821\n    # F401: Module imported but unused\n    # F841: Unused variables\n    # We have some valid cases for both, but PyFlakes does not allow to ignore\n    # them case by case. flake8 also checks this, so this is redundant.\n    - F401\n    - F841\n    # Ignore syntax errors as reported by PyFlakes since on Codacy this does\n    # not support Python 3 syntax.\n    # - F999\n"
  },
  {
    "path": ".pylintrc",
    "content": "[MASTER]\n\n# A comma-separated list of package or module names from where C extensions may\n# be loaded. Extensions are loading into the active Python interpreter and may\n# run arbitrary code.\nextension-pkg-whitelist=\n\n# Add files or directories to the ignore list. They should be base names, not\n# paths.\nignore=CVS\n\n# Add files or directories matching the regex patterns to the ignore list. The\n# regex matches against base names, not paths.\nignore-patterns=ui_.*\\.py\n\n# Python code to execute, usually for sys.path manipulation such as\n# pygtk.require().\n#init-hook=\n\n# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the\n# number of processors available to use.\njobs=0\n\n# Control the amount of potential inferred values when inferring a single\n# object. This can help the performance when dealing with large functions or\n# complex, nested conditions.\nlimit-inference-results=100\n\n# List of plugins (as comma separated values of python modules names) to load,\n# usually to register additional checkers.\nload-plugins=\n\n# Pickle collected data for later comparisons.\npersistent=yes\n\n# Specify a configuration file.\n#rcfile=\n\n# When enabled, pylint would attempt to guess common misconfiguration and emit\n# user-friendly hints instead of false-positive error messages.\nsuggestion-mode=yes\n\n# Allow loading of arbitrary C extensions. Extensions are imported into the\n# active Python interpreter and may run arbitrary code.\nunsafe-load-any-extension=no\n\n\n[MESSAGES CONTROL]\n\n# Only show warnings with the listed confidence levels. Leave empty to show\n# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.\nconfidence=\n\n# Disable the message, report, category or checker with the given id(s). You\n# can either give multiple identifiers separated by comma (,) or put this\n# option multiple times (only on the command line, not in the configuration\n# file where it should appear only once). You can also use \"--disable=all\" to\n# disable everything first and then reenable specific checks. For example, if\n# you want to run only the similarities checker, you can use \"--disable=all\n# --enable=similarities\". If you want to run only the classes checker, but have\n# no Warning level messages displayed, use \"--disable=all --enable=classes\n# --disable=W\".\ndisable=all\n\n# Enable the message, report, category or checker with the given id(s). You can\n# either give multiple identifier separated by comma (,) or put this option\n# multiple time (only on the command line, not in the configuration file where\n# it should appear only once). See also the \"--disable\" option for examples.\nenable=consider-using-enumerate,\n       format-combined-specification,\n       return-in-init,\n       catching-non-exception,\n       bad-except-order,\n       unexpected-special-method-signature,\n       # Enforce list comprehensions\n       # Newline at EOF\n       raising-bad-type,\n       raising-non-exception,\n       format-needs-mapping,\n       invalid-all-object,\n       bad-super-call,\n       nonexistent-operator,\n       missing-kwoa,\n       missing-format-argument-key,\n       init-is-generator,\n       access-member-before-definition,\n       used-before-assignment,\n       redundant-keyword-arg,\n       assert-on-tuple,\n       assignment-from-no-return,\n       expression-not-assigned,\n       misplaced-bare-raise,\n       redefined-argument-from-local,\n       not-in-loop,\n       bad-exception-context,\n       unidiomatic-typecheck,\n       no-staticmethod-decorator,\n       nonlocal-and-global,\n       confusing-with-statement,\n       global-variable-undefined,\n       global-variable-not-assigned,\n       inconsistent-mro,\n       no-classmethod-decorator,\n       nonlocal-without-binding,\n       duplicate-bases,\n       duplicate-argument-name,\n       duplicate-key,\n       useless-else-on-loop,\n       arguments-differ,\n       logging-too-many-args,\n       too-few-format-args,\n       bad-format-string-key,\n       invalid-sequence-index,\n       inherit-non-class,\n       bad-format-string,\n       invalid-format-index,\n       invalid-star-assignment-target,\n       no-method-argument,\n       no-value-for-parameter,\n       missing-format-attribute,\n       logging-too-few-args,\n       too-few-format-args,\n       mixed-format-string,\n       # Old style class\n       logging-format-truncated,\n       truncated-format-string,\n       notimplemented-raised,\n       # Builtin redefined\n       function-redefined,\n       reimported,\n       repeated-keyword,\n       lost-exception,\n       return-outside-function,\n       return-arg-in-generator,\n       non-iterator-returned,\n       method-hidden,\n       too-many-star-expressions,\n       trailing-whitespace,\n       unexpected-keyword-arg,\n       missing-format-string-key,\n       unnecessary-lambda,\n       unnecessary-pass,\n       unreachable,\n       logging-unsupported-format,\n       bad-format-character,\n       unused-import,\n       exec-used,\n       pointless-statement,\n       pointless-string-statement,\n       undefined-all-variable,\n       misplaced-future,\n       continue-in-finally,\n       invalid-slots,\n       invalid-slice-index,\n       invalid-slots-object,\n       star-needs-assignment-target,\n       global-at-module-level,\n       yield-outside-function,\n       mixed-indentation,\n       non-parent-init-called,\n       bare-except,\n       #no-self-use,\n       dangerous-default-value,\n       arguments-differ,\n       signature-differs,\n       duplicate-except,\n       abstract-class-instantiated,\n       binary-op-exception,\n       undefined-variable\n\n\n\n[REPORTS]\n\n# Python expression which should return a note less than 10 (10 is the highest\n# note). You have access to the variables errors warning, statement which\n# respectively contain the number of errors / warnings messages and the total\n# number of statements analyzed. This is used by the global evaluation report\n# (RP0004).\nevaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)\n\n# Template used to display messages. This is a python new-style format string\n# used to format the message information. See doc for all details.\n#msg-template=\n\n# Set the output format. Available formats are text, parseable, colorized, json\n# and msvs (visual studio). You can also give a reporter class, e.g.\n# mypackage.mymodule.MyReporterClass.\noutput-format=text\n\n# Tells whether to display a full report or only the messages.\nreports=no\n\n# Activate the evaluation score.\nscore=yes\n\n\n[REFACTORING]\n\n# Maximum number of nested blocks for function / method body\nmax-nested-blocks=5\n\n# Complete name of functions that never returns. When checking for\n# inconsistent-return-statements if a never returning function is called then\n# it will be considered as an explicit return statement and no message will be\n# printed.\nnever-returning-functions=sys.exit\n\n\n[FORMAT]\n\n# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.\nexpected-line-ending-format=\n\n# Regexp for a line that is allowed to be longer than the limit.\nignore-long-lines=^\\s*(# )?<?https?://\\S+>?$\n\n# Number of spaces of indent required inside a hanging or continued line.\nindent-after-paren=4\n\n# String used as indentation unit. This is usually \"    \" (4 spaces) or \"\\t\" (1\n# tab).\nindent-string='    '\n\n# Maximum number of characters on a single line.\nmax-line-length=100\n\n# Maximum number of lines in a module.\nmax-module-lines=1000\n\n# List of optional constructs for which whitespace checking is disabled. `dict-\n# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\\n222: 2}.\n# `trailing-comma` allows a space between comma and closing bracket: (a, ).\n# `empty-line` allows space-only lines.\nno-space-check=trailing-comma,\n               dict-separator\n\n# Allow the body of a class to be on the same line as the declaration if body\n# contains single statement.\nsingle-line-class-stmt=no\n\n# Allow the body of an if to be on the same line as the test if there is no\n# else.\nsingle-line-if-stmt=no\n\n\n[SPELLING]\n\n# Limits count of emitted suggestions for spelling mistakes.\nmax-spelling-suggestions=4\n\n# Spelling dictionary name. Available dictionaries: none. To make it working\n# install python-enchant package..\nspelling-dict=\n\n# List of comma separated words that should not be checked.\nspelling-ignore-words=\n\n# A path to a file that contains private dictionary; one word per line.\nspelling-private-dict-file=\n\n# Tells whether to store unknown words to indicated private dictionary in\n# --spelling-private-dict-file option instead of raising a message.\nspelling-store-unknown-words=no\n\n\n[SIMILARITIES]\n\n# Ignore comments when computing similarities.\nignore-comments=yes\n\n# Ignore docstrings when computing similarities.\nignore-docstrings=yes\n\n# Ignore imports when computing similarities.\nignore-imports=no\n\n# Minimum lines number of a similarity.\nmin-similarity-lines=4\n\n\n[VARIABLES]\n\n# List of additional names supposed to be defined in builtins. Remember that\n# you should avoid defining new builtins when possible.\nadditional-builtins=_, N_, ngettext, gettext_countries,\n                    gettext_attributes, pgettext_attributes\n\n# Tells whether unused global variables should be treated as a violation.\nallow-global-unused-variables=yes\n\n# List of strings which can identify a callback function by name. A callback\n# name must start or end with one of those strings.\ncallbacks=cb_,\n          _cb\n\n# A regular expression matching the name of dummy variables (i.e. expected to\n# not be used).\ndummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_\n\n# Argument names that match this expression will be ignored. Default to name\n# with leading underscore.\nignored-argument-names=_.*|^ignored_|^unused_\n\n# Tells whether we should check for unused import in __init__ files.\ninit-import=no\n\n# List of qualified module names which can have objects that can redefine\n# builtins.\nredefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io\n\n\n[MISCELLANEOUS]\n\n# List of note tags to take in consideration, separated by a comma.\nnotes=FIXME,\n      XXX,\n      TODO\n\n\n[LOGGING]\n\n# Format style used to check logging format string. `old` means using %\n# formatting, while `new` is for `{}` formatting.\nlogging-format-style=old\n\n# Logging modules to check that the string format arguments are in logging\n# function parameter format.\nlogging-modules=logging\n\n\n[BASIC]\n\n# Naming style matching correct argument names.\nargument-naming-style=snake_case\n\n# Regular expression matching correct argument names. Overrides argument-\n# naming-style.\n#argument-rgx=\n\n# Naming style matching correct attribute names.\nattr-naming-style=snake_case\n\n# Regular expression matching correct attribute names. Overrides attr-naming-\n# style.\n#attr-rgx=\n\n# Bad variable names which should always be refused, separated by a comma.\nbad-names=foo,\n          bar,\n          baz,\n          toto,\n          tutu,\n          tata\n\n# Naming style matching correct class attribute names.\nclass-attribute-naming-style=any\n\n# Regular expression matching correct class attribute names. Overrides class-\n# attribute-naming-style.\n#class-attribute-rgx=\n\n# Naming style matching correct class names.\nclass-naming-style=PascalCase\n\n# Regular expression matching correct class names. Overrides class-naming-\n# style.\n#class-rgx=\n\n# Naming style matching correct constant names.\nconst-naming-style=UPPER_CASE\n\n# Regular expression matching correct constant names. Overrides const-naming-\n# style.\n#const-rgx=\n\n# Minimum line length for functions/classes that require docstrings, shorter\n# ones are exempt.\ndocstring-min-length=-1\n\n# Naming style matching correct function names.\nfunction-naming-style=snake_case\n\n# Regular expression matching correct function names. Overrides function-\n# naming-style.\n#function-rgx=\n\n# Good variable names which should always be accepted, separated by a comma.\ngood-names=i,\n           j,\n           k,\n           ex,\n           Run,\n           _\n\n# Include a hint for the correct naming format with invalid-name.\ninclude-naming-hint=no\n\n# Naming style matching correct inline iteration names.\ninlinevar-naming-style=any\n\n# Regular expression matching correct inline iteration names. Overrides\n# inlinevar-naming-style.\n#inlinevar-rgx=\n\n# Naming style matching correct method names.\nmethod-naming-style=snake_case\n\n# Regular expression matching correct method names. Overrides method-naming-\n# style.\n#method-rgx=\n\n# Naming style matching correct module names.\nmodule-naming-style=snake_case\n\n# Regular expression matching correct module names. Overrides module-naming-\n# style.\n#module-rgx=\n\n# Colon-delimited sets of names that determine each other's naming style when\n# the name regexes allow several styles.\nname-group=\n\n# Regular expression which should only match function or class names that do\n# not require a docstring.\nno-docstring-rgx=^_\n\n# List of decorators that produce properties, such as abc.abstractproperty. Add\n# to this list to register other decorators that produce valid properties.\n# These decorators are taken in consideration only for invalid-name.\nproperty-classes=abc.abstractproperty\n\n# Naming style matching correct variable names.\nvariable-naming-style=snake_case\n\n# Regular expression matching correct variable names. Overrides variable-\n# naming-style.\n#variable-rgx=\n\n\n[TYPECHECK]\n\n# List of decorators that produce context managers, such as\n# contextlib.contextmanager. Add to this list to register other decorators that\n# produce valid context managers.\ncontextmanager-decorators=contextlib.contextmanager\n\n# List of members which are set dynamically and missed by pylint inference\n# system, and so shouldn't trigger E1101 when accessed. Python regular\n# expressions are accepted.\ngenerated-members=\n\n# Tells whether missing members accessed in mixin class should be ignored. A\n# mixin class is detected if its name ends with \"mixin\" (case insensitive).\nignore-mixin-members=yes\n\n# Tells whether to warn about missing members when the owner of the attribute\n# is inferred to be None.\nignore-none=yes\n\n# This flag controls whether pylint should warn about no-member and similar\n# checks whenever an opaque object is returned when inferring. The inference\n# can return multiple potential results while evaluating a Python object, but\n# some branches might not be evaluated, which results in partial inference. In\n# that case, it might be useful to still emit no-member and other checks for\n# the rest of the inferred objects.\nignore-on-opaque-inference=yes\n\n# List of class names for which member attributes should not be checked (useful\n# for classes with dynamically set attributes). This supports the use of\n# qualified names.\nignored-classes=optparse.Values,thread._local,_thread._local\n\n# List of module names for which member attributes should not be checked\n# (useful for modules/projects where namespaces are manipulated during runtime\n# and thus existing member attributes cannot be deduced by static analysis. It\n# supports qualified module names, as well as Unix pattern matching.\nignored-modules=\n\n# Show a hint with possible names when a member name was not found. The aspect\n# of finding the hint is based on edit distance.\nmissing-member-hint=yes\n\n# The minimum edit distance a name should have in order to be considered a\n# similar match for a missing member name.\nmissing-member-hint-distance=1\n\n# The total number of similar names that should be taken in consideration when\n# showing a hint for a missing member.\nmissing-member-max-choices=1\n\n\n[CLASSES]\n\n# List of method names used to declare (i.e. assign) instance attributes.\ndefining-attr-methods=__init__,\n                      __new__,\n                      setUp\n\n# List of member names, which should be excluded from the protected access\n# warning.\nexclude-protected=_asdict,\n                  _fields,\n                  _replace,\n                  _source,\n                  _make\n\n# List of valid names for the first argument in a class method.\nvalid-classmethod-first-arg=cls\n\n# List of valid names for the first argument in a metaclass class method.\nvalid-metaclass-classmethod-first-arg=cls\n\n\n[IMPORTS]\n\n# Allow wildcard imports from modules that define __all__.\nallow-wildcard-with-all=no\n\n# Analyse import fallback blocks. This can be used to support both Python 2 and\n# 3 compatible code, which means that the block might have code that exists\n# only in one or another interpreter, leading to false positives when analysed.\nanalyse-fallback-blocks=no\n\n# Deprecated modules which should not be used, separated by a comma.\ndeprecated-modules=optparse,tkinter.tix\n\n# Create a graph of external dependencies in the given file (report RP0402 must\n# not be disabled).\next-import-graph=\n\n# Create a graph of every (i.e. internal and external) dependencies in the\n# given file (report RP0402 must not be disabled).\nimport-graph=\n\n# Create a graph of internal dependencies in the given file (report RP0402 must\n# not be disabled).\nint-import-graph=\n\n# Force import order to recognize a module as part of the standard\n# compatibility libraries.\nknown-standard-library=\n\n# Force import order to recognize a module as part of a third party library.\nknown-third-party=enchant\n\n\n[DESIGN]\n\n# Maximum number of arguments for function / method.\nmax-args=5\n\n# Maximum number of attributes for a class (see R0902).\nmax-attributes=7\n\n# Maximum number of boolean expressions in an if statement.\nmax-bool-expr=5\n\n# Maximum number of branch for function / method body.\nmax-branches=12\n\n# Maximum number of locals for function / method body.\nmax-locals=15\n\n# Maximum number of parents for a class (see R0901).\nmax-parents=7\n\n# Maximum number of public methods for a class (see R0904).\nmax-public-methods=20\n\n# Maximum number of return / yield for function / method body.\nmax-returns=6\n\n# Maximum number of statements in function / method body.\nmax-statements=50\n\n# Minimum number of public methods for a class (see R0903).\nmin-public-methods=2\n\n\n[EXCEPTIONS]\n\n# Exceptions that will emit a warning when being caught. Defaults to\n# \"Exception\".\novergeneral-exceptions=Exception\n"
  },
  {
    "path": "README.md",
    "content": "# MusicBrainz Picard Plugins\n\nThis repository hosts plugins for [MusicBrainz Picard](https://picard.musicbrainz.org/). If you're a plugin author and would like to include your plugin here, simply open a pull request.\n\n> [!NOTE]\n> This repository is for Picard v1 and v2 plugins only. Plugins for Picard v3 are managed through the [Picard Plugins Registry](https://github.com/metabrainz/picard-plugins-registry).\n\nNote that new plugins being added to the repository should be under the GNU General Public License version 2 (\"GPL\") or a license compatible with it. See https://www.gnu.org/licenses/license-list.html for a list of compatible licenses.\n\n## Development Notes\n\nThe script `generate.py` will generate a file called `plugins.json`, which contains metadata about all the plugins in this repository. `plugins.json` is used by [picard-website](https://github.com/musicbrainz/picard-website) and Picard itself to display information about downloadable plugins.\n"
  },
  {
    "path": "build_ui.py",
    "content": "#!/usr/bin/env python3\n\nimport glob\nimport os\n\nfrom PyQt5 import uic\n\n\nos.chdir(os.path.dirname(__file__))\nplugin_dir = os.path.relpath('plugins')\n\n\ndef compile_ui(uifile, pyfile):\n    if newer(uifile, pyfile):\n        print(\"compiling %s -> %s\" % (uifile, pyfile))\n        with open(pyfile, \"w\") as out:\n            uic.compileUi(uifile, out)\n    else:\n        print(\"skipping %s -> %s: up to date\" % (uifile, pyfile))\n\n\ndef newer(file1, file2):\n    \"\"\"Returns True, if file1 has been modified after file2\n    \"\"\"\n    if not os.path.exists(file2):\n        return True\n    return os.path.getmtime(file1) > os.path.getmtime(file2)\n\n\nif __name__ == '__main__':\n    for uifile in glob.glob(os.path.join(plugin_dir, '*/*.ui')):\n        pyfile = os.path.splitext(os.path.basename(uifile))[0] + '.py'\n        pyfile = os.path.join(os.path.dirname(uifile), pyfile)\n        compile_ui(uifile, pyfile)\n"
  },
  {
    "path": "generate.py",
    "content": "#!/usr/bin/env python3\n\nfrom __future__ import print_function\nimport argparse\nimport os\nimport json\nimport zipfile\n\nfrom hashlib import md5\nfrom subprocess import check_call\n\nfrom get_plugin_data import get_plugin_data\n\nVERSION_TO_BRANCH = {\n    None: '2.0',\n    '1.0': '1.0',\n    '2.0': '2.0',\n}\n\n\ndef build_json(dest_dir):\n    \"\"\"\n    Traverse the plugins directory to generate json data.\n    \"\"\"\n\n    plugins = {}\n\n    # All top level directories in plugin_dir are plugins\n    for dirname in next(os.walk(plugin_dir))[1]:\n\n        files = {}\n        data = {}\n\n        if dirname in [\".git\"]:\n            continue\n\n        dirpath = os.path.join(plugin_dir, dirname)\n        for root, dirs, filenames in os.walk(dirpath):\n            for filename in filenames:\n                ext = os.path.splitext(filename)[1]\n\n                if ext not in [\".pyc\"]:\n                    file_path = os.path.join(root, filename)\n                    with open(file_path, \"rb\") as md5file:\n                        md5Hash = md5(md5file.read()).hexdigest()\n                    files[file_path.split(os.path.join(dirpath, ''))[1]] = md5Hash\n\n                    if ext in ['.py'] and not data:\n                        data = get_plugin_data(os.path.join(plugin_dir, dirname, filename))\n\n        if files and data:\n            print(\"Added: \" + dirname)\n            data['files'] = files\n            plugins[dirname] = data\n    out_path = os.path.join(dest_dir, plugin_file)\n    with open(out_path, \"w\") as out_file:\n        json.dump({\"plugins\": plugins}, out_file, sort_keys=True, indent=2)\n\n\ndef zip_files(dest_dir):\n    \"\"\"\n    Zip up plugin folders\n    \"\"\"\n\n    for dirname in next(os.walk(plugin_dir))[1]:\n        archive_path = os.path.join(dest_dir, dirname)\n        archive = zipfile.ZipFile(archive_path + \".zip\", \"w\")\n\n        dirpath = os.path.join(plugin_dir, dirname)\n        plugin_files = []\n\n        for root, dirs, filenames in os.walk(dirpath):\n            for filename in filenames:\n                file_path = os.path.join(root, filename)\n                plugin_files.append(file_path)\n\n        if (len(plugin_files) == 1\n            and os.path.basename(plugin_files[0]) != '__init__.py'):\n            # There's only one file, put it directly into the zipfile\n            archive.write(plugin_files[0],\n                          os.path.basename(plugin_files[0]),\n                          compress_type=zipfile.ZIP_DEFLATED)\n        else:\n            for filename in plugin_files:\n                # Preserve the folder structure relative to plugin_dir\n                # in the zip file\n                name_in_zip = os.path.join(os.path.relpath(filename,\n                                                           plugin_dir))\n                archive.write(filename,\n                              name_in_zip,\n                              compress_type=zipfile.ZIP_DEFLATED)\n\n        print(\"Created: \" + dirname + \".zip\")\n\n\n# The file that contains json data\nplugin_file = \"plugins.json\"\n\n# The directory which contains plugin files\nplugin_dir = \"plugins\"\n\nif __name__ == '__main__':\n    parser = argparse.ArgumentParser(description='Generate plugin files for Picard website.')\n    parser.add_argument('version', nargs='?', help=\"Build output files for the specified version\")\n    parser.add_argument('--build_dir', default=\"build\", help=\"Path for the build output. DEFAULT = %(default)s\")\n    parser.add_argument('--pull', action='store_true', dest='pull', help=\"Pulls the remote origin and updates the files before building\")\n    parser.add_argument('--no-zip', action='store_false', dest='zip', help=\"Do not generate the zip files in the build output\")\n    parser.add_argument('--no-json', action='store_false', dest='json', help=\"Do not generate the json file in the build output\")\n    args = parser.parse_args()\n    check_call([\"git\", \"checkout\", \"-q\", VERSION_TO_BRANCH[args.version], '--', 'plugins'])\n    dest_dir = os.path.abspath(os.path.join(args.build_dir, args.version or ''))\n    if not os.path.exists(dest_dir):\n        os.makedirs(dest_dir)\n    if args.pull:\n        check_call([\"git\", \"pull\", \"-q\"])\n    if args.json:\n        build_json(dest_dir)\n    if args.zip:\n        zip_files(dest_dir)\n"
  },
  {
    "path": "get_plugin_data.py",
    "content": "# -*- coding: utf-8 -*-\n\nfrom __future__ import print_function\nimport ast\n\nKNOWN_DATA = [\n    'PLUGIN_NAME',\n    'PLUGIN_AUTHOR',\n    'PLUGIN_VERSION',\n    'PLUGIN_API_VERSIONS',\n    'PLUGIN_LICENSE',\n    'PLUGIN_LICENSE_URL',\n    'PLUGIN_DESCRIPTION',\n]\n\n\ndef get_plugin_data(filepath):\n    \"\"\"Parse a python file and return a dict with plugin metadata\"\"\"\n    data = {}\n    with open(filepath, 'r', encoding='utf-8') as plugin_file:\n        source = plugin_file.read()\n        try:\n            root = ast.parse(source, filepath)\n        except:\n            print(\"Cannot parse \" + filepath)\n            raise\n        for node in ast.iter_child_nodes(root):\n            if isinstance(node, ast.Assign) and len(node.targets) == 1:\n                target = node.targets[0]\n                if (isinstance(target, ast.Name)\n                    and isinstance(target.ctx, ast.Store)\n                        and target.id in KNOWN_DATA):\n                    name = target.id.replace('PLUGIN_', '', 1).lower()\n                    if name not in data:\n                        try:\n                            data[name] = ast.literal_eval(node.value)\n                        except ValueError:\n                            print('Cannot evaluate value in '\n                                  + filepath + ':' +\n                                  ast.dump(node))\n        return data\n"
  },
  {
    "path": "plugins/abbreviate_artistsort/abbreviate_artistsort.py",
    "content": "# -*- coding: utf-8 -*-\n\n# This is the Sort Multivalue Tags plugin for MusicBrainz Picard.\n# Copyright (C) 2013 Sophist\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n\nPLUGIN_NAME = \"Abbreviate artist-sort\"\nPLUGIN_AUTHOR = \"Sophist\"\nPLUGIN_DESCRIPTION = '''Abbreviate Artist-Sort and Album-Artist-Sort Tags.\ne.g. \"Vivaldi, Antonio\" becomes \"Vivaldi, A.\"\nThis is particularly useful for classical albums that can have a long list of artists.\n%artistsort% is abbreviated into %_artistsort_abbrev% and\n%albumartistsort% is abbreviated into %_albumartistsort_abbrev%.'''\nPLUGIN_VERSION = \"0.4.1\"\nPLUGIN_API_VERSIONS = [\"1.0\", \"2.0\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\n\nfrom picard import log\nfrom picard.metadata import register_track_metadata_processor\n\n# NOTE: This plugin will not work consistently if you have not enabled the 'Standardize Artist Names' option!\n# The algorithm for this is complicated because the tags can contain multiple names separated by various characters\n# As an example from http://musicbrainz.org/release/6c0cfb20-2606-46c1-9306-ee5e7cb5bfdf\n#   Sorted:   Vivaldi, Antonio, Caldara, Antonio; Queyras, Jean-Guihen, Kallweit, Georg, Akademie für Alte Musik Berlin\n#   Unsorted: Antonio Vivaldi, Antonio Caldara; Jean-Guihen Queyras, Georg Kallweit, Akademie für Alte Musik Berlin\n# As you can see, in unsorted, names are separated by ',' as well as ';' but could be e.g. 'feat:'\n# The only known is that in sorted, surname is separated from forename(s) by a ','\n# It is further complicated by non-latin (e.g. japanese) script artist names\n#   where unsorted is in local script, but sorted can be in latin, and names can be in local locale or general.\n\n# If the names are just reversed, and we shift Unsorted to the right by one character (for the ',' in sorted),\n# then the punctuation should start to match up:\n#   Sorted:   Vivaldi, Antonio, Caldara, Antonio; Queyras, Jean-Guihen, Kallweit, Georg, Akademie für Alte Musik Berlin\n#   Unsorted:  Antonio Vivaldi, Antonio Caldara; Jean-Guihen Queyras, Georg Kallweit, Akademie für Alte Musik Berlin\n#                             ^\n# Of course if we have non-latin or locale names, alignment could be way off:\n#   Sorted: Verdi, Giuseppe, Vivaldi, Antonio\n#   Unsorted: Joe Green, Antonio Vivaldi\n\n# In the absence of an array version of the tags (PR pending)\n# we need to process the tag and sorted tag together in a special way as follows:\n#\n# 1. Look for the first ',' in sorted and tentatively set surname to the string up to that point.\n#   Case a. Sorted:   Stuff, ...\n#           Unsorted: Stuff, ...\n#   Case b. Sorted:   Surname, Forename(s)...\n#           Unsorted: Forename(s) Surname...\n#     Special case: Sorted:   Major, Major...\n#                   Unsorted: Major Major...\n#   Case c. Sorted:   Stuff; Surname, Forename(s)...\n#           Unsorted: Stuff; Forename(s) Surname...\n#   Case d. Sorted:   Latin Surname, Latin Forename(s)...\n#           Unsorted: Foreign...\n#   Case e. Sorted:   Beatles, The...\n#           Unsorted: The Beatles...\n# 2. Locate surname in unsorted:\n#   Case a. unsorted starts with surname - move both to new strings\n#   Case b. surname can be found in unsorted - forename(s) are what is before and match beginning of rest\n#   Case c. If first word is same in sorted and unsorted, move words that match to new strings, then treat as b.\n#   Case d. Try to handle without abbreviating and get to next name which might not be foreign\n\n_abbreviate_tags = [\n    ('albumartistsort', 'albumartist', '~albumartistsort_abbrev'),\n    ('artistsort', 'artist', '~artistsort_abbrev'),\n]\n_prefixes = [\"A\", \"The\"]\n_split = \", \"\n_abbreviate_cache = {}\n\n\ndef abbreviate_artistsort(tagger, metadata, track, release):\n\n    for sortTag, unsortTag, sortTagNew in _abbreviate_tags:\n        if not (sortTag in metadata and unsortTag in metadata):\n            continue\n\n        sorts = list(metadata.getall(sortTag))\n        unsorts = list(metadata.getall(unsortTag))\n        for i in range(0, min(len(sorts), len(unsorts))):\n            sort = sorts[i]\n            log.debug(\"%s: Trying to abbreviate '%s'.\" % (PLUGIN_NAME, sort))\n            if sort in _abbreviate_cache:\n                log.debug(\"  Using abbreviation found in cache: '%s'.\" % (_abbreviate_cache[sort]))\n                sorts[i] = _abbreviate_cache[sort]\n                continue\n            unsort = unsorts[i]\n            new_sort = \"\"\n            new_unsort = \"\"\n\n            while len(sort) > 0 and len(unsort) > 0:\n\n                if not _split in sort:\n                    log.debug(\"  Ending without separator '%s' - moving '%s'.\" % (_split, sort))\n                    new_sort += sort\n                    new_unsort += unsort\n                    sort = unsort = \"\"\n                    continue\n\n                surname, rest = sort.split(_split, 1)\n                if rest == \"\":\n                    log.debug(\"  Ending with separator '%s' - moving '%s'.\" % (_split, surname))\n                    new_sort += sort\n                    new_unsort += unsort\n                    sort = unsort = \"\"\n                    continue\n\n                # Move leading whitespace\n                new_unsort += unsort[0:len(unsort) - len(unsort.lstrip())]\n                unsort = unsort.lstrip()\n\n                # Sorted:   Stuff, ...\n                # Unsorted: Stuff, ...\n                temp = surname + _split\n                l = len(temp)\n                if unsort[:l] == temp:\n                    log.debug(\"  No forename - moving '%s'.\" % (surname))\n                    new_sort += temp\n                    new_unsort += temp\n                    sort = sort[l:]\n                    unsort = unsort[l:]\n                    continue\n\n                # Sorted:   Stuff; Surname, Forename(s)...\n                # Unsorted: Stuff; Forename(s) Surname...\n                # Move matching words plus white-space one by one\n                if unsort.find(' ' + surname) == -1:\n                    while surname.split(None, 1)[0] == unsort.split(None, 1)[0]:\n                        x = unsort.split(None, 1)[0]\n                        log.debug(\"  Moving matching word '%s'.\" % (x))\n                        new_sort += x\n                        new_unsort += x\n                        surname = surname[len(x):]\n                        unsort = unsort[len(x):]\n                        new_sort += surname[0:len(surname) - len(surname.lstrip())]\n                        surname = surname.lstrip()\n                        new_unsort += unsort[0:len(unsort) - len(unsort.lstrip())]\n                        unsort = unsort.lstrip()\n\n                # If we still can't find surname then we are up a creek...\n                pos = unsort.find(' ' + surname)\n                if pos == -1:\n                    log.debug(\n                        _(\"%s: Track %s: Unable to abbreviate surname '%s' - not matched in unsorted %s: '%s'.\"),\n                        PLUGIN_NAME,\n                        metadata['tracknumber'],\n                        surname,\n                        unsortTag,\n                        unsort[i],\n                    )\n                    log.warning(\"  Could not match surname '%s' in remaining unsorted: %s\" % (surname, unsort))\n                    break\n\n                # Sorted:   Surname, Forename(s)...\n                # Unsorted: Forename(s) Surname...\n                forename = unsort[:pos]\n                if rest[:len(forename)] != forename:\n                    log.debug(\n                        _(\"%s: Track %s: Unable to abbreviate surname (%s) - forename (%s) not matched in unsorted %s: '%s'.\"),\n                        PLUGIN_NAME,\n                        metadata['tracknumber'],\n                        surname,\n                        forename,\n                        unsortTag,\n                        unsort[i],\n                    )\n                    log.warning(\"  Could not match forename (%s) for surname (%s) in remaining unsorted (%s):\" % (forename, surname, unsort))\n                    break\n\n                inits = ' '.join([x[0] + '.' for x in forename.split()])\n\n                # Sorted:   Beatles, The...\n                # Unsorted: The Beatles...\n                if forename in _prefixes:\n                    inits = forename\n\n                new_sort += surname + _split + inits\n                sort = rest[len(forename):]\n                new_sort += sort[0:len(sort) - len(sort[1:].lstrip())]\n                sort = sort[1:].lstrip()\n                new_unsort += forename\n                unsort = unsort[len(forename):]\n                new_unsort += unsort[0:len(unsort) - len(unsort.lstrip())]\n                unsort = unsort.lstrip()\n                new_unsort += surname\n                unsort = unsort[len(surname):]\n                new_unsort += unsort[0:len(unsort) - len(unsort[1:].lstrip())]\n                unsort = unsort[1:].lstrip()\n\n                if forename != inits:\n                    log.debug(\n                        _(\"%s: Abbreviated surname (%s, %s) to (%s, %s) in '%s'.\"),\n                        PLUGIN_NAME,\n                        surname,\n                        forename,\n                        surname,\n                        inits,\n                        sortTag,\n                    )\n                    log.debug(\"Abbreviated (%s, %s) to (%s, %s).\" % (surname, forename, surname, inits))\n            else:  # while loop ended without a break i.e. no errors\n                if unsorts[i] != new_unsort:\n                    log.error(\n                        _(\"%s: Track %s: Logic error - mangled %s from '%s' to '%s'.\"),\n                        PLUGIN_NAME,\n                        metadata['tracknumber'],\n                        unsortTag,\n                        unsorts[i],\n                        new_unsort,\n                    )\n                    log.warning(\"Error: Unsorted text for %s has changed from '%s' to '%s'!\" % (unsortTag, unsorts[i], new_unsort))\n                _abbreviate_cache[sorts[i]] = new_sort\n                log.debug(\"  Abbreviated and cached (%s) as (%s).\" % (sorts[i], new_sort))\n                if sorts[i] != new_sort:\n                    log.debug(_(\"%s: Abbreviated tag '%s' to '%s'.\"),\n                              PLUGIN_NAME,\n                              sorts[i],\n                              new_sort,\n                              )\n                    sorts[i] = new_sort\n        metadata[sortTagNew] = sorts\n\nregister_track_metadata_processor(abbreviate_artistsort)\n"
  },
  {
    "path": "plugins/acousticbrainz/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n# AcousticBrainz plugin for Picard\n#\n# Copyright (C) 2021 Wargreen <wargreen@lebib.org>\n# Copyright (C) 2021 Philipp Wolfer <ph.wolfer@gmail.com>\n#\n# This program is free software; you can redistribute it and/or modify\n# it under the terms of the GNU General Public License version 2 as\n# published by the Free Software Foundation.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License along\n# with this program; if not, write to the Free Software Foundation, Inc.,\n# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n#\n\n# Plugin metadata\n# =============================================================================\n\nPLUGIN_NAME = 'AcousticBrainz Tags'\nPLUGIN_AUTHOR = ('Wargreen <wargreen@lebib.org>, '\n                 'Hugo Geoffroy \"pistache\" <pistache@lebib.org>, '\n                 'Philipp Wolfer <ph.wolfer@gmail.com>, '\n                 'Regorxxx <regorxxx@protonmail.com>')\nPLUGIN_DESCRIPTION = '''\nTag files with tags from the AcousticBrainz database, all highlevel classifiers\nand tonal/rhythm data.\n<br/><br/>\nBy default, only simple mood and genre information is saved, but the plugin can\nbe configured to include all highlevel data.\n<br/><br/>\nBased on code from Andrew Cook, Sambhav Kothari\n<br/><br/>\n<b>WARNING:</b> Experimental plugin. All guarantees voided by use.'''\n\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.txt\"\nPLUGIN_VERSION = \"2.2.3\"\nPLUGIN_API_VERSIONS = [\"2.0\", \"2.1\", \"2.2\", \"2.3\", \"2.4\", \"2.5\", \"2.6\", \"2.7\"]\n\n# Plugin configuration\n# =============================================================================\n\nACOUSTICBRAINZ_HOST = \"acousticbrainz.org\"\nACOUSTICBRAINZ_PORT = 80\n\n\n# Subset of the low level data to add as tags.\n# Represent the data as nested dicts.\n\nSUBLOWLEVEL_SUBSET = {\"rhythm\": {\"bpm\": None},\n                      \"tonal\": {\"chords_changes_rate\": None,\n                                \"chords_key\": None,\n                                \"chords_scale\": None,\n                                \"key_key\": None,\n                                \"key_scale\": None}}\n\n# Imports\n# =============================================================================\n\nfrom functools import partial\nfrom json import dumps as dump_json\n\nfrom picard import (\n    config,\n    log,\n)\nfrom picard.metadata import (\n    register_album_metadata_processor,\n    register_track_metadata_processor,\n)\nfrom picard.ui.options import (\n    OptionsPage,\n    register_options_page,\n)\nfrom picard.webservice import ratecontrol\nfrom picard.plugins.acousticbrainz.ui_options_acousticbrainz_tags import Ui_AcousticBrainzOptionsPage\n\nratecontrol.set_minimum_delay((ACOUSTICBRAINZ_HOST, ACOUSTICBRAINZ_PORT), 1000)\n\n# Constants\n# =============================================================================\n\nLOWLEVEL = \"lowlevel\"\nHIGHLEVEL = \"highlevel\"\n\n\n# Logging utilities\n# -------------------------------------------------------------------------\n\ndef log_msg(logger, text, *args):\n    logger(\"%s: \" + text, PLUGIN_NAME, *args)\n\ndef debug(*args):\n    log_msg(log.debug, *args)\n\ndef warning(*args):\n    log_msg(log.warning, *args)\n\ndef error(*args):\n    log_msg(log.error, *args)\n\n\n# TrackDataProcessor class\n# =============================================================================\n# (used to apply AcousticBrainz data to Track metadata)\n\nclass TrackDataProcessor:\n    def __init__(self, recording_id, metadata, level, data, files=None):\n        self.recording_id = recording_id\n        self.metadata = metadata\n        self.level = level\n        self.data = self._extract_data(data)\n        self.files = files\n        self.do_simplemood = config.setting[\"acousticbrainz_add_simplemood\"]\n        self.simplemood_tagname = config.setting[\"acousticbrainz_simplemood_tagname\"]\n        self.do_simplegenre = config.setting[\"acousticbrainz_add_simplegenre\"]\n        self.simplegenre_tagname = config.setting[\"acousticbrainz_simplegenre_tagname\"]\n        self.do_keybpm = config.setting[\"acousticbrainz_add_keybpm\"]\n        self.do_fullhighlevel = config.setting[\"acousticbrainz_add_fullhighlevel\"]\n        self.do_sublowlevel = config.setting[\"acousticbrainz_add_sublowlevel\"]\n\n    # Logging utilities\n    # -------------------------------------------------------------------------\n\n    def log(self, logger, text, *args):\n        log_msg(logger, \"[%s: %s] \" + text, self.shortid, self.title, *args)\n\n    def debug(self, *args):\n        self.log(log.debug, *args)\n\n    def warning(self, *args):\n        self.log(log.warning, *args)\n\n    def error(self, *args):\n        self.log(log.error, *args)\n\n    # Read-only properties\n    # -------------------------------------------------------------------------\n\n    @property\n    def shortid(self):\n        return self.recording_id[:8]\n\n    @property\n    def title(self):\n        return self.metadata[\"title\"]\n\n    # Callback\n    # -------------------------------------------------------------------------\n\n    def process(self):\n        if not self.data:\n            self.warning('No %s data for track %s', self.level, self.recording_id)\n\n        if self.level == HIGHLEVEL:\n            if self.do_simplemood:\n                self.process_simplemood()\n            if self.do_simplegenre:\n                self.process_simplegenre()\n            if self.do_fullhighlevel:\n                self.process_fullhighlevel()\n\n        if self.level == LOWLEVEL:\n            if self.do_keybpm:\n                self.process_keybpm()\n            if self.do_sublowlevel:\n                self.process_sublowlevel()\n\n    # Processing helper methods\n    # -------------------------------------------------------------------------\n\n    def _extract_data(self, data):\n        if not self.recording_id in data:\n            return {}\n        if self.level == LOWLEVEL:\n            return data[self.recording_id][\"0\"]\n        elif self.level == HIGHLEVEL:\n            return data[self.recording_id][\"0\"][\"highlevel\"]\n\n    def filter_data(self, data, subset):\n        result = {}\n        for key, value in subset.items():\n            if key in data:\n                if isinstance(value, dict):\n                    self.debug(\"filter_data : traversing %s\", key)\n                    result[key] = self.filter_data(data[key], value)\n                else:\n                    self.debug(\"filter_data : adding result %s\", key)\n                    result[key] = data[key]\n            else:\n                self.debug(\"filter_data : Subset key %s not found in data\", key)\n                continue\n        self.debug(\"filter_data : result : %s\", result)\n        return result\n\n    def update_metadata(self, name, values):\n        self.metadata[name] = values\n        if self.files:\n            for file in self.files:\n                file.metadata[name] = values\n\n    # Processing methods\n    # -------------------------------------------------------------------------\n    # (fill metadata with fetched data)\n\n    def process_simplemood(self):\n        self.debug(\"processing simplemood data\")\n\n        mood_tagname = self.simplemood_tagname;\n        moods = []\n\n        for classifier, data in self.data.items():\n            if \"value\" in data:\n                value = data[\"value\"]\n                inverted = value.startswith(\"not_\")\n                if classifier.startswith(\"mood_\") and not inverted:\n                    moods.append(value)\n\n        self.update_metadata(mood_tagname, moods)\n\n    def process_simplegenre(self):\n        self.debug(\"processing simplegenre data\")\n\n        genre_tagname = self.simplegenre_tagname;\n        genres = []\n\n        for classifier, data in self.data.items():\n            if \"value\" in data:\n                value = data[\"value\"]\n                inverted = value.startswith(\"not_\")\n                if classifier.startswith(\"genre_\") and not inverted:\n                    genres.append(value)\n\n        self.update_metadata(genre_tagname, genres)\n\n    def process_fullhighlevel(self):\n        self.debug(\"processing fullhighlevel data\")\n\n        f_count, c_count = 0, 0\n        for classifier, data in self.data.items():\n            classifier = classifier.lower()\n            if \"all\" in data:\n                c_count += 1\n                for feature, proba in data[\"all\"].items():\n                    feature = feature.lower()\n                    f_count += 1\n                    self.update_metadata(\"ab:hi:{}:{}\".format(classifier, feature), dump_json(proba))\n            else:\n                self.warning(\"fullhighlevel : ignored invalid classifier data (%s)\", classifier)\n\n        self.debug(\"fullhighlevel : parsed %d features from %d classifiers\", f_count, c_count)\n\n    def process_keybpm(self):\n        self.debug(\"processing keybpm data\")\n        if \"tonal\" in self.data:\n            tonal_data = self.data[\"tonal\"]\n            if \"key_key\" in tonal_data:\n                key = tonal_data[\"key_key\"]\n                if \"key_scale\" in tonal_data:\n                    if tonal_data[\"key_scale\"] == \"minor\":\n                        key += \"m\"\n                self.update_metadata('key', key)\n                self.debug(\"track '%s' is in key %s\", self.title, key)\n\n        if \"rhythm\" in self.data:\n            rhythm_data = self.data[\"rhythm\"]\n            if \"bpm\" in rhythm_data:\n                bpm = int(rhythm_data[\"bpm\"] + 0.5)\n                self.update_metadata('bpm', bpm)\n                self.debug(\"keybpm : Track '%s' has %s bpm\", self.title, bpm)\n\n    def process_sublowlevel(self):\n        self.debug(\"processing sublowlevel data\")\n        subset = SUBLOWLEVEL_SUBSET\n\n        filtered_data = self.filter_data(self.data, subset)\n\n        f_count, c_count = 0, 0\n        for classifier, data in filtered_data.items():\n            classifier = classifier.lower()\n            c_count += 1\n            for feature, proba in data.items():\n                feature = feature.lower()\n                f_count += 1\n                self.update_metadata(\"ab:lo:{}:{}\".format(classifier, feature), proba)\n\n        self.debug(\"sublowlevel : parsed %d features from %d classifiers\", f_count, c_count)\n\n\nclass AcousticBrainzRequest:\n\n    MAX_BATCH_SIZE = 25\n\n    def __init__(self, webservice, recording_ids):\n        self.webservice = webservice\n        self.recording_ids = recording_ids\n\n    def request_highlevel(self, callback):\n        self._batch('high-level', self.recording_ids, callback, {})\n\n    def request_lowlevel(self, callback):\n        self._batch('low-level', self.recording_ids, callback, {})\n\n    def _batch(self, action, recording_ids, callback, result, response=None, reply=None, error=None):\n        if response and not error:\n            self._merge_results(result, response)\n\n        if not recording_ids or error:\n            callback(result, error)\n            return\n\n        batch = recording_ids[:self.MAX_BATCH_SIZE]\n        recording_ids = recording_ids[self.MAX_BATCH_SIZE:]\n        self._do_request(action, batch,\n            callback=partial(self._batch, action, recording_ids, callback, result))\n\n    def _do_request(self, action, recording_ids, callback):\n        self.webservice.get(\n            ACOUSTICBRAINZ_HOST,\n            ACOUSTICBRAINZ_PORT,\n            '/api/v1/%s' % action,\n            callback,\n            priority=True,\n            parse_response_type='json',\n            queryargs=self._get_query_args(action, recording_ids)\n        )\n\n    def _get_query_args(self, action, recording_ids):\n        queryargs = {\n            'recording_ids': ';'.join(recording_ids),\n        }\n        if action == 'high-level':\n            queryargs['map_classes'] = 'true'\n        return queryargs\n\n    def _merge_results(self, full, new):\n        mapping = new.get('mbid_mapping', {})\n        new = {mapping.get(k, k): v for (k, v) in new.items() if k != 'mbid_mapping'}\n        full.update(new)\n\n\n# Plugin class\n# =============================================================================\n# (provides track processing callback)\n\nclass AcousticBrainzPlugin:\n\n    result_cache = {\n        LOWLEVEL: {},\n        HIGHLEVEL: {},\n    }\n\n    def process_album(self, album, metadata, release):\n        debug('Processing album %s', album.id)\n        recording_ids = self.get_recording_ids(release)\n        self.run_requests(album, recording_ids, self.album_callback)\n\n    def process_track(self, album, metadata, track_node, release_node):\n        # Run requests for standalone recordings\n        if not release_node and 'id' in track_node:\n            recording_id = track_node['id']\n            debug('Processing recording %s', recording_id)\n            self.run_requests(album, [recording_id], partial(self.nat_callback, recording_id))\n        # Apply metadata changes for already loaded results for album tracks\n        elif 'recording' in track_node:\n            recording_id = track_node['recording']['id']\n            for level in (LOWLEVEL, HIGHLEVEL):\n                result = self.result_cache[level].get(album.id)\n                if result:\n                    self.apply_result(recording_id, metadata, level, result)\n\n    def run_requests(self, album, recording_ids, callback):\n        request = AcousticBrainzRequest(album.tagger.webservice, recording_ids)\n        if self.do_highlevel:\n            album._requests += 1\n            request.request_highlevel(partial(callback, HIGHLEVEL, album))\n        if self.do_lowlevel:\n            album._requests += 1\n            request.request_lowlevel(partial(callback, LOWLEVEL, album))\n\n    @property\n    def do_highlevel(self):\n        return (config.setting[\"acousticbrainz_add_simplemood\"]\n            or config.setting[\"acousticbrainz_add_simplegenre\"]\n            or config.setting[\"acousticbrainz_add_fullhighlevel\"])\n\n    @property\n    def do_lowlevel(self):\n        return (config.setting[\"acousticbrainz_add_keybpm\"]\n            or config.setting[\"acousticbrainz_add_sublowlevel\"])\n\n    def get_recording_ids(self, release):\n        return [track['recording']['id'] for track in self.iter_tracks(release)]\n\n    @staticmethod\n    def iter_tracks(release):\n        for media in release['media']:\n            if 'pregap' in media:\n                yield media['pregap']\n\n            if 'tracks' in media:\n                yield from media['tracks']\n\n            if 'data-tracks' in media:\n                yield from media['data-tracks']\n\n    def album_callback(self, level, album, result=None, error=None):\n        if not error:\n            # Store the result, the actual processing will be done by the\n            # track metadata processor.\n            self.result_cache[level][album.id] = result\n            album.run_when_loaded(partial(self.clear_cache, level, album))\n        album._requests -= 1\n        album._finalize_loading(error)\n\n    def nat_callback(self, recording_id, level, album, result=None, error=None):\n        for track in album.tracks:\n            if track.id == recording_id:\n                self.apply_result(track.id, track.metadata, level, result, files=track.files)\n\n    def apply_result(self, recording_id, metadata, level, result, files=None):\n        debug('Updating recording %s with %s results', recording_id, level)\n        processor = TrackDataProcessor(recording_id, metadata, level, result, files=files)\n        processor.process()\n\n    def clear_cache(self, level, album):\n        try:\n            del self.result_cache[level][album.id]\n        except KeyError:\n            pass\n\n\n# Plugin options page\n# =============================================================================\n# (define plugin options and link with user interface)\n\nclass AcousticBrainzOptionsPage(OptionsPage):\n    NAME = \"acousticbrainz_tags\"\n    TITLE = \"AcousticBrainz tags\"\n    PARENT = \"plugins\"\n\n    options = [\n        config.BoolOption(\"setting\", \"acousticbrainz_add_simplemood\", True),\n        config.TextOption(\"setting\", \"acousticbrainz_simplemood_tagname\", \"ab:mood\"),\n        config.BoolOption(\"setting\", \"acousticbrainz_add_simplegenre\", True),\n        config.TextOption(\"setting\", \"acousticbrainz_simplegenre_tagname\", \"ab:genre\"),\n        config.BoolOption(\"setting\", \"acousticbrainz_add_keybpm\", False),\n        config.BoolOption(\"setting\", \"acousticbrainz_add_fullhighlevel\", False),\n        config.BoolOption(\"setting\", \"acousticbrainz_add_sublowlevel\", False)\n    ]\n\n    def __init__(self, parent=None):\n        super(AcousticBrainzOptionsPage, self).__init__(parent)\n        self.ui = Ui_AcousticBrainzOptionsPage()\n        self.ui.setupUi(self)\n\n    def load(self):\n        setting = config.setting\n        self.ui.add_simplemood.setChecked(setting[\"acousticbrainz_add_simplemood\"])\n        self.ui.simplemood_tagname.setText(setting[\"acousticbrainz_simplemood_tagname\"])\n        self.ui.add_simplegenre.setChecked(setting[\"acousticbrainz_add_simplegenre\"])\n        self.ui.simplegenre_tagname.setText(setting[\"acousticbrainz_simplegenre_tagname\"])\n        self.ui.add_fullhighlevel.setChecked(setting[\"acousticbrainz_add_fullhighlevel\"])\n        self.ui.add_keybpm.setChecked(setting[\"acousticbrainz_add_keybpm\"])\n        self.ui.add_sublowlevel.setChecked(setting[\"acousticbrainz_add_sublowlevel\"])\n\n    def save(self):\n        setting = config.setting\n        setting[\"acousticbrainz_add_simplemood\"] = self.ui.add_simplemood.isChecked()\n        setting[\"acousticbrainz_simplemood_tagname\"] = str(self.ui.simplemood_tagname.text())\n        setting[\"acousticbrainz_add_simplegenre\"] = self.ui.add_simplegenre.isChecked()\n        setting[\"acousticbrainz_simplegenre_tagname\"] = str(self.ui.simplegenre_tagname.text())\n        setting[\"acousticbrainz_add_keybpm\"] = self.ui.add_keybpm.isChecked()\n        setting[\"acousticbrainz_add_fullhighlevel\"] = self.ui.add_fullhighlevel.isChecked()\n        setting[\"acousticbrainz_add_sublowlevel\"] = self.ui.add_sublowlevel.isChecked()\n\n\nplugin = AcousticBrainzPlugin()\nregister_album_metadata_processor(plugin.process_album)\nregister_track_metadata_processor(plugin.process_track)\nregister_options_page(AcousticBrainzOptionsPage)\n"
  },
  {
    "path": "plugins/acousticbrainz/ui_options_acousticbrainz_tags.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/acousticbrainz/ui_options_acousticbrainz_tags.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.7\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_AcousticBrainzOptionsPage(object):\n    def setupUi(self, AcousticBrainzOptionsPage):\n        AcousticBrainzOptionsPage.setObjectName(\"AcousticBrainzOptionsPage\")\n        AcousticBrainzOptionsPage.setEnabled(True)\n        AcousticBrainzOptionsPage.resize(527, 443)\n        AcousticBrainzOptionsPage.setWindowTitle(\"\")\n        self.verticalLayout = QtWidgets.QVBoxLayout(AcousticBrainzOptionsPage)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.acousticbrainzTags_groupBox = QtWidgets.QGroupBox(AcousticBrainzOptionsPage)\n        self.acousticbrainzTags_groupBox.setObjectName(\"acousticbrainzTags_groupBox\")\n        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.acousticbrainzTags_groupBox)\n        self.verticalLayout_2.setObjectName(\"verticalLayout_2\")\n        self.add_simplemood = QtWidgets.QCheckBox(self.acousticbrainzTags_groupBox)\n        self.add_simplemood.setObjectName(\"add_simplemood\")\n        self.verticalLayout_2.addWidget(self.add_simplemood)\n        self.simplemood_tagname_label = QtWidgets.QLabel(self.acousticbrainzTags_groupBox)\n        self.simplemood_tagname_label.setObjectName(\"simplemood_tagname_label\")\n        self.verticalLayout_2.addWidget(self.simplemood_tagname_label)\n        self.simplemood_tagname = QtWidgets.QLineEdit(self.acousticbrainzTags_groupBox)\n        self.simplemood_tagname.setObjectName(\"simplemood_tagname\")\n        self.verticalLayout_2.addWidget(self.simplemood_tagname)\n        self.add_simplegenre = QtWidgets.QCheckBox(self.acousticbrainzTags_groupBox)\n        self.add_simplegenre.setObjectName(\"add_simplegenre\")\n        self.verticalLayout_2.addWidget(self.add_simplegenre)\n        self.simplegenre_tagname_label = QtWidgets.QLabel(self.acousticbrainzTags_groupBox)\n        self.simplegenre_tagname_label.setObjectName(\"simplegenre_tagname_label\")\n        self.verticalLayout_2.addWidget(self.simplegenre_tagname_label)\n        self.simplegenre_tagname = QtWidgets.QLineEdit(self.acousticbrainzTags_groupBox)\n        self.simplegenre_tagname.setObjectName(\"simplegenre_tagname\")\n        self.verticalLayout_2.addWidget(self.simplegenre_tagname)\n        self.add_keybpm = QtWidgets.QCheckBox(self.acousticbrainzTags_groupBox)\n        self.add_keybpm.setObjectName(\"add_keybpm\")\n        self.verticalLayout_2.addWidget(self.add_keybpm)\n        self.add_fullhighlevel = QtWidgets.QCheckBox(self.acousticbrainzTags_groupBox)\n        self.add_fullhighlevel.setObjectName(\"add_fullhighlevel\")\n        self.verticalLayout_2.addWidget(self.add_fullhighlevel)\n        self.add_sublowlevel = QtWidgets.QCheckBox(self.acousticbrainzTags_groupBox)\n        self.add_sublowlevel.setObjectName(\"add_sublowlevel\")\n        self.verticalLayout_2.addWidget(self.add_sublowlevel)\n        spacerItem = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)\n        self.verticalLayout_2.addItem(spacerItem)\n        self.sublowlevel_descr = QtWidgets.QLabel(self.acousticbrainzTags_groupBox)\n        self.sublowlevel_descr.setTextFormat(QtCore.Qt.RichText)\n        self.sublowlevel_descr.setObjectName(\"sublowlevel_descr\")\n        self.verticalLayout_2.addWidget(self.sublowlevel_descr)\n        spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.verticalLayout_2.addItem(spacerItem1)\n        self.verticalLayout.addWidget(self.acousticbrainzTags_groupBox)\n\n        self.retranslateUi(AcousticBrainzOptionsPage)\n        QtCore.QMetaObject.connectSlotsByName(AcousticBrainzOptionsPage)\n\n    def retranslateUi(self, AcousticBrainzOptionsPage):\n        _translate = QtCore.QCoreApplication.translate\n        self.acousticbrainzTags_groupBox.setTitle(_translate(\"AcousticBrainzOptionsPage\", \"AcousticBrainz Tags\"))\n        self.add_simplemood.setText(_translate(\"AcousticBrainzOptionsPage\", \"Add simple Mood tags\"))\n        self.simplemood_tagname_label.setText(_translate(\"AcousticBrainzOptionsPage\", \"Mood tag name:\"))\n        self.add_simplegenre.setText(_translate(\"AcousticBrainzOptionsPage\", \"Add simple Genre tags\"))\n        self.simplegenre_tagname_label.setText(_translate(\"AcousticBrainzOptionsPage\", \"Genre tag name:\"))\n        self.add_keybpm.setText(_translate(\"AcousticBrainzOptionsPage\", \"Add simple BPM and Key tags\"))\n        self.add_fullhighlevel.setText(_translate(\"AcousticBrainzOptionsPage\", \"Add all highlevel AcousticBrainz tags\"))\n        self.add_sublowlevel.setText(_translate(\"AcousticBrainzOptionsPage\", \"Add a subset of the lowlevel AcousticBrainz tags\"))\n        self.sublowlevel_descr.setText(_translate(\"AcousticBrainzOptionsPage\", \"<html><head/><body><p>The low level subset include:</p><ul style=\\\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;\\\"><li style=\\\" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">rhythm:bpm</li><li style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">tonal:chords_change_rate</li><li style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">tonal:chords_key</li><li style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">tonal:chords_scale</li><li style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">tonal:key_key</li><li style=\\\" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">tonal:key_scale</li></ul></body></html>\"))\n"
  },
  {
    "path": "plugins/acousticbrainz/ui_options_acousticbrainz_tags.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>AcousticBrainzOptionsPage</class>\n <widget class=\"QWidget\" name=\"AcousticBrainzOptionsPage\">\n  <property name=\"enabled\">\n   <bool>true</bool>\n  </property>\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>527</width>\n    <height>443</height>\n   </rect>\n  </property>\n  <property name=\"windowTitle\">\n   <string/>\n  </property>\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n   <item>\n    <widget class=\"QGroupBox\" name=\"acousticbrainzTags_groupBox\">\n     <property name=\"title\">\n      <string>AcousticBrainz Tags</string>\n     </property>\n     <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\n      <item>\n       <widget class=\"QCheckBox\" name=\"add_simplemood\">\n        <property name=\"text\">\n         <string>Add simple Mood tags</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"simplemood_tagname_label\">\n        <property name=\"text\">\n         <string>Mood tag name:</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLineEdit\" name=\"simplemood_tagname\"/>\n      </item>\n      <item>\n       <widget class=\"QCheckBox\" name=\"add_simplegenre\">\n        <property name=\"text\">\n         <string>Add simple Genre tags</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"simplegenre_tagname_label\">\n        <property name=\"text\">\n         <string>Genre tag name:</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLineEdit\" name=\"simplegenre_tagname\"/>\n      </item>\n      <item>\n       <widget class=\"QCheckBox\" name=\"add_keybpm\">\n        <property name=\"text\">\n         <string>Add simple BPM and Key tags</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QCheckBox\" name=\"add_fullhighlevel\">\n        <property name=\"text\">\n         <string>Add all highlevel AcousticBrainz tags</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QCheckBox\" name=\"add_sublowlevel\">\n        <property name=\"text\">\n         <string>Add a subset of the lowlevel AcousticBrainz tags</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <spacer name=\"verticalSpacer_2\">\n        <property name=\"orientation\">\n         <enum>Qt::Vertical</enum>\n        </property>\n        <property name=\"sizeType\">\n         <enum>QSizePolicy::Preferred</enum>\n        </property>\n        <property name=\"sizeHint\" stdset=\"0\">\n         <size>\n          <width>20</width>\n          <height>20</height>\n         </size>\n        </property>\n       </spacer>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"sublowlevel_descr\">\n        <property name=\"text\">\n         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The low level subset include:&lt;/p&gt;&lt;ul style=&quot;margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;&quot;&gt;&lt;li style=&quot; margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;rhythm:bpm&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;tonal:chords_change_rate&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;tonal:chords_key&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;tonal:chords_scale&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;tonal:key_key&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;tonal:key_scale&lt;/li&gt;&lt;/ul&gt;&lt;/body&gt;&lt;/html&gt;</string>\n        </property>\n        <property name=\"textFormat\">\n         <enum>Qt::RichText</enum>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <spacer name=\"verticalSpacer\">\n        <property name=\"orientation\">\n         <enum>Qt::Vertical</enum>\n        </property>\n        <property name=\"sizeHint\" stdset=\"0\">\n         <size>\n          <width>20</width>\n          <height>40</height>\n         </size>\n        </property>\n       </spacer>\n      </item>\n     </layout>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py",
    "content": "# -*- coding: utf-8 -*-\n# Acousticbrainz Tonal/Rhythm plugin for Picard\n# Copyright (C) 2015  Sophist\n#\n# This program is free software; you can redistribute it and/or modify\n# it under the terms of the GNU General Public License version 2 as\n# published by the Free Software Foundation.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License along\n# with this program; if not, write to the Free Software Foundation, Inc.,\n# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n#\n\nPLUGIN_NAME = 'AcousticBrainz Tonal-Rhythm'\nPLUGIN_AUTHOR = 'Sophist, Sambhav Kothari'\nPLUGIN_DESCRIPTION = '''Add's the following tags:\n<ul>\n<li>Key (in ID3v2.3 format)</li>\n<li>Beats Per Minute (BPM)</li>\n</ul>\nfrom the AcousticBrainz database.<br/><br/>\n<em>This plugin is deprecated, please consider using the AcousticBrainz Tags\nplugin instead.</em>\n'''\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.txt\"\nPLUGIN_VERSION = '1.1.6'\nPLUGIN_API_VERSIONS = [\"2.0\"]  # Requires support for TKEY which is in 1.4\n\nfrom json import JSONDecodeError\n\nfrom picard import log\nfrom picard.metadata import register_track_metadata_processor\nfrom functools import partial\nfrom picard.webservice import ratecontrol\nfrom picard.util import load_json\n\nACOUSTICBRAINZ_HOST = \"acousticbrainz.org\"\nACOUSTICBRAINZ_PORT = 80\n\nratecontrol.set_minimum_delay((ACOUSTICBRAINZ_HOST, ACOUSTICBRAINZ_PORT), 1000)\n\n\nclass AcousticBrainz_Key:\n\n    def get_data(self, album, track_metadata, track_node, release_node):\n        if \"musicbrainz_recordingid\" not in track_metadata:\n            log.error(\"%s: Error parsing response. No MusicBrainz recording id found.\",\n                      PLUGIN_NAME)\n            return\n        recordingId = track_metadata['musicbrainz_recordingid']\n        if recordingId:\n            log.debug(\"%s: Add AcousticBrainz request for %s (%s)\",\n                      PLUGIN_NAME, track_metadata['title'], recordingId)\n            self.album_add_request(album)\n            path = \"/%s/low-level\" % recordingId\n            return album.tagger.webservice.get(\n                        ACOUSTICBRAINZ_HOST,\n                        ACOUSTICBRAINZ_PORT,\n                        path,\n                        partial(self.process_data, album, track_metadata),\n                        priority=True,\n                        important=False,\n                        parse_response_type=None)\n\n    def process_data(self, album, track_metadata, response, reply, error):\n        if error:\n            log.error(\"%s: Network error retrieving acousticBrainz data for recordingId %s\",\n                      PLUGIN_NAME, track_metadata['musicbrainz_recordingid'])\n            self.album_remove_request(album)\n            return\n        try:\n            data = load_json(response)\n        except JSONDecodeError:\n            log.error(\"%s: Network error retrieving AcousticBrainz data for recordingId %s\",\n                      PLUGIN_NAME, track_metadata['musicbrainz_recordingid'])\n            self.album_remove_request(album)\n            return\n        if \"tonal\" in data:\n            if \"key_key\" in data[\"tonal\"]:\n                key = data[\"tonal\"][\"key_key\"]\n                if \"key_scale\" in data[\"tonal\"]:\n                    scale = data[\"tonal\"][\"key_scale\"]\n                    if scale == \"minor\":\n                        key += \"m\"\n                track_metadata[\"key\"] = key\n                log.debug(\"%s: Track '%s' is in key %s\", PLUGIN_NAME, track_metadata[\"title\"], key)\n        if \"rhythm\" in data:\n            if \"bpm\" in data[\"rhythm\"]:\n                bpm = int(data[\"rhythm\"][\"bpm\"] + 0.5)\n                track_metadata[\"bpm\"] = bpm\n                log.debug(\"%s: Track '%s' has %s bpm\", PLUGIN_NAME, track_metadata[\"title\"], bpm)\n        self.album_remove_request(album)\n\n    def album_add_request(self, album):\n        album._requests += 1\n\n    def album_remove_request(self, album):\n        album._requests -= 1\n        if album._requests == 0:\n            album._finalize_loading(None)\n\n\nregister_track_metadata_processor(AcousticBrainz_Key().get_data)\n"
  },
  {
    "path": "plugins/add_to_collection/README.md",
    "content": "# Add to Collection\n\nThis plugin allows you to add any saved release to one of your user release [collections](https://musicbrainz.org/doc/Collections).\n\nDownload [here](https://picard.musicbrainz.org/api/v2/download?id=add_to_collection).\n\n---\n\nThe plugin adds a settings page under the \"Plugins\" section under \"Options...\" from Picard's main menu that lets you choose\nthe collection you want to add the releases to\n\n![settings](assets/settings.png)\n"
  },
  {
    "path": "plugins/add_to_collection/__init__.py",
    "content": "from picard.plugins.add_to_collection.manifest import *\nfrom picard.plugins.add_to_collection import options, post_save_processor\n\noptions.register_options()\npost_save_processor.register_processor()\n"
  },
  {
    "path": "plugins/add_to_collection/manifest.py",
    "content": "PLUGIN_NAME = \"Add to Collection\"\nPLUGIN_AUTHOR = \"Dvir Yitzchaki (dvirtz@gmail.com)\"\nPLUGIN_DESCRIPTION = \"Adds any saved release to one of your user collections\"\nPLUGIN_VERSION = \"0.1.2\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\nPLUGIN_LICENSE = (\"MIT\",)\nPLUGIN_LICENSE_URL = \"https://spdx.org/licenses/MIT.html\"\nPLUGIN_USER_GUIDE_URL = (\n    \"https://github.com/metabrainz/picard-plugins/blob/2.0/plugins/add_to_collection/README.md\"\n)\n"
  },
  {
    "path": "plugins/add_to_collection/options.py",
    "content": "from picard.collection import Collection, user_collections\nfrom picard.ui.options import OptionsPage, register_options_page\n\nfrom picard.plugins.add_to_collection import settings\nfrom picard.plugins.add_to_collection.manifest import PLUGIN_NAME\nfrom picard.plugins.add_to_collection.ui_add_to_collection_options import (\n    Ui_AddToCollectionOptions,\n)\nfrom picard.plugins.add_to_collection.override_module import override_module\n\n\nclass AddToCollectionOptionsPage(OptionsPage):\n    NAME = \"add-to-collection\"\n    TITLE = PLUGIN_NAME\n    PARENT = \"plugins\"\n\n    options = [settings.collection_id_option()]\n\n    def __init__(self, parent=None) -> None:\n        super().__init__(parent)\n        self.ui = Ui_AddToCollectionOptions()\n        self.ui.setupUi(self)\n\n    def load(self) -> None:\n        self.set_collection_name(settings.collection_id())\n\n    def save(self) -> None:\n        settings.set_collection_id(self.ui.collection_name.currentData())\n\n    def set_collection_name(self, value: str) -> None:\n        self.ui.collection_name.clear()\n        collection: Collection\n        for collection in sorted(user_collections.values(), key=lambda c: c.name.lower()):\n            self.ui.collection_name.addItem(collection.name, collection.id)\n        idx = self.ui.collection_name.findData(value)\n        if idx != -1:\n            self.ui.collection_name.setCurrentIndex(idx)\n\n\ndef register_options() -> None:\n    with override_module(AddToCollectionOptionsPage):\n        register_options_page(AddToCollectionOptionsPage)\n"
  },
  {
    "path": "plugins/add_to_collection/override_module.py",
    "content": "from contextlib import contextmanager\nfrom typing import Generator\n\n\n@contextmanager\ndef override_module(obj: object) -> Generator[None, None, None]:\n    # picard expects hooks to be defined at module level\n    module = obj.__module__\n    obj.__module__ = \".\".join(module.split(\".\")[:-1])\n    yield\n    obj.__module__ = module\n"
  },
  {
    "path": "plugins/add_to_collection/post_save_processor.py",
    "content": "from picard import log\nfrom picard.collection import Collection, user_collections\nfrom picard.file import File, register_file_post_save_processor\n\nfrom picard.plugins.add_to_collection import settings\nfrom picard.plugins.add_to_collection.override_module import override_module\n\n\ndef post_save_processor(file: File) -> None:\n    collection_id = settings.collection_id()\n    if not collection_id:\n        log.error(\"cannot find collection ID setting\")\n        return\n    collection: Collection = user_collections.get(collection_id)\n    if not collection:\n        log.error(f\"cannot find collection with id {collection_id}\")\n        return\n    release_id = file.metadata[\"musicbrainz_albumid\"]\n    if release_id and release_id not in collection.releases:\n        log.debug(\"Adding release %r to %r\", release_id, collection.name)\n        collection.add_releases(set([release_id]), callback=lambda: None)\n\n\ndef register_processor() -> None:\n    with override_module(post_save_processor):\n        register_file_post_save_processor(post_save_processor)\n"
  },
  {
    "path": "plugins/add_to_collection/settings.py",
    "content": "from picard.config import TextOption, get_config\nfrom typing import Optional\n\nCOLLECTION_ID = \"add_to_collection_id\"\n\n\ndef collection_id_option() -> TextOption:\n    return TextOption(section=\"setting\", name=COLLECTION_ID, default=None)\n\n\ndef collection_id() -> Optional[str]:\n    config = get_config()\n    if COLLECTION_ID in config.setting:\n        return config.setting[COLLECTION_ID]\n    return None\n\n\ndef set_collection_id(value: str) -> None:\n    config = get_config()\n    config.setting[COLLECTION_ID] = value\n"
  },
  {
    "path": "plugins/add_to_collection/ui_add_to_collection_options.py",
    "content": "from PyQt5 import QtCore, QtWidgets\n\n\nclass Ui_AddToCollectionOptions(object):\n    def setupUi(self, AddToCollectionOptions):\n        AddToCollectionOptions.setObjectName(\"AddToCollectionOptions\")\n        AddToCollectionOptions.resize(472, 215)\n        self.verticalLayout = QtWidgets.QVBoxLayout(AddToCollectionOptions)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.collection_label = QtWidgets.QLabel(AddToCollectionOptions)\n        self.collection_label.setObjectName(\"collection_label\")\n        self.verticalLayout.addWidget(self.collection_label)\n        sizePolicy = QtWidgets.QSizePolicy(\n            QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed\n        )\n        self.collection_name = QtWidgets.QComboBox(AddToCollectionOptions)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(\n            self.collection_name.sizePolicy().hasHeightForWidth()\n        )\n        self.collection_name.setSizePolicy(sizePolicy)\n        self.collection_name.setEditable(False)\n        self.collection_name.setObjectName(\"collection_name\")\n        self.verticalLayout.addWidget(self.collection_name)\n        spacerItem = QtWidgets.QSpacerItem(\n            20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding\n        )\n        self.verticalLayout.addItem(spacerItem)\n\n        self.retranslateUi(AddToCollectionOptions)\n        QtCore.QMetaObject.connectSlotsByName(AddToCollectionOptions)\n\n    def retranslateUi(self, AddToCollectionOptions):\n        _translate = QtCore.QCoreApplication.translate\n        AddToCollectionOptions.setWindowTitle(_(\"Form\"))\n        self.collection_label.setText(_(\"Collection to add releases to:\"))\n"
  },
  {
    "path": "plugins/additional_artists_details/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Additional Artists Details\n\"\"\"\n# Copyright (C) 2023-2024 Bob Swift (rdswift)\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\n# pylint: disable=line-too-long\n# pylint: disable=import-error\n# pylint: disable=too-many-arguments\n# pylint: disable=too-many-locals\n\nfrom collections import namedtuple\nfrom functools import partial\n\nfrom picard import (\n    config,\n    log,\n)\nfrom picard.album import register_album_post_removal_processor\nfrom picard.metadata import (\n    register_album_metadata_processor,\n    register_track_metadata_processor,\n)\nfrom picard.plugin import PluginPriority\nfrom picard.plugins.additional_artists_details.ui_options_additional_artists_details import (\n    Ui_AdditionalArtistsDetailsOptionsPage,\n)\nfrom picard.webservice.api_helpers import MBAPIHelper\n\nfrom picard.ui.options import (\n    OptionsPage,\n    register_options_page,\n)\n\n\nPLUGIN_NAME = 'Additional Artists Details'\nPLUGIN_AUTHOR = 'Bob Swift (rdswift)'\nPLUGIN_DESCRIPTION = '''\nThis plugin provides specialized album and track variables with artist details for use in tagging and naming scripts.  Note that this creates\nadditional calls to the MusicBrainz API for the artist and area information, and this will slow down processing.  This will be particularly\nnoticable when there are many different album or track artists, such as on a [Various Artists] release.  There is an option to disable track\nartist processing, which can significantly increase the processing speed if you are only interested in album artist details.\n<br /><br />\nPlease see the <a href=\"https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/additional_artists_details/docs/README.md\">user\nguide</a> on GitHub for more information.\n'''\nPLUGIN_VERSION = '0.4'\nPLUGIN_API_VERSIONS = ['2.0', '2.1', '2.2', '2.7', '2.8', '2.11']\nPLUGIN_LICENSE = 'GPL-2.0-or-later'\nPLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl-2.0.html'\n\nPLUGIN_USER_GUIDE_URL = 'https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/additional_artists_details/docs/README.md'\n\n# Named tuples for code clarity\nArea = namedtuple('Area', ['parent', 'name', 'country', 'type', 'type_text'])\nMetadataPair = namedtuple('MetadataPair', ['artists', 'target'])\n\n# MusicBrainz ID codes for relationship types\nRELATIONSHIP_TYPE_PART_OF = 'de7cc874-8b1b-3a05-8272-f3834c968fb7'\n\n# MusicBrainz ID codes for area types\nAREA_TYPE_COUNTRY = '06dd0ae4-8c74-30bb-b43d-95dcedf961de'\nAREA_TYPE_COUNTY = 'bcecec27-8bdb-3e00-8254-d948dda502fa'\nAREA_TYPE_MUNICIPALITY = '17246454-5ac4-36a1-b81a-4753eb2dab20'\n\n# Area types to exclude from the location string\nEXCLUDE_AREA_TYPES = {AREA_TYPE_MUNICIPALITY, AREA_TYPE_COUNTY}\n\n# Standard text for arguments\nALBUM_ARTISTS = 'album_artists'\nARTIST = 'artist'\nARTIST_REQUESTS = 'artist_requests'\nAREA = 'area'\nAREA_REQUESTS = 'area_requests'\nISO_CODES_1 = 'iso-3166-1-codes'\nISO_CODES_2 = 'iso-3166-2-codes'\nOPT_AREA_DETAILS = 'aad_area_details'\nOPT_PROCESS_TRACKS = 'aad_process_tracks'\nTRACKS = 'tracks'\n\n\ndef log_helper(text, *args):\n    \"\"\"Logging helper to prepend the plugin name to the text.\n\n    Args:\n        text (str): Text to log.\n        args (list): List of text replacement arguments.\n\n    Returns:\n        list: updated text and replacement arguments.\n    \"\"\"\n    retval = [\"%s: \" + text, PLUGIN_NAME]\n    retval.extend(args)\n    return retval\n\n\nclass CustomHelper(MBAPIHelper):\n    \"\"\"Custom MusicBrainz API helper to retrieve artist and area information.\n    \"\"\"\n\n    def get_artist_by_id(self, _id, handler, inc=None, priority=False, important=False,\n                         mblogin=False, refresh=False):\n        \"\"\"Get information for the specified artist MBID.\n\n        Args:\n            _id (str): Artist MBID to retrieve.\n            handler (object): Callback used to process the returned information.\n            inc (list, optional): List of includes to add to the API call. Defaults to None.\n            priority (bool, optional): Process the request at a high priority. Defaults to False.\n            important (bool, optional): Identify the request as important. Defaults to False.\n            mblogin (bool, optional): Request requires logging into MusicBrainz. Defaults to False.\n            refresh (bool, optional): Request triggers a refresh. Defaults to False.\n\n        Returns:\n            RequestTask: Requested task object.\n        \"\"\"\n        return self._get_by_id(ARTIST, _id, handler, inc, priority=priority, important=important, mblogin=mblogin, refresh=refresh)\n\n    def get_area_by_id(self, _id, handler, inc=None, priority=False, important=False, mblogin=False, refresh=False):\n        \"\"\"Get information for the specified area MBID.\n\n        Args:\n            _id (str): Area MBID to retrieve.\n            handler (object): Callback used to process the returned information.\n            inc (list, optional): List of includes to add to the API call. Defaults to None.\n            priority (bool, optional): Process the request at a high priority. Defaults to False.\n            important (bool, optional): Identify the request as important. Defaults to False.\n            mblogin (bool, optional): Request requires logging into MusicBrainz. Defaults to False.\n            refresh (bool, optional): Request triggers a refresh. Defaults to False.\n\n        Returns:\n            RequestTask: Requested task object.\n        \"\"\"\n        if inc is None:\n            inc = ['area-rels']\n        return self._get_by_id(AREA, _id, handler, inc, priority=priority, important=important, mblogin=mblogin, refresh=refresh)\n\n\nclass ArtistDetailsPlugin:\n    \"\"\"Plugin to retrieve artist details, including area and country information.\n    \"\"\"\n    result_cache = {\n        ARTIST: {},\n        ARTIST_REQUESTS: set(),\n        AREA: {},\n        AREA_REQUESTS: set(),\n    }\n    album_processing_count = {}\n    albums = {}\n\n    def _make_empty_target(self, album_id):\n        \"\"\"Create an empty album target node if it doesn't exist.\n\n        Args:\n            album_id (str): MBID of the album.\n        \"\"\"\n        if album_id not in self.albums:\n            self.albums[album_id] = {ALBUM_ARTISTS: set(), TRACKS: []}\n\n    def _add_target(self, album_id, artists, target_metadata):\n        \"\"\"Add a metadata target to update for an album.\n\n        Args:\n            album_id (str): MBID of the album.\n            artists (set): Set of artists to include.\n            target_metadata (Metadata): Target metadata to update.\n        \"\"\"\n        self._make_empty_target(album_id)\n        self.albums[album_id][TRACKS].append(MetadataPair(artists, target_metadata))\n\n    def _remove_album(self, album_id):\n        \"\"\"Removes an album from the metadata processing dictionary.\n\n        Args:\n            album_id (str): MBID of the album to remove.\n        \"\"\"\n        log.debug(*log_helper(\"Removing album '%s'\", album_id))\n        self.albums.pop(album_id, None)\n        self.album_processing_count.pop(album_id, None)\n\n    def _album_add_request(self, album):\n        \"\"\"Increment the number of pending requests for an album.\n\n        Args:\n            album (Album): The Album object to use for the processing.\n        \"\"\"\n        if album.id not in self.album_processing_count:\n            self.album_processing_count[album.id] = 0\n        self.album_processing_count[album.id] += 1\n        album._requests += 1\n\n    def _album_remove_request(self, album):\n        \"\"\"Decrement the number of pending requests for an album.\n\n        Args:\n            album (Album): The Album object to use for the processing.\n        \"\"\"\n        if album.id not in self.album_processing_count:\n            self.album_processing_count[album.id] = 1\n        self.album_processing_count[album.id] -= 1\n        album._requests -= 1\n        album._finalize_loading(None)   # pylint: disable=protected-access\n\n    def remove_album(self, album):\n        \"\"\"Remove the album from the albums processing dictionary.\n\n        Args:\n            album (Album): The album object to remove.\n        \"\"\"\n        self._remove_album(album.id)\n\n    def make_album_vars(self, album, album_metadata, _release_metadata):\n        \"\"\"Process album artists.\n\n        Args:\n            album (Album): The Album object to use for the processing.\n            album_metadata (Metadata): Metadata object for the album.\n            _release_metadata (dict): Dictionary of release data from MusicBrainz api.\n        \"\"\"\n        artists = set(artist.id for artist in album.get_album_artists())\n        self._make_empty_target(album.id)\n        self.albums[album.id][ALBUM_ARTISTS] = artists\n        if not config.setting[OPT_PROCESS_TRACKS]:\n            log.info(*log_helper(\"Track artist processing is disabled.\"))\n        self._artist_processing(artists, album, album_metadata, 'Album')\n\n    def make_track_vars(self, album, album_metadata, track_metadata, _release_metadata):\n        \"\"\"Process track artists.\n\n        Args:\n            album (Album): The Album object to use for the processing.\n            album_metadata (Metadata): Metadata object for the album.\n            track_metadata (dict): Dictionary of track data from MusicBrainz api.\n            _release_metadata (dict): Dictionary of release data from MusicBrainz api.\n        \"\"\"\n        artists = set()\n        source_type = 'track'\n        # Test for valid metadata node.\n        # The 'artist-credit' key should always be there.\n        # This check is to avoid a runtime error if it doesn't exist for some reason.\n        if config.setting[OPT_PROCESS_TRACKS]:\n            if 'artist-credit' in track_metadata:\n                for artist_credit in track_metadata['artist-credit']:\n                    if 'artist' in artist_credit:\n                        if 'id' in artist_credit['artist']:\n                            artists.add(artist_credit['artist']['id'])\n                    else:\n                        # No 'artist' specified.  Log as an error.\n                        self._metadata_error(album.id, 'artist-credit.artist', source_type)\n            else:\n                # No valid metadata found.  Log as error.\n                self._metadata_error(album.id, 'artist-credit', source_type)\n        self._artist_processing(artists, album, album_metadata, 'Track')\n\n    def _artist_processing(self, artists, album, destination_metadata, source_type):\n        \"\"\"Retrieves the information for each artist not already processed.\n\n        Args:\n            artists (set): Set of artist MBIDs to process.\n            album (Album): Album object to use for the processing.\n            destination_metadata (Metadata): Metadata object to update with the new variables.\n            source_type (str): Source type (album or track) for logging messages.\n        \"\"\"\n        for temp_id in artists:\n            if temp_id not in self.result_cache[ARTIST_REQUESTS]:\n                self.result_cache[ARTIST_REQUESTS].add(temp_id)\n                log.debug(*log_helper('Retrieving artist ID %s information from MusicBrainz.', temp_id))\n                self._get_artist_info(temp_id, album)\n            else:\n                log.debug(*log_helper('%s artist ID %s information available from cache.', source_type, temp_id))\n        self._add_target(album.id, artists, destination_metadata)\n        self._save_artist_metadata(album.id)\n\n    def _save_artist_metadata(self, album_id):\n        \"\"\"Saves the new artist details variables to the metadata targets for the specified album.\n\n        Args:\n            album_id (str): MBID of the album to process.\n        \"\"\"\n        if album_id in self.album_processing_count and self.album_processing_count[album_id]:\n            return\n        if album_id not in self.albums or not self.albums[album_id][TRACKS]:\n            log.error(*log_helper(\"No metadata targets found for album '%s'\", album_id))\n            return\n        for item in self.albums[album_id][TRACKS]:\n            # Add album artists to track so they are available in the metadata\n            artists = self.albums[album_id][ALBUM_ARTISTS].copy().union(item.artists)\n            destination_metadata = item.target\n            for artist in artists:\n                if artist in self.result_cache[ARTIST]:\n                    self._set_artist_metadata(destination_metadata, artist, self.result_cache[ARTIST][artist])\n\n    def _set_artist_metadata(self, destination_metadata, artist_id, artist_info):\n        \"\"\"Adds the artist information to the destination metadata.\n\n        Args:\n            destination_metadata (Metadata): Metadata object to update with new variables.\n            artist_id (str): MBID of the artist to update.\n            artist_info (dict): Dictionary of information for the artist.\n        \"\"\"\n        def _set_item(key, value):\n            destination_metadata[f\"~artist_{artist_id}_{key.replace('-', '_')}\"] = value\n\n        for item in artist_info.keys():\n            if item in {'area', 'begin-area', 'end-area'}:\n                country, location = self._drill_area(artist_info[item])\n                if country:\n                    _set_item(item.replace('area', 'country'), country)\n                if location:\n                    _set_item(item.replace('area', 'location'), location)\n            else:\n                _set_item(item, artist_info[item])\n\n    def _get_artist_info(self, artist_id, album):\n        \"\"\"Gets the artist information from the MusicBrainz website.\n\n        Args:\n            artist_id (str): MBID of the artist to retrieve.\n            album (Album): The Album object to use for the processing.\n        \"\"\"\n        self._album_add_request(album)\n        helper = CustomHelper(album.tagger.webservice)\n        handler = partial(\n            self._artist_submission_handler,\n            artist=artist_id,\n            album=album,\n        )\n        return helper.get_artist_by_id(artist_id, handler)\n\n    def _artist_submission_handler(self, document, _reply, error, artist=None, album=None):\n        \"\"\"Handles the response from the webservice requests for artist information.\n        \"\"\"\n        try:\n            if error:\n                log.error(*log_helper(\"Artist '%s' information retrieval error.\", artist))\n                return\n            artist_info = {}\n            for item in ['type', 'gender', 'name', 'sort-name', 'disambiguation']:\n                if item in document and document[item]:\n                    artist_info[item] = document[item]\n            if 'life-span' in document:\n                for item in ['begin', 'end']:\n                    if item in document['life-span'] and document['life-span'][item]:\n                        artist_info[item] = document['life-span'][item]\n            for item in ['area', 'begin-area', 'end-area']:\n                if item in document and document[item] and 'id' in document[item] and document[item]['id']:\n                    area_id = document[item]['id']\n                    artist_info[item] = area_id\n                    if area_id not in self.result_cache[AREA_REQUESTS]:\n                        self._get_area_info(area_id, album)\n            self.result_cache[ARTIST][artist] = artist_info\n        finally:\n            self._album_remove_request(album)\n            self._save_artist_metadata(album.id)\n\n    def _get_area_info(self, area_id, album):\n        \"\"\"Gets the area information from the MusicBrainz website.\n\n        Args:\n            area_id (str): MBID of the area to retrieve.\n            album (Album): The Album object to use for the processing.\n        \"\"\"\n        self.result_cache[AREA_REQUESTS].add(area_id)\n        self._album_add_request(album)\n        log.debug(*log_helper('Retrieving area ID %s from MusicBrainz.', area_id))\n        helper = CustomHelper(album.tagger.webservice)\n        handler = partial(\n            self._area_submission_handler,\n            area=area_id,\n            album=album,\n        )\n        return helper.get_area_by_id(area_id, handler)\n\n    def _area_submission_handler(self, document, _reply, error, area=None, album=None):\n        \"\"\"Handles the response from the webservice requests for area information.\n        \"\"\"\n        try:\n            if error:\n                log.error(*log_helper(\"Area '%s' information retrieval error.\", area))\n                return\n            (_id, name, country, _type, type_text) = self._parse_area(document)\n            if _type == AREA_TYPE_COUNTRY and _id not in self.result_cache[AREA]:\n                self._area_logger(_id, f\"{name} ({country})\", type_text)\n                self.result_cache[AREA][_id] = Area('', name, country, _type, type_text)\n            if 'relations' in document:\n                for rel in document['relations']:\n                    self._parse_area_relation(_id, rel, album, name, _type, type_text)\n        finally:\n            self._album_remove_request(album)\n            self._save_artist_metadata(album.id)\n\n    @staticmethod\n    def _area_logger(area_id, area_name, area_type):\n        \"\"\"Adds a log entry for the area retrieved.\n\n        Args:\n            area_id (str): MBID of the area added.\n            area_name (str): Name of the area added.\n            area_type (str): Type of area added.\n        \"\"\"\n        log.debug(*log_helper(\"Adding area: %s => %s of type '%s'\", area_id, area_name, area_type))\n\n    def _parse_area_relation(self, area_id, area_relation, album, area_name, area_type, area_type_text):\n        \"\"\"Parse an area relation to extract the area information.\n\n        Args:\n            area_id (str): MBID of the area providing the relationship.\n            area_relation (dict): Dictionary of the area relationship.\n            album (Album): The Album object to use for the processing.\n            area_name (str): Name of the area providing the relationship.\n            area_type (str): MBID of the type of area providing the relationship.\n            area_type_text (str): Text description of the area providing the relationship.\n        \"\"\"\n        if 'type-id' not in area_relation or 'area' not in area_relation or area_relation['type-id'] != RELATIONSHIP_TYPE_PART_OF:\n            return\n        (_id, name, country, _type, type_text) = self._parse_area(area_relation['area'])\n        if not _id:\n            return\n\n        if 'direction' in area_relation and area_relation['direction'] == 'backward':\n            if area_id not in self.result_cache[AREA]:\n                self._area_logger(area_id, area_name, area_type_text)\n                self.result_cache[AREA][area_id] = Area(_id, area_name, '', area_type, type_text)\n                self.result_cache[AREA_REQUESTS].add(area_id)\n            if _type == AREA_TYPE_COUNTRY:\n                if _id not in self.result_cache[AREA]:\n                    self._area_logger(_id, f\"{name} ({country})\", type_text)\n                    self.result_cache[AREA][_id] = Area('', name, country, _type, type_text)\n                    self.result_cache[AREA_REQUESTS].add(_id)\n            else:\n                if _id not in self.result_cache[AREA] and _id not in self.result_cache[AREA_REQUESTS]:\n                    self._get_area_info(_id, album)\n        else:\n            self._area_logger(_id, name, type_text)\n            self.result_cache[AREA_REQUESTS].add(_id)\n            self.result_cache[AREA][_id] = Area(area_id, name, '', _type, type_text)\n\n    @staticmethod\n    def _parse_area(area_info):\n        \"\"\"Parse a dictionary of area information to return selected elements.\n\n        Args:\n            area_info (dict): Area information to parse.\n\n        Returns:\n            tuple: Selected information for the area (id, name, country code, type code, type text).\n        \"\"\"\n        if 'id' not in area_info:\n            return ('', '', '', '', '')\n        area_id = area_info['id']\n        area_name = area_info['name'] if 'name' in area_info else 'Unknown Name'\n        area_type = area_info['type-id'] if 'type-id' in area_info else ''\n        area_type_text = area_info['type'] if 'type' in area_info else 'Unknown Area Type'\n        country = ''\n        if area_type == AREA_TYPE_COUNTRY:\n            if ISO_CODES_1 in area_info and area_info[ISO_CODES_1]:\n                country = area_info[ISO_CODES_1][0]\n            elif ISO_CODES_2 in area_info and area_info[ISO_CODES_2]:\n                country = area_info[ISO_CODES_2][0][:2]\n        return (area_id, area_name, country, area_type, area_type_text)\n\n    @staticmethod\n    def _metadata_error(album_id, metadata_element, metadata_group):\n        \"\"\"Logs metadata-related errors.\n\n        Args:\n            album_id (str): MBID of the album being processed.\n            metadata_element (str): Metadata element initiating the error.\n            metadata_group (str): Metadata group initiating the error.\n        \"\"\"\n        log.error(*log_helper(\"Album '%s' missing '%s' in %s metadata.\", album_id, metadata_element, metadata_group))\n\n    def _drill_area(self, area_id):\n        \"\"\"Drills up from the specified area to determine the two-character\n        country code and the full location description for the area.\n\n        Args:\n            area_id (str): MBID of the area to process.\n\n        Returns:\n            tuple: The two-character country code and full location description for the area.\n        \"\"\"\n        country = ''\n        location = []\n        i = 7   # Counter to avoid potential runaway processing\n        while i and area_id and not country:\n            i -= 1\n            area = self.result_cache[AREA][area_id] if area_id in self.result_cache[AREA] else Area('', '', '', '', '')\n            country = area.country\n            area_id = area.parent\n            if not location or config.setting[OPT_AREA_DETAILS] or area.type not in EXCLUDE_AREA_TYPES:\n                location.append(area.name)\n        return country, ', '.join(location)\n\n\nclass AdditionalArtistsDetailsOptionsPage(OptionsPage):\n    \"\"\"Options page for the Additional Artists Details plugin.\n    \"\"\"\n\n    NAME = \"additional_artists_details\"\n    TITLE = \"Additional Artists Details\"\n    PARENT = \"plugins\"\n\n    options = [\n        config.BoolOption('setting', OPT_PROCESS_TRACKS, False),\n        config.BoolOption('setting', OPT_AREA_DETAILS, False),\n    ]\n\n    def __init__(self, parent=None):\n        super(AdditionalArtistsDetailsOptionsPage, self).__init__(parent)\n        self.ui = Ui_AdditionalArtistsDetailsOptionsPage()\n        self.ui.setupUi(self)\n\n        # Enable external link\n        self.ui.format_description.setOpenExternalLinks(True)\n\n    def load(self):\n        \"\"\"Load the option settings.\n        \"\"\"\n        self.ui.cb_process_tracks.setChecked(config.setting[OPT_PROCESS_TRACKS])\n        self.ui.cb_area_details.setChecked(config.setting[OPT_AREA_DETAILS])\n\n    def save(self):\n        \"\"\"Save the option settings.\n        \"\"\"\n        # self._set_settings(config.setting)\n        config.setting[OPT_PROCESS_TRACKS] = self.ui.cb_process_tracks.isChecked()\n        config.setting[OPT_AREA_DETAILS] = self.ui.cb_area_details.isChecked()\n\n\nplugin = ArtistDetailsPlugin()\n\n# Register the plugin to run at a LOW priority so that other plugins that\n# modify the artist information can complete their processing and this plugin\n# is working with the latest updated data.\nregister_album_metadata_processor(plugin.make_album_vars, priority=PluginPriority.LOW)\nregister_track_metadata_processor(plugin.make_track_vars, priority=PluginPriority.LOW)\n\nregister_album_post_removal_processor(plugin.remove_album)\nregister_options_page(AdditionalArtistsDetailsOptionsPage)\n"
  },
  {
    "path": "plugins/additional_artists_details/docs/README.md",
    "content": "# Additional Artists Details\n\n## Overview\n\nThis plugin provides specialized album and track variables with artist details such as type, gender, begin and end dates, location (begin, end and current) and country code (begin, end and current) for use in tagging and naming scripts.\n\n***NOTE:*** This plugin makes additional calls to the MusicBrainz website api for the information, which will slow down retrieving album information from MusicBrainz.  This will be particularly noticable when there are many different album or track artists, such as on a \\[Various Artists\\] release.  There is an option to disable track artist processing, which can significantly increase the processing speed if you are only interested in album artist details.\n\n---\n\n## Option Settings\n\nThe plugin adds a settings page under the \"Plugins\" section under \"Options...\" from Picard's main menu.  This allows you to control how the plugin operates with respect to processing track artists and detail included in the artist location variables' content.\n\n![Additional Artists Details Option Settings](option_settings.png \"Additional Artists Details Option Settings\")\n\n---\n\n## What it Does\n\nThis plugin reads the album and track metadata provided to Picard, extracts the list of associated artists, retrieves the detailed information from the MusicBrainz website, and exposes the information in a number of additional variables for use in Picard scripts.\n\n### Variables Created\n\n* **\\_artist_\\{artist_id\\}_begin** - The begin date (birth date) of the artist\n* **\\_artist_\\{artist_id\\}_begin_country** - The begin two-character country code of the artist\n* **\\_artist_\\{artist_id\\}_begin_location** - The begin location of the artist\n* **\\_artist_\\{artist_id\\}_country** - The two-character country code for the artist\n* **\\_artist_\\{artist_id\\}_disambiguation** - The disambiguation information for the artist\n* **\\_artist_\\{artist_id\\}_end** - The end date (death date) of the artist\n* **\\_artist_\\{artist_id\\}_end_country** - The end two-character country code of the artist\n* **\\_artist_\\{artist_id\\}_end_location** - The end location of the artist\n* **\\_artist_\\{artist_id\\}_gender** - The gender of the artist\n* **\\_artist_\\{artist_id\\}_name** - The name of the artist\n* **\\_artist_\\{artist_id\\}_sort_name** - The sort name of the artist\n* **\\_artist_\\{artist_id\\}_type** - The type of artist (person, group, etc.)\n\nA variable will only be created if the information is returned from MusicBrainz.  For example, if a gender has not been specified in the MusicBrainz data then the **%\\_artist_\\{artist_id\\}_gender%** variable will not be created.\n\n---\n\n## Example\n\nIf you load the release **Wrecking Ball** (release [8c759d7a-2ade-4201-abc2-a2a7c1a6ad6c](https://musicbrainz.org/release/8c759d7a-2ade-4201-abc2-a2a7c1a6ad6c)) by Sarah Blackwood (artist [af7e5ea9-bd58-4346-8f78-d672e9f297f7](https://musicbrainz.org/artist/af7e5ea9-bd58-4346-8f78-d672e9f297f7)), Jenni Pleau (artist [07fa21a9-c253-4ed0-b711-d63f7965b723](https://musicbrainz.org/artist/07fa21a9-c253-4ed0-b711-d63f7965b723)) & Emily Bones (artist [541d331c-f041-4895-b8f2-7db9e27dc5ab](https://musicbrainz.org/artist/541d331c-f041-4895-b8f2-7db9e27dc5ab)), the following variables will be created:\n\nSarah Blackwood (primary artist):\n\n* **\\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_begin** = \"1980-10-18\"\n* **\\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_begin_country** = \"CA\"\n* **\\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_begin_location** = \"Burlington, Ontario, Canada\"\n* **\\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_country** = \"CA\"\n* **\\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_disambiguation** = \"rockabilly, + \"Somebody That I Used to Know\"\"\n* **\\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_gender** = \"Female\"\n* **\\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_location** = \"Canada\"\n* **\\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_name** = \"Sarah Blackwood\"\n* **\\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_sort_name** = \"Blackwood, Sarah\"\n* **\\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_type** = \"Person\"\n\nJenny Pleau (additional artist)\n\n* **\\_artist_07fa21a9_c253_4ed0_b711_d63f7965b723_country** = \"CA\"\n* **\\_artist_07fa21a9_c253_4ed0_b711_d63f7965b723_gender** = \"Female\"\n* **\\_artist_07fa21a9_c253_4ed0_b711_d63f7965b723_location** = \"Kitchener, Ontario, Canada\"\n* **\\_artist_07fa21a9_c253_4ed0_b711_d63f7965b723_name** = \"Jenni Pleau\"\n* **\\_artist_07fa21a9_c253_4ed0_b711_d63f7965b723_sort_name** = \"Pleau, Jenni\"\n* **\\_artist_07fa21a9_c253_4ed0_b711_d63f7965b723_type** = \"Person\"\n\nEmily Bones (additional artist)\n\n* **\\_artist_541d331c_f041_4895_b8f2_7db9e27dc5ab_gender** = \"Female\"\n* **\\_artist_541d331c_f041_4895_b8f2_7db9e27dc5ab_name** = \"Emily Bones\"\n* **\\_artist_541d331c_f041_4895_b8f2_7db9e27dc5ab_sort_name** = \"Bones, Emily\"\n* **\\_artist_541d331c_f041_4895_b8f2_7db9e27dc5ab_type** = \"Person\"\n\nNote that variables will only be created for information that exists for the artist's record.\n\nThis could be used to set a **%country%** tag to the country code of the (primary) album artist with a tagging script like:\n\n```taggerscript\n$set(_tmp,_artist_$getmulti(%musicbrainz_albumartistid%,0)_)\n$set(country,$if2($get(%_tmp%country),$get(%_tmp%begin_country),$get(%_tmp%end_country),xx))\n```\n"
  },
  {
    "path": "plugins/additional_artists_details/options_additional_artists_details.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<ui version=\"4.0\">\r\n <class>AdditionalArtistsDetailsOptionsPage</class>\r\n <widget class=\"QWidget\" name=\"AdditionalArtistsDetailsOptionsPage\">\r\n  <property name=\"geometry\">\r\n   <rect>\r\n    <x>0</x>\r\n    <y>0</y>\r\n    <width>561</width>\r\n    <height>802</height>\r\n   </rect>\r\n  </property>\r\n  <property name=\"minimumSize\">\r\n   <size>\r\n    <width>100</width>\r\n    <height>0</height>\r\n   </size>\r\n  </property>\r\n  <property name=\"windowTitle\">\r\n   <string>Form</string>\r\n  </property>\r\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\r\n   <item>\r\n    <widget class=\"QScrollArea\" name=\"scrollArea\">\r\n     <property name=\"frameShape\">\r\n      <enum>QFrame::NoFrame</enum>\r\n     </property>\r\n     <property name=\"widgetResizable\">\r\n      <bool>true</bool>\r\n     </property>\r\n     <widget class=\"QWidget\" name=\"scrollAreaWidgetContents\">\r\n      <property name=\"geometry\">\r\n       <rect>\r\n        <x>0</x>\r\n        <y>0</y>\r\n        <width>543</width>\r\n        <height>784</height>\r\n       </rect>\r\n      </property>\r\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\r\n       <item>\r\n        <widget class=\"QGroupBox\" name=\"gb_description\">\r\n         <property name=\"title\">\r\n          <string>Additional Artists Details</string>\r\n         </property>\r\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_3\">\r\n          <item>\r\n           <widget class=\"QLabel\" name=\"format_description\">\r\n            <property name=\"sizePolicy\">\r\n             <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Minimum\">\r\n              <horstretch>0</horstretch>\r\n              <verstretch>0</verstretch>\r\n             </sizepolicy>\r\n            </property>\r\n            <property name=\"text\">\r\n             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;These settings will determine how the &lt;span style=&quot; font-weight:600;&quot;&gt;Additional Artists Details&lt;/span&gt; plugin operates.&lt;/p&gt;&lt;p&gt;Please visit the repository on GitHub for &lt;a href=&quot;https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/additional_artists_details/docs/README.md&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;additional information&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\r\n            </property>\r\n            <property name=\"wordWrap\">\r\n             <bool>true</bool>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n         </layout>\r\n        </widget>\r\n       </item>\r\n       <item>\r\n        <widget class=\"QGroupBox\" name=\"gb_process_track_artists\">\r\n         <property name=\"title\">\r\n          <string>Process Track Artists</string>\r\n         </property>\r\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_4\">\r\n          <item>\r\n           <widget class=\"QLabel\" name=\"label\">\r\n            <property name=\"text\">\r\n             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This option determines whether or not details are retrieved for all track artists on the release. If you are only interested in details for the album artists then this should be disabled, thus significantly reducing the number of additional calls made to the MusicBrainz api and reducing the time required to load a release.  Album artists are always processed.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\r\n            </property>\r\n            <property name=\"wordWrap\">\r\n             <bool>true</bool>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n          <item>\r\n           <widget class=\"QCheckBox\" name=\"cb_process_tracks\">\r\n            <property name=\"text\">\r\n             <string>Process track artists</string>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n         </layout>\r\n        </widget>\r\n       </item>\r\n       <item>\r\n        <widget class=\"QGroupBox\" name=\"gb_area_details\">\r\n         <property name=\"title\">\r\n          <string>Include Area Details</string>\r\n         </property>\r\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_5\">\r\n          <item>\r\n           <widget class=\"QLabel\" name=\"label_2\">\r\n            <property name=\"text\">\r\n             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This option determines whether or not County and Municipality information is included in the artist location variables created. Regardless of this setting, this information will be included if a County or Municipality is the area specified for an artist.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\r\n            </property>\r\n            <property name=\"wordWrap\">\r\n             <bool>true</bool>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n          <item>\r\n           <widget class=\"QCheckBox\" name=\"cb_area_details\">\r\n            <property name=\"text\">\r\n             <string>Include area details</string>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n         </layout>\r\n        </widget>\r\n       </item>\r\n       <item>\r\n        <spacer name=\"verticalSpacer\">\r\n         <property name=\"orientation\">\r\n          <enum>Qt::Vertical</enum>\r\n         </property>\r\n         <property name=\"sizeHint\" stdset=\"0\">\r\n          <size>\r\n           <width>20</width>\r\n           <height>40</height>\r\n          </size>\r\n         </property>\r\n        </spacer>\r\n       </item>\r\n      </layout>\r\n     </widget>\r\n    </widget>\r\n   </item>\r\n  </layout>\r\n </widget>\r\n <resources/>\r\n <connections/>\r\n</ui>\r\n"
  },
  {
    "path": "plugins/additional_artists_details/ui_options_additional_artists_details.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file './plugins/additional_artists_details/options_additional_artists_details.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.6\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_AdditionalArtistsDetailsOptionsPage(object):\n    def setupUi(self, AdditionalArtistsDetailsOptionsPage):\n        AdditionalArtistsDetailsOptionsPage.setObjectName(\"AdditionalArtistsDetailsOptionsPage\")\n        AdditionalArtistsDetailsOptionsPage.resize(561, 802)\n        AdditionalArtistsDetailsOptionsPage.setMinimumSize(QtCore.QSize(100, 0))\n        self.verticalLayout = QtWidgets.QVBoxLayout(AdditionalArtistsDetailsOptionsPage)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.scrollArea = QtWidgets.QScrollArea(AdditionalArtistsDetailsOptionsPage)\n        self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame)\n        self.scrollArea.setWidgetResizable(True)\n        self.scrollArea.setObjectName(\"scrollArea\")\n        self.scrollAreaWidgetContents = QtWidgets.QWidget()\n        self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 543, 784))\n        self.scrollAreaWidgetContents.setObjectName(\"scrollAreaWidgetContents\")\n        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents)\n        self.verticalLayout_2.setObjectName(\"verticalLayout_2\")\n        self.gb_description = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)\n        self.gb_description.setObjectName(\"gb_description\")\n        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.gb_description)\n        self.verticalLayout_3.setObjectName(\"verticalLayout_3\")\n        self.format_description = QtWidgets.QLabel(self.gb_description)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.format_description.sizePolicy().hasHeightForWidth())\n        self.format_description.setSizePolicy(sizePolicy)\n        self.format_description.setWordWrap(True)\n        self.format_description.setObjectName(\"format_description\")\n        self.verticalLayout_3.addWidget(self.format_description)\n        self.verticalLayout_2.addWidget(self.gb_description)\n        self.gb_process_track_artists = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)\n        self.gb_process_track_artists.setObjectName(\"gb_process_track_artists\")\n        self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.gb_process_track_artists)\n        self.verticalLayout_4.setObjectName(\"verticalLayout_4\")\n        self.label = QtWidgets.QLabel(self.gb_process_track_artists)\n        self.label.setWordWrap(True)\n        self.label.setObjectName(\"label\")\n        self.verticalLayout_4.addWidget(self.label)\n        self.cb_process_tracks = QtWidgets.QCheckBox(self.gb_process_track_artists)\n        self.cb_process_tracks.setObjectName(\"cb_process_tracks\")\n        self.verticalLayout_4.addWidget(self.cb_process_tracks)\n        self.verticalLayout_2.addWidget(self.gb_process_track_artists)\n        self.gb_area_details = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)\n        self.gb_area_details.setObjectName(\"gb_area_details\")\n        self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.gb_area_details)\n        self.verticalLayout_5.setObjectName(\"verticalLayout_5\")\n        self.label_2 = QtWidgets.QLabel(self.gb_area_details)\n        self.label_2.setWordWrap(True)\n        self.label_2.setObjectName(\"label_2\")\n        self.verticalLayout_5.addWidget(self.label_2)\n        self.cb_area_details = QtWidgets.QCheckBox(self.gb_area_details)\n        self.cb_area_details.setObjectName(\"cb_area_details\")\n        self.verticalLayout_5.addWidget(self.cb_area_details)\n        self.verticalLayout_2.addWidget(self.gb_area_details)\n        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.verticalLayout_2.addItem(spacerItem)\n        self.scrollArea.setWidget(self.scrollAreaWidgetContents)\n        self.verticalLayout.addWidget(self.scrollArea)\n\n        self.retranslateUi(AdditionalArtistsDetailsOptionsPage)\n        QtCore.QMetaObject.connectSlotsByName(AdditionalArtistsDetailsOptionsPage)\n\n    def retranslateUi(self, AdditionalArtistsDetailsOptionsPage):\n        _translate = QtCore.QCoreApplication.translate\n        AdditionalArtistsDetailsOptionsPage.setWindowTitle(_translate(\"AdditionalArtistsDetailsOptionsPage\", \"Form\"))\n        self.gb_description.setTitle(_translate(\"AdditionalArtistsDetailsOptionsPage\", \"Additional Artists Details\"))\n        self.format_description.setText(_translate(\"AdditionalArtistsDetailsOptionsPage\", \"<html><head/><body><p>These settings will determine how the <span style=\\\" font-weight:600;\\\">Additional Artists Details</span> plugin operates.</p><p>Please visit the repository on GitHub for <a href=\\\"https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/additional_artists_details/docs/README.md\\\"><span style=\\\" text-decoration: underline; color:#0000ff;\\\">additional information</span></a>.</p></body></html>\"))\n        self.gb_process_track_artists.setTitle(_translate(\"AdditionalArtistsDetailsOptionsPage\", \"Process Track Artists\"))\n        self.label.setText(_translate(\"AdditionalArtistsDetailsOptionsPage\", \"<html><head/><body><p>This option determines whether or not details are retrieved for all track artists on the release. If you are only interested in details for the album artists then this should be disabled, thus significantly reducing the number of additional calls made to the MusicBrainz api and reducing the time required to load a release.  Album artists are always processed.</p></body></html>\"))\n        self.cb_process_tracks.setText(_translate(\"AdditionalArtistsDetailsOptionsPage\", \"Process track artists\"))\n        self.gb_area_details.setTitle(_translate(\"AdditionalArtistsDetailsOptionsPage\", \"Include Area Details\"))\n        self.label_2.setText(_translate(\"AdditionalArtistsDetailsOptionsPage\", \"<html><head/><body><p>This option determines whether or not County and Municipality information is included in the artist location variables created. Regardless of this setting, this information will be included if a County or Municipality is the area specified for an artist.</p></body></html>\"))\n        self.cb_area_details.setText(_translate(\"AdditionalArtistsDetailsOptionsPage\", \"Include area details\"))\n"
  },
  {
    "path": "plugins/additional_artists_variables/additional_artists_variables.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2018-2023 Bob Swift (rdswift)\n# Copyright (C) 2023 Ruud van Asseldonk (ruuda)\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = 'Additional Artists Variables'\nPLUGIN_AUTHOR = 'Bob Swift (rdswift)'\nPLUGIN_DESCRIPTION = '''\nThis plugin provides specialized album and track variables for use in\nnaming scripts. It is based on the \"Album Artist Extension\" plugin, but\nexpands the functionality to also include track artists. Note that it\ncannot be used as a direct drop-in replacement for the \"Album Artist\nExtension\" plugin because the variables are provided with different\nnames.  This will require changes to existing scripts if switching to\nthis plugin.\n<br /><br />\nPlease see the <a href=\"https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/additional_artists_variables/docs/README.md\">user guide</a> on GitHub for more information.\n'''\nPLUGIN_VERSION = '1.0'\nPLUGIN_API_VERSIONS = ['2.0', '2.1', '2.2', '2.7', '2.9', '2.10', '2.11']\nPLUGIN_LICENSE = 'GPL-2.0-or-later'\nPLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl-2.0.html'\n\nPLUGIN_USER_GUIDE_URL = 'https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/additional_artists_variables/docs/README.md'\n\nfrom operator import itemgetter\n\nfrom picard import config, log\nfrom picard.metadata import (register_album_metadata_processor,\n                             register_track_metadata_processor)\nfrom picard.plugin import PluginPriority\n\n\nID_ALIASES_ARTIST_NAME = '894afba6-2816-3c24-8072-eadb66bd04bc'\nID_ALIASES_LEGAL_NAME = 'd4dcd0c0-b341-3612-a332-c0ce797b25cf'\n\n\ndef process_artists(album_id, source_metadata, destination_metadata, source_type):\n    # Test for valid metadata node.\n    # The 'artist-credit' key should always be there.\n    # This check is to avoid a runtime error if it doesn't exist for some reason.\n    if 'artist-credit' in source_metadata:\n        # Initialize variables to default values\n        sort_pri_artist = ''\n        sort_pri_artist_cred = ''\n        std_artist = ''\n        cred_artist = ''\n        sort_artist = ''\n        cred_sort_artist = ''\n        legal_artist = ''\n        additional_std_artist = ''\n        additional_cred_artist = ''\n        additional_sort_artist = ''\n        additional_cred_sort_artist = ''\n        additional_legal_artist = ''\n        std_artist_list = []\n        cred_artist_list = []\n        sort_artist_list = []\n        cred_sort_artist_list = []\n        legal_artist_list = []\n        artist_count = 0\n        artist_ids = []\n        artist_types = []\n        artist_join_phrases = []\n        for artist_credit in source_metadata['artist-credit']:\n            # Initialize temporary variables for each loop.\n            temp_std_name = ''\n            temp_sort_name = ''\n            temp_cred_name = ''\n            temp_cred_sort_name = ''\n            temp_legal_name = ''\n            temp_legal_sort_name = ''\n            temp_phrase = ''\n            temp_id = ''\n            temp_type = ''\n            # Check if there is a 'joinphrase' specified.\n            if 'joinphrase' in artist_credit:\n                temp_phrase = artist_credit['joinphrase']\n            else:\n                metadata_error(album_id, 'artist-credit.joinphrase', source_type)\n            # Check if there is a 'name' specified.\n            if 'name' in artist_credit:\n                temp_cred_name = artist_credit['name']\n            else:\n                metadata_error(album_id, 'artist-credit.name', source_type)\n            # Check if there is an 'artist' specified.\n            if 'artist' in artist_credit:\n                if 'id' in artist_credit['artist']:\n                    temp_id = artist_credit['artist']['id']\n                else:\n                    metadata_error(album_id, 'artist-credit.artist.id', source_type)\n                if 'name' in artist_credit['artist']:\n                    temp_std_name = artist_credit['artist']['name']\n                else:\n                    metadata_error(album_id, 'artist-credit.artist.name', source_type)\n                if 'sort-name' in artist_credit['artist']:\n                    temp_sort_name = artist_credit['artist']['sort-name']\n                    temp_cred_sort_name = temp_sort_name\n                else:\n                    metadata_error(album_id, 'artist-credit.artist.sort-name', source_type)\n                if 'type' in artist_credit['artist']:\n                    temp_type = artist_credit['artist']['type']\n                else:\n                    metadata_error(album_id, 'artist-credit.artist.type', source_type)\n                if 'aliases' in artist_credit['artist']:\n                    for item in artist_credit['artist']['aliases']:\n                        if 'type-id' in item and item['type-id'] == ID_ALIASES_LEGAL_NAME:\n                            if 'ended' in item and not item['ended']:\n                                if 'name' in item:\n                                    temp_legal_name = item['name']\n                                if 'sort-name' in item:\n                                    temp_legal_sort_name = item['sort-name']\n                        if 'type-id' in item and item['type-id'] == ID_ALIASES_ARTIST_NAME:\n                            if 'name' in item and 'sort-name' in item and str(item['name']).lower() == temp_cred_name.lower():\n                                temp_cred_sort_name = item['sort-name']\n                tag_list = []\n                if config.setting['max_genres']:\n                    for tag_type in ['user-genres', 'genres', 'user-tags', 'tags']:\n                        if tag_type in artist_credit['artist']:\n                            for item in sorted(sorted(artist_credit['artist'][tag_type], key=itemgetter('name')), key=itemgetter('count'), reverse=True):\n                                if item['count'] > 0:\n                                    tag_list.append(item['name'])\n                    tag_list = tag_list[:config.setting['max_genres']]\n            else:\n                # No 'artist' specified.  Log as an error.\n                metadata_error(album_id, 'artist-credit.artist', source_type)\n            std_artist += temp_std_name + temp_phrase\n            cred_artist += temp_cred_name + temp_phrase\n            sort_artist += temp_sort_name + temp_phrase\n            cred_sort_artist += temp_cred_sort_name + temp_phrase\n            artist_types.append(temp_type if temp_type else 'unknown',)\n            artist_join_phrases.append(temp_phrase if temp_phrase else '\\u200B',)\n            if temp_legal_name:\n                legal_artist += temp_legal_name + temp_phrase\n                legal_artist_list.append(temp_legal_name,)\n            else:\n                # Use standardized name for combined string if legal name not available\n                legal_artist += temp_std_name + temp_phrase\n                # Use 'n/a' for list if legal name not available\n                legal_artist_list.append('n/a',)\n            if temp_std_name:\n                std_artist_list.append(temp_std_name,)\n            if temp_sort_name:\n                sort_artist_list.append(temp_sort_name,)\n            if temp_cred_name:\n                cred_artist_list.append(temp_cred_name,)\n            if temp_cred_sort_name:\n                cred_sort_artist_list.append(temp_cred_sort_name,)\n            if temp_id:\n                artist_ids.append(temp_id,)\n            if artist_count < 1:\n                if temp_id:\n                    destination_metadata['~artists_{0}_primary_id'.format(source_type,)] = temp_id\n                destination_metadata['~artists_{0}_primary_std'.format(source_type,)] = temp_std_name\n                destination_metadata['~artists_{0}_primary_cred'.format(source_type,)] = temp_cred_name\n                destination_metadata['~artists_{0}_primary_sort'.format(source_type,)] = temp_sort_name\n                destination_metadata['~artists_{0}_primary_cred_sort'.format(source_type,)] = temp_cred_sort_name\n                destination_metadata['~artists_{0}_primary_legal'.format(source_type,)] = temp_legal_name\n                destination_metadata['~artists_{0}_primary_sort_legal'.format(source_type,)] = temp_legal_sort_name\n                sort_pri_artist += temp_sort_name + temp_phrase\n                sort_pri_artist_cred += temp_cred_sort_name + temp_phrase\n                if tag_list and source_type == 'album':\n                    destination_metadata['~artists_{0}_primary_tags'.format(source_type,)] = tag_list\n            else:\n                sort_pri_artist += temp_std_name + temp_phrase\n                additional_std_artist += temp_std_name + temp_phrase\n                additional_cred_artist += temp_cred_name + temp_phrase\n                additional_sort_artist += temp_sort_name + temp_phrase\n                if temp_legal_name:\n                    additional_legal_artist += temp_legal_name + temp_phrase\n                else:\n                    additional_legal_artist += temp_std_name + temp_phrase\n                if temp_cred_sort_name:\n                    additional_cred_sort_artist += temp_cred_sort_name + temp_phrase\n                    sort_pri_artist_cred += temp_cred_sort_name + temp_phrase\n                else:\n                    additional_cred_sort_artist += temp_sort_name + temp_phrase\n                    sort_pri_artist_cred += temp_sort_name + temp_phrase\n            artist_count += 1\n    else:\n        # No valid metadata found.  Log as error.\n        metadata_error(album_id, 'artist-credit', source_type)\n    additional_std_artist_list = std_artist_list[1:]\n    additional_cred_artist_list = cred_artist_list[1:]\n    additional_sort_artist_list = sort_artist_list[1:]\n    additional_cred_sort_artist_list = cred_sort_artist_list[1:]\n    additional_legal_artist_list = legal_artist_list[1:]\n    additional_artist_ids = artist_ids[1:]\n    if additional_artist_ids:\n        destination_metadata['~artists_{0}_additional_id'.format(source_type,)] = additional_artist_ids\n    if additional_std_artist:\n        destination_metadata['~artists_{0}_additional_std'.format(source_type,)] = additional_std_artist\n    if additional_cred_artist:\n        destination_metadata['~artists_{0}_additional_cred'.format(source_type,)] = additional_cred_artist\n    if additional_sort_artist:\n        destination_metadata['~artists_{0}_additional_sort'.format(source_type,)] = additional_sort_artist\n    if additional_cred_sort_artist:\n        destination_metadata['~artists_{0}_additional_cred_sort'.format(source_type,)] = additional_cred_sort_artist\n    if additional_legal_artist:\n        destination_metadata['~artists_{0}_additional_legal'.format(source_type,)] = additional_legal_artist\n    if additional_std_artist_list:\n        destination_metadata['~artists_{0}_additional_std_multi'.format(source_type,)] = additional_std_artist_list\n    if additional_cred_artist_list:\n        destination_metadata['~artists_{0}_additional_cred_multi'.format(source_type,)] = additional_cred_artist_list\n    if additional_sort_artist_list:\n        destination_metadata['~artists_{0}_additional_sort_multi'.format(source_type,)] = additional_sort_artist_list\n    if additional_cred_sort_artist_list:\n        destination_metadata['~artists_{0}_additional_cred_sort_multi'.format(source_type,)] = additional_cred_sort_artist_list\n    if additional_legal_artist_list:\n        destination_metadata['~artists_{0}_additional_legal_multi'.format(source_type,)] = additional_legal_artist_list\n    if std_artist:\n        destination_metadata['~artists_{0}_all_std'.format(source_type,)] = std_artist\n    if cred_artist:\n        destination_metadata['~artists_{0}_all_cred'.format(source_type,)] = cred_artist\n    if cred_sort_artist:\n        destination_metadata['~artists_{0}_all_cred_sort'.format(source_type,)] = cred_sort_artist\n    if sort_artist:\n        destination_metadata['~artists_{0}_all_sort'.format(source_type,)] = sort_artist\n    if legal_artist:\n        destination_metadata['~artists_{0}_all_legal'.format(source_type,)] = legal_artist\n    if std_artist_list:\n        destination_metadata['~artists_{0}_all_std_multi'.format(source_type,)] = std_artist_list\n    if cred_artist_list:\n        destination_metadata['~artists_{0}_all_cred_multi'.format(source_type,)] = cred_artist_list\n    if sort_artist_list:\n        destination_metadata['~artists_{0}_all_sort_multi'.format(source_type,)] = sort_artist_list\n    if cred_sort_artist_list:\n        destination_metadata['~artists_{0}_all_cred_sort_multi'.format(source_type,)] = cred_sort_artist_list\n    if legal_artist_list:\n        destination_metadata['~artists_{0}_all_legal_multi'.format(source_type,)] = legal_artist_list\n    if sort_pri_artist:\n        destination_metadata['~artists_{0}_all_sort_primary'.format(source_type,)] = sort_pri_artist\n    if artist_types:\n        destination_metadata['~artists_{0}_all_types'.format(source_type,)] = artist_types\n    if artist_join_phrases:\n        destination_metadata['~artists_{0}_all_join_phrases'.format(source_type,)] = artist_join_phrases\n    if artist_count:\n        destination_metadata['~artists_{0}_all_count'.format(source_type,)] = artist_count\n\n\ndef make_album_vars(album, album_metadata, release_metadata):\n    album_id = release_metadata['id'] if release_metadata else 'No Album ID'\n    process_artists(album_id, release_metadata, album_metadata, 'album')\n\n\ndef make_track_vars(album, album_metadata, track_metadata, release_metadata):\n    album_id = release_metadata['id'] if release_metadata else 'No Album ID'\n    process_artists(album_id, track_metadata, album_metadata, 'track')\n\n\ndef metadata_error(album_id, metadata_element, metadata_group):\n    log.error(\"{0}: {1!r}: Missing '{2}' in {3} metadata.\".format(\n        PLUGIN_NAME, album_id, metadata_element, metadata_group,))\n\n\n# Register the plugin to run at a LOW priority so that other plugins that\n# modify the artist information can complete their processing and this plugin\n# is working with the latest updated data.\nregister_album_metadata_processor(make_album_vars, priority=PluginPriority.LOW)\nregister_track_metadata_processor(make_track_vars, priority=PluginPriority.LOW)\n"
  },
  {
    "path": "plugins/addrelease/addrelease.py",
    "content": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = \"Add Cluster As Release\"\nPLUGIN_AUTHOR = 'Frederik \"Freso\" S. Olesen, Lukáš Lalinský, Philip Jägenstedt'\nPLUGIN_DESCRIPTION = \"Adds a plugin context menu option to clusters and single\\\n files to help you quickly add them as releases or standalone recordings to\\\n the MusicBrainz database via the website by pre-populating artists,\\\n track names and times.\"\nPLUGIN_VERSION = \"0.7.3\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\n\nfrom picard import config, log\nfrom picard.cluster import Cluster\nfrom picard.const import MUSICBRAINZ_SERVERS\nfrom picard.file import File\nfrom picard.util import webbrowser2\nfrom picard.ui.itemviews import BaseAction, register_cluster_action, register_file_action\n\nimport os\nimport tempfile\n\nHTML_HEAD = \"\"\"<!doctype html>\n<meta charset=\"UTF-8\">\n<title>%s</title>\n<form action=\"%s\" method=\"post\">\n\"\"\"\nHTML_INPUT = \"\"\"<input type=\"hidden\" name=\"%s\" value=\"%s\">\n\"\"\"\nHTML_TAIL = \"\"\"<input type=\"submit\" value=\"%s\">\n</form>\n<script>document.forms[0].submit()</script>\n\"\"\"\nHTML_ATTR_ESCAPE = {\n    \"&\": \"&amp;\",\n    '\"': \"&quot;\"\n}\n\n\ndef mbserver_url(path):\n    host = config.setting[\"server_host\"]\n    port = config.setting[\"server_port\"]\n    if host in MUSICBRAINZ_SERVERS or port == 443:\n        urlstring = \"https://%s%s\" % (host, path)\n    elif port is None or port == 80:\n        urlstring = \"http://%s%s\" % (host, path)\n    else:\n        urlstring = \"http://%s:%d%s\" % (host, port, path)\n    return urlstring\n\n\nclass AddObjectAsEntity(BaseAction):\n    NAME = \"Add Object As Entity...\"\n    objtype = None\n    submit_path = '/'\n\n    def __init__(self):\n        super(AddObjectAsEntity, self).__init__()\n        self.form_values = {}\n\n    def check_object(self, objs, objtype):\n        \"\"\"\n        Checks if a given object array is valid (ie., has one item) and that\n        its item is an object of the given type.\n\n        Returns either False (if conditions are not met), or the object in the\n        array.\n        \"\"\"\n        if not isinstance(objs[0], objtype) or len(objs) != 1:\n            return False\n        else:\n            return objs[0]\n\n    def add_form_value(self, key, value):\n        \"Add global (e.g., release level) name-value pair.\"\n        self.form_values[key] = value\n\n    def set_form_values(self, objdata):\n        return\n\n    def generate_html_file(self, form_values):\n        (fd, fp) = tempfile.mkstemp(suffix=\".html\")\n\n        with os.fdopen(fd, \"w\", encoding=\"utf-8\") as f:\n            def esc(s):\n                return \"\".join(HTML_ATTR_ESCAPE.get(c, c) for c in s)\n            # add a global (release-level) name-value\n\n            def nv(n, v):\n                f.write(HTML_INPUT % (esc(n), esc(v)))\n\n            f.write(HTML_HEAD % (self.NAME, mbserver_url(self.submit_path)))\n\n            for key in form_values:\n                nv(key, form_values[key])\n\n            f.write(HTML_TAIL % (self.NAME))\n\n        return fp\n\n    def open_html_file(self, fp):\n        webbrowser2.open(\"file://\" + fp)\n\n    def callback(self, objs):\n        objdata = self.check_object(objs, self.objtype)\n        try:\n            if objdata:\n                self.set_form_values(objdata)\n                html_file = self.generate_html_file(self.form_values)\n                self.open_html_file(html_file)\n        finally:\n            self.form_values.clear()\n\n\nclass AddClusterAsRelease(AddObjectAsEntity):\n    NAME = \"Add Cluster As Release...\"\n    objtype = Cluster\n    submit_path = '/release/add'\n\n    def __init__(self):\n        super().__init__()\n        self.discnumber_shift = -1\n\n    def extract_discnumber(self, metadata):\n        \"\"\"\n        >>> from picard.metadata import Metadata\n        >>> m = Metadata()\n        >>> AddClusterAsRelease().extract_discnumber(m)\n        0\n        >>> m[\"discnumber\"] = \"boop\"\n        >>> AddClusterAsRelease().extract_discnumber(m)\n        0\n        >>> m[\"discnumber\"] = \"1\"\n        >>> AddClusterAsRelease().extract_discnumber(m)\n        0\n        >>> m[\"discnumber\"] = 1\n        >>> AddClusterAsRelease().extract_discnumber(m)\n        0\n        >>> m[\"discnumber\"] = -1\n        >>> AddClusterAsRelease().extract_discnumber(m)\n        0\n        >>> m[\"discnumber\"] = \"1/1\"\n        >>> AddClusterAsRelease().extract_discnumber(m)\n        0\n        >>> m[\"discnumber\"] = \"2/2\"\n        >>> AddClusterAsRelease().extract_discnumber(m)\n        1\n        >>> a = AddClusterAsRelease()\n        >>> m[\"discnumber\"] = \"-2/2\"\n        >>> a.extract_discnumber(m)\n        0\n        >>> m[\"discnumber\"] = \"-1/4\"\n        >>> a.extract_discnumber(m)\n        1\n        >>> m[\"discnumber\"] = \"1/4\"\n        >>> a.extract_discnumber(m)\n        3\n\n        \"\"\"\n        # As per https://musicbrainz.org/doc/Development/Release_Editor_Seeding#Tracklists_data\n        # the medium numbers (\"m\") must be starting with 0.\n        # Maybe the existing tags don't have disc numbers in them or\n        # they're starting with something smaller than or equal to 0, so try\n        # to produce a sane disc number.\n        try:\n            discnumber = metadata.get(\"discnumber\", \"1\")\n            # Split off any totaldiscs information\n            discnumber = discnumber.split(\"/\", 1)[0]\n            m = int(discnumber)\n            if m <= 0:\n                # A disc number was smaller than or equal to 0 - all other\n                # disc numbers need to be changed to accommodate that.\n                self.discnumber_shift = max(self.discnumber_shift, 0 - m)\n            m = m + self.discnumber_shift\n        except ValueError as e:\n            # The most likely reason for an exception at this point is because\n            # the disc number in the tags was not a number. Just log the\n            # exception and assume the medium number is 0.\n            log.info(\"Trying to get the disc number of %s caused the following error: %s; assuming 0\",\n                     metadata[\"~filename\"], e)\n            m = 0\n        return m\n\n    def set_form_values(self, cluster):\n        nv = self.add_form_value\n\n        nv(\"artist_credit.names.0.artist.name\", cluster.metadata[\"albumartist\"])\n        nv(\"name\", cluster.metadata[\"album\"])\n\n        for i, file in enumerate(cluster.files):\n            try:\n                i = int(file.metadata[\"tracknumber\"]) - 1\n            except:\n                pass\n\n            m = self.extract_discnumber(file.metadata)\n\n            # add a track-level name-value\n            def tnv(n, v):\n                nv(\"mediums.%d.track.%d.%s\" % (m, i, n), v)\n\n            tnv(\"name\", file.metadata[\"title\"])\n            if file.metadata[\"artist\"] != cluster.metadata[\"albumartist\"]:\n                tnv(\"artist_credit.names.0.name\", file.metadata[\"artist\"])\n            tnv(\"length\", str(file.metadata.length))\n\n\nclass AddFileAsRecording(AddObjectAsEntity):\n    NAME = \"Add File As Standalone Recording...\"\n    objtype = File\n    submit_path = '/recording/create'\n\n    def set_form_values(self, track):\n        nv = self.add_form_value\n        nv(\"edit-recording.name\", track.metadata[\"title\"])\n        nv(\"edit-recording.artist_credit.names.0.artist.name\", track.metadata[\"artist\"])\n        nv(\"edit-recording.length\", track.metadata[\"~length\"])\n\n\nclass AddFileAsRelease(AddObjectAsEntity):\n    NAME = \"Add File As Release...\"\n    objtype = File\n    submit_path = '/release/add'\n\n    def set_form_values(self, track):\n        nv = self.add_form_value\n\n        # Main album attributes\n        if track.metadata[\"albumartist\"]:\n            nv(\"artist_credit.names.0.artist.name\", track.metadata[\"albumartist\"])\n        else:\n            nv(\"artist_credit.names.0.artist.name\", track.metadata[\"artist\"])\n        if track.metadata[\"album\"]:\n            nv(\"name\", track.metadata[\"album\"])\n        else:\n            nv(\"name\", track.metadata[\"title\"])\n\n        # Tracklist\n        nv(\"mediums.0.track.0.name\", track.metadata[\"title\"])\n        nv(\"mediums.0.track.0.artist_credit.names.0.name\", track.metadata[\"artist\"])\n        nv(\"mediums.0.track.0.length\", str(track.metadata.length))\n\n\nregister_cluster_action(AddClusterAsRelease())\nregister_file_action(AddFileAsRecording())\nregister_file_action(AddFileAsRelease())\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod()\n"
  },
  {
    "path": "plugins/albumartist_website/albumartist_website.py",
    "content": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = 'Album Artist Website'\nPLUGIN_AUTHOR = 'Sophist, Sambhav Kothari, Philipp Wolfer'\nPLUGIN_DESCRIPTION = '''Add's the album artist(s) Official Homepage(s)\n(if they are defined in the MusicBrainz database).'''\nPLUGIN_VERSION = '1.2'\nPLUGIN_API_VERSIONS = [\"2.0\", \"2.1\", \"2.2\"]\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom picard import config, log\nfrom picard.util import LockableObject\nfrom picard.metadata import register_track_metadata_processor\nfrom functools import partial\n\nclass AlbumArtistWebsite:\n\n    class ArtistWebsiteQueue(LockableObject):\n\n        def __init__(self):\n            LockableObject.__init__(self)\n            self.queue = {}\n\n        def __contains__(self, name):\n            return name in self.queue\n\n        def __iter__(self):\n            return self.queue.__iter__()\n\n        def __getitem__(self, name):\n            self.lock_for_read()\n            value = self.queue[name] if name in self.queue else None\n            self.unlock()\n            return value\n\n        def __setitem__(self, name, value):\n            self.lock_for_write()\n            self.queue[name] = value\n            self.unlock()\n\n        def append(self, name, value):\n            self.lock_for_write()\n            if name in self.queue:\n                self.queue[name].append(value)\n                value = False\n            else:\n                self.queue[name] = [value]\n                value = True\n            self.unlock()\n            return value\n\n        def remove(self, name):\n            self.lock_for_write()\n            value = None\n            if name in self.queue:\n                value = self.queue[name]\n                del self.queue[name]\n            self.unlock()\n            return value\n\n    def __init__(self):\n        self.website_cache = {}\n        self.website_queue = self.ArtistWebsiteQueue()\n\n    def add_artist_website(self, album, track_metadata, track_node, release_node):\n        albumArtistIds = track_metadata.getall('musicbrainz_albumartistid')\n        for artistId in albumArtistIds:\n            # Jump through hoops to get track object!!\n            track = album._new_tracks[-1]\n            if artistId in self.website_cache:\n                if self.website_cache[artistId]:\n                    self.add_websites_to_track(track, self.website_cache[artistId])\n            else:\n                self.website_add_track(album, track, artistId)\n\n    def website_add_track(self, album, track, artistId):\n        self.album_add_request(album)\n        if self.website_queue.append(artistId, (track, album)):\n            host = config.setting[\"server_host\"]\n            port = config.setting[\"server_port\"]\n            path = \"/ws/2/%s/%s\" % ('artist', artistId)\n            queryargs = {\"inc\": \"url-rels\"}\n            return album.tagger.webservice.get(host, port, path,\n                        partial(self.website_process, artistId),\n                                parse_response_type=\"xml\", priority=True, important=False,\n                                queryargs=queryargs)\n\n    def website_process(self, artistId, response, reply, error):\n        if error:\n            log.error(\"%s: %r: Network error retrieving artist record\", PLUGIN_NAME, artistId)\n            tuples = self.website_queue.remove(artistId)\n            for track, album in tuples:\n                self.album_remove_request(album)\n            return\n        urls = self.artist_process_metadata(artistId, response)\n        self.website_cache[artistId] = urls\n        tuples = self.website_queue.remove(artistId)\n        for track, album in tuples:\n            self.add_websites_to_track(track, urls)\n            self.album_remove_request(album)\n\n    def add_websites_to_track(self, track, urls):\n        tm = track.metadata\n        websites = tm.getall('website')\n        websites += urls\n        websites.sort()\n        tm['website'] = websites\n        for file in track.iterfiles(True):\n            file.metadata['website'] = websites\n\n    def album_add_request(self, album):\n        album._requests += 1\n\n    def album_remove_request(self, album):\n        album._requests -= 1\n        album._finalize_loading(None)\n\n    def artist_process_metadata(self, artistId, response):\n        log.debug(\"%s: %r: Processing Artist record for official website urls: %r\", PLUGIN_NAME, artistId, response)\n        relations = self.artist_get_relations(response)\n        if not relations:\n            log.info(\"%s: %r: Artist does have any associated urls.\", PLUGIN_NAME, artistId)\n            return []\n\n        urls = []\n        for relation in relations:\n            log.debug(\"%s: %r: Examining: %r\", PLUGIN_NAME, artistId, relation)\n            if 'type' in relation.attribs and relation.type == 'official homepage':\n                if 'target' in relation.children and len(relation.target) > 0:\n                    if not 'ended' in relation.children or relation.ended[0].text != 'true':\n                        log.debug(\"%s: Adding artist url: %s\", PLUGIN_NAME, relation.target[0].text)\n                        urls.append(relation.target[0].text)\n                    else:\n                        log.debug(\"%s: Artist url has ended: %s\", PLUGIN_NAME, relation.target[0].text)\n                else:\n                    log.debug(\"%s: No url in relation: %r\", PLUGIN_NAME, relation)\n\n        if urls:\n            log.info(\"%s: %r: Artist Official Homepages: %r\", PLUGIN_NAME, artistId, urls)\n        else:\n            log.info(\"%s: %r: Artist does not have any official website urls.\", PLUGIN_NAME, artistId)\n        return sorted(urls)\n\n    def artist_get_relations(self, response):\n        log.debug(\"%s: artist_get_relations called\", PLUGIN_NAME)\n        if 'metadata' in response.children and len(response.metadata) > 0:\n            if 'artist' in response.metadata[0].children and len(response.metadata[0].artist) > 0:\n                if 'relation_list' in response.metadata[0].artist[0].children and len(response.metadata[0].artist[0].relation_list) > 0:\n                    if 'relation' in response.metadata[0].artist[0].relation_list[0].children:\n                        log.debug(\"%s: artist_get_relations returning: %r\", PLUGIN_NAME, response.metadata[0].artist[0].relation_list[0].relation)\n                        return response.metadata[0].artist[0].relation_list[0].relation\n                    else:\n                        log.debug(\"%s: artist_get_relations - no relation in relation_list\", PLUGIN_NAME)\n                else:\n                    log.debug(\"%s: artist_get_relations - no relation_list in artist\", PLUGIN_NAME)\n            else:\n                log.debug(\"%s: artist_get_relations - no artist in metadata\", PLUGIN_NAME)\n        else:\n            log.debug(\"%s: artist_get_relations - no metadata in response\", PLUGIN_NAME)\n        return None\n\n\nregister_track_metadata_processor(AlbumArtistWebsite().add_artist_website)\n"
  },
  {
    "path": "plugins/albumartistextension/albumartistextension.py",
    "content": "PLUGIN_NAME = 'AlbumArtist Extension'\nPLUGIN_AUTHOR = 'Bob Swift (rdswift)'\nPLUGIN_DESCRIPTION = '''\nThis plugin provides standardized, credited and sorted artist information\nfor the album artist.  This is useful when your tagging or renaming scripts\nrequire both the standardized artist name and the credited artist name, or\nmore detailed information about the album artists.\n<br /><br />\nThe information is provided in the following variables:\n<ul>\n<li>_aaeStdAlbumArtists = The standardized version of the album artists.\n<li>_aaeCredAlbumArtists = The credited version of the album artists.\n<li>_aaeSortAlbumArtists = The sorted version of the album artists.\n<li>_aaeStdPrimaryAlbumArtist = The standardized version of the first\n    (primary) album artist.\n<li>_aaeCredPrimaryAlbumArtist = The credited version of the first (primary)\n    album artist.\n<li>_aaeSortPrimaryAlbumArtist = The sorted version of the first (primary)\n    album artist.\n<li>_aaeAlbumArtistCount = The number of artists comprising the album artist.\n</ul>\nPLEASE NOTE: Once the plugin is installed, it automatically makes these \nvariables available to File Naming Scripts and other scripts in Picard. \nLike other variables, you must mention them in a script for them to affect \nthe file name or other data.\n<br /><br />\nThis plugin is no longer being maintained. \nConsider switching to the \n[Additional Artists Variables plugin](https://github.com/rdswift/picard-plugins/tree/2.0/plugins/additional_artists_variables), \nwhich fills this \nrole, and also includes additional variables. That other plugin uses different \nnames for the album artist names provided here, so you if you switch plugins, you\nwill need to update your scripts with the different names.\n<br /><br />\nVersion 0.6.1 of this plugin functions identically to Version 0.6. Only this \ndescription (and the version number) has changed.\n'''\n\nPLUGIN_VERSION = \"0.6.1\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom picard import config, log\nfrom picard.metadata import register_album_metadata_processor\nfrom picard.plugin import PluginPriority\n\ndef add_artist_std_name(album, album_metadata, release_metadata):\n    album_id = release_metadata['id']\n    # Test for valid metadata node for the release\n    if 'artist-credit' in release_metadata:\n        # Initialize variables to default values\n        cred_artist = \"\"\n        std_artist = \"\"\n        sort_artist = \"\"\n        artist_count = 0\n        # The 'artist-credit' key should always be there.\n        # This check is to avoid a runtime error if it doesn't exist for some reason.\n        for artist_credit in release_metadata['artist-credit']:\n            # Initialize temporary variables for each loop.\n            temp_std_name = \"\"\n            temp_cred_name = \"\"\n            temp_sort_name = \"\"\n            temp_phrase = \"\"\n            # Check if there is a 'joinphrase' specified.\n            if 'joinphrase' in artist_credit:\n                temp_phrase = artist_credit['joinphrase']\n            else:\n                metadata_error(album_id, 'artist-credit.joinphrase')\n            # Check if there is a 'name' specified.\n            if 'name' in artist_credit:\n                temp_cred_name = artist_credit['name']\n            else:\n                metadata_error(album_id, 'artist-credit.name')\n            # Check if there is an 'artist' specified.\n            if 'artist' in artist_credit:\n                # Check if there is a 'name' specified.\n                if 'name' in artist_credit['artist']:\n                    temp_std_name = artist_credit['artist']['name']\n                else:\n                    metadata_error(album_id, 'artist-credit.artist.name')\n                if 'sort-name' in artist_credit['artist']:\n                    temp_sort_name = artist_credit['artist']['sort-name']\n                else:\n                    metadata_error(album_id, 'artist-credit.artist.sort-name')\n            else:\n                metadata_error(album_id, 'artist-credit.artist')\n            std_artist += temp_std_name + temp_phrase\n            cred_artist += temp_cred_name + temp_phrase\n            sort_artist += temp_sort_name + temp_phrase\n            if artist_count < 1:\n                album_metadata[\"~aaeStdPrimaryAlbumArtist\"] = temp_std_name\n                album_metadata[\"~aaeCredPrimaryAlbumArtist\"] = temp_cred_name\n                album_metadata[\"~aaeSortPrimaryAlbumArtist\"] = temp_sort_name\n            artist_count += 1\n    else:\n        metadata_error(album_id, 'artist-credit')\n    if std_artist:\n        album_metadata[\"~aaeStdAlbumArtists\"] = std_artist\n    if cred_artist:\n        album_metadata[\"~aaeCredAlbumArtists\"] = cred_artist\n    if sort_artist:\n        album_metadata[\"~aaeSortAlbumArtists\"] = sort_artist\n    if artist_count:\n        album_metadata[\"~aaeAlbumArtistCount\"] = artist_count\n    return None\n\n\ndef metadata_error(album_id, metadata_element):\n    log.error(\"%s: %r: Missing '%s' in release metadata.\",\n            PLUGIN_NAME, album_id, metadata_element)\n\n# Register the plugin to run at a LOW priority so that other plugins that\n# modify the artist information can complete their processing and this plugin\n# is working with the latest updated data.\nregister_album_metadata_processor(add_artist_std_name, priority=PluginPriority.LOW)\n"
  },
  {
    "path": "plugins/amazon/amazon.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Picard, the next-generation MusicBrainz tagger\n# Copyright (C) 2007 Oliver Charles\n# Copyright (C) 2007-2011, 2019, 2021 Philipp Wolfer\n# Copyright (C) 2007, 2010, 2011 Lukáš Lalinský\n# Copyright (C) 2011 Michael Wiencek\n# Copyright (C) 2011-2012 Wieland Hoffmann\n# Copyright (C) 2013-2016 Laurent Monin\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n\nPLUGIN_NAME = 'Amazon cover art'\nPLUGIN_AUTHOR = 'MusicBrainz Picard developers'\nPLUGIN_DESCRIPTION = 'Use cover art from Amazon.'\nPLUGIN_VERSION = \"1.1\"\nPLUGIN_API_VERSIONS = [\"2.2\", \"2.3\", \"2.4\", \"2.5\", \"2.6\", \"2.7\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom picard import log\nfrom picard.coverart.image import CoverArtImage\nfrom picard.coverart.providers import (\n    CoverArtProvider,\n    register_cover_art_provider,\n)\nfrom picard.util import parse_amazon_url\n\n# amazon image file names are unique on all servers and constructed like\n# <ASIN>.<ServerNumber>.[SML]ZZZZZZZ.jpg\n# A release sold on amazon.de has always <ServerNumber> = 03, for example.\n# Releases not sold on amazon.com, don't have a \"01\"-version of the image,\n# so we need to make sure we grab an existing image.\nAMAZON_SERVER = {\n    \"amazon.com\": {\n        \"server\": \"ec1.images-amazon.com\",\n        \"id\": \"01\",\n    },\n    \"amazon.jp\": {\n        \"server\": \"ec1.images-amazon.com\",\n        \"id\": \"09\",\n    },\n    \"amazon.co.jp\": {\n        \"server\": \"ec1.images-amazon.com\",\n        \"id\": \"09\",\n    },\n    \"amazon.co.uk\": {\n        \"server\": \"ec1.images-amazon.com\",\n        \"id\": \"02\",\n    },\n    \"amazon.de\": {\n        \"server\": \"ec2.images-amazon.com\",\n        \"id\": \"03\",\n    },\n    \"amazon.ca\": {\n        \"server\": \"ec1.images-amazon.com\",\n        \"id\": \"01\",  # .com and .ca are identical\n    },\n    \"amazon.fr\": {\n        \"server\": \"ec1.images-amazon.com\",\n        \"id\": \"08\"\n    },\n}\n\nAMAZON_IMAGE_PATH = '/images/P/%(asin)s.%(serverid)s.%(size)s.jpg'\n\n# First item in the list will be tried first\nAMAZON_SIZES = (\n    # huge size option is only available for items\n    # that have a ZOOMing picture on its amazon web page\n    # and it doesn't work for all of the domain names\n    # '_SCRM_',        # huge size\n    'LZZZZZZZ',      # large size, option format 1\n    # '_SCLZZZZZZZ_',  # large size, option format 3\n    'MZZZZZZZ',      # default image size, format 1\n    # '_SCMZZZZZZZ_',  # medium size, option format 3\n    # 'TZZZZZZZ',      # medium image size, option format 1\n    # '_SCTZZZZZZZ_',  # small size, option format 3\n    # 'THUMBZZZ',      # small size, option format 1\n)\n\n\nclass CoverArtProviderAmazon(CoverArtProvider):\n\n    \"\"\"Use Amazon ASIN Musicbrainz relationships to get cover art\"\"\"\n\n    NAME = \"Amazon\"\n    TITLE = N_('Amazon')\n\n    def __init__(self, coverart):\n        super().__init__(coverart)\n        self._has_url_relation = False\n\n    def enabled(self):\n        return (super().enabled()\n                and not self.coverart.front_image_found)\n\n    def queue_images(self):\n        self.match_url_relations(('amazon asin', 'has_Amazon_ASIN'),\n                                 self._queue_from_asin_relation)\n        # No URL relationships loaded, try by ASIN\n        if not self._has_url_relation:\n            self._queue_from_asin()\n        return CoverArtProvider.FINISHED\n\n    def _queue_from_asin_relation(self, url):\n        \"\"\"Queue cover art images from Amazon\"\"\"\n        amz = parse_amazon_url(url)\n        if amz is None:\n            return\n        log.debug(\"Found ASIN relation : %s %s\", amz['host'], amz['asin'])\n        self._has_url_relation = True\n        if amz['host'] in AMAZON_SERVER:\n            server_info = AMAZON_SERVER[amz['host']]\n        else:\n            server_info = AMAZON_SERVER['amazon.com']\n        self._queue_asin(server_info, amz['asin'])\n\n    def _queue_from_asin(self):\n        asin = self.release.get('asin')\n        if asin:\n            log.debug(\"Found ASIN : %s\", asin)\n            for server_info in AMAZON_SERVER.values():\n               self._queue_asin(server_info, asin)\n\n    def _queue_asin(self, server_info, asin):\n        host = server_info['server']\n        for size in AMAZON_SIZES:\n            path = AMAZON_IMAGE_PATH % {\n                'asin': asin,\n                'serverid': server_info['id'],\n                'size': size\n            }\n            self.queue_put(CoverArtImage(\"http://%s%s\" % (host, path)))\n\n\nregister_cover_art_provider(CoverArtProviderAmazon)\n"
  },
  {
    "path": "plugins/bpm/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Changelog:\n# [2015-09-15] Initial version\n# [2017-11-24] Qt5, Python3 for Picard-plugins branch 2\n# [2020-12-25] Move access to config.settings outside of thread\n# Dependancies:\n# aubio, numpy\n#\n\nPLUGIN_NAME = \"BPM Analyzer\"\nPLUGIN_AUTHOR = \"Len Joubert, Sambhav Kothari, Philipp Wolfer\"\nPLUGIN_DESCRIPTION = \"\"\"Calculate BPM for selected files and albums. Linux only version with dependancy on Aubio and Numpy\"\"\"\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\nPLUGIN_VERSION = \"1.5.2\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\n# PLUGIN_INCOMPATIBLE_PLATFORMS = [\n#    'win32', 'cygwyn', 'darwin', 'os2', 'os2emx', 'riscos', 'atheos']\n\nfrom aubio import source, tempo\nfrom functools import partial\nfrom numpy import median, diff\n\nfrom picard.config import config, IntOption\nfrom picard.file import File\nfrom picard.plugins.bpm.ui_options_bpm import Ui_BPMOptionsPage\nfrom picard.track import Track\nfrom picard.ui.itemviews import BaseAction, register_file_action\nfrom picard.ui.options import register_options_page, OptionsPage\nfrom picard.util import thread\n\n\nbpm_slider_settings = {\n    1: (44100, 1024, 512),\n    2: (8000, 512, 128),\n    3: (4000, 128, 64),\n}\n\n\nclass FileBPM(BaseAction):\n    NAME = N_(\"Calculate BPM...\")\n\n    def __init__(self):\n        super().__init__()\n        self._close = False\n        self.tagger.aboutToQuit.connect(self._cleanup)\n\n    def _cleanup(self):\n        self._close = True\n\n    def _add_file_to_queue(self, file):\n        settings = bpm_slider_settings[config.setting[\"bpm_slider_parameter\"]]\n        thread.run_task(\n            partial(self._calculate_bpm, file, settings),\n            partial(self._calculate_bpm_callback, file))\n\n    def callback(self, objs):\n        for obj in objs:\n            if isinstance(obj, Track):\n                for file_ in obj.linked_files:\n                    self._add_file_to_queue(file_)\n            elif isinstance(obj, File):\n                self._add_file_to_queue(obj)\n\n    def _calculate_bpm(self, file, settings):\n        self.tagger.window.set_statusbar_message(\n            N_('Calculating BPM for \"%(filename)s\"...'),\n            {'filename': file.filename}\n        )\n        calculated_bpm = self._get_file_bpm(file.filename, settings)\n        # self.tagger.log.debug('%s' % (calculated_bpm))\n        if self._close:\n            return\n        file.metadata[\"bpm\"] = str(round(calculated_bpm, 1))\n\n    def _calculate_bpm_callback(self, file, result=None, error=None):\n        if not error:\n            file.update()\n            self.tagger.window.set_statusbar_message(\n                N_('BPM for \"%(filename)s\" successfully calculated.'),\n                {'filename': file.filename}\n            )\n        else:\n            self.tagger.window.set_statusbar_message(\n                N_('Could not calculate BPM for \"%(filename)s\".'),\n                {'filename': file.filename}\n            )\n\n    def _get_file_bpm(self, path, settings):\n        \"\"\" Calculate the beats per minute (bpm) of a given file.\n            path: path to the file\n            buf_size    length of FFT\n            hop_size    number of frames between two consecutive runs\n            samplerate  sampling rate of the signal to analyze\n        \"\"\"\n\n        samplerate, buf_size, hop_size = settings\n        mediasource = source(path, samplerate, hop_size)\n        samplerate = mediasource.samplerate\n        beattracking = tempo(\"specdiff\", buf_size, hop_size, samplerate)\n        # List of beats, in samples\n        beats = []\n        # Total number of frames read\n        total_frames = 0\n\n        while True:\n            if self._close:\n                return\n            samples, read = mediasource()\n            is_beat = beattracking(samples)\n            if is_beat:\n                this_beat = beattracking.get_last_s()\n                beats.append(this_beat)\n            total_frames += read\n            if read < hop_size:\n                break\n\n        # Convert to periods and to bpm\n        bpms = 60. / diff(beats)\n        return median(bpms)\n\n\nclass BPMOptionsPage(OptionsPage):\n\n    NAME = \"bpm\"\n    TITLE = \"BPM\"\n    PARENT = \"plugins\"\n    ACTIVE = True\n\n    options = [\n        IntOption(\"setting\", \"bpm_slider_parameter\", 1)\n    ]\n\n    def __init__(self, parent=None):\n        super(BPMOptionsPage, self).__init__(parent)\n        self.ui = Ui_BPMOptionsPage()\n        self.ui.setupUi(self)\n        self.ui.slider_parameter.valueChanged.connect(self.update_parameters)\n        self.update_parameters()\n\n    def load(self):\n        cfg = self.config.setting\n        self.ui.slider_parameter.setValue(cfg[\"bpm_slider_parameter\"])\n\n    def save(self):\n        cfg = self.config.setting\n        cfg[\"bpm_slider_parameter\"] = self.ui.slider_parameter.value()\n\n    def update_parameters(self):\n        val = self.ui.slider_parameter.value()\n        samplerate, buf_size, hop_size = [str(v) for v in\n                                          bpm_slider_settings[val]]\n        self.ui.samplerate_value.setText(samplerate)\n        self.ui.win_s_value.setText(buf_size)\n        self.ui.hop_s_value.setText(hop_size)\n\n\nregister_file_action(FileBPM())\nregister_options_page(BPMOptionsPage)\n"
  },
  {
    "path": "plugins/bpm/ui_options_bpm.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/bpm/ui_options_bpm.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.4\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_BPMOptionsPage(object):\n    def setupUi(self, BPMOptionsPage):\n        BPMOptionsPage.setObjectName(\"BPMOptionsPage\")\n        BPMOptionsPage.resize(611, 273)\n        self.verticalLayout = QtWidgets.QVBoxLayout(BPMOptionsPage)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.bpm_options = QtWidgets.QGroupBox(BPMOptionsPage)\n        self.bpm_options.setObjectName(\"bpm_options\")\n        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.bpm_options)\n        self.verticalLayout_2.setObjectName(\"verticalLayout_2\")\n        self.verticalWidget = QtWidgets.QWidget(self.bpm_options)\n        self.verticalWidget.setMaximumSize(QtCore.QSize(500, 16777215))\n        self.verticalWidget.setObjectName(\"verticalWidget\")\n        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.verticalWidget)\n        self.verticalLayout_3.setObjectName(\"verticalLayout_3\")\n        self.slider_parameter = QtWidgets.QSlider(self.verticalWidget)\n        self.slider_parameter.setMinimum(1)\n        self.slider_parameter.setMaximum(3)\n        self.slider_parameter.setPageStep(1)\n        self.slider_parameter.setOrientation(QtCore.Qt.Horizontal)\n        self.slider_parameter.setTickPosition(QtWidgets.QSlider.TicksBelow)\n        self.slider_parameter.setTickInterval(1)\n        self.slider_parameter.setObjectName(\"slider_parameter\")\n        self.verticalLayout_3.addWidget(self.slider_parameter)\n        self.slider_labels = QtWidgets.QHBoxLayout()\n        self.slider_labels.setSpacing(6)\n        self.slider_labels.setObjectName(\"slider_labels\")\n        self.slider_super_fast = QtWidgets.QLabel(self.verticalWidget)\n        self.slider_super_fast.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.slider_super_fast.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)\n        self.slider_super_fast.setObjectName(\"slider_super_fast\")\n        self.slider_labels.addWidget(self.slider_super_fast)\n        self.slider_default = QtWidgets.QLabel(self.verticalWidget)\n        self.slider_default.setToolTip(\"\")\n        self.slider_default.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTop|QtCore.Qt.AlignTrailing)\n        self.slider_default.setObjectName(\"slider_default\")\n        self.slider_labels.addWidget(self.slider_default)\n        self.verticalLayout_3.addLayout(self.slider_labels)\n        self.line = QtWidgets.QFrame(self.verticalWidget)\n        self.line.setFrameShape(QtWidgets.QFrame.HLine)\n        self.line.setFrameShadow(QtWidgets.QFrame.Sunken)\n        self.line.setObjectName(\"line\")\n        self.verticalLayout_3.addWidget(self.line)\n        self.gridLayout = QtWidgets.QGridLayout()\n        self.gridLayout.setObjectName(\"gridLayout\")\n        self.samplerate_label = QtWidgets.QLabel(self.verticalWidget)\n        self.samplerate_label.setObjectName(\"samplerate_label\")\n        self.gridLayout.addWidget(self.samplerate_label, 2, 0, 1, 1)\n        self.samplerate_value = QtWidgets.QLabel(self.verticalWidget)\n        self.samplerate_value.setText(\"\")\n        self.samplerate_value.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)\n        self.samplerate_value.setObjectName(\"samplerate_value\")\n        self.gridLayout.addWidget(self.samplerate_value, 2, 1, 1, 1)\n        self.hop_s_value = QtWidgets.QLabel(self.verticalWidget)\n        self.hop_s_value.setText(\"\")\n        self.hop_s_value.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)\n        self.hop_s_value.setObjectName(\"hop_s_value\")\n        self.gridLayout.addWidget(self.hop_s_value, 1, 1, 1, 1)\n        self.hop_s_label = QtWidgets.QLabel(self.verticalWidget)\n        self.hop_s_label.setObjectName(\"hop_s_label\")\n        self.gridLayout.addWidget(self.hop_s_label, 1, 0, 1, 1)\n        self.win_s_value = QtWidgets.QLabel(self.verticalWidget)\n        self.win_s_value.setText(\"\")\n        self.win_s_value.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)\n        self.win_s_value.setObjectName(\"win_s_value\")\n        self.gridLayout.addWidget(self.win_s_value, 0, 1, 1, 1)\n        self.win_s_label = QtWidgets.QLabel(self.verticalWidget)\n        self.win_s_label.setObjectName(\"win_s_label\")\n        self.gridLayout.addWidget(self.win_s_label, 0, 0, 1, 1)\n        self.gridLayout.setColumnStretch(0, 4)\n        self.verticalLayout_3.addLayout(self.gridLayout)\n        self.verticalLayout_2.addWidget(self.verticalWidget)\n        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.verticalLayout_2.addItem(spacerItem)\n        self.verticalLayout.addWidget(self.bpm_options)\n\n        self.retranslateUi(BPMOptionsPage)\n        QtCore.QMetaObject.connectSlotsByName(BPMOptionsPage)\n\n    def retranslateUi(self, BPMOptionsPage):\n        _translate = QtCore.QCoreApplication.translate\n        self.bpm_options.setTitle(_translate(\"BPMOptionsPage\", \"BPM Analyze Parameters:\"))\n        self.slider_super_fast.setText(_translate(\"BPMOptionsPage\", \"Super Fast\"))\n        self.slider_default.setText(_translate(\"BPMOptionsPage\", \"Default\"))\n        self.samplerate_label.setText(_translate(\"BPMOptionsPage\", \"Samplerate:\"))\n        self.hop_s_label.setText(_translate(\"BPMOptionsPage\", \"Number of frames between two consecutive runs:\"))\n        self.win_s_label.setText(_translate(\"BPMOptionsPage\", \"Length of FFT:\"))\n"
  },
  {
    "path": "plugins/bpm/ui_options_bpm.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>BPMOptionsPage</class>\n <widget class=\"QWidget\" name=\"BPMOptionsPage\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>611</width>\n    <height>273</height>\n   </rect>\n  </property>\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n   <item>\n    <widget class=\"QGroupBox\" name=\"bpm_options\">\n     <property name=\"title\">\n      <string>BPM Analyze Parameters:</string>\n     </property>\n     <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\n      <item>\n       <widget class=\"QWidget\" name=\"verticalWidget\" native=\"true\">\n        <property name=\"maximumSize\">\n         <size>\n          <width>500</width>\n          <height>16777215</height>\n         </size>\n        </property>\n        <layout class=\"QVBoxLayout\" name=\"verticalLayout_3\">\n         <item>\n          <widget class=\"QSlider\" name=\"slider_parameter\">\n           <property name=\"minimum\">\n            <number>1</number>\n           </property>\n           <property name=\"maximum\">\n            <number>3</number>\n           </property>\n           <property name=\"pageStep\">\n            <number>1</number>\n           </property>\n           <property name=\"orientation\">\n            <enum>Qt::Horizontal</enum>\n           </property>\n           <property name=\"tickPosition\">\n            <enum>QSlider::TicksBelow</enum>\n           </property>\n           <property name=\"tickInterval\">\n            <number>1</number>\n           </property>\n          </widget>\n         </item>\n         <item>\n          <layout class=\"QHBoxLayout\" name=\"slider_labels\">\n           <property name=\"spacing\">\n            <number>6</number>\n           </property>\n           <item>\n            <widget class=\"QLabel\" name=\"slider_super_fast\">\n             <property name=\"layoutDirection\">\n              <enum>Qt::RightToLeft</enum>\n             </property>\n             <property name=\"text\">\n              <string>Super Fast</string>\n             </property>\n             <property name=\"alignment\">\n              <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>\n             </property>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QLabel\" name=\"slider_default\">\n             <property name=\"toolTip\">\n              <string extracomment=\"Most accurate\"/>\n             </property>\n             <property name=\"text\">\n              <string>Default</string>\n             </property>\n             <property name=\"alignment\">\n              <set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>\n             </property>\n            </widget>\n           </item>\n          </layout>\n         </item>\n         <item>\n          <widget class=\"Line\" name=\"line\">\n           <property name=\"orientation\">\n            <enum>Qt::Horizontal</enum>\n           </property>\n          </widget>\n         </item>\n         <item>\n          <layout class=\"QGridLayout\" name=\"gridLayout\" columnstretch=\"4,0\">\n           <item row=\"2\" column=\"0\">\n            <widget class=\"QLabel\" name=\"samplerate_label\">\n             <property name=\"text\">\n              <string>Samplerate:</string>\n             </property>\n            </widget>\n           </item>\n           <item row=\"2\" column=\"1\">\n            <widget class=\"QLabel\" name=\"samplerate_value\">\n             <property name=\"text\">\n              <string/>\n             </property>\n             <property name=\"alignment\">\n              <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>\n             </property>\n            </widget>\n           </item>\n           <item row=\"1\" column=\"1\">\n            <widget class=\"QLabel\" name=\"hop_s_value\">\n             <property name=\"text\">\n              <string/>\n             </property>\n             <property name=\"alignment\">\n              <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>\n             </property>\n            </widget>\n           </item>\n           <item row=\"1\" column=\"0\">\n            <widget class=\"QLabel\" name=\"hop_s_label\">\n             <property name=\"text\">\n              <string>Number of frames between two consecutive runs:</string>\n             </property>\n            </widget>\n           </item>\n           <item row=\"0\" column=\"1\">\n            <widget class=\"QLabel\" name=\"win_s_value\">\n             <property name=\"text\">\n              <string/>\n             </property>\n             <property name=\"alignment\">\n              <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>\n             </property>\n            </widget>\n           </item>\n           <item row=\"0\" column=\"0\">\n            <widget class=\"QLabel\" name=\"win_s_label\">\n             <property name=\"text\">\n              <string>Length of FFT:</string>\n             </property>\n            </widget>\n           </item>\n          </layout>\n         </item>\n        </layout>\n       </widget>\n      </item>\n      <item>\n       <spacer name=\"verticalSpacer\">\n        <property name=\"orientation\">\n         <enum>Qt::Vertical</enum>\n        </property>\n        <property name=\"sizeHint\" stdset=\"0\">\n         <size>\n          <width>20</width>\n          <height>40</height>\n         </size>\n        </property>\n       </spacer>\n      </item>\n     </layout>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/classical_extras/Readme.md",
    "content": "# General Information\nThis is the documentation for version 2.0.11 of \"classical\\_extras\". There may be beta versions later than this - check [my github site](https://github.com/MetaTunes/picard-plugins/tree/metabrainz/2.0/plugins/classical_extras) for newer releases. For further help, please review [the forum thread](https://community.metabrainz.org/t/classical-extras-2-0/394627) or post any new questions there. It only works with Picard versions 2.0 and above, **NOT** earlier versions. If you are using Picard 1.4.x, please choose the [\"1.0\" branch on github](https://github.com/MetaTunes/picard-plugins/tree/1.0/plugins/classical_extras) and use the latest release there - also use the [earlier forum thread](https://community.metabrainz.org/t/classical-extras-plugin/300217).\n\nThis version has only been tested with FLAC and mp3 files. It does work with m4a files, but Picard does not write all m4a tags (see further notes for iTunes users at the end of the \"works and parts tab\" section). \"Classical Extras\" populates tags and hidden variables in Picard with information from the MusicBrainz database about the recording, artists and work(s), and of any containing works, passing up through multiple work-part levels until the top is reached. The \"Options\" page (Options->Options->Plugins->Classical Extras) allows the user to determine how hidden variables are written to file tags, as well as a variety of other options.\n\nThis plugin is particularly designed to assist with tagging of classical music so that player or library manager software, which can display multiple work levels, different artist types and custom tags, can have access to these details.  \nIt has two main components - \"Artists\" and \"Works and parts\" - which can be used independently or together. \"Works and parts\" will take at least as many seconds to process as there are works to look up (owing to MusicBrainz  throttling) so users who only want the extra artist information and the tag-mapping feature, but not the work details, may turn it off (e.g. perhaps for 'popular' music).  There are also two tabs - \"Genres etc.\" and \"Tag mapping\" which may be used provided either \"Artists\" or \"Works and parts\" (or both) are run. Finally, an \"Advanced\" tab contains additional options.\n\nHidden metadata variables produced by this plugin are (mostly) prefixed with \"\\_cwp\\_\" or  \"\\_cea\\_\" depending on which component of the plugin created them. Full details of these variables are given in a later section.\nTags are output depending on the choices specified by the user in the Options Page. Defaults are provided for these tags which can be added to / modified / deleted according to user requirements. \nIf the Options Page does not provide sufficient flexibility, users familiar with scripting can write Tagger Scripts to access the hidden variables directly.\n\n## Updates\nVersion 2.0.11: Fix error when colons used to infer work names.\n\nVersion 2.0.10: Add hidden variable \\_cwp_worktype_genres for types obtained from work (or any of its parents)\n\nVersion 2.0.9: Bug fix\n\nVersion 2.0.8: Add error trapping for certain MB database inconsistencies.\n\nVersion 2.0.7: Bug fixes for compatibility with Picard 2.2+. Ability to specify additional columns in Picard UI (see detailed notes at the end of the \"Advanced\" tab section). Minor enhancements.\n\nVersion 2.0.6: Fixed crash on Picard 2.2.\n\nVersion 2.0.5: Add extra error trapping for circular work references. Alpha test of release series tags if Picard provides series-rels with release lookup.\n\nVersion 2.0.4: Fix occasional regex backtracking crash. Make naming of movement tags consistent with Picard docs. \nAdded an option to attempt to get works and movement info from title if there are no work relationships (requires title in form \"work: movement\"). \nIf Muso-specific genre processing is selected (or XML reference file is provided including classical composers) and there is no composer (because of a lack of work relationship) \nthen the plugin will check the listed artist against the reference list of classical composers and, if there is a match, will populate the composer metadata and set the genre to classical.\n\nVersion 2.0.3: Fix exception when references XML file does not exist\n\nVersion 2.0.2: Changed layout of tabs - the order is now Artists, Works, Genres and Tag-mapping. The help tab has been much reduced as it was of limited assistance and difficult to maintain. There is a lot of context-sensitive help and  the readme file contains the latest full documentation.  \nAdded a check-box on the genres tab to enable/disable genre filtering (previously the genre names would have to be blank to eliminate filtering and in any case this was buggy).   \nA general code tidy-up has led to significant performance enhancements. The only significant slowing factor is now the unavoidable MusicBrainz 1 look-up per second constraint (for looking up works).   \nA number of changes have been made to the way in which \"extended\" metadata is supplied - i.e. where title metadata is combined with the \"canonical\" MusicBrainz work names. Hopefully the result is a more consistent and helpful presentation. Also some minor changes to the way in which text is eliminated to arrive at part names, including a new option on the \"advanced\" tab to \"Allow blank part names for arrangements and part recordings, if an arrangement/partial label is provided\" (see documentation under the advanced tab section for more details). Plus bug fixes.\n\nVersion 2.0.1: Minor update to add _composer_lastnames variable.\n\nVersion 2.0: Major overhaul of version 0.9.4 to achieve Picard 2.0 and Python 3.7 compatibility. All webservice calls re-written for JSON rather than XmlNode responses. Periods are written to tag in date order. Addition of sub-options for inclusion of key signature information in work names. If the MB database has circular work references (i.e a parent is a descendant of itself) then these will be trapped, ignored and reported. Numerous small refinements, especially of text comparison algorithms (e.g. option to control removal of prepositions - see advanced tab). Bug fixes.\n\nFor a list of previous version changes, see the end of this document.\n\n# Installation\nInstall the zip file in your plugins folder in the usual fashion.\n\n# Usage\nAfter installation, go to the Options Page and modify choices as required. There are 5 tabs - \"Artists\", \"Works and parts\", \"Genres etc.\", \"Tag mapping\" and \"Advanced\". The sections below describe each of these. If the options provided do not allow sufficient flexibility for a user's need (hopefully unlikely!) then Tagger Scripts may be used to process the hidden variables or other tags. Alternatively, it may be possible to achieve the required result by running and saving twice (or more!) with different options each time. This is not recommended for more than a one-off - a script would be better.\n\n**Important**: \n1.  The plugin **will not work fully unless** \"Use release relationships\" and \"Use track relationships\" are enabled in Picard->Options->Metadata. The plugin will enable these options by default when starting Picard. However, it may be that the MusicBrainz database has conflicting data between track and release relationships, in which case you may wish to temporarily turn off one of these options, but it is better to fix the incorrect data using \"Edit relationships\" in MusicBrainz.  \n2.  It is recommended only to use the plugin on one or a few release(s) at a time, particularly for initial tagging if the \"Works and parts\" function is being used. The plugin is not designed to do \"bulk tagging\" of untagged files - it may be better to use a tool such as SongKong for that and then use the plugin to enhance the results as required. However, once you have tagged files (either in Picard or another tool) such that they all have MusicBrainz IDs, you should be able to re-tag multiple releases by dragging the containing folder into Picard; this is useful to pick up changed MusicBrainz data or if you change the Classical Extras version or options (but bear in mind that the \"Works and parts\" function will still take at least 1 second per track.  \n3.  **Check for error messages before saving a release**. The plugin will write out special \"error message\" tags which should appear prominently in the bottom Picard pane. In particular, look for \"000\\_major\\_warning\" and \"001\\_errors\". please read the messages carefully and follow any recommended actions.\n\n    **Watch out for \"002\\_important\\_warning\" - \"No file with matching trackid - IF THERE SHOULD BE ONE, TRY 'REFRESH' - (unable to process any saved options, lyrics or 'keep' tags)\"; this will always occur if you load a file without MusicBrainz ids - just refresh it to pick up any existing file tags such as lyrics, if required.**  This will also occur if you have manually matched files rather than used Picard's \"lookup\" or \"scan\" functions. It may also be due to Picard processing issues - more likely if the files are on a network server; if you are getting it a lot then it may be better to move the files onto local storage to do the updates. \n4.  If you are just changing option settings then you can usually \"use cache\" (see \"work and parts\" tab section 1) when refreshing, to avoid the 1-second per work delay. However, if the works data in MusicBrainz has been changed then obviously you will need to do a full look-up, so disable cache. If the work structure has been fundamentally changed (i.e. a different hierarchy of existing works) - either within the MusicBrainz database or by selecting/deselecting the \"include collection relations\", partial\" or \"arrangements\" options - then you may need to quit and restart Picard to correctly pick up the new structure.  \n5.  Keep a backup of your picard.ini file (C:\\Users\\[user name]\\AppData\\Roaming\\MusicBrainz in Windows) in case you erase your settings or Picard crashes and loses them for you.\n\n## Artists tab\nThere are five coloured sections as shown in the screen image below:\n\n![Artist options](https://music.highmossergate.co.uk/classical-extras-screenshots/artists/)\n\n1. \"Create extra artist metadata\" should be selected otherwise this section will not run. This is the default.\n\n2. \"Work-artist/performer naming options\". \n  This section deals primarily with the application of aliases and \"credited as\" names to replace the MusicBrainz standard names. The first box allows you to choose whether to replace MusicBrainz standard  names by aliases - either for all work-artists/performers or only work-artists (writers, composers, arrangers, lyricists etc.). The second box sets the usage of \"credited as\" names: the first part of this lists all the places where \"credited as\" names can occur (really!) and the second part allows you to apply these to performing artists and/or work-artists. \n\n   Please note that, in the current version of this plugin, only aliases and \"credited as\" names which are in the \"release XML node\" are available (i.e. roughly those relating to the metadata shown in the release overview page in MusicBrainz). So, for example, if a recording is an arrangement of another work and that other work (but not the arrangement itself) has a composer linked to it, then the composer's alias will not be available (nor is the composer shown on the MB release overview page). In some cases (if appropriate) this can be remedied by adding the relevant composer relationship to the lowest-level work.\n\n    >Note regarding aliases and \"credited as\" names:  \n    In a MB release, an artist can appear in one of seven contexts. Each of these is accessible in releaseXmlNode\n    and the track and recording contexts are also accessible in trackXmlNode.  \n    (They are applied in sequence - e.g. track artist credit will over-ride release artist credit)  \n    The seven contexts are:  \n    Recording: credited-as and alias (this is applied first as it is the most general - i.e. it may apply to more than one release)  \n    Release-group: credited-as and alias  \n    Release: credited-as and alias  \n    Release relationship: credited-as only (see note)  \n    Recording relationship (direct): credited-as only (see note)  \n    Recording relationship (via work): credited-as only (see note)  \n    Track: credited-as and alias  \n    Note: Aliases **may** be retrieved for \"relationship\" artists, but the retrieval is not reliable (MusicBrainz webservice issue)\n\n    N.B. if more than one release is loaded in Picard, any available alias names loaded so far will be available and used. However, \"credited as\" names will only be used from the current release. If you do not want these names to be available then you may need to restart Picard after changing the option settings (otherwise they will still be cached).\n\n    In addition to the plugin options, the main Picard options also have an effect on how 'track artists' (or any tags derived from them through tag-mapping) are displayed. In Options->Metadata, if \"Translate artist names...\" is selected then the alias will be used for the track artist (or failing that, a name based on the sort-name), rather than the \"credited as\" name. If \"Use standardized artist names\" is selected then neither the alias nor the \"credited as\" name will be used. In order to facilitate consistency, Classical Extras will save these Picard options along with its own options in specific tags (see \"Advanced options\" section 6).\n\n     The bottom box then (a) allows a choice as to whether aliases will over-ride \"credited as\" names or vice versa and (b) whether, if there are still some names in non-Latin script, these should be replaced (this will always remove middle [patronymic] names from Cyrillic-script names [but does not deal fully with other non-Latin scripts]; it is based on the sort names wherever possible).\n\n     Note that **none of this processing affects the contents of the \"artist\" or \"album\\_artist\" tags, unless they are replaced by a \"tag mapping\" action**. These tags may be either work-artists or performing artists. Their contents are determined by the standard Picard options \"translate artist names\" and \"use standardized artist names\" in Options-->Metadata. If \"translate name\" is selected, the name will be the alias or (if no alias) the 'unsorted' sort-name; otherwise the name will be the MusicBrainz name if \"use standardized artist names\" is selected or the \"credited as\" name (if available) if it is not selected. Using the saved options to over-ride the displayed options has no effect on this as the processing takes place in Picard itself, not the plugin (but **over-write** will over-write all Picard and plugin options with the saved ones).\n\n3. \"Recording artist options\".\n  In MusicBrainz, the recording artist may be different from the track artist. For classical music, the MusicBrainz guidelines state that the track artist should be the composer; however the recording artist(s) is/are usually the principal performer(s).  \n  Because, in classical music (in MusicBrainz), recording artists will usually be performers whereas track artists are composers, by default the naming convention for performers (set in the previous section) will be used (although only the as-credited name set for the recording artist will be applied). Alternatively, the naming convention for track artists can be used - which is determined by the main Picard metadata options.\n\n    Classical Extras puts the recording artists into 'hidden variables' (as a minimum) using the chosen naming convention.\n  There is also option to allow you to replace the track artist by the recording artist (or to merge them). The chosen action will be applied to the 'artist', 'artists', 'artistsort' and 'artists\\_sort' tags. Note that 'artist' is usually a single-valued string with a \"join phrase\" such as a semi-colon for multiple artists, whereas 'artists' is a list and may be multi-valued. Lists are simply merged but, because the 'artist' string may have different join-phrases etc, a merged tag may have the recording artist(s) in brackets after the track artist(s). Obviously, for classical music, if you use \"merge\" then the artist tag will have both the composer and the recording artists: this may be desirable for simple players (with no composer recognition) but otherwise may look odd.\n\n     Note that, if the original track artist is required in tag mapping (i.e. as it was before replacement/merge with recording artist), it is available through the hidden variable \\_cea\\_MB\\_artists.\n\n     Note also that, if @loujin's browser script has been used to fill the recording artist data, this will be the same as the performing artists in the Recording-Artist relationship - i.e. it may be a lengthy list rather than the principal artist for the track.\n\n4. \"Other artist options\":\n\n    \"Modify host tags and include annotations\" (Previously called \"Include arrangers from all work levels\"). This will gather together, for example, any arranger-type information from the recording, work or parent works and place it in the \"arranger\" tag ('host' tag), with the annotation (see below) in brackets. All arranger types will also be put in a hidden variable, e.g. \\_cwp\\_orchestrators. The table below shows the artist types, host tag and hidden variable for each artist type.\n        <table>\n        <tr><td>Artist type</td><td>Host tag</td><td>Hidden variable</td></tr>\n        <tr><td>writer</td><td>composer</td><td>writers</td></tr>\n        <tr><td>lyricist</td><td>lyricist</td><td>lyricists</td></tr>\n        <tr><td>revised by</td><td>arranger</td><td>revisors</td></tr>\n        <tr><td>translator</td><td>lyricist</td><td>translators</td></tr>\n        <tr><td>arranger</td><td>arranger</td><td>arrangers</td></tr>\n        <tr><td>reconstructed by</td><td>arranger</td><td>reconstructors</td></tr>\n        <tr><td>orchestrator</td><td>arranger</td><td>orchestrators</td></tr>\n        <tr><td>instrument arranger</td><td>arranger</td><td>arrangers (with instrument type in brackets)</td></tr>\n        <tr><td>vocal arranger</td><td>arranger</td><td>arrangers (with voice type in brackets)</td></tr>\n        <tr><td>chorus master</td><td>conductor</td><td>chorusmasters</td></tr>\n        <tr><td>concertmaster</td><td>performer (with annotation as a sub-key)</td><td>leaders</td></tr>\n        </table>\n\n    If you want to be more selective as to what is included in host tags, then disable this option and use the tag mapping section to get the data from the hidden variables. If you want to add arrangers as composers, do so in the tag mapping section also. \n\n     (Note that Picard does not normally pick up all arrangers, but that the plugin will do so, provided the \"Works and parts\" section is run.)\n\n     \"Name album as 'Composer Last Name(s): Album Name'\" will add the composer(s) last name(s) before the album name, if they are listed as album artists. If there is more than one composer, they will be listed in the descending order of the length of their music on the release. MusicBrainz style is to exclude the composer name unless it is actually part of the album name, but it can be useful to add it for library organisation. The default is checked.\n     \n     \"Do not write 'lyricist' tag if no vocal performers\". Hopefully self-evident. This applies to both the Picard 'lyricist' tag and the related internal plugin hidden variables '\\_cwp\\_lyricists' etc. \n\n     Note that the plugin will search for lyricists at all work levels (bottom up), but will stop after finding the first one (unless that was just a translator).\n\n     \"Do not include attributes in an instrument type\" (previously just referred to the attribute 'solo'). MusicBrainz permits the use of \"solo\", \"guest\" and \"additional\" as instrument attributes although, for classical music, its use should be fairly rare - usually only if explicitly stated as a \"solo\" on the the sleevenotes. Classical Extras provides the option to exclude these attributes (the default), but you may wish to enable them for certain releases or non-Classical / cross-over releases.\n\n     \"Annotations\": The chosen text will be used to annotate the artist type within the host tag (see table above for host tags), but only if \"Modify host tags\" is selected.\n\n     Please note that the use of the word \"master\" is the MusicBrainz term and is not intended to be gender-specific. Users can specify whatever text they please.\n\n5. \"Lyrics\". **Please note that this section operates on the underlying input file tags, not the Picard-generated tags (MusicBrainz does not have lyrics)**\n  Sometimes \"lyrics\" tags can contain album notes (repeated for every track in an album) as well as track notes and lyrics. This section will filter out the common text for a release and place it in a different tag from the text which is unique to each track.\n\n   \"Split lyrics tag\": enables this section.\n\n   \"Incoming lyrics tag\": The name of the lyrics file tag in the input file (normally just 'lyrics').\n\n   \"Tag for album notes\": The name of the tag where common text should be placed.\n\n   \"Tag for track notes\": The name of the tag where notes/lyrics unique to a track should be placed.\n\n   Note that if the 'output' tags are not specified, then internal 'hidden' variables will still be available for use in the tag-mapping section (called album\\_notes and track\\_notes).\n\n## Work and parts tab\n\nThere six coloured sections as shown in the screen print below:\n\n![Works and parts options](https://music.highmossergate.co.uk/classical-extras-screenshots/work-parts/)\n\n1. \"Include all work levels\" should be selected otherwise this section will not run. This is the default.\n\n    \"Include collection relationships\" (selected by default) will include parent works where the relationship has the attribute 'part of collection'. See [Discussion](https://community.metabrainz.org/t/levels-in-the-structure-of-works/293047/109) for the background to this. Note that only \"work\" entity types will be included, not \"series\" entities. If this option is changed, it will not take effect on releases already loaded in Picard - you will need to quit and restart. PLEASE BE CONSISTENT and do not use different options on albums with the same works, otherwise you may not get what you want.\n\n    \"Use cache (if available)\" prevents excessive look-ups of the MB database. Every look-up of a work needs to be performed separately (hopefully the MB database might make this easier some day). Network usage constraints by MB means that each look-up takes a minimum of 1 second. Once a release has been looked-up, the works are retained in cache, significantly reducing the time required if, say, the options are changed and the data refreshed. However, if the user edits the works in the MB database then the cache will need to be turned off temporarily for the refresh to find the new/changed works. Also some types of work (e.g. arrangements) will require a full look-up if options have been changed. **Do not leave this option turned off** as it will make the plugin slower and may cause problems. This option will always be set on when Picard is started, regardless of how it was left when it was last closed.\n\n2. \"Tagging style\". This section determines how the hierarchy of works will be sourced.\n\n    * **Works source**: There are 3 options for determing the principal source of the works metadata\n      - \"Use only metadata from title text\". The plugin will attempt to extract the hierarchy of works from the track title by looking for repetitions and patterns, using the work structure in MusicBrainz as a guide. If the title does not contain all the work names in the hierarchy then obviously this will limit what can be provided.\n      - \"Use only metadata from canonical works\". The names from the hierarchy in the MB database will be used. Assuming the work is correctly entered in MB, this should provide all the data. However the text may differ from the track titles and will be the same for all recordings. It may also be in the language of the composer whereas the titles will probably be in the language of the release. (This language issue can also be addressed by using aliases - see below).\n      - \"Use canonical work metadata enhanced with title text\". This supplements the canonical data with text from the titles **where it is significantly different**. The supplementary title data will be in curly brackets. This is clearly the most complete metadata style of the three but may lead to long descriptions. It is particularly useful for providing translations - see image below for an example (using the Muso library manager). In this example, title text that is similar to that in the canonical text has been eliminated to make the text shorter - the mannr of doing this is controlled by settings on the Advanced tab.\n\n      ![Respighi](https://music.highmossergate.co.uk/classical-extras-screenshots/respighi/)\n\n    * **Source of canonical work text**. Where either of the second two options above are chosen, there is a further choice to be made:\n      - \"Full MusicBrainz work hierarchy\". The names of each level of work are used to populate the relevant tags. E.g. if \"\"Concert Fantasy for Piano and Orchestra, op. 56: I. Quasi Rondo\" (level 0) is part of \"Concert Fantasia, op. 56\" (level 1) then that is how they will appear, since there is no repetition of text between parent and child. So, while accurate, this option might sometimes be rather verbose.\n      - \"Consistent with lowest level work description\". The names of the level 0 work are used to populate the relevant tags. So, in the above example, \"Concert Fantasy for Piano and Orchestra, op. 56\" will be shown as the work and \"I. Quasi Rondo\" will be shown as the movement. Sometimes this may look better, but not always, **particularly if the level 0 work name does not contain all the parent work detail**. If the full structure is not implicit in the level 0 name then a warning will be logged and written to the \"warning\" tag.\n      \n      **Version 2.0 update**: the second option is needed less often now as there is a more sophisticated matching algorithm for the canonical work names. Text may be eliminated at places other than the start, and synonyms may be used to achieve greater matching. These options are set on the Advanced tab. Setting \"Removal of common text between parent and child works\" to 2 (the default) and including \"Fantasia\" as a synonym of \"Fantasy\" yields the following result:  \n      Work: \"Concert Fantasia, op. 56\", Movement: \"for Piano and Orchestra, … : I. Quasi Rondo\"  \n      This still repeats \"for Piano and Orchestra\" for each movement as this text is in level 0, not level 1 (where it only appears as disambiguation). Arguably the best way to fix this is to have consistent work names in MB. (Of course, this specific example may have been fixed in MB by now, but the principle still holds). The strategy below has been updated to reflect this\n\n   **Strategy for setting style:** *It is suggested that you start with \"extended/enhanced\" style and the \"Full MusicBrainz work hierarchy\" as the source (this is the default) and tweak the advanced settings if necessary. If this does not give acceptable results, try switching to \"Consistent with lowest level work description\". If the \"enhanced\" details in curly brackets (from the track title) give odd results then, again, try tweaking the advanced settings (see later section) or switch the style to \"canonical works\" only. Any remaining oddities are probably in the MusicBrainz data, which may require editing.*  \n   \n   * **\"Attempt to get works and movement info from title if there are no work relationships? (Requires title in form \"work: movement\")\"**. \n   Pretty much what it says. It may be that the track is classical, but no work relationships exist in MusicBrainz. In this case, Classical Extras will attempt to infer work and movement from the title, provided they are separated by \": \" (which is the Classical Style Guideline). \n   In this case, the other tag style settings are irrelevant. Note that if there is no related work, then there will not be a composer metadata item in MusicBrainz. However, you can use tag mapping to set this or (better) use Muso (or and XML reference file) to determine classical composers (see Genres section).\n\n3. \"Aliases\"\n\n     \"Replace work names by aliases\" will use **primary** aliases for the chosen locale instead of standard MusicBrainz work names. To choose the locale, use the drop-down under \"translate artist names\" in the main Picard Options-->Metadata page. Note that this option is not saved as a file tag since, if different choices are made for different releases, different work names may be stored and therefore cannot be grouped together in your player/library manager. The sub-options then allow either the replacement of all work names, where a primary alias exists, just the replacement of work names which are in non-Latin script, or only replace those which are flagged with user \"Folksonomy\" tags. The tag text needs to be included in the text box, in which case flagged works will be 'aliased' as well as non-Latin script works, if the second sub-option is chosen. Note that the tags may either be anyone's tags (\"Look in all tags\") or the user's own tags. If selecting \"Look in user's own tags only\" you **must** be logged in to your MusicBrainz user account (in the Picard Options->General page), otherwise repeated dialogue boxes may be generated and you may need to force restart Picard.\n\n4. \"Tags to create\" sets the names of the tags that will be created from the sources described above. All these tags will be blanked before filling as specified. Tags specified against more than one source will have later sources appended in the sequence specified, separated by separators as specified.\n\n    * **Work tags**:\n      - \"Tags for Work - for software with 2-level capability\". Some software (notably Muso) can display a 2-level work hierarchy as well as the work-movement hierarchy. This tag can be use to store the 2-level work name (a double colon :: is used to separate the levels within the tag).\n      - \"Tags for Work - for software with 1-level capability\". Software which can display a movement and work (but no higher levels) could use any tags specified here. Note that if there are multiple work levels, the intermediate levels will not be tagged. Users wanting all the information should use the tags from the previous option (but it may cause some breaks in the display if levels change) - alternatively the missing work levels can be included in a movement tag (see below).\n      - \"Tags for top-level (canonical) work\". This is the top-level work held in MB. This can be useful for cataloguing and searching (if the library software is capable).\n\n    * **Movement/Part tags**:\n      (a) \"Tags for (computed) movement number\". This is not necessarily the embedded movt/part number, but is the sequence number of the movement within its parent work **on the current release**. \n      (For these purposes, the \"parent work\" is the highest level work of which the track/movement is a a part but which is not a collection)   \n      (b) \"Tags for (computed) total number of movements\". This will be the total number of movements in the parent work as numbered above.  \n      (c) \"Tags for Movement - excluding embedded movt/part numbers\". As below, but without the movement part/number prefix (if applicable)  \n      (d) \"Tags for Movement - including embedded movt/part numbers\". This tag(s) will contain the full lowest-level part name extracted from the lowest-level work name, according to the chosen tagging style.  \n      For options (c) and (d), the tags can either be filled \"for use with multi-level work tags\" or \"for use with 1-level work tags (intermediate works will prefix movement)\" - or different tags for each column.  The latter option will include any intermediate work levels which are missing from a single-level work tag. Use different tag names for these, from the multi-level version, otherwise both versions will be appended, creating a multi-valued tag (a warning will be given).  \n      The default tags for (a), (b), and (c) are movementnumber, movementtotal and movement respectively - these are the standard Picard tags for these items.  \n      Note that if a tag is included in (a) and either of (c) or (d), the movement number will be prepended at the beginning of the tag, followed by the selected separator. For more complex combinations, use the Tag Mapping tab (e.g. movementnumber + \\ of + movementtotal).   \n      If you wish to use items (a) and (b) in the tag-mapping section without populating the Picard standard tags, then use the hidden variables movt_num and movt_tot.  \n      For more details, see the hidden variables section.\n\n   **Strategy for setting tags:** *It is suggested that initially you just use the multi-level work tag and related movement tags, even if your software only has a single-level work capability. This may result in work names being repeated in work headings, but may look better than the alternative of having work names repeated in movement names. This is the default.* \n\n   *If this does not look good you can then compare it with the alternative approach and change as required for specific releases. If your software does not have any \"work\" capability, then you can still get the full work details by, for example, specifying \"title\" as both a work and a movement tag.*\n\n5. \"Partial recordings, arrangements and medleys\" gives various options where recordings are not just simply of a named complete work. These only apply if one of the two \"canonical work\" styles is in operation (i.e. not if \"Use only metadata from title text\" is selected).\n\n    * **Partial recordings**:\n      If this option is selected, partial recordings will be treated as a sub-part of the whole recording and will have the related text (in the adjacent box) included in its name. Note that this text is placed at the start of the canonical name, but the latter will probably be stripped from the sub-part as it duplicates the recording work name; any title text (for \"extended\" style) will be appended to the whole. Note that, if \"Consistent with lowest level work description\" is chosen in section 2, the text may be treated as a \"prefix\" similar to those in the \"Advanced\" tab. If this eliminates other similar prefixes and has unwanted effects, then either change the desired text slightly (e.g. surround with brackets) or use the \"Full MusicBrainz work hierarchy\" option in section 2.  Note that similar text between the partial work and its 'parent' will be removed which will frequently result in no text other than the specified 'partial text', unless extended metadata is used resulting in appended text in {} - this behaviour can be controlled by disabling the setting \"Allow blank part names for arrangements and part recordings...\" on the advanced tab.\n\n    * **Arrangements**:\n      If this option is selected, works which are arrangements of other works will have the latter treated in the same manner as \"parent\" works, except that the arrangement work name will be prefixed by the text provided. Note that similar text between the arranged work and its parent will be removed unless this results in no text, in which case a stricter comparison (as for the derivation of 'part' names from works) will be used.\n      \n    **Important note:** *If the Partial or Arrangement options are changed (i.e. selected/deselected) then quit and restart Picard as the work structure is fundamentally different. If the related text (only) is changed then the release can simply be refreshed.*\n\n    * **Medleys**\n      These can occur in two ways in MusicBrainz: (a) the recording is described as a \"medley of\" a number of works and (b) the track is described as (more than one) \"medley including a recording of\" a work. See [Homecoming](https://musicbrainz.org/release/393913a2-7fde-4ed5-8be6-ca5c2c0ccf0d) for examples of both (tracks 8, 9 and 11). In the first case, the specified text will be included in brackets after the work name, whereas in the second case, the track will be treated as a recording of multiple works and the specified text will appear in the **parent** work name.\n\n6. \"SongKong-compatible tag usage\".\n\n    \"Use work tags on file (no look up on MB) if Use Cache selected\": This will enable the existing work tags on the file to be used in preference to looking up on MusicBrainz, if those tags are SongKong-compatible (which should be the case if SongKong has been used or if the SongKong tags have been previously written by this plugin). If present, this can speed up processing considerably, but obviously any new data on MusicBrainz will be missed. For the option to operate, \"Use cache\" also needs to be selected. Although faster, many of the subtleties of a full look-up will be missed - for example, parent works which are arrangements will not be highlighted as such, some arrangers or composers of original works may be omitted and some medley information may be missed. Other information, such as composed-dates will also be missing. **In general, therefore, the use of this option will result in poorer metadata than allowing the full database look-up to run. It is not recommended unless you have already tagged your files with SongKong and speed is more important than quality.**\n\n    \"Write SongKong-compatible work tags\" does what it says. These can then be used by the previous option, if the release is subsequently reloaded into Picard, to speed things up (assuming the reload was not to pick up new work data). The same caveats as those above apply.\n    \n    **Note that, as from version 2.0.2, there is no significant speed difference between (basic) SongKong and Picard with Classical Extras, so this feature is only ever useful if the album has already been tagged with SongKong.**\n\n    The default for both these options is unchecked.\n\n    Note that Picard and SongKong use the tag musicbrainz\\_workid to mean different things. If Picard has overwritten the SongKong tag (not a problem if this plugin is used) then a warning will be given and the works will be looked up on MusicBrainz. Also note that once a release is loaded, subsequent refreshes will use the cache (if option is ticked) in preference to the file tags.\n\n**Note for iTunes users:** *iTunes and Picard do not work well together. iTunes can display work and movement for m4a(mp4) files, but Picard does not write the movement tag. To work round this, write the movement to the \"subtitle\" tag assuming that is not otherwise used, and use a simple Mp3tag action to convert it to MOVEMENTNAME before importing to iTunes. If you are writing to a FLAC file which will subsequently be converted to m4a then different tag names may be required; e.g. using dBpoweramp, write the movement to \"movement name\". In both cases use \"work\" for the work. To store the top\\_work, use \"grouping\" if writing directly to m4a, but \"style\" if writing to FLAC followed by dBpoweramp conversion. You can put multiple tags into the boxes described above so that your options are multi-purpose. N.B. if work tags are specified and the work has at least one level (i.e. at least work: movement), then the tag \"show work movement\" will be set to 1. This is used by iTunes to trigger the hierarchical display and should work both directly with m4a files and indirectly via files which are subsequently converted.*\n\n## Genres etc. tab\n\nThis section is dependent on both the artists and workparts sections. If either of those sections are not run then this section will not operate correctly. At the very top of the tab is a checkbox \"Use Muso reference database...\". For [Muso](http://klarita.net/muso.html) users, selecting this enables you to use reference data for genres, composers and periods which have been entered in Muso's \"Options->Classical Music\" section. Regardless as to whether this is selected, there are then three main coloured sections, each with a number of subsections. The details in each section differ depending on whether the \"Muso\" option is selected.  The screen print below shows the options assuming it is not selected (differences occurring when \"Muso\" is selected are discussed later):\n\n![Genres etc.](https://music.highmossergate.co.uk/classical-extras-screenshots/genres-plain/)\n\n1. \"Genres\". Two separate tags may be used to store genre information, a main genre tage (usually just \"genre\") and a sub-genre tag. These need to be specified at the top of the section. If either is left blank then the related processing will not run.\n\n    * **Source of genres**\n    Any or all of four sources may be selected. In each case, any values found are treated as \"candidate genres\" - they will only be applied to the specified genre and sub-genre tags in accordance with the criteria in the \"allowed genres\" section, if any (see below).\n      \n      (a) \"Existing file tag\". The contents of the existing file tag (as specified above - main genre tag only) will be included as candidate genres. Note that, if this tag name is not \"genre\", then the contents of the tag \"genre\" will be included as well.\n      \n      (b) \"Folksonomy work tags\". This will use the folksonomy tags for **works** (including parent works) as a possible source of genres. To use the folksonomy tags for **releases/tracks**, select the main Picard option in Options->Metadata->\"Use folksonomy tags as genre\". Again (unlike vanilla Picard) these are candidate genres, and will only be published if they match the allowed genres.\n      \n      (c) \"Work-type\". The work-type attribute of works or parent works will be used as a candidate genre.\n      \n      (d) \"Infer from artist metadata\". This option was on the artist tab in version 0.9.1 and prior. Owing to the additional genre processing now available, the operation of this option is slightly restricted compared to the earlier versions. It attempts to create candidate genres based on information in the artist-related tags. Values provided are:\n\tOrchestral, Concerto, Choral, Opera, Duet,Trio, Quartet, Chamber music, Aria ('classical values') and Vocal, Song, Instrumental ('generic values'). If the track is a recorded work and the track artist is the composer (i.e. MusicBrainz 'classical style'), the candidate genre values will also include \"Classical\". The 'classical values' will only be included as candidate genres if the track is deemed to be 'classical' by some part of the genre processing section.\n\t\n\t* **Allowed genres**\n\tA check-box (ticked by default) enables this section. If it is unchecked, then no genre filtering is applied - all 'candidate genres' will be written to the genre tab.  \n\tFour boxes are provided for lists of genres which are \"allowed\" to appear in the specified tags. Each list should be comma-separated (and no commas in any genre name). Candidate genres matching those in a \"main genre\" box will be added to the specified main genre tag. Similarly for sub-genres. If a candidate genre matches a 'classical genre' (in one of the top two boxes), then the track will be deemed to be \"Classical\" (see next part for more details).  \nYou may also enter a genre name to be used if no matching main genre is found (otherwise the tag will be blank). \n\n    * **\"Classical\" genre**\n     Normally (i.e. by default) a work will only be deemed to be 'classical' if it is inferred from the MusicBrainz style (see \"source of genres\") or if a candidate genre matches a \"Classical\" genre or sub-genre list. However, you may select that all tracks are 'classical' regardless. There is also an option to exclude the word \"Classical\" from any genre tag, but still treat the work as classical. If a work is deemed to be classical, a tag may be written with a specified value as set out in the last two boxes of this section. For example, to be consistent with SonKong/Jaikoz, you could set \"is\\_classical\" to \"1\".  \n  \n2. \"Instruments and keys\".\n    * **Instruments**\n    Specify the tag name you wish instrument names to appear in. Instruments will be sourced from performer relationships. Instrument names may either be the standard MusicBrainz names or the \"credited as\" names in the performer relationship, or both. Vocal types are treated similarly to instruments. (Note that, in v0.9.1 and prior, instruments were written to the same tag as inferred genres. If you wish to continue this, then you may use the same tag name here as for the genre tag.)\n    \n    * **Keys**\n    Specify the tag name in which you wish the key signatures of works to appear. Keys will be obtained from all work levels (assuming these have been looked up): for example, Dvořák's Largo From the New World will be shown as D♭ major, C# minor (the main keys of the movement) and E minor (the home key of the overall work).  \n\"Include key(s) in work names\" gives the option to include the key signature for a work in brackets after the name of the work in the metadata. Keys will be added in the appropriate levels: e.g. Dvořák's New World Symphony will get (E minor) at the work level, but only movements with different keys will be annotated viz. \"II. Largo (D-flat major, C-Sharp minor)\". The default sub-option is to only add these details if the key signature is missing from the work title (other sub-options are to never or to always include the information.  \n    \n3. \"Periods and dates\".\n\n    * **Work dates**\n    Specify the tag name to hold work dates. Work dates will be given as a \"year\" value only, e.g. \"1808\" or a range: \"1808-1810\". The sources of these dates is specified in the next part. If the movement has a composed date(s), this will be used, otherwise the the dates from the parent work will be used (if available).\n    \n        \"Source of work dates\". Select which sources to use - from composed, published and premiered, then decide whether to use them in preferential order (e.g. if \"composed date\" exists, then the others will not be used) or to show them all.\n    \n        \"Include workdate in work name ...\" operates analogously to  \"Include key(s) in work names\" described above. (Work dates will be used in preference order, i.e. composed - published - premiered, with only the first available date being shown).\n    \n    * **Periods**\n    This section will use work dates, where available, to determine the \"classical period\" to which it belongs, by means of a \"period map\" (Muso users can also use composer dates - see below). \n    \n        Specify the tag name to hold the period data. The period map should then be entered in the format \"Period name, Start\\_year, End\\_year; Period name2, Start\\_year, End\\_year;\"  etc. Periods may overlap. Do not use commas or semi-colons within period names. Start and end years must be integers.\n    \n## Genres etc. tab - Muso-specific processing\n\nUsers of [Muso](http://klarita.net/muso.html) have additional capabilities, illustrated in the following screen, which appear when the option \"Use Muso reference database ...\" is selected at the top of the tab.\n\n![Genres etc. - Muso](https://music.highmossergate.co.uk/classical-extras-screenshots/genres-muso/)\n\nFor these options to work, the path/name of the Muso reference database needs to be specified on the advanced tab. The default path is \"C:\\\\Users\\\\Public\\\\Music\\\\muso\\\\database\" and the default filename is \"Reference.xml\". The additional options are as follows.\n\n1. \"Use Muso classical genres\". If this is selected, the box for classical main genres is eliminated and the genre list from Muso's \"Tools->Options->Classical Music->Classical Music Genres\" is used instead.\n\n2. \"Use Muso composer list to determine if classical\". If the composer name is in Muso's list \"Tools->Options->Classical Music->Composer Roster\", then the work will be deemed to be classical. If this option is selected, a further option appears to \"Treat arrangers as for composers\" - if selected then arrangers will also be looked up in the roster.\n\n3. \"Use Muso composer dates (if no work date) to determine period\". The birth date + 20  -> death dates of Muso's composer roster will be used to assign periods if no work date is available. If this option is selected, a further option appears to \"Treat arrangers as for composers\" - if selected then arrangers' working lives will also be used to determine periods.\n\n   (This might be replaced / supplemented by MusicBrainz in  the future, but would involve another 1-second lookup per composer).\n   \n4. \"Use Muso map\". Replace the period map with the one in Muso at \"Tools->Options->Classical Music->Classical Music Periods\"\n\nNote that non-Muso users may also use this functionality, if they wish, by manually creating a reference xml file with the relevant tags, e.g.:\n\n    <ClassicalGenre>  \n    <Name>Cantata</Name>  \n    </ClassicalGenre>   \n    <Composer>  \n    <Name>Max REGER</Name>  \n    <Birth>1873</Birth>  \n    <Death>1916</Death>  \n    </Composer>  \n    <ClassicalPeriod>  \n    <Name>Early Romantic</Name>  \n    <Start_x0020_Date>1800</Start_x0020_Date>  \n    <End_x0020_Date>1850</End_x0020_Date>  \n    </ClassicalPeriod>  \n\nIf the Muso reference (or XML) database is selected (which includes a classical composers list) and there is no composer for the track (because of a lack of work relationship) \nthen the plugin will check the listed artist against the reference list of classical composers and, if there is a match, will populate the composer metadata and\n (if \"use Muso composer list to determine if classical\" is selected) will set the genre to classical.\n\n## Tag mapping tab\nThere are two coloured sections as shown in the screen image below:\n\n![Tag mapping options](https://music.highmossergate.co.uk/classical-extras-screenshots/tag-mapping/)\n\nNote that either the \"Create extra artist metadata\" option on the Artist tab or \"Include all work levels\" on the Works tab needs to be selected for these sections to run.\n\n1. \"Initial tag processing\": This takes place before any of the detailed tag mapping in the second section.\n\n    \"Remove Picard-generated tags before applying subsequent actions?\". Any tags specified in the next two rows will be blanked before applying the tag sources described in the following section. NB this applies only to Picard-generated tags, not to other tags which might pre-exist on the file: to blank those, use the main Options->Tags page. Comma-separate the tag names within the rows and note that these names are case-sensitive.\n\n    \"List existing file tags which will be appended ...\": This refers to the tags which already exist on files which have been matched to MusicBrainz, not the tags generated by Picard from the MusicBrainz database. Normally, Picard cannot process these tags - either it will overwrite them (if it creates a similarly named tag), clear them (if 'Clear existing tags' is specified in the main Options->Tags screen) or keep them (if 'Preserve these tags...' is specified after the 'Clear existing tags' option). Classical Extras allows a further option - for the tags to be appended to in the tag mapping section (see below) or otherwise used. List file tags which will be appended to rather than over-written by tag mapping (NB this will keep tags even if \"Clear existing tags\" is selected on main options). In addition, certain existing tags may be used by Classical Extras - in particular \"is\\_classical\" (which is set by SongKong to '1' if the track is deemed to be classical, based on an extensive database) is used to add 'classical' to the variable \"\\_cea\\_worktype\", if \"Infer work types\" is selected in the first section of the Artists tab. If you include \"is\\_classical\" in this list then any files which have \"is\\_classical\" = 1 will be treated as being classical, regardless of the genre tag.\n    \n    **Make sure that any tags listed here are not also included in Picard's \"Preserve .. tags..\" option, otherwise Picard will prevent the tag from being amended**\n\n    Note that if \"Split lyrics tag\" is specified (see the Artists tab), then the tag named there will be included in the \"...existing file tags...\" list and does not need to be added in this section.\n\n    \"Clear any previous file tags...\": This operates in an almost similar way to the main Picard option (Options->Tags->\"Clear existing tags\"). All existing file tags will be cleared **unless** they are in the main Picard \"Preserve tags...\" option or the \"...existing file tags...\" list. The main differences from the basic Picard option are that (a) artwork is always preserved - i.e. this largely addresses [PICARD-257](https://tickets.metabrainz.org/browse/PICARD-257) and (b) the tags that are not kept are **not actually deleted or shown as deleted** in the bottom pane of Picard. They are just not displayed in the bottom pane -  however, a warning tag is written.\n\n2. \"Tag map details\". This section permits the contents of any hidden variable or tag to be written to one or more tags.\n\n    * **Sources**:\n\tSome of the most useful sources are available from the drop-down list. Otherwise they can simply be typed in the box. Click on the \"source from\" button to enable entry (otherwise the text box / drop-down for the source is locked). Some useful names are:\n\n      - soloists : List of performers (with instruments in brackets), who are NOT ensembles or conductors, separated by semi-colons. Note they may not strictly be \"soloists\" in that they may be part of an ensemble.\n      - soloist\\_names : Names of the above (i.e. no instruments).\n      - vocalists / instrumentalists / other\\_soloists : Soloists who are vocalists, instrumentalists or not specified, respectively.\n      - vocalist\\_names / instrumentalist\\_names : Names of vocalists / instrumentalists (i.e. no instrument / voice).\n      - ensembles : List of performers who are ensembles (with type / instruments - e.g. \"orchestra\" - in brackets), separated by semi-colons.\n      - ensemble\\_names : Names of the above (i.e. no instruments).\n      - album\\_soloists : Sub-list of soloist\\_names who are also album artists.\n      - album\\_conductors : List of conductors who are also album artists.\n      - album\\_ensembles: Sub-list of ensemble\\_names who are also album artists.\n      - album\\_composers : List of composers who are also album artists.\n      - album\\_composer\\_lastnames : Last names of composers, of ANY track on the album, who are also album artists. This is the source used to prefix the album name (when that option is selected).\n      - support\\_performers : Sub-list of soloist\\_names who are NOT album artists.\n      - composers : List of composers, after applying the naming options in the artists tab.\n      - conductors : List of conductors, after applying the naming options in the artists tab.\n      - arrangers : Includes all arrangers and instrument arrangers (except more specific roles such as orchestrators) - the standard Picard tag omits some.\n      - orchestrators : Arrangers who are orchestrators.\n      - leaders : AKA concertmasters.\n      - chorusmasters : as distinct from conductors (chorus masters may rehearse the choir but not conduct the performance).\n\n\t   Note that the Classical Extras sources for all artist types are spelled in the plural (to differentiate from the native Picard tags). Most of the names are for artist data and are sourced from hidden variables (prefixed with \"\\_cea\\_\" or \"\\_cwp\\_\"). In specifying the source, the prefix is not necessary - e.g. \"arrangers\" will pick up all data in \\_cea\\_arrangers and \\_cwp\\_arrangers (covering those with recording and work relationships respectively). Using the prefix will get just the specific variable.\n\n\t   In addition, the drop-down contains some typical combinations of multiple sources (see note on multiple sources below).\n\n       Any Picard tag names can also be typed in as sources. Any hidden variables may also be used. Any source names which are prefixed by a backslash will be treated as string constants; blanks may also be used.\n\n       It is possible to specify multiple sources. If these are separated by a comma, then each will be appended to the mapped tag(s) (if not already filled or if not \"conditional\"). So, for example, a source of \"album\\_soloists, album\\_conductors, album\\_ensembles\" mapped to a tag of \"artist\" with \"conditional\" ticked will fill artist (if blanked) by album\\_soloists, if any, otherwise album\\_conductors etc. Sources separated by a + will be concatenated before being used to fill the mapped tags. The concatenated result **will only be applied if the contents of each of the sources to be concatenated is non-blank** (note that this constraint only applies to **concatenation** of multiple sources). No spaces will be added on concatenation, so these have to be added explicitly by concatenating \"\\ \".  So, for example \"ensemble\\_names + \\ (conducted by + conductors +\\\\), ensemble\\_names\", with \"Conditional\" selected, will yield something like \"BBC Symphony Orchestra (conducted by Walter Weller)\" or just \"BBC Symphony Orchestra\" if there is no conductor. **Do not use any commas in text strings**.\n\n       Another example: to add the leader's name in brackets to the tag with the performing orchestra, put \"\\\\ (leader +leaders+\\\\)\" in the source box and the tag containing the orchestra in the tag box. If there is no leader, the text will not be appended.\n\n     The tag mapping section is not restricted to artist metadata - any metadata or hidden variablescan be used.\n\n    * **Tags**:\n\t   Enter the (comma-separated) \"destination\" tag names into which the sources should be written (case sensitive). Note that this will result in the source data being APPENDED in the tag - it will not overwrite the existing contents. Check \"Conditional?\" if the tag is only to be updated if it is previously blank (all non-empty sources in the current line will be applied in sequence). The lines will be applied in the order shown. Users should be able to achieve most requirements via a combination of blanking tags, using the right source order and \"conditional\" flags. For example, to overwrite a tag sourced from \"composer\" with \"conductor\", specify \"conductor\" first, then \"composer\" as conditional. Note that, for example, to demote the MB-supplied artist to only appear if no other listed choices are present, blank the artist tag and then add it as a conditional source at the end of the list.\n\n    * **\"Also populate sort tags\"**:\n     If a sort tag is associated with the source tag then the sort names will be placed in a sort tag corresponding to the destination tag. Note that the only explicit sort tags written by Picard are for artist, albumartist and composer. Picard also writes hidden variables '\\_artists\\_sort' and 'albumartists\\_sort' (note the plurals - these are the sort tags for multi-valued alternatives 'artists' and '\\_albumartists'). To be consistent with this approach, the plugin writes hidden variables for other tags - e.g. '\\_arranger\\_sort'. The plugin also writes hidden sort variables for the various hidden artist variables - e.g. '\\_cwp\\_librettists' has a matching sort variable '\\_cwp\\_librettists\\_sort'. Therefore most artist-type sources **will** have a sort tag/variable associated with them and these will be placed in a destination sort tag if this option is selected - **in other words, selecting this option will cause most destination tags to have associated sort tags. Furthermore, any hidden sort variables associated with tags which are not listed explicitly in the tag mapping section will also be written out as tags** (i.e. even if the related tags are not included as destination tags). Note, however, that composite sources (e.g. \" ensemble\\_names + \\;  + conductors\") do not have sort tags associated with them.\n\n      If this option is not selected, no additional sort tags will be written, but the hidden variables will still be available, so if a sort tag is required explicitly, just map the sort tag directly - e.g. map 'conductors\\_sort' to 'conductor\\_sort'.\n\n  More complex operations can be built using tagger scripts. If required, these can be set to run conditionally by setting a tag or hidden variable in this section and then testing it in the script.\n\n\n  \n## Advanced tab\n\nHopefully, this tab should not be much used. In any case, it should not need to be changed frequently. There are six main sections as shown in the screeen print below:\n\n![Advanced options](https://music.highmossergate.co.uk/classical-extras-screenshots/advanced/)\n\n1. \"General\". There is only one checkbox - \"Do not run Classical Extras for tracks where no pre-existing file is detected (warning tag will be written)\". This option will disable Classical Extras processing if no file is present; this means (for example) that single discs from box sets can be loaded without incurring the additional processing overhead (work look-ups etc.) for all the other discs. Also if a compilation album is loaded, where the tracks are on multiple releases, the plugin will only process the release tracks which match. If a file is present but it does not yet have a MusicBrainz trackid tag, then it will initially be treated in the same way as a non-existent file; however, after the initial loading it will (if matched by Picard) be given a MB trackid and \"refreshing\" the release will result in any such tracks being processed by Classical Extras, while the unmatched tracks are left untouched.\n\n2. \"Artists\". This has only one subsection - \"Ensemble strings\" - which permits the listing of strings by which ensembles of different types may be identified. This is used by the plugin to place performer details in the relevant hidden variables and thus make them available for use in the \"Tag mapping\" tab as sources for any required tags. \nIf it is important that only whole words are to be matched, be sure to include a space after the string.\n\n3. \"Works and parts\". This section has parameters applicable to the \"works and parts\" functions.\n\n\t* **Max number of re-tries to access works (in case of server errors)**. Sometimes MB lookups fail. Unfortunately Picard (currently) has no automatic \"retry\" function. The plugin will attempt to retry for the specified number of attempts. If it still fails, the hidden variable \\_cwp\\_error will be set with a message; if error logging is checked in section 5, an error message will be written to the log and the contents of \\_cwp\\_error will be written out to a special tag called \"001\\_errors\" which should appear prominently in the bottom pane of Picard. The problem may be resolved by refreshing, otherwise there may be a problem with the MB database availability. It is unlikely to be a software problem with the plugin.\n\t\n\t* **Allow blank part names for arrangements and part recordings, if an arrangement/partial label is provided**. The default is checked (true) - in which case where an arrangement has the same name as the original work, the part will just show the arrangement/partial label (e.g. \"Arrangement:\"). If the option is blank (false), there will be text for the part name (which may be the same name as the parent work). Note that if \"extended\" metadata is being used, then blank part names will always have the title metadata added {in curly brackets} regardless of whether it is similar to the canonical part name.\n\n\t* **Removal of common text between parent and child works**. This section controls the naming of parts, by stripping parent text from the work name. If the work begins with the parent name followed by punctuation then the common text will always be stripped to give the part name, even if there are punctuation or some other minor differences (synonyms will also be matched using the patterns in the next section). \n\t\n\t  However, common text which is not followed by punctuation or which is not at the start may also be stripped: to prevent this, set \"Minimum number of similar words required before eliminating (other than at start)\" to zero. Otherwise common text longer than the specified number of words (default = 2) will be stripped. (Note that this minimum is over-ridden by the previous option - i.e. if a smaller number of words than the minimum could be eliminated and would result in a blank part name, and that is allowed by the previous option, then the words will be removed).\n\t\n\t* **How title metadata should be included in extended metadata**. This subsection contains various parameters affecting the processing of strings in titles, where these are used to \"extend\" the text in work names. This is generally only relevant if \"Use canonical work metadata enhanced with title text\" is selected on the \"Works and parts\" tab, although the \"prefixes\" section is also relevant if \"Use only metadata from title text\" is selected and the synonyms are also used in eliminating parent-child duplication as described above. Because titles are free-form, not all circumstances can be anticipated. If pure canonical works are used (\"Use only metadata from canonical works\" and, if necessary, \"Full MusicBrainz work hierarchy\" on the Works and parts tab, section 2) then this processing should largely be irrelevant, but no text from titles will be included. Some explanations are given below:\n\n      * \"Proximity of new words\". When using extended metadata - i.e. \"metadata enhanced with title text\", the plugin will attempt to remove similar words in the canonical work name (in MusicBrainz) and the title before extending the canonical name. After removing such words, a rather \"bitty\" result may occur. To avoid this, any new words with the specified proximity will have the words between them (or up to the start/end) included even if they repeat words in the work name.\n      \n      * \"Treat hyphenated words as two words for comparison purposes\" (default = True). In comparing words, hyphenated words will be considered as separate words unless this option is deselected.\n      \n      * \"Proportion of a string to be matched ... for it to be considered essentially similar...\" (default = 66%). If the title and work descriptions are largely the same then the title text will not be used to extend the work text, even if there are some new words.\n      \n      * \"Prepositions and conjunctions\". Words listed here will not generally be treated as \"new\" (i.e. if they are in the title text but not in the work text, they will not be included in the \"extended\" text) unless they precede a new word which is not itself a preposition. Note that, although the term \"preposition\" is used here, because that is the obvious usage, any word can be listed. A group of more than one such words will be treated as new if they are not in the work text and they precede a new word which is not listed as a preposition.\n\n      * \"Prefixes\". When using \"metadata from titles\" or extended metadata, the structure of the works in MusicBrainz is used to infer the structure in the title text, so strings that are repeated between tracks which are part of the same MusicBrainz work will be treated as \"higher level\". This can lead to anomalies if, for instance, the titles are \"Work name: Part 1\", \"Work name: Part 2\", \"Part\" is repeated and so will be treated as part of the parent work name. Specifying such words in \"Prefixes\" will prevent this.\n\n      * \"Synonyms\". These words/phrases will be considered equivalent when comparing work name and title text  (or parent and child text). Thus if one word/phrase appears in the work name, it and its synonyms will be removed from the title in extending the metadata (subject to the proximity setting above). Each entry should be a tuple in the form *(key word/phrase, ...,  equivalent word/phrase)* - no quote marks are necessary. Each tuple should be separated by a forward slash - /. Tuples may contain multiple synonyms - each item in a tuple will be treated as similar when comparing works/parts and titles. The text in tags will be unaltered. Spaces and punctuation are permitted, as are regular expressions, but unless entering regex, use backslash \\ to escape any regex metacharacters \\ ^ $ . | ? * + ( ) [ ] {   \n      Also escape commas , and forward slashes /. Do not enclose strings in quote marks. The last member of the tuple should be the canonical form (i.e. the one to which others are converted for comparison) and must be a normal string (not a regex). The sequence of synonyms within a tuple may be important, particularly if one synonym is a subset of another - always list the longer one first, as the matching will proceed in the listed order. Thus if \"nro\" and \"nr\" are synonyms of \"no\", then list them thus: (nro, nr, no). This will ensure that \"nro\" gets matched to \"nro\" and not to \"nr\".\n\n      * \"Replacements\". These are entered in a similar fashion to synonyms. The difference is that the last item in a tuple will be used to replace any text in earlier items in the tuple. This last element may also be left blank (after the preceding comma) in order to remove any text matching any of the earlier items.\n\n4. \"Genres etc. ...\". This is only required if Muso-specific options are used for genres/periods. Specify the path and file name for the reference database (the default is the Muso default for a shared  database). Note that the database is only loaded when Picard starts so you will need to restart Picard is these options are changed.\n\n5. \"Logging options\". These options are in addition to the options chosen in Picard's \"Help->View error/debug log\" settings. They only affect messages written by this plugin. To enable debug messages to be shown in the Picard log, the flag needs to be set here and \"Debug mode\" needs to be turned on in the log. **It is strongly advised to keep the \"debug\" flag unchecked unless debugging is required** as it slows up processing and may even cause Picard to hang if there is a large number of files (better to use the 'full' custom logging option - see below). The \"error\" and \"warning\" flags should be left checked, unless it is required to suppress messages (the messages are also written to the tags 001\\_errors and 002\\_warnings).\n\n   As well as the main Picard log, a custom logging function is provided. This may be either \"basic\" or \"full\". If \"basic\" is selected, a file \"session.log\" will be written (over-written each session) to a \"Classical Extras\" directory inside the same directory as the plugins folder. (Note - the easy way to find this directory is to select options-->plugins in Picard and the \"Open plugins folder\" button, then go up one level). The session log gives a processing summary for each release and includes errors, warnings and debug messages if those options have been selected.\n\n   If \"full\" is selected, all errors, warnings and debugs will be written, along with additional debugging messages, to a custom log file for each release processed. These files are stored in the \"Classical Extras\" directory inside the same directory as the plugins folder. The log file for a release is named using the release MBID. Debugging from these files requires an understanding of the source code.\n   \n   Selecting \"full\" will slow Picard, but should not normally result in hanging or crashing.\n\n6. \"Classical Extras Special Tags\". This has a number of subsections:\n \n   *\"Save plugin details and options in a tag?\"* can be used so that the user has a record of the version of Classical Extras which generated the tags and which options were selected to achieve the resulting tags. Note that the tags will be blanked first so this will only show the last options used on a particular file. The same tag can be used for both sets of options, resulting in a multi-valued tag. All the options in the Classical Extras UI are saved **except** those which are asterisked.\n\n   The tag contents are in dict format. The options in these tags can then be used to over-ride the displayed options subsequently (see below).\n\n   N.B. The \"Tag name for artist/misc. options\" also saves the Picard options for 'translate\\_artist\\_names' and 'standardize\\_artists' as these interact with the Classical Extras options.\n\n   *\"Over-ride plugin options displayed in UI with options from local file tags\"*. If options have previously been saved (see above), selecting these will cause the saved options to be used in preference to the displayed options. The displayed options will not be affected and will be used if no saved options are present. The default is for no over-ride.\n\n   ***Note that* *very occasionally (if the tag containing the options has been corrupted) use of this option may cause an error. In such a case you will need to deselect the \"over-ride\" option and set the required options manually; then save the resulting tags and the corrupted tag should be over-written***\n\n   *\"Overwrite options in Options Pages\"*, is for **VERY CAREFUL USE ONLY**. It will cause any options read from the saved tags to over-write the options on the plugin Options Page UI. (Note that it will only operate if all the \"Over-ride plugin options...\" boxes are checked as well.) The intended use of this is if for some reason the user's preferred options have been erased/reverted to default - by using this option, the previously-used choices from a reliable filed album can be used to populate the Options Page. The box will automatically be unticked after loading/refreshing one album, and will always be turned off when starting Picard, to prevent inadvertant use. Far better is to make a **backup copy** of the picard.ini file.\n   \n   *Additional section added in v2.0.7 \"Show additional tags in Picard UI\"*. This enables display of any tags as columns in the Picard right-hand panel. Also tags (or groups of tags) which are different from file tags can be flagged - see the screen copy below:\n   \n   ![Special tags](https://music.highmossergate.co.uk/classical-extras-screenshots/special-tags/)\n   \n   Follow the instructions on the screen to enter the options. Then, if you right-click on the column headings in the right-hand panel of Picard, you should see all the options listed from which you can select which columns to show.\n\n# Information on hidden variables\n\nThis section is for users who want to write their own scripts, or add additional tags (in the tag mapping section) based on hidden variables. The definition and source of each hidden variable is listed. Apologies if there are errors and omissions in this section - to double check the actual hidden variables for any track, use the plugin \"View script variables\".\n\n## Works and parts\n\n- \\_cwp\\_work\\_n, where n is an integer >=0 : The MB work name at level n. For n=0, the tag is the same as the current standard Picard tag \"work\"\n- \\_cwp\\_work\\_top : The top work name (i.e. for maximal n). Thus, if max n = N, \\_cwp\\_work\\_top will be equivalent to \\_cwp\\_work\\_N. Note, however, that this will always be the \"canonical\" MB name, not one derived from titles or the lowest level work name and that no annotations (e.g. key or work year) will be added (whereas they will be added to \\_cwp\\_work\\_N). Nevertheless, if \"replace work names by aliases\" has been selected and is applicable, the relevant alias will be used.\n- \\_cwp\\_workid\\_n : The matching work id for each work name. For n=0, the tag is the same as the standard Picard tag \"MusicBrainz Work Id\"\n- \\_cwp\\_workid\\_top : The matching work id for the top work name.\n- \\_cwp\\_part\\_n : A \"stripped\" version of \\_cwp\\_work\\_n, where higher-level work text has been removed wherever possible, to avoid duplication on display.\n\tThus in theory, \\_cwp\\_work\\_0 will be the same as \"\\_cwp\\_work\\_top: \\_cwp\\_part\\_(N-1): ...: \\_cwp\\_part\\_0\" (punctuation excepted), but may differ in more complex situations where there is not an exact hierarchy of text as the work levels are traversed. (See below for the \"\\_X0\" series which attempts to address any such inconsistencies)\n- \\_cwp\\_part\\_levels : The number of work levels attached to THIS TRACK. Should be equal to N = max(n) referred to above.\n- \\_cwp\\_work\\_part\\_levels : The maximum number of levels for ANY TRACK in the album which has the same top work as this track.\n- \\_cwp\\_single\\_work\\_album : A flag = 1 if there is only one top work in this album, else = 0.?\n- \\_cwp\\_work : the level selected by the plugin to be the source of the single-level work name if \"Use only metadata from canonical works\" is selected (usually the top level, but one lower in the case of a single work album).\n- \\_cwp\\_groupheading : the level selected by the plugin to be the source of the multi-level work name if \"Use only metadata from canonical works\" is selected.\n- \\_cwp\\_part : The movement name derived from the MB work names (generally = \\_cwp\\_part\\_0) and used as the source for the movement name used for \"Tags for Movement - including embedded movt/part numbers\".\n- \\_cwp\\_inter\\_work : Intermediate works between \\_cwp\\_part and \\_cwp\\_work (if any).\n- \\_cwp\\_movt\\_num : The number sequence of the movement track within its parent **on the current release** (see more details below).\n- \\_cwp\\_movt\\_tot : The total number of movement tracks for the parent (see more details below).\n\nIf there is more than one work any level, then \\_cwp\\_work\\_n and \\_cwp\\_workid\\_n will have multiple entries. Another common situation is that a \"bottom level\" work is spread across more than one track. Rather than artificially split the work into sub-parts, this is often shown in MusicBrainz as a track being a \"partial recording of\" a work. The plugin deals with this by creating a notional lowest-level with the suffix \" (part)\" (or other text as defined in the works and parts options tab) appended to the work it is a partial recording of. In order that this notional part can be separately identified from the full work, the musicbrainz\\_recordingid is used as the identifier rather than the workid.\nIf there is more than one \"parent\" work of a lower level work, multi-valued tags are generated.  \n\nNote regarding movement numbers: \n- All movements will be considered as a part of the largest grouping (on a release) to which they belong, other than as “part of collection”\n- They will be numbered in the sequence in which they appear on the release\n- If a track comprises more than one movement, they will be treated as one for these purposes\n- If a movement is split over more than one track, then each will get a separate movement number\n- One-movement works will not receive a movement number unless the movement is split over multiple tracks\n- The movementtotal tag will be the total number of movements for the aforesaid grouping\n- If a movement grouping is split on a release by other tracks (not part of the grouping) then they will be treated as a whole and the numbering will resume where it left off.\n\nItems based on level 0 work data:  \n- \\_cwp\\_X0\\_part\\_0 : A \"stripped\" version of \\_cwp\\_work\\_0 (see above), where elements of \\_cwp\\_work\\_0 which repeat within level 1 have been stripped.\n- \\_cwp\\_X0\\_work\\_n : The elements of \\_cwp\\_work\\_0 which repeat within level n\n\nAs well as variables derived from MB's work structure, some variables are produced which are derived from the track title. Typically titles may be in the format \"Work: Movement\", but not always. Sometimes the title is prefixed by the name of the composer; in this case the variable\n- \\_cwp\\_title\nis provided which excludes the composer name and subsequent processing is carried out using this rather than the full title. \n\nThe plugin uses a number of methods attempt to extract the works and movement from the title. The resulting variables are:\n- \\_cwp\\_title\\_work\\_n, and\n- \\_cwp\\_title\\_part\\_n\nwhich mirror those for the ones based on MB works described above.\n- \\_cwp\\_title\\_part\\_levels which similarly mirrors \\_cwp\\_part\\_levels\n- \\_cwp\\_title\\_work\\_levels which similarly mirrors \\_cwp\\_work\\_part\\_levels\n\n- \\_cwp\\_title\\_work is the level selected by the plugin to be the source of the single-level work name if \"Use only metadata from title text\" is selected (usually the top level, but one lower in the case of a single work album).\n- \\_cwp\\_title\\_groupheading is similarly the level selected by the plugin to be the source of the multi-level work name if \"Use only metadata from title text\" is selected.\n\n- \\_cwp\\_extended\\_part : = \\_cwp\\_part with additional movement information from the title - given in {}.\n- \\_cwp\\_extended\\_groupheading : = \\_cwp\\_groupheading with additional work information from the title - given in {}.\n- \\_cwp\\_extended\\_work : = \\_cwp\\_work with additional work information from the title - given in {}.\n- \\_cwp\\_extended\\_inter\\_work : = \\_cwp\\_inter\\_work with additional work information from the title - given in {}.\nThe \"extended\" variables can be useful where the \"canonical\" work names in MB are in the original language and the titles are in English (say). Various heuristics are used to try and add (and only add) meaningful additional information, but oddities may occur which require manual editing.\n\nArtist tags which derive from work-artist relationships are also set in this section:\n- \\_cwp\\_composers & \\_cwp_composers_sort\n- \\_cwp\\_composer_lastnames\n- \\_cwp\\_writers & \\_cwp_writers_sort\n- \\_cwp\\_arrangers : This is for arrangers of the work and also \"instrument arrangers\" and \"vocal arrangers\" with appropriate annotation for instrument and voice types. (Picard does not currently write the latter to the Arranger tag if they are part of the work-artists relationship, despite style guidance saying to use specific instrument types instead of generic arranger.) \n- \\_cwp\\_arranger\\_names & \\_cwp_arrangers_sort: Just the names of the above (no annotations)\n- \\_cwp\\_orchestrators & \\_cwp_orchestrators_sort\n- \\_cwp\\_reconstructors & \\_cwp_reconstructors _sort - 'reconstructed by' relationships\n- \\_cwp\\_revisors & \\_cwp_revisors _sort - 'revised by' relationships\n- \\_cwp\\_lyricists & \\_cwp_lyricists _sort\n- \\_cwp\\_librettists & \\_cwp_librettists _sort\n- \\_cwp\\_translators & \\_cwp_translators _sort\n\nFinally, the tags \\_cwp\\_error and\\_cwp\\_warning are provided to supply warnings and error messages to the user.\n\n## Artists\n\nAll the additional hidden variables for artists written by Classical Extras are prefixed by \\_cea\\_. Note that these are generally in the plural, whereas the standard tags are singular. If the user blanks a tag then the original value is stored in the singular with the \\_cea\\_ prefix. Thus \\_cea\\_arranger would be the contents of the Picard tag \"arranger\" before blanking, whereas \\_cea\\_arrangers is hidden variable created by Classical Extras.\n\n- \\_cea\\_recording\\_artist : The artist credited with the recording (not necessarily the track artist). Note that this is the only \"\\_cea\\_\" tag which is singular, because it is in the same format as the 'artist' tag, whereas...\n- \\_cea\\_recording\\_artists : The list/multiple value version of the above. (This follows the approach in Picard for 'artist' and 'artists', being the track artists.)\n- \\_cea\\_MB\\_artists: The original track artists per MusicBrainz before any replacement by / merging with recording artists.\n- \\_cea\\_soloists : List of performers (with instruments in brackets), who are NOT ensembles or conductors, separated by semi-colons. Note they may not strictly be \"soloists\" in that they may be part of an ensemble.\n- \\_cea\\_recording\\_artistsort : Sort names of \\_cea\\_recording\\_artist\n- \\_cea\\_recording\\_artists\\_sort : Sort names of \\_cea\\_recording\\_artists\n- \\_cea\\_soloist\\_names : Names of the soloists (i.e. no instruments).\n- \\_cea\\_soloists\\_sort : Sort\\_names of the above.\n- \\_cea\\_vocalists : Soloists who are vocalists (with voice in brackets).\n- \\_cea\\_vocalist\\_names : Names of the above (no voice).\n- \\_cea\\_instrumentalists : Soloists who have instruments but are not vocalists.\n- \\_cea\\_instrumentalist\\_names : Names of the above (no instrument).\n- \\_cea\\_other\\_soloists : Soloists who do not have specified instrument/voice.\n- \\_cea\\_ensembles : List of performers which are ensembles (with type / instruments - e.g. \"orchestra\" - in brackets), separated by semi-colons.\n- \\_cea\\_ensemble\\_names : Names of the above (i.e. no instruments).\n- \\_cea\\_ensembles\\_sort : Sort\\_names of the above.\n- \\_cea\\_album\\_soloists : Sub-list of soloist\\_names who are also album artists\n- \\_cea\\_album\\_soloists\\_sort : Sort\\_names of the above.\n- \\_cea\\_album\\_conductors : List of conductors who are also album artists\n- \\_cea\\_album\\_conductors\\_sort : Sort\\_names of the above.\n- \\_cea\\_album\\_ensembles: Sub-list of ensemble\\_names who are also album artists\n- \\_cea\\_album\\_ensembles\\_sort : Sort\\_names of the above.\n- \\_cea\\_album\\_composers : List of composers who are also album artists\n- \\_cea\\_album\\_composers\\_sort : Sort\\_names of the above.\n- \\_cea\\_album\\_track\\_composer\\_lastnames : Last names of the above. (N.B. This only includes the composers of the current track - compare with \\_cea\\_album\\_composer\\_lastnames below).\n- \\_cea\\_album\\_composer\\_lastnames : Last names of composers of ANY track on the album who are also album artists. This can be used to prefix the album name if required. (cf \\_cea\\_album\\_track\\_composer\\_lastnames)\n- \\_cea\\_support\\_performers : Sub-list of soloist\\_names who are NOT album artists\n- \\_cea\\_support\\_performers\\_sort : Sort\\_names of the above.\n- \\_cea\\_composers : Alternative to 'composer', incorporating 'naming options' on the artists tab.\n- \\_cea\\_composer_lastnames: Last names of above.\n- \\_cea\\_conductors : Alternative to 'conductor', incorporating 'naming options' on the artists tab.\n- \\_cea\\_performers : Alternative to 'performer', incorporating 'naming options' on the artists tab.\n- \\_cea\\_arrangers : All arrangers for the **recording** with instrument/voice type in brackets, if provided. If the work and parts functionality has also been selected, the arrangers of works, which Picard also currently omits will be put in \\_cwp\\_arrangers.\n- \\_cea\\_orchestrators : Arrangers (per Picard) included in the MB database as type \"orchestrator\".\n- \\_cea\\_chorusmasters : A person who (per Picard) is a conductor, but is \"chorus master\" in the MB database (i.e. not necessarily conducting the performance).\n- \\_cea\\_leaders : The leader of the orchestra (\"concertmaster\" in MusicBrainz) - not created by Picard as standard. \n\n## Genres etc.\n\nMost of the genres, keys and date information requires the works and parts section to have been run, and so the variables are prefixed \\_cwp\\_, but instruments and genres which are derived from artist information and are prefixed \\_cea\\_.\n\n- \\_cea\\_instruments : Names of all instruments on the track (MusicBrainz names)\n- \\_cea\\_instuments\\_credited : As above, but MB names replaced by as-credited names, if any\n- \\_cea\\_instruments\\_all : MB and as-credited names\n- \\_cea\\_work\\_type : The genre(s) inferred from artist information\n- \\_cea\\_work\\_type\\_if\\_classical : As above, but only of relevance if the work is classical\n- \\_cwp\\_candidate\\_genres : List of all tags, work types etc. found (depending on specified sources) before filtering for \"allowed\" genres.\n- \\_cwp_worktype_genres : Any \"type\" attribute for the work or any of its parents.\n- \\_cwp\\_keys : keys associated with this track (from all work levels).\n- \\_cwp\\_composed\\_dates : Date composed (integer) or range (integer-integer).\n- \\_cwp\\_published\\_dates : Date published (integer) or range (integer-integer).\n- \\_cwp\\_premiered\\_dates : Date premiered (integer) or range (integer-integer).\n- \\_cwp\\_untagged\\_genres : Genres in \\_cwp\\_candidate\\_genres which have been filtered out as they are not in any \"allowed\" list.\n- \\_cwp\\_unrostered\\_composers : For Muso users: composers who are not in Muso's classical composers roster.\n\n## Special tags for ui\n\nA number of hidden variables are created (v.2.0.7+) as part of the processing for the additional tags for the UI as described at the end of the \"Advanced\" tab section. The ones of the form \\_tagname_OLD hold the previous value of the tag 'tagname' on the file. The ones of the form \\_tagname_DIFF hold a flag '*****' if the value of 'tagname' has changed from the previous one on file (only for tags specified on the options page). The ones of the form \\_columnheading_VAL hold the values to be displayed in the column 'columnheading' as specified in the options.\n\n# Software-specific notes\n\nNote that \\_cwp\\_part\\_levels > 0 will indicate that the track recording is part of a work and so could be used to set other software-specific flags (e.g. for iTunes \"show work movement\") to indicate a multi-level \"work: movement\".\n\n## SongKong\n\nSongKong users may wish to map the \\_cwp variables to tags produced by SongKong if consistency is desired, in which case the mappings are principally:\n- \\_cwp\\_work\\_0 => musicbrainz\\_work\\_composition\n- \\_cwp\\_workid\\_0 => musicbrainz\\_work\\_composition\\_id\n- \\_cwp\\_work\\_n => musicbrainz\\_work\\_part\\_leveln, for n = 1..6\n- \\_cwp\\_workid\\_n => musicbrainz\\_work\\_part\\_leveln\\_id, for n = 1..6\n- \\_cwp\\_work\\_top => musicbrainz\\_work\n\nThese mappings are carried out automatically if the relevant options on the \"Work and parts\" tab are selected.  \nIn addition, \\_cwp\\_title\\_work and \\_cwp\\_title\\_part\\_0 are intended to be equivalent to SongKong's work and part tags.\n(N.B. Full consistency between SongKong and Picard may also require the modification of Artist and related tags via a script, or the preservation of the related file tags)\n\n## Muso\n\nThe tag \"groupheading\" should be set as the \"Tags for Work - for software with 2-level capability\". Muso will use this directly and extract the levels from it (split by the double colon). Muso permits a variety of import options which should be capable of combination with the tagging options in this plugin to achieve most desired effects. To avoid the use of import options in Muso, set the output tags from the plugin to be the native ones used by Muso (NB \"title\" may include or exclude groupheading - Muso should recognise it and extract it).\n\nTo make use of Muso's in-built classical music processing to set explicit tags in Picard, enable the \"Use Muso reference database ...\" option on the \"Genres etc.\" tab.\n\n## Players with no \"work\" capability\n\nLeave the \"title\" tag unchanged or make it a combination of work and movement.\n\n# Possible Enhancements\nAll planned functionality has been included in version 2.0. Please post any suggestions for improvements to the forum.\n\n# Technical Matters (of possible interest to developers)\nIssues were encountered with the Picard API in that there is not a documented way to let Picard know that it is still doing asynchronous tasks in the background and has not finished processing metadata. Many thanks to @dns\\_server for assistance in dealing with this and to @sophist for the albumartist\\_website code which I have borrowed from. I have tried to add some more comments to help any others trying the same techniques.\n\nAlso, the documentation of the XML lookup is virtually non-existent. The response is an XmlNode object (not a dict, although it is represented as one). Each node has a name with {attribs, text, children} values. The structure is more clearly understood if the web-based lookup is used (which is well documented at https://musicbrainz.org/doc/Development/XML_Web_Service/Version_2 ) as this gives an XML response. I wrote a function (parse\\_data) to parse XmlNode objects, or lists thereof, for a (parameterised) hierarchy of nodes (and optional attribs value tests) in order to extract required data from the response. This may be of use to other plugin authors. The situation has been made slightly better in Picard 2.0 as the objects returned from register_track_metadata_processor are standard JSON. See https://musicbrainz.org/doc/Development/JSON_Web_Service. My parse_data function works with JSON as well as XmlNode formats, but the arguments you need to provide to it are different as the formats are structured differently. Picard 2.0 allows tagger.webservice calls to specify XML or default to JSON. This plugin has now migrated over to JSON-only.\n\nTo get the whole picture, in XML, for a release, use (for example) https://musicbrainz.org/ws/2/release/f3bb4fdd-5db0-43a8-be73-7a1747f6c2ef?inc=release-groups+media+recordings+artist-credits+artists+aliases+labels+isrcs+collections+artist-rels+release-rels+url-rels+recording-rels+work-rels+recording-level-rels+work-level-rels. (Add &fmt=json at the end to get the JSON response). This simulates the response given by Picard with the \"Use release relationships\" and \"Use track relationships\" options selected. Note that the Picard album\\_metadata\\_processor returns releaseXmlNode (as JSON from Picard 2.0 on) which is everything from the Release node of the XML downwards, whereas track\\_metadata\\_processor returns trackXmlNode which is everything from a Track node downwards (release -> medium-list -> medium -> track-list is above track). Replace all hyphens in XML with underscores when parsing the Python object (JSON uses hyphens).\n\nCare needs to be taken when using the metadata property in Picard (e.g. as in track.metadata). This returns an object of class Metadata, which looks just like a dictionary but isn't. All values are in fact lists, which works as follows: when a value is set, it is always set as a list - i.e. a list value is entered as it is, but a string  is set as a list : viz. ['string']. Then, when getting an item from the object, multiple values are joined to create a string. Thus track.metadata['item'] and track.metadata.get('item') will return list.join('; ') - i.e. a string with the list values separated by semi-colons - not the list. To get the list, you need to use track.metadata.getall('item'). The plugin stores work ids, internally and in \"hidden variables\" (such as _cwp_workid_0), as tuples. The Metadata class puts these into a string before setting the value in the object as a list, so the resulting value is [\"(wid1, wid2, ...)\"]. In order to extract the original tuple, the eval function is required.\n\nA large variety of releases were used to test this plugin, but there may still be bugs, so further testing is welcome. The following release was particularly complex and useful for testing: https://musicbrainz.org/release/ec519fde-94ee-4812-9717-659d91be11d4. Also this release was a bit tricky - a large box set with some works appearing as originals and in arrangements: https://musicbrainz.org/release/5288f266-bab8-45bd-83e4-555730f02fa0.\n\n# Very technical matters (probably not of interest to anyone)\nI've done a bit of research and observed the following behaviour in Picard when using the  `register_track_metadata_processor()` API:\n1. If a new album (i.e. not yet tagged by Picard and with no MBIDs) is loaded, clustered and looked-up/scanned, resulting in the matched files being shown in the right-hand pane, then:\n    * `track.metadata` gives the lookup result from MB - i.e. no file information, just the track info.\n    * `album.tagger.files[xxxx].metadata` (where xxxx is the path/filename of the track) gives the file information (dirname etc.) and the tags on the original file.\n    * `album.tagger.files[xxxx].orig_metadata` gives the same as `album.tagger.files[xxxx].metadata`\n\n2. However, if the album is then \"refreshed\", this does not just carry out a repeat operation, instead:\n    * `track.metadata` gives the same as before\n    * `album.tagger.files[xxxx].metadata` gives the metadata as in `track.metadata` and also includes all the file information as before.\n    * `album.tagger.files[xxxx].orig_metadata` gives the same as before (i.e. the original tags).\n\nSo, the 'post-refreshment' metadata is actually usable - by matching the `musicbrainz_trackid` of `track.metadata` and `album.tagger.files[xxxx].metadata`, you can get the file details of the track. Not elegant, but it works.\nBut why is it necessary to \"refresh\" to get what seems like a logical data set? Basically, the metadata process is not complete when the plugin is called, so not all metadata is available to it. \n\nI don't know why `get_files_from_objects(objs)` doesn't work in the `register_track_metadata_processor()` API when `objs` is a list of tracks, but does provide a list of file objects when `objs` is a list of albums. Also it does work in the `register_file_action(xxx)`/`register_track_action(xxx)` API, but I assume that is because `itemsview.py` can identify that you have clicked on an item that is a track and a file (this is after the metadata processing has run).\n\nIn order to get round these problems, I have adopted a 'hack' which looks up the files for an album and finds which file matches the required track on tracknumber and discnumber. This seems to work, but there may be circumstances when it does not. As a consequence, refreshment should only be required if `get_files_from_objects(objs)` doesn't get all the album files.\n\n# List of previous updates\n\nVersion 0.9.4: Bug fixes. The last released version for Picard 1.4 and the fork for version 2.0.\n\nVersion 0.9.3: Custom logging if enabled on \"Advanced\" tab - writes one log file per album in addition to standard Picard log. Also a session log with basic data is written. Performance improvements (especially for lyrics processing) and various bug fixes.\n\nVersion 0.9.2: A new tab in the options page has been created - \"Genres etc.\" This includes special processing  for genres, instruments, keys, work dates and periods. The \"Infer work types\" option on the \"Artists\" tab has been moved to this tab (but will be reset to the default of \"False\" and will need to be reset as required, because it operates slightly differently now that there is more control over the setting of genres). A separate section has ben added to this readme giving more details on the options in this tab.\n\nVersion 0.9.1: Bug fixes.\n\nVersion 0.9: Additional option to clear previous file tags which are not wanted, without interfering with cover art. Additional option to replace instrument names with as-credited names. Also instrument names are now saved to hidden variables tags (instruments, instruments\\_credited and instruments\\_all) which can be mapped to file tags as required. Sub-option added to the 'override artist options' option on the \"advanced\" tab - to allow tag map details to be included or not in this over-ride. Minor bug fixes and UI improvements. This is the next 'official' version after 0.7.\n\nVersion 0.8.9: Provide option (in advanced tab) to disable Classical Extras processing if no file is present; this enables (for example) single discs from box sets to be loaded without incurring the additional processing overhead for all the other discs. The settings of the main Picard options \"translate\\_artist\\_names\" and \"standardize\\_artists\" is now saved along with the Classical Extras options so that they can be used to over-ride the displayed options. This is because they interact with the Classical Extras options in certain cases. Also:- graceful recovery from authentication failure; improved UI - more scalable; minor bug fixes.\n\nVersion 0.8.8: Fixes to allow for (1) disabling of 'use\\_cache' across releases which may share works and (2) works which might appear in their own right and also have arrangements. Also, re-set certain important options to defaults on starting Picard, namely: 'use\\_cache' set to True, 'log\\_debug', 'log\\_info' and 'options\\_overwrite' set to False; the user will need to deliberately re-set these on starting Picard if required - this is to prevent inadvertently leaving these flags in an abnormal state.\n\nVersion 0.8.7: Revised treatment of \"conditional\" tag mapping. Previously, if multiple sources were specified for a tag mapping and the \"conditional\" flag set, only the first non-empty source was used. Now all sources will be mapped to a tag if it was empty before executing the current tag mapping line. This is considered to be more intuitive and leads to less complex mapping lines. However, it some cases it may be necessary to split a line from a previous version if the previous behaviour was specifically desired. Improved algorithms for extending metadata with title info. Bug fixes.\n\nVersion 0.8.6: More consistent approach to sort tags and hidden variables. Bug fixes.\n\nVersion 0.8.5: Improved handling of instruments. Bug fixes.\n\nVersion 0.8.4: Improved UI, bug fixes and code improvements.\n\nVersion 0.8.3: Ability to use aliases of work names instead of the MusicBrainz standard names. Various options to use aliases and as-credited names for performers and writers (i.e. relationship artists). Option to use recording artists as well as (or instead of) track artists. All relationship artists are now tagged for all work levels (with some special processing for lyricists). New UI layout to separate tag mapping from artist options. Option to split lyrics into common (release) and unique (track) components. Various code improvements as well as bug fixes.\n\nVersion 0.8.2: Improved algorithms and a few minor bug fixes.\n\nVersion 0.8.1: Bug fixes and minor enhancements - including revison and extension of \"synonyms\" section on the \"advanced\" tab.\n\nVersion 0.8: Handle multiple recordings and/or multiple parents of a work. \nHandle multiple albumartist composers for one track. \nOption to use \"credited as\" name for artists (inc. performers and composers) who are \"release artist\" or \"track artist\". \nOption to exclude \"solo\" from instrument types. \nOption to over-ride plugin options with those used when the album was last saved. \nOption to keep (and append to) specified existing file tags - furthermore if \"is\\_classical\" is present, the work-type variable will include \"Classical\". \nOption to use (and write) SongKong-compatible work tags (saves processing time if SongKong is used to pre-process large numbers of files). \nInclude the work (and its parents) of which a work is an arrangement (as a \"pseudo-parent\"). \nInclude medleys in movement/part description (as [Medley of:...] or other descriptor specified in options). \nAllow for multiple parents of recordings and works (and multiple parents of those) - multiples are given as multiple tag instances, where permitted, otherwise separated by semi-colons. \nOption as to whether to include parent works where the relationship attribute is \"part of collection\". \nPlus minor enhancements and bug fixes too numerous to mention!\n(Note that some old versions of the plugin may say v0.8 when they are only v0.7)\n\nVersion 0.7: Bug fixes. Pull request issued for this version.\n\nVersion 0.6.6: Bug fixes and algorithm improvements. Allow multiple sources for a tag (each will be appended) and use + as separator for concantenating sources, not a comma, to distinguish from the use of comma to separate multiples. Provide additional hidden variables (\"sources\") for vocalists and instrumentalists. Option to include intermediate work levels in a movement tag (for systems which cannot display multiple work levels).\n\nVersion 0.6.5: Include ability to use multiple (concatenated) sources in tag mapping (see notes under \"tag mapping\"). All artist \"sources\" using hidden variables (\\_cea\\_...) are now consistently in the plural, to distinguish from standard tags. Note that if \"album\" is used as a source in tag mapping, it will now be the revised name, where composer prefixing is used. To use the original name, use \"release\" as the source. Also various bug fixes, notably to ensure that all arrangers get picked up for use in tag mapping.\n\nVersion 0.6.4: Write out version number to user-specified tag. Provide default comment tags for writing ui options. Provide better m4a and iTunes compatibility (see notes). Added functionality for chorus master, orchestrator and concert master (leader). Re-arranged artist tab in ui. Various bug fixes.\n\nVersion 0.6.3: Bug fixes. Modified ui default options.\n\nVersion 0.6.2: Bug fixes. More flexible handling of artists (can blank and then add back later). Modified ui default options.\n\nVersion 0.6.1: Amended regex to permit non-Latin characters in work text."
  },
  {
    "path": "plugins/classical_extras/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2018 Mark Evens\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = u'Classical Extras'\nPLUGIN_AUTHOR = u'Mark Evens'\nPLUGIN_DESCRIPTION = u\"\"\"Classical Extras provides tagging enhancements for Picard and, in particular,\nutilises MusicBrainz’s hierarchy of works to provide work/movement tags. All options are set through a\nuser interface in Picard options->plugins. This interface provides separate sections\nto enhance artist/performer tags, works and parts, genres and also allows for a generalised\n\"tag mapping\" (simple scripting).\nWhile it is designed to cater for the complexities of classical music tagging,\nit may also be useful for other music which has more than just basic song/artist/album data.\n<br /><br />\nThe options screen provides five tabs for users to control the tags produced:\n<br /><br />\n1. Artists: Options as to whether artist tags will contain standard MB names, aliases or as-credited names.\nAbility to include and annotate names for specialist roles (chorus master, arranger, lyricist etc.).\nAbility to read lyrics tags on the file which has been loaded and assign them to track and album levels if required.\n(Note: Picard will not normally process incoming file tags).\n<br /><br />\n2. Works and parts: The plugin will build a hierarchy of works and parts (e.g. Work -> Part -> Movement or\nOpera -> Act -> Number) based on the works in MusicBrainz's database. These can then be displayed in tags in a variety\nof ways according to user preferences. Furthermore partial recordings, medleys, arrangements and collections of works\nare all handled according to user choices. There is a processing overhead for this at present because MusicBrainz limits\nlook-ups to one per second.\n<br /><br />\n3. Genres etc.: Options are available to customise the source and display of information relating to genres,\ninstruments, keys, work dates and periods. Additional capabilities are provided for users of Muso (or others who\nprovide the relevant XML files) to use pre-existing databases of classical genres, classical composers and classical\nperiods.\n<br /><br />\n4. Tag mapping: in some ways, this is a simple substitute for some of Picard's scripting capability. The main advantage\n is that the plugin will remember what tag mapping you use for each release (or even track).\n<br /><br />\n5. Advanced: Various options to control the detailed processing of the above.\n<br /><br />\nAll user options can be saved on a per-album (or even per-track) basis so that tweaks can be used to deal with\ninconsistencies in the MusicBrainz data (e.g. include English titles from the track listing where the MusicBrainz works\nare in the composer's language and/or script).\nAlso existing file tags can be processed (not possible in native Picard).\n<br /><br />\nSee the readme file <a href=\"https://github.com/MetaTunes/picard-plugins/tree/metabrainz/2.0/plugins/classical_extras\">\non GitHub here</a> for full details.\n\"\"\"\n\n########################\n# DEVELOPERS NOTES: ####\n########################\n#  This plugin contains 3 classes:\n#\n# I. (\"EXTRA ARTISTS\") Create sorted fields for all performers. Creates a number of variables with alternative values\n# for \"artists\" and \"artist\".\n# Creates an ensemble variable for all ensemble-type performers.\n# Also creates matching sort fields for artist and artists.\n# Additionally create tags for artist types which are not normally created in Picard - particularly for classical music\n#  (notably instrument arrangers).\n#\n# II. (\"PART LEVELS\" [aka Work Parts]) Create tags for the hierarchy of works which contain a given track recording\n# - particularly for classical music'\n# Variables provided for each work level, with implied part names\n# Mixed metadata provided including work and title elements\n#\n# III. (\"OPTIONS\") Allows the user to set various options including what tags will be written\n# (otherwise the classes above will just write outputs to \"hidden variables\")\n#\n# The main control routine is at the end of the module\n\nPLUGIN_VERSION = '2.0.14'\nPLUGIN_API_VERSIONS = [\"2.0\", \"2.1\", \"2.2\", \"2.3\", \"2.4\", \"2.5\", \"2.6\", \"2.7\"]\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom picard.ui.options import register_options_page, OptionsPage\nfrom picard.plugins.classical_extras.ui_options_classical_extras import Ui_ClassicalExtrasOptionsPage\nimport picard.plugins.classical_extras.suffixtree\nfrom picard import config, log\nfrom picard.config import ConfigSection, BoolOption, IntOption, TextOption\nfrom picard.util import LockableObject, uniqify\n\n# note that in 2.0 picard.webservice changed to picard.util.xml\nfrom picard.util.xml import XmlNode\nfrom picard.util import translate_from_sortname\nfrom picard.metadata import register_track_metadata_processor, Metadata\nfrom functools import partial\nfrom datetime import datetime\nimport collections\nimport re\nimport unicodedata\nimport json\nimport copy\nimport os\nfrom PyQt5.QtCore import QXmlStreamReader\nfrom picard.const import USER_DIR\nimport operator\nimport ast\nimport picard.plugins.classical_extras.const\n\n\n\n##########################\n# MODULE-WIDE COMPONENTS #\n##########################\n# CONSTANTS\n# N.B. Constants with long definitions are set in const.py\nDATE_SEP = '-'\n\n# COMMONLY USED REGEX\nROMAN_NUMERALS = r'\\b((?=[MDCLXVI])(M{0,4}(CM|CD|D?)?C{0,3}(XC|XL|L?)?X{0,3}(IX|IV|V?)?I{0,3}))(?:\\.|\\-|:|;|,|\\s|$)'\nROMAN_NUMERALS_AT_START = r'^\\W*' + ROMAN_NUMERALS\nRE_ROMANS = re.compile(ROMAN_NUMERALS, re.IGNORECASE)\nRE_ROMANS_AT_START = re.compile(ROMAN_NUMERALS_AT_START, re.IGNORECASE)\n# KEYS\nRE_NOTES = r'(\\b[ABCDEFG])'\nRE_ACCENTS = r'(\\-sharp(?:\\s+|\\b)|\\-flat(?:\\s+|\\b)|\\ssharp(?:\\s+|\\b)|\\sflat(?:\\s+|\\b)|\\u266F(?:\\s+|\\b)|\\u266D(?:\\s+|\\b)|(?:[:,.]?\\s+|$|\\-))'\nRE_SCALES = r'(major|minor)?(?:\\b|$)'\nRE_KEYS = re.compile(\n    RE_NOTES + RE_ACCENTS + RE_SCALES,\n    re.UNICODE | re.IGNORECASE)\n\n# LOGGING\n\n# If logging occurs before any album is loaded, the startup log file will\n# be written\nlog_files = collections.defaultdict(dict)\n# entries are release-ids: to keep track of which log files are open\nrelease_status = collections.defaultdict(dict)\n# release_status[release_id]['works'] = True indicates that we are still processing works for release_id\n# & similarly for 'artists'\n# release_status[release_id]['start'] holds start time of release processing\n# release_status[release_id]['name'] holds the album name\n# release_status[release_id]['lookups'] holds number of lookups for this release\n# release_status[release_id]['file_objects'] holds a cumulative list of file objects (tagger seems a bit unreliable)\n# release_status[release_id]['file_found'] = False indicates that \"No file\n# with matching trackid\" has (yet) been found\n\n\ndef write_log(release_id, log_type, message, *args):\n    \"\"\"\n    Custom logging function - if log_info is set, all messages will be written to a custom file in a 'Classical_Extras'\n    subdirectory in the same directory as the main Picard log. A different file is used for each album,\n    to aid in debugging - the log file is release_id.log. Any startup messages (i.e. before a release has been loaded)\n    are written to session.log. Summary information for each release is also written to session.log even if log_info\n    is not set.\n    :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n    :param log_type: 'error', 'warning', 'debug' or 'info'\n    :param message: string, e.g. 'error message for workid: %s'\n    :param args: arguments for parameters in string, e.g. if workId then str(workId) will replace %s in the above\n    :return:\n    \"\"\"\n    options = config.setting\n    if not isinstance(message, str):\n        msg = repr(message)\n    else:\n        msg = message\n    if args:\n        msg = msg % args\n\n    if options[\"log_info\"] or log_type == \"basic\":\n        # if log_info is True, all log messages will be written to the custom log, regardless of other log_... settings\n        # basic session log will always be written (summary of releases and\n        # processing times)\n        filename = release_id + \".log\"\n        log_dir = os.path.join(USER_DIR, \"Classical_Extras\")\n        if not os.path.exists(log_dir):\n            os.makedirs(log_dir)\n        if release_id not in log_files:\n            try:\n                if release_id == 'session':\n                    log_file = open(\n                        os.path.join(\n                            log_dir,\n                            filename),\n                        'w',\n                        encoding='utf8',\n                        buffering=1)\n                    # buffering=1 so that session log (low volume) is up to\n                    # date even if not closed\n                else:\n                    log_file = open(\n                        os.path.join(\n                            log_dir,\n                            filename),\n                        'w',\n                        encoding='utf8')  # , buffering=1)\n                    # default buffering for speed, buffering = 1 for currency\n                log_files[release_id] = log_file\n                log_file.write(\n                    PLUGIN_NAME +\n                    ' Version:' +\n                    PLUGIN_VERSION +\n                    '\\n')\n                if release_id == 'session':\n                    log_file.write('session' + '\\n')\n                else:\n                    log_file.write('Release id: ' + release_id + '\\n')\n                    if release_id in release_status and 'name' in release_status[release_id]:\n                        log_file.write(\n                            'Album name: ' + release_status[release_id]['name'] + '\\n')\n            except IOError:\n                log.error('Unable to open file %s for writing log', filename)\n                return\n        else:\n            log_file = log_files[release_id]\n        try:\n            log_file.write(log_type[0].upper() + ': ')\n            log_file.write(str(datetime.now()) + ' : ')\n            log_file.write(msg)\n            log_file.write(\"\\n\")\n        except IOError:\n            log.error('Unable to write to log file %s', filename)\n            return\n    # Only debug, warning and error messages will be written to the main\n    # Picard log, if those options have been set\n    if log_type != 'info' and log_type != 'basic':  # i.e. non-custom log items\n        message2 = PLUGIN_NAME + ': ' + message\n    else:\n        message2 = message\n    if log_type == 'debug' and options[\"log_debug\"]:\n        if release_id in release_status and 'debug' in release_status[release_id]:\n            add_list_uniquely(release_status[release_id]['debug'], msg)\n        else:\n            release_status[release_id]['debug'] = [msg]\n        log.debug(message2, *args)\n    if log_type == 'warning' and options[\"log_warning\"]:\n        if release_id in release_status and 'warnings' in release_status[release_id]:\n            add_list_uniquely(release_status[release_id]['warnings'], msg)\n        else:\n            release_status[release_id]['warnings'] = [msg]\n        if args:\n            log.warning(message2, *args)\n        else:\n            log.warning(message2)\n    if log_type == 'error' and options[\"log_error\"]:\n        if release_id in release_status and 'errors' in release_status[release_id]:\n            add_list_uniquely(release_status[release_id]['errors'], msg)\n        else:\n            release_status[release_id]['errors'] = [msg]\n        if args:\n            log.error(message2, *args)\n        else:\n            log.error(message2)\n\n\ndef close_log(release_id, caller):\n    # close the custom log file if we are done\n    if release_id == 'session':   # shouldn't happen but, just in case, don't close the session log\n        return\n    if caller in ['works', 'artists']:\n        release_status[release_id][caller] = False\n    if (caller == 'works' and release_status[release_id]['artists']) or \\\n            (caller == 'artists' and release_status[release_id]['works']):\n        # log.error('exiting close_log. only %s done', caller) # debug line\n        return\n    duration = 'N/A'\n    lookups = 'N/A'\n    artists_time = 0\n    works_time = 0\n    lookup_time = 0\n    album_process_time = 0\n    if release_id in release_status:\n        duration = datetime.now() - release_status[release_id]['start']\n        lookups = release_status[release_id]['lookups']\n        done_lookups = release_status[release_id]['done-lookups']\n        lookup_time = done_lookups - release_status[release_id]['start']\n        album_process_time = duration - lookup_time\n        artists_time = release_status[release_id]['artists-done'] - \\\n            release_status[release_id]['start']\n        works_time = release_status[release_id]['works-done'] - \\\n            release_status[release_id]['start']\n        del release_status[release_id]['start']\n        del release_status[release_id]['lookups']\n        del release_status[release_id]['done-lookups']\n        del release_status[release_id]['artists-done']\n        del release_status[release_id]['works-done']\n    if release_id in log_files:\n        write_log(\n            release_id,\n            'info',\n            'Duration = %s. Number of lookups = %s.',\n            duration,\n            lookups)\n        write_log(release_id, 'info', 'Closing log file for %s', release_id)\n        log_files[release_id].close()\n        del log_files[release_id]\n    if 'session' in log_files and release_id in release_status:\n        write_log(\n            'session',\n            'basic',\n            '\\n Completed processing release id %s. Details below:-',\n            release_id)\n        if 'name' in release_status[release_id]:\n            write_log('session', 'basic', 'Album name %s',\n                      release_status[release_id]['name'])\n        if 'errors' in release_status[release_id]:\n            write_log(\n                'session',\n                'basic',\n                '-------------------- Errors --------------------')\n            for error in release_status[release_id]['errors']:\n                write_log('session', 'basic', error)\n            del release_status[release_id]['errors']\n        if 'warnings' in release_status[release_id]:\n            write_log(\n                'session',\n                'basic',\n                '-------------------- Warnings --------------------')\n            for warning in release_status[release_id]['warnings']:\n                write_log('session', 'basic', warning)\n            del release_status[release_id]['warnings']\n        if 'debug' in release_status[release_id]:\n            write_log(\n                'session',\n                'basic',\n                '-------------------- Debug log --------------------')\n            for debug in release_status[release_id]['debug']:\n                write_log('session', 'basic', debug)\n            del release_status[release_id]['debug']\n        write_log(\n            'session',\n            'basic',\n            'Duration = %s. Artists time = %s. Works time = %s. Of which: Lookup time = %s. '\n            'Album-process time = %s. Number of lookups = %s.',\n            duration,\n            artists_time,\n            works_time,\n            lookup_time,\n            album_process_time,\n            lookups)\n    if release_id in release_status:\n        del release_status[release_id]\n\n\n# FILE READING AND OBJECT PARSING\n\n_node_name_re = re.compile('[^a-zA-Z0-9]')\n\n\ndef _node_name(n):\n    return _node_name_re.sub('_', str(n))\n\n\ndef _read_xml(stream):\n    document = XmlNode()\n    current_node = document\n    path = []\n    while not stream.atEnd():\n        stream.readNext()\n        if stream.isStartElement():\n            node = XmlNode()\n            attrs = stream.attributes()\n            for i in range(attrs.count()):\n                attr = attrs.at(i)\n                node.attribs[_node_name(attr.name())] = str(attr.value())\n            current_node.append_child(_node_name(stream.name()), node)\n            path.append(current_node)\n            current_node = node\n        elif stream.isEndElement():\n            current_node = path.pop()\n        elif stream.isCharacters():\n            current_node.text += str(stream.text())\n    return document\n\n\ndef get_preferred_artist_language(config):\n    locales = config.setting[\"artist_locales\"]\n    if locales:\n        artist_locale = locales[0]\n    else:\n        artist_locale = config.setting[\"artist_locale\"]\n    if artist_locale:\n        return artist_locale.split('_')[0]\n    else:\n        return ''\n\n\ndef parse_data(release_id, obj, response_list, *match):\n    \"\"\"\n    This function takes any XmlNode object, or list thereof, or a JSON object\n    and extracts a list of all objects exactly matching the hierarchy listed in match.\n    match should contain list of each node in hierarchical sequence, with no gaps in the sequence\n     of nodes, to lowest level required.\n    :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n    :param obj: an XmlNode or JSON object, list or dictionary containing nodes\n    :param response_list: working memory for recursive calls\n    :param match: list of items to search for in node (see detailed notes below)\n    :return: a list of matching items (always a list, even if only one item)\n\n    Insert attribs.attribname:attribvalue in the list to select only branches where attribname\n     is attribvalue. (Omit the attribs prefix if the obj is JSON)\n    Insert childname.text:childtext in the list to select only branches where\n     a sibling with childname has text childtext.\n      (Note: childname can be a dot-list if the text is more than one level down - e.g. child1.child2\n      # TODO - Check this works fully )\n    \"\"\"\n    if '!log' in response_list:\n        DEBUG = True\n        INFO = True\n    else:\n        DEBUG = False\n        INFO = False\n    # Normally logging options are off as these can be VERY wordy\n    # They can be turned on by using !log in the call\n\n    # XmlNode instances are not iterable, so need to convert to dict\n    if isinstance(obj, XmlNode):\n        obj = obj.__dict__\n    if DEBUG or INFO:\n        write_log(release_id, 'debug', 'Parsing data - looking for %s', match)\n    if INFO:\n        write_log(release_id, 'info', 'Looking in object: %s', obj)\n    if isinstance(obj, list):\n        objlen = len(obj)\n        for i, item in enumerate(obj):\n            if isinstance(item, XmlNode):\n                item = item.__dict__\n            if INFO:\n                write_log(\n                    release_id,\n                    'info',\n                    'Getting response for list item no.%s of %s - object is: %s',\n                    i + 1,\n                    objlen,\n                    item)\n            parse_data(release_id, item, response_list, *match)\n            if INFO:\n                write_log(\n                    release_id,\n                    'info',\n                    'response_list for list item no.%s of %s is %s',\n                    i + 1,\n                    objlen,\n                    response_list)\n        return response_list\n    elif isinstance(obj, dict):\n        if match[0] in obj:\n            if len(match) == 1:\n                response = obj[match[0]]\n                if response is not None:  # To prevent adding NoneTypes to list\n                    response_list.append(response)\n                if INFO:\n                    write_log(\n                        release_id,\n                        'info',\n                        'response_list (last match item): %s',\n                        response_list)\n            else:\n                match_list = list(match)\n                match_list.pop(0)\n                parse_data(release_id, obj[match[0]],\n                           response_list, *match_list)\n                if INFO:\n                    write_log(\n                        release_id,\n                        'info',\n                        'response_list (passing up): %s',\n                        response_list)\n            return response_list\n        elif ':' in match[0]:\n            test = match[0].split(':')\n            match2 = test[0].split('.')\n            test_data = parse_data(release_id, obj, [], *match2)\n            if INFO:\n                write_log(\n                    release_id,\n                    'info',\n                    'Value comparison - looking in %s for value %s',\n                    test_data,\n                    test[1])\n            if len(test) > 1:\n                # latter is because Booleans are stored as such, not as\n                # strings, in JSON\n                if (test[1] in test_data) or (\n                        (test[1] == 'True') in test_data):\n                    if len(match) == 1:\n                        response = obj\n                        if response is not None:\n                            response_list.append(response)\n                    else:\n                        match_list = list(match)\n                        match_list.pop(0)\n                        parse_data(release_id, obj, response_list, *match_list)\n            else:\n                parse_data(release_id, obj, response_list, *match2)\n            if INFO:\n                write_log(\n                    release_id,\n                    'info',\n                    'response_list (from value look-up): %s',\n                    response_list)\n            return response_list\n        else:\n            if 'children' in obj:\n                parse_data(release_id, obj['children'], response_list, *match)\n            if INFO:\n                write_log(\n                    release_id,\n                    'info',\n                    'response_list (from children): %s',\n                    response_list)\n            return response_list\n    else:\n        if INFO:\n            write_log(\n                release_id,\n                'info',\n                'response_list (obj is not a list or dict): %s',\n                response_list)\n        return response_list\n\n\ndef create_dict_from_ref_list(options, release_id, ref_list, keys, tags):\n    ref_dict_list = []\n    for refs in ref_list:\n        for ref in refs:\n            parsed_refs = [\n                parse_data(\n                    release_id,\n                    ref,\n                    [],\n                    t,\n                    'text') for t in tags]\n            ref_dict_list.append(dict(zip(keys, parsed_refs)))\n    return ref_dict_list\n\n\ndef get_references_from_file(release_id, path, filename):\n    \"\"\"\n    Lookup Muso Reference.xml or similar\n    :param release_id: name of log file\n    :param path: Reference file path\n    :param filename: Reference file name\n    :return:\n    \"\"\"\n    options = config.setting\n    composer_dict_list = []\n    period_dict_list = []\n    genre_dict_list = []\n    xml_file = None\n    try:\n        xml_file = open(os.path.join(path, filename), encoding=\"utf8\")\n        reply = xml_file.read()\n        xml_file.close()\n        document = _read_xml(QXmlStreamReader(reply))\n        # Composers\n        composer_list = parse_data(\n            release_id, document, [], 'ReferenceDB', 'Composer')\n        keys = ['name', 'sort', 'birth', 'death', 'country', 'core']\n        tags = ['Name', 'Sort', 'Birth', 'Death', 'CountryCode', 'Core']\n        composer_dict_list = create_dict_from_ref_list(\n            options, release_id, composer_list, keys, tags)\n        # Periods\n        period_list = parse_data(\n            release_id,\n            document,\n            [],\n            'ReferenceDB',\n            'ClassicalPeriod')\n        keys = ['name', 'start', 'end']\n        tags = ['Name', 'Start_x0020_Date', 'End_x0020_Date']\n        period_dict_list = create_dict_from_ref_list(\n            options, release_id, period_list, keys, tags)\n        # Genres\n        genre_list = parse_data(\n            release_id,\n            document,\n            [],\n            'ReferenceDB',\n            'ClassicalGenre')\n        keys = ['name']\n        tags = ['Name']\n        genre_dict_list = create_dict_from_ref_list(\n            options, release_id, genre_list, keys, tags)\n\n    except (IOError, FileNotFoundError, UnicodeDecodeError):\n        if options['cwp_muso_genres'] or options['cwp_muso_classical'] or options['cwp_muso_dates'] or options['cwp_muso_periods']:\n            write_log(\n                release_id,\n                'error',\n                'File %s does not exist or is corrupted',\n                os.path.join(\n                    path,\n                    filename))\n    finally:\n        if xml_file:\n            xml_file.close()\n    return {\n            'composers': composer_dict_list,\n            'periods': period_dict_list,\n            'genres': genre_dict_list}\n\n# OPTIONS\n\n\ndef get_preserved_tags():\n    preserved = config.setting[\"preserved_tags\"]\n    if isinstance(preserved, str):\n        preserved = [x.strip() for x in preserved.split(',')]\n    return preserved\n\n\ndef get_options(release_id, album, track):\n    \"\"\"\n    Get the saved options from a release and use them according to flags set on the \"advanced\" tab\n    :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n    :param album: current release\n    :param track: current track\n    :return: None (result is passed via tm)\n    A common function for both Artist and Workparts, so that the first class to process a track will execute\n    this function so that the results are available to both (via a track metadata item)\n    \"\"\"\n    release_status[release_id]['done'] = False\n    set_options = collections.defaultdict(dict)\n    main_sections = ['artists', 'workparts']\n    all_sections = ['artists', 'tag', 'workparts', 'genres']\n    parent_sections = {\n        'artists': 'artists',\n        'tag': 'artists',\n        'workparts': 'workparts',\n        'genres': 'workparts'}\n    # The above needs to be done for legacy reasons - there  are only two tags which store options - artists and workparts\n    # This dates from when there were only two sections\n    # To split these now will create compatibility issues\n    override = {\n        'artists': 'cea_override',\n        'tag': 'ce_tagmap_override',\n        'workparts': 'cwp_override',\n        'genres': 'ce_genres_override'}\n    sect_text = {'artists': 'Artists', 'workparts': 'Works'}\n    prefix = {'artists': 'cea', 'workparts': 'cwp'}\n\n    if album.tagger.config.setting['ce_options_overwrite'] and all(\n            album.tagger.config.setting[override[sect]] for sect in main_sections):\n        set_options[track] = album.tagger.config.setting  # mutable\n    else:\n        set_options[track] = option_settings(\n            album.tagger.config.setting)  # make a copy\n        if set_options[track][\"log_info\"]:\n            write_log(\n                release_id,\n                'info',\n                'Default (i.e. per UI) options for track %s are %r',\n                track,\n                set_options[track])\n\n    # As we use some of the main Picard options and may over-write them, save them here\n    # set_options[track]['translate_artist_names'] = config.setting['translate_artist_names']\n    # set_options[track]['standardize_artists'] = config.setting['standardize_artists']\n    # (not sure this is needed - TODO reconsider)\n\n    options = set_options[track]\n    tm = track.metadata\n    new_metadata = None\n    orig_metadata = None\n    # Only look up files if needed\n    file_options = {}\n    music_file = ''\n    music_file_found = None\n    release_status[release_id]['file_found'] = False\n    start = datetime.now()\n    if options[\"log_info\"]:\n        write_log(release_id, 'info', 'Clock start at %s', start)\n    trackno = tm['tracknumber']\n    discno = tm['discnumber']\n\n    album_filenames = album.tagger.get_files_from_objects([album])\n    if options[\"log_info\"]:\n        write_log(\n            release_id,\n            'info',\n            'No. of album files found = %s',\n            len(album_filenames))\n    # Note that sometimes Picard fails to get all the file objects, even if they are there (network issues)\n    # so we will cache whatever we can get!\n    if release_id in release_status and 'file_objects' in release_status[release_id]:\n        add_list_uniquely(\n            release_status[release_id]['file_objects'],\n            album_filenames)\n    else:\n        release_status[release_id]['file_objects'] = album_filenames\n    if options[\"log_info\"]:\n        write_log(release_id, 'info', 'No. of album files cached = %s',\n                  len(release_status[release_id]['file_objects']))\n    track_file = None\n    for album_file in release_status[release_id]['file_objects']:\n        if options[\"log_info\"]:\n            write_log(release_id,\n                      'info',\n                      'Track file = %s, tracknumber = %s, discnumber = %s. Metadata trackno = %s, discno = %s',\n                      album_file.filename,\n                      str(album_file.tracknumber),\n                      str(album_file.discnumber),\n                      trackno,\n                      discno)\n        if str(\n                album_file.tracknumber) == trackno and str(\n                album_file.discnumber) == discno:\n            if options[\"log_info\"]:\n                write_log(\n                    release_id,\n                    'info',\n                    'Track file found = %r',\n                    album_file.filename)\n            track_file = album_file.filename\n            break\n\n    # Note: It would have been nice to do a rough check beforehand of total tracks,\n    # but ~totalalbumtracks is not yet populated\n    if not track_file:\n        album_fullnames = [\n            x.filename for x in release_status[release_id]['file_objects']]\n        if options[\"log_info\"]:\n            write_log(\n                release_id,\n                'info',\n                'Album files found = %r',\n                album_fullnames)\n        for music_file in album_fullnames:\n            new_metadata = album.tagger.files[music_file].metadata\n\n            if 'musicbrainz_trackid' in new_metadata and 'musicbrainz_trackid' in tm:\n                if new_metadata['musicbrainz_trackid'] == tm['musicbrainz_trackid']:\n                    track_file = music_file\n                    break\n        # Nothing found...\n        if new_metadata and 'musicbrainz_trackid' not in new_metadata:\n            if options['log_warning']:\n                write_log(\n                    release_id,\n                    'warning',\n                    'No trackid in file %s',\n                    music_file)\n        if 'musicbrainz_trackid' not in tm:\n            if options['log_warning']:\n                write_log(\n                    release_id,\n                    'warning',\n                    'No trackid in track %s',\n                    track)\n    #\n    # Note that, on initial load, new_metadata == orig_metadata; but, after refresh, new_metadata will have\n    # the same track metadata as tm (plus the file metadata as per orig_metadata), so a trackid match\n    # is then possible for files that do not have musicbrainz_trackid in orig_metadata. That is why\n    # new_metadata is used in the above test, rather than orig_metadata, but orig_metadata is then used below\n    # to get the saved options.\n    #\n\n    # Find the tag with the options:-\n    if track_file:\n        orig_metadata = album.tagger.files[track_file].orig_metadata\n        music_file_found = track_file\n        if options['log_info']:\n            write_log(\n                release_id,\n                'info',\n                'orig_metadata for file %s is',\n                music_file)\n            write_log(release_id, 'info', orig_metadata)\n        for child_section in all_sections:\n            section = parent_sections[child_section]\n            if options[override[child_section]]:\n                if options[prefix[section] + '_options_tag'] + ':' + \\\n                        section + '_options' in orig_metadata:\n                    file_options[section] = interpret(\n                        orig_metadata[options[prefix[section] + '_options_tag'] + ':' + section + '_options'])\n                elif options[prefix[section] + '_options_tag'] in orig_metadata:\n                    options_tag_contents = orig_metadata[options[prefix[section] + '_options_tag']]\n                    if isinstance(options_tag_contents, list):\n                        options_tag_contents = options_tag_contents[0]\n                    combined_options = ''.join(options_tag_contents.split(\n                        '(workparts_options)')).split('(artists_options)')\n                    for i, _ in enumerate(combined_options):\n                        combined_options[i] = interpret(\n                            combined_options[i].lstrip('; '))\n                        if isinstance(\n                                combined_options[i],\n                                dict) and 'Classical Extras' in combined_options[i]:\n                            if sect_text[section] + \\\n                                    ' options' in combined_options[i]['Classical Extras']:\n                                file_options[section] = combined_options[i]\n                else:\n                    for om in orig_metadata:\n                        if ':' + section + '_options' in om:\n                            file_options[section] = interpret(\n                                orig_metadata[om])\n                if section not in file_options or not file_options[section]:\n                    if options['log_error']:\n                        write_log(\n                            release_id,\n                            'error',\n                            'Saved ' +\n                            section +\n                            ' options cannot be read for file %s. Using current settings',\n                            music_file)\n                    append_tag(\n                        release_id,\n                        tm,\n                        '~' +\n                        prefix[section] +\n                        '_error',\n                        '1. Saved ' +\n                        section +\n                        ' options cannot be read. Using current settings')\n\n        release_status[release_id]['file_found'] = True\n\n    end = datetime.now()\n    if options['log_info']:\n        write_log(release_id, 'info', 'Clock end at %s', end)\n        write_log(release_id, 'info', 'Duration = %s', end - start)\n\n    if not release_status[release_id]['file_found']:\n        if options['log_warning']:\n            write_log(\n                release_id,\n                'warning',\n                \"No file with matching trackid for track %s. IF THERE SHOULD BE ONE, TRY 'REFRESH'\",\n                track)\n        append_tag(\n            release_id,\n            tm,\n            \"002_important_warning\",\n            \"No file with matching trackid - IF THERE SHOULD BE ONE, TRY 'REFRESH' - \"\n            \"(unable to process any saved options, lyrics or 'keep' tags)\")\n        # Nothing else is done with this info as yet - ideally we need to refresh and re-run\n        # for all releases where, say, release_status[release_id]['file_prob']\n        # == True  TODO?\n\n    else:\n        if options['log_info']:\n            write_log(\n                release_id,\n                'info',\n                'Found music file: %r',\n                music_file_found)\n        for section in all_sections:\n            if options[override[section]]:\n                parent_section = parent_sections[section]\n                if parent_section in file_options and file_options[parent_section]:\n                    try:\n                        options_dict = file_options[parent_section]['Classical Extras'][sect_text[parent_section] + ' options']\n                    except TypeError as err:\n                        if options['log_error']:\n                            write_log(\n                                release_id,\n                                'error',\n                                'Error: %s. Saved ' +\n                                section +\n                                ' options cannot be read for file %s. Using current settings',\n                                err,\n                                music_file)\n                        append_tag(\n                            release_id,\n                            tm,\n                            '~' +\n                            prefix[parent_section] +\n                            '_error',\n                            '1. Saved ' +\n                            parent_section +\n                            ' options cannot be read. Using current settings')\n                        break\n                    for opt in options_dict:\n                        if isinstance(\n                                options_dict[opt],\n                                dict) and options[override['tag']]:  # for tag line options\n                            # **NB tag mapping lines are the only entries of type dict**\n                            opt_list = []\n                            for opt_item in options_dict[opt]:\n                                opt_list.append(\n                                    {opt + '_' + opt_item: options_dict[opt][opt_item]})\n                        else:\n                            opt_list = [{opt: options_dict[opt]}]\n                        for opt_dict in opt_list:\n                            for opt_det in opt_dict:\n                                opt_value = opt_dict[opt_det]\n                                addn = []\n                                if section == 'artists':\n                                    addn = plugin_options('picard')\n                                if section == 'tag':\n                                    addn = plugin_options('tag_detail')\n                                for ea_opt in plugin_options(section) + addn:\n                                    displayed_option = options[ea_opt['option']]\n                                    if ea_opt['name'] == opt_det:\n                                        if 'value' in ea_opt:\n                                            if ea_opt['value'] == opt_value:\n                                                options[ea_opt['option']] = True\n                                            else:\n                                                options[ea_opt['option']\n                                                        ] = False\n                                        else:\n                                            options[ea_opt['option']\n                                                    ] = opt_value\n                                        if options[ea_opt['option']\n                                                   ] != displayed_option:\n                                            if options['log_debug'] or options['log_info']:\n                                                write_log(\n                                                    release_id,\n                                                    'info',\n                                                    'Options overridden for option %s = %s',\n                                                    ea_opt['option'],\n                                                    opt_value)\n\n                                            opt_text = str(opt_value)\n                                            append_tag(\n                                                release_id, tm, '003_information:options_overridden', str(\n                                                    ea_opt['name']) + ' = ' + opt_text)\n\n        if orig_metadata:\n            keep_list = options['cea_keep'].split(\",\")\n            if options['cea_split_lyrics'] and options['cea_lyrics_tag']:\n                keep_list.append(options['cea_lyrics_tag'])\n            if options['cwp_genres_use_file']:\n                if 'genre' in orig_metadata:\n                    append_tag(\n                        release_id,\n                        tm,\n                        '~cwp_candidate_genres',\n                        orig_metadata['genre'])\n                if options['cwp_genre_tag'] and options['cwp_genre_tag'] in orig_metadata:\n                    keep_list.append(options['cwp_genre_tag'])\n            really_keep_list = get_preserved_tags()[:]\n            really_keep_list.append(\n                options['cwp_options_tag'] +\n                ':workparts_options')\n            really_keep_list.append(\n                options['cea_options_tag'] +\n                ':artists_options')\n            for tagx in keep_list:\n                tag = tagx.strip()\n                really_keep_list.append(tag)\n                if tag in orig_metadata:\n                    append_tag(release_id, tm, tag, orig_metadata[tag])\n            if options['cea_clear_tags']:\n                delete_list = []\n                for tag_item in orig_metadata:\n                    if tag_item not in really_keep_list and tag_item[0] != '~':\n                        # the second condition is to ensure that (hidden) file variables are not deleted,\n                        #  as these are in orig_metadata, not track_metadata\n                        delete_list.append(tag_item)\n                # this will be used in map_tags to delete unwanted tags\n                options['delete_tags'] = delete_list\n            ## Create a \"mirror\" tag with the old data, for comparison purposes\n            mirror_tags = []\n            for tag_item in orig_metadata:\n                mirror_name = tag_item + '_OLD'\n                if mirror_name[0] == '~' :\n                    mirror_name.replace('~', '_')\n                mirror_name = '~' + mirror_name\n                mirror_tags.append((mirror_name, tag_item))\n                append_tag(release_id, tm, mirror_name, orig_metadata[tag_item])\n            append_tag(release_id, tm, '~ce_mirror_tags', mirror_tags)\n\n        if not isinstance(options, dict):\n            options_dict = option_settings(config.setting)\n            write_log(\n                'session',\n                'info',\n                'Using option_settings(config.setting): %s',\n                options_dict)\n        else:\n            options_dict = options\n            write_log(\n                'session',\n                'info',\n                'Using options: %s',\n                options_dict)\n        tm['~ce_options'] = str(options_dict)\n        tm['~ce_file'] = music_file_found\n\n\ndef plugin_options(option_type):\n    \"\"\"\n    :param option_type: artists, tag, workparts, genres or other\n    :return: the relevant dictionary for the type\n    This function contains all the options data in one place - to prevent multiple repetitions elsewhere\n    \"\"\"\n    if option_type == 'artists':\n        return const.ARTISTS_OPTIONS\n    elif option_type == 'tag':\n        return const.TAG_OPTIONS\n    elif option_type == 'tag_detail':\n        return const.TAG_DETAIL_OPTIONS\n    elif option_type == 'workparts':\n        return const.WORKPARTS_OPTIONS\n    elif option_type == 'genres':\n        return const.GENRE_OPTIONS\n    elif option_type == 'picard':\n        return const.PICARD_OPTIONS\n    elif option_type == 'other':\n        return const.OTHER_OPTIONS\n    else:\n        return None\n\ndef option_settings(config_settings):\n    \"\"\"\n    :param config_settings: options from UI\n    :return: a (deep) copy of the Classical Extras options\n    \"\"\"\n    options = {}\n    for option in plugin_options('artists') + plugin_options('tag') + plugin_options('tag_detail') + plugin_options(\n            'workparts') + plugin_options('genres') + plugin_options('picard') + plugin_options('other'):\n        options[option['option']] = copy.deepcopy(\n            config_settings[option['option']])\n    return options\n\n\ndef get_aliases(self, release_id, album, options, releaseXmlNode):\n    \"\"\"\n    :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n    :param self:\n    :param album:\n    :param options:\n    :param releaseXmlNode: all the metadata for the release\n    :return: Data is returned via self.artist_aliases and self.artist_credits[album]\n\n    Note regarding aliases and credited-as names:\n    In a MB release, an artist can appear in one of seven contexts. Each of these is accessible in releaseXmlNode\n    and the track and recording contexts are also accessible in trackXmlNode.\n    The seven contexts are:\n    Recording: credited-as and alias\n    Release-group: credited-as and alias\n    Release: credited-as and alias\n    Release relationship: credited-as and (not reliably?) alias\n    Recording relationship (direct): credited-as and (not reliably?) alias\n    Recording relationship (via work): credited-as and (not reliably?) alias\n    Track: credited-as and alias\n    (The above are applied in sequence - e.g. track artist credit will over-ride release artist credit. \"Recording\" gets\n    the lowest priority as it is more generic than the release data {may apply to multiple releases})\n    This function collects all the available aliases and as-credited names once (on processing the first track).\n    N.B. if more than one release is loaded in Picard, any available alias names loaded so far will be available\n    and used. However, as-credited names will only be used from the current release.\"\"\"\n\n    lang = get_preferred_artist_language(config)\n    if lang and options['cea_aliases'] or options['cea_aliases_composer']:\n        # Track and recording aliases/credits are gathered by parsing the\n        # media, track and recording nodes\n        # Do the recording relationship first as it may apply to multiple releases, so release and track data\n        # is more specific.\n        media = parse_data(release_id, releaseXmlNode, [], 'media')\n        for m in media:\n            # disc_num = int(parse_data(options, m, [], 'position', 'text')[0])\n            # not currently used\n            tracks = parse_data(release_id, m, [], 'tracks')\n            for track in tracks:\n                for t in track:\n                    # track_num = int(parse_data(options, t, [], 'number',\n                    # 'text')[0]) # not currently used\n\n                    # Recording artists\n                    obj = parse_data(release_id, t, [], 'recording')\n                    get_aliases_and_credits(\n                        self,\n                        options,\n                        release_id,\n                        album,\n                        obj,\n                        lang,\n                        options['cea_recording_credited'])\n\n        # Get the release data before the recording relationshiops and track data\n        # Release group artists\n        obj = parse_data(release_id, releaseXmlNode, [], 'release-group')\n        get_aliases_and_credits(\n            self,\n            options,\n            release_id,\n            album,\n            obj,\n            lang,\n            options['cea_group_credited'])\n\n        # Release artists\n        get_aliases_and_credits(\n            self,\n            options,\n            release_id,\n            album,\n            releaseXmlNode,\n            lang,\n            options['cea_credited'])\n        # Next bit needed to identify artists who are album artists\n        self.release_artists_sort[album] = parse_data(\n            release_id, releaseXmlNode, [], 'artist-credit', 'artist', 'sort-name')\n        # Release relationship artists\n        get_relation_credits(\n            self,\n            options,\n            release_id,\n            album,\n            releaseXmlNode,\n            lang,\n            options['cea_release_relationship_credited'])\n\n        # Now get the rest:\n        for m in media:\n            tracks = parse_data(release_id, m, [], 'tracks')\n            for track in tracks:\n                for t in track:\n                    # Recording relationship artists\n                    obj = parse_data(release_id, t, [], 'recording')\n                    get_relation_credits(\n                        self,\n                        options,\n                        release_id,\n                        album,\n                        obj,\n                        lang,\n                        options['cea_recording_relationship_credited'])\n                    # Track artists\n                    get_aliases_and_credits(\n                        self,\n                        options,\n                        release_id,\n                        album,\n                        t,\n                        lang,\n                        options['cea_track_credited'])\n\n    if options['log_info']:\n        write_log(release_id, 'info', 'Alias and credits info for %s', self)\n        write_log(release_id, 'info', 'Aliases :%s', self.artist_aliases)\n        write_log(\n            release_id,\n            'info',\n            'Credits :%s',\n            self.artist_credits[album])\n\n\ndef get_artists(options, release_id, tm, relations, relation_type):\n    \"\"\"\n    Get artist info from XML lookup\n    :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n    :param options:\n    :param tm:\n    :param relations:\n    :param relation_type: 'release', 'recording' or 'work' (NB 'work' does not pass a param for tm)\n    :return:\n    \"\"\"\n    if options['log_debug'] or options['log_info']:\n        write_log(\n            release_id,\n            'debug',\n            'In get_artists. relation_type: %s, relations: %s',\n            relation_type,\n            relations)\n    log_options = {\n        'log_debug': options['log_debug'],\n        'log_info': options['log_info']}\n    artists = []\n    instruments = []\n    artist_types = const.RELATION_TYPES[relation_type]\n    for artist_type in artist_types:\n        artists, instruments = create_artist_data(release_id, options, log_options, tm, relations,\n                                                  relation_type, artist_type, artists, instruments)\n    artist_dict = {'artists': artists, 'instruments': instruments}\n    return artist_dict\n\n\ndef create_artist_data(release_id, options, log_options, tm, relations,\n                       relation_type, artist_type, artists, instruments):\n    \"\"\"\n    Update the artists and instruments\n    :param release_id: the current album id\n    :param options:\n    :param log_options:\n    :param tm: track metadata\n    :param relations:\n    :param relation_type: release', 'recording' or 'work' (NB 'work' does not pass a param for tm)\n    :param artist_type: from const.RELATION_TYPES[relation_type]\n    :param artists: current artist list - updated with each call\n    :param instruments: current instruments list - updated with each call\n    :return: artists, instruments\n    \"\"\"\n    type_list = parse_data(\n        release_id,\n        relations,\n        [],\n        'target-type:artist',\n        'type:' +\n        artist_type)\n    for type_item in type_list:\n        artist_name_list = parse_data(\n            release_id, type_item, [], 'artist', 'name')\n        artist_sort_name_list = parse_data(\n            release_id, type_item, [], 'artist', 'sort-name')\n        if artist_type not in [\n            'instrument',\n            'vocal',\n            'instrument arranger',\n            'vocal arranger']:\n            instrument_list = None\n            credited_inst_list = None\n        else:\n            instrument_list_list = parse_data(\n                release_id, type_item, [], 'attributes')\n            if instrument_list_list:\n                instrument_list = instrument_list_list[0]\n            else:\n                instrument_list = []\n            credited_inst_list = instrument_list[:]\n            credited_inst_dict_list = parse_data(\n                release_id, type_item, [], 'attribute-credits')  # keyed to insts\n            if credited_inst_dict_list:\n                credited_inst_dict = credited_inst_dict_list[0]\n            else:\n                credited_inst_dict = {}\n            for i, inst in enumerate(instrument_list):\n                if inst in credited_inst_dict:\n                    credited_inst_list[i] = credited_inst_dict[inst]\n\n            if artist_type == 'vocal':\n                if not instrument_list:\n                    instrument_list = ['vocals']\n                elif not any('vocals' in x for x in instrument_list):\n                    instrument_list.append('vocals')\n                    credited_inst_list.append('vocals')\n        # fill the hidden vars before we choose to use the as-credited\n        # version\n        if relation_type != 'work':\n            inst_tag = []\n            cred_tag = []\n            if instrument_list:\n                inst_tag = list(set(instrument_list))\n            if credited_inst_list:\n                cred_tag = list(set(credited_inst_list))\n            for attrib in ['solo', 'guest', 'additional']:\n                if attrib in inst_tag:\n                    inst_tag.remove(attrib)\n                if attrib in cred_tag:\n                    cred_tag.remove(attrib)\n            if inst_tag:\n                if tm['~cea_instruments']:\n                    tm['~cea_instruments'] = add_list_uniquely(\n                        tm['~cea_instruments'], inst_tag)\n                else:\n                    tm['~cea_instruments'] = inst_tag\n            if cred_tag:\n                if tm['~cea_instruments_credited']:\n                    tm['~cea_instruments_credited'] = add_list_uniquely(\n                        tm['~cea_instruments_credited'], cred_tag)\n                else:\n                    tm['~cea_instruments_credited'] = cred_tag\n            if inst_tag or cred_tag:\n                if tm['~cea_instruments_all']:\n                    tm['~cea_instruments_all'] = add_list_uniquely(\n                        tm['~cea_instruments_all'], list(set(inst_tag + cred_tag)))\n                else:\n                    tm['~cea_instruments_all'] = list(\n                        set(inst_tag + cred_tag))\n        if '~cea_instruments' in tm and '~cea_instruments_credited' in tm and '~cea_instruments_all' in tm:\n            instruments = [\n                tm['~cea_instruments'],\n                tm['~cea_instruments_credited'],\n                tm['~cea_instruments_all']]\n        if options['cea_inst_credit'] and credited_inst_list:\n            instrument_list = credited_inst_list\n        if instrument_list:\n            instrument_sort = 3\n            s_key = {\n                'lead vocals': 1,\n                'solo': 2,\n                'guest': 4,\n                'additional': 5}\n            for inst in s_key:\n                if inst in instrument_list:\n                    instrument_sort = s_key[inst]\n        else:\n            instrument_sort = 0\n\n        if artist_type in const.ARTIST_TYPE_ORDER:\n            type_sort = const.ARTIST_TYPE_ORDER[artist_type]\n        else:\n            type_sort = 99\n            if log_options['log_error']:\n                write_log(\n                    release_id,\n                    'error',\n                    \"Error in artist type. Type '%s' is not in ARTIST_TYPE_ORDER dictionary\",\n                    artist_type)\n\n        artist = (\n            artist_type,\n            instrument_list,\n            artist_name_list,\n            artist_sort_name_list,\n            instrument_sort,\n            type_sort)\n        artists.append(artist)\n        # Sorted by sort name then instrument_sort then artist type\n        artists = sorted(artists, key=lambda x: (x[5], x[3], x[4], x[1]))\n        if log_options['log_info']:\n            write_log(release_id, 'info', 'sorted artists = %s', artists)\n    return artists, instruments\n\n\ndef get_series(options, release_id, relations):\n    \"\"\"\n    Get series info (depends on lookup having used inc=series-rel)\n    :param options:\n    :param release_id:\n    :param relations:\n    :return:\n    \"\"\"\n    # if options['log_debug'] or options['log_info']:\n    #     write_log(\n    #         release_id,\n    #         'debug',\n    #         'In get_series.  relations: %s',\n    #         relations)\n    # series_name_list =[]\n    # series_id_list = []\n    # for series_rels in relations:\n    #     series_rel = parse_data(\n    #         release_id,\n    #         series_rels,\n    #         [],\n    #         'target-type:series',\n    #         'type:part-of')\n    #     if options['log_debug'] or options['log_info']:\n    #         write_log(\n    #             release_id,\n    #             'debug',\n    #             'series_rel =  %s',\n    #             series_rel)\n    #     series_name_list.extend(\n    #         parse_data(release_id, series_rel, [], 'series', 'name')\n    #     )\n    #     series_id_list.extend(\n    #         parse_data(release_id, series_rel, [], 'series', 'id')\n    #     )\n    type_list = parse_data(\n        release_id,\n        relations,\n        [],\n        'target-type:series',\n        'type:part of')\n    if type_list:\n        series_name_list = []\n        series_id_list = []\n        series_number_list = []\n        for type_item in type_list:\n            series_name_list = parse_data(\n                release_id, type_item, [], 'series', 'name')\n            series_id_list = parse_data(\n                release_id, type_item, [], 'series', 'id')\n            series_number_list = parse_data(\n                release_id, type_item, [], 'attribute-values', 'number')\n        return {'name_list': series_name_list, 'id_list': series_id_list, 'number_list': series_number_list}\n    else:\n        return None\n\n\n\ndef apply_artist_style(\n        options,\n        release_id,\n        lang,\n        a_list,\n        name_style,\n        name_tag,\n        sort_tag,\n        names_tag,\n        names_sort_tag):\n    # Get  artist and apply style\n    for a_item in a_list:\n        for acs in a_item:\n            artistlist = parse_data(release_id, acs, [], 'name')\n            sortlist = parse_data(release_id, acs, [], 'artist', 'sort-name')\n            names = {}\n            if lang:\n                names['alias'] = parse_data(\n                    release_id,\n                    acs,\n                    [],\n                    'artist',\n                    'aliases',\n                    'locale:' + lang,\n                    'primary:True',\n                    'name')\n            else:\n                names['alias'] = []\n            names['credit'] = parse_data(release_id, acs, [], 'name')\n            pairslist = list(zip(artistlist, sortlist))\n            names['sort'] = [\n                translate_from_sortname(\n                    *pair) for pair in pairslist]\n            for style in name_style:\n                if names[style]:\n                    artistlist = names[style]\n                    break\n            joinlist = parse_data(release_id, acs, [], 'joinphrase')\n\n            if artistlist:\n                name_tag.append(artistlist[0])\n                sort_tag.append(sortlist[0])\n                names_tag.append(artistlist[0])\n                names_sort_tag.append(sortlist[0])\n\n            if joinlist:\n                name_tag.append(joinlist[0])\n                sort_tag.append(joinlist[0])\n\n    name_tag_str = ''.join(name_tag)\n    sort_tag_str = ''.join(sort_tag)\n\n    return {\n        'artists': names_tag,\n        'artists_sort': names_sort_tag,\n        'artist': name_tag_str,\n        'artistsort': sort_tag_str}\n\n\ndef set_work_artists(self, release_id, album, track, writerList, tm, count):\n    \"\"\"\n    :param release_id:\n    :param self is the calling object from Artists or WorkParts\n    :param album: the current album\n    :param track: the current track\n    :param writerList: format [(artist_type, [instrument_list], [name list],[sort_name list]),(.....etc]\n    :param tm: track metadata\n    :param count: depth count of recursion in process_work_artists (should equate to part level)\n    :return:\n    \"\"\"\n\n    options = self.options[track]\n    if not options['classical_work_parts']:\n        caller = 'ExtraArtists'\n        pre = '~cea'\n    else:\n        caller = 'PartLevels'\n        pre = '~cwp'\n    write_log(\n        release_id,\n        'debug',\n        'Class: %s: in set_work_artists for track %s. Count (level) is %s. Writer list is %s',\n        caller,\n        track,\n        count,\n        writerList)\n    # tag strings are a tuple (Picard tag, cwp tag, Picard sort tag, cwp sort\n    # tag) (NB this is modelled on set_performer)\n    tag_strings = const.tag_strings(pre)\n    # insertions lists artist types where names in the main Picard tags may be\n    # updated for annotations\n    insertions = const.INSERTIONS\n    no_more_lyricists = False\n    if caller == 'PartLevels' and self.lyricist_filled[track]:\n        no_more_lyricists = True\n\n    for writer in writerList:\n        writer_type = writer[0]\n        if writer_type not in tag_strings:\n            break\n        if no_more_lyricists and (\n                writer_type == 'lyricist' or writer_type == 'librettist'):\n            break\n        if writer[1]:\n            inst_list = writer[1][:]\n            # take a copy of the list in case (because of list\n            # mutability) we need the old one\n            instrument = \", \".join(inst_list)\n        else:\n            instrument = None\n        sub_strings = {  # 'instrument arranger': instrument, 'vocal arranger': instrument\n        }\n        if options['cea_arranger']:\n            if instrument:\n                arr_inst = options['cea_arranger'] + ' ' + instrument\n            else:\n                arr_inst = options['cea_arranger']\n        else:\n            arr_inst = instrument\n        annotations = {'writer': options['cea_writer'],\n                       'lyricist': options['cea_lyricist'],\n                       'librettist': options['cea_librettist'],\n                       'revised by': options['cea_revised'],\n                       'translator': options['cea_translator'],\n                       'arranger': options['cea_arranger'],\n                       'reconstructed by': options['cea_reconstructed'],\n                       'orchestrator': options['cea_orchestrator'],\n                       'instrument arranger': arr_inst,\n                       'vocal arranger': arr_inst}\n        tag = tag_strings[writer_type][0]\n        sort_tag = tag_strings[writer_type][2]\n        cwp_tag = tag_strings[writer_type][1]\n        cwp_sort_tag = tag_strings[writer_type][3]\n        cwp_names_tag = cwp_tag[:-1] + '_names'\n        cwp_instrumented_tag = cwp_names_tag + '_instrumented'\n        if writer_type in sub_strings:\n            if sub_strings[writer_type]:\n                tag += sub_strings[writer_type]\n        if tag:\n            if '~ce_tag_cleared_' + \\\n                    tag not in tm or not tm['~ce_tag_cleared_' + tag] == \"Y\":\n                if tag in tm:\n                    if options['log_info']:\n                        write_log(release_id, 'info', 'delete tag %s', tag)\n                    del tm[tag]\n            tm['~ce_tag_cleared_' + tag] = \"Y\"\n        if sort_tag:\n            if '~ce_tag_cleared_' + \\\n                    sort_tag not in tm or not tm['~ce_tag_cleared_' + sort_tag] == \"Y\":\n                if sort_tag in tm:\n                    del tm[sort_tag]\n            tm['~ce_tag_cleared_' + sort_tag] = \"Y\"\n\n        name_list = writer[2]\n        for ind, name in enumerate(name_list):\n            sort_name = writer[3][ind]\n            no_credit = True\n            write_log(\n                    release_id,\n                    'info',\n                    'In set_work_artists. Name before changes = %s',\n                    name)\n            # change name to as-credited\n            if options['cea_composer_credited']:\n                if album in self.artist_credits and sort_name in self.artist_credits[album]:\n                    no_credit = False\n                    name = self.artist_credits[album][sort_name]\n            # over-ride with aliases if appropriate\n            if (options['cea_aliases'] or options['cea_aliases_composer']) and (\n                    no_credit or options['cea_alias_overrides']):\n                if sort_name in self.artist_aliases:\n                    name = self.artist_aliases[sort_name]\n            # fix cyrillic names if not already fixed\n            if options['cea_cyrillic']:\n                if not only_roman_chars(name):\n                    name = remove_middle(unsort(sort_name))\n                    # Only remove middle name where the existing\n                    # performer is in non-latin script\n            annotated_name = name\n            write_log(\n                    release_id,\n                    'info',\n                    'In set_work_artists. Name after changes = %s',\n                    name)\n            # add annotations and write performer tags\n            if writer_type in annotations:\n                if annotations[writer_type]:\n                    annotated_name += ' (' + annotations[writer_type] + ')'\n            if instrument:\n                instrumented_name = name + ' (' + instrument + ')'\n            else:\n                instrumented_name = name\n\n            if writer_type in insertions and options['cea_arrangers']:\n                self.append_tag(release_id, tm, tag, annotated_name)\n            else:\n                if options['cea_arrangers'] or writer_type == tag:\n                    self.append_tag(release_id, tm, tag, name)\n\n            if options['cea_arrangers'] or writer_type == tag:\n                if sort_tag:\n                    self.append_tag(release_id, tm, sort_tag, sort_name)\n                    if options['cea_tag_sort'] and '~' in sort_tag:\n                        explicit_sort_tag = sort_tag.replace('~', '')\n                        self.append_tag(\n                            release_id, tm, explicit_sort_tag, sort_name)\n            self.append_tag(release_id, tm, cwp_tag, annotated_name)\n            self.append_tag(release_id, tm, cwp_names_tag, name)\n            if instrumented_name != name:\n                self.append_tag(\n                    release_id,\n                    tm,\n                    cwp_instrumented_tag,\n                    instrumented_name)\n\n            if cwp_sort_tag:\n                self.append_tag(release_id, tm, cwp_sort_tag, sort_name)\n\n            if caller == 'PartLevels' and (\n                    writer_type == 'lyricist' or writer_type == 'librettist'):\n                self.lyricist_filled[track] = True\n                write_log(\n                        release_id,\n                        'info',\n                        'Filled lyricist for track %s. Not looking further',\n                        track)\n\n            if writer_type == 'composer':\n                composerlast = sort_name.split(\",\")[0]\n                write_log(\n                        release_id,\n                        'info',\n                        'composerlast = %s',\n                        composerlast)\n                self.append_tag(\n                    release_id,\n                    tm,\n                    pre +\n                    '_composer_lastnames',\n                    composerlast)\n                if sort_name in self.release_artists_sort[album]:\n                    self.append_tag(\n                        release_id, tm, '~cea_album_composers', name)\n                    self.append_tag(\n                        release_id, tm, '~cea_album_composers_sort', sort_name)\n                    self.append_tag(\n                        release_id,\n                        tm,\n                        '~cea_album_track_composer_lastnames',\n                        composerlast)\n                    composer_last_names(self, release_id, tm, album)\n\n\n# Non-Latin character processing\nlatin_letters = {}\n\ndef is_latin(uchr):\n    \"\"\"Test whether character is in Latin script\"\"\"\n    try:\n        return latin_letters[uchr]\n    except KeyError:\n        return latin_letters.setdefault(\n            uchr, 'LATIN' in unicodedata.name(uchr))\n\n\ndef only_roman_chars(unistr):\n    \"\"\"Test whether string is in Latin script\"\"\"\n    return all(is_latin(uchr)\n               for uchr in unistr\n               if uchr.isalpha())\n\n\ndef get_roman(string):\n    \"\"\"Transliterate cyrillic script to Latin script\"\"\"\n    translit_string = \"\"\n    for index, char in enumerate(string):\n        if char in const.CYRILLIC_LOWER.keys():\n            char = const.CYRILLIC_LOWER[char]\n        elif char in const.CYRILLIC_UPPER.keys():\n            char = const.CYRILLIC_UPPER[char]\n            if string[index + 1] not in const.CYRILLIC_LOWER.keys():\n                char = char.upper()\n        translit_string += char\n    # fix multi-chars\n    translit_string = translit_string.replace('ks', 'x').replace('iy ', 'i ')\n    return translit_string\n\n\ndef remove_middle(performer):\n    \"\"\"To remove middle names of Russian composers\"\"\"\n    plist = performer.split()\n    if len(plist) == 3:\n        return plist[0] + ' ' + plist[2]\n    else:\n        return performer\n\n\n# Sorting etc.\n\ndef unsort(performer):\n    \"\"\"\n    To take a sort field and recreate the name\n    Only now used for last-ditch cyrillic translation - superseded by 'translate_from_sortname'\n    \"\"\"\n    sorted_list = performer.split(', ')\n    sorted_list.reverse()\n    for i, item in enumerate(sorted_list):\n        if item[-1] != \"'\":\n            sorted_list[i] += ' '\n    return ''.join(sorted_list).strip()\n\n\ndef _reverse_sortname(sortname):\n    \"\"\"\n    Reverse sortnames.\n    Code is from picard/util/__init__.py\n    \"\"\"\n\n    chunks = [a.strip() for a in sortname.split(\",\")]\n    chunk_len = len(chunks)\n    if chunk_len == 2:\n        return \"%s %s\" % (chunks[1], chunks[0])\n    elif chunk_len == 3:\n        return \"%s %s %s\" % (chunks[2], chunks[1], chunks[0])\n    elif chunk_len == 4:\n        return \"%s %s, %s %s\" % (chunks[1], chunks[0], chunks[3], chunks[2])\n    else:\n        return sortname.strip()\n\n\ndef stripsir(performer):\n    \"\"\"\n    Remove honorifics from names\n    Also standardize hyphens and apostrophes in names\n    \"\"\"\n    performer = performer.replace(u'\\u2010', u'-').replace(u'\\u2019', u\"'\")\n    sir = re.compile(r'(.*)\\b(Sir|Maestro|Dame)\\b\\s*(.*)', re.IGNORECASE)\n    match = sir.search(performer)\n    if match:\n        return match.group(1) + match.group(3)\n    else:\n        return performer\n\n\n# def swap_prefix(performer):\n#     \"\"\"NOT CURRENTLY USED. Create sort fields for ensembles etc., by placing the prefix (see constants) at the end\"\"\"\n#     prefix = '|'.join(prefixes)\n#     swap = re.compile(r'^(' + prefix + r')\\b\\s*(.*)', re.IGNORECASE)\n#     match = swap.search(performer)\n#     if match:\n#         return match.group(2) + \", \" + match.group(1)\n#     else:\n#         return performer\n\n\ndef replace_roman_numerals(s):\n    \"\"\"Replaces roman numerals include in s, where followed by certain punctuation, by digits\"\"\"\n    romans = RE_ROMANS.findall(s)\n    for roman in romans:\n        if roman[0]:\n            numerals = str(roman[0])\n            digits = str(from_roman(numerals))\n            to_replace = r'\\b' + roman[0] + r'\\b'\n            s = re.sub(to_replace, digits, s)\n    return s\n\n\ndef from_roman(s):\n    romanNumeralMap = (('M', 1000),\n                       ('CM', 900),\n                       ('D', 500),\n                       ('CD', 400),\n                       ('C', 100),\n                       ('XC', 90),\n                       ('L', 50),\n                       ('XL', 40),\n                       ('X', 10),\n                       ('IX', 9),\n                       ('V', 5),\n                       ('IV', 4),\n                       ('I', 1),\n                       ('m', 1000),\n                       ('cm', 900),\n                       ('d', 500),\n                       ('cd', 400),\n                       ('c', 100),\n                       ('xc', 90),\n                       ('l', 50),\n                       ('xl', 40),\n                       ('x', 10),\n                       ('ix', 9),\n                       ('v', 5),\n                       ('iv', 4),\n                       ('i', 1))\n    result = 0\n    index = 0\n    for numeral, integer in romanNumeralMap:\n        while s[index:index + len(numeral)] == numeral:\n            result += integer\n            index += len(numeral)\n    return result\n\n\ndef turbo_lcs(release_id, multi_list):\n    \"\"\"\n    Picks the best longest common string method to use\n    Works with a list of lists or a list of strings\n    :param release_id:\n    :param multi_list: a list of strings or a list of lists\n    :return: longest common substring/list\n    \"\"\"\n    write_log(release_id, 'debug', 'In turbo_lcs')\n    if not isinstance(multi_list, list):\n        return None\n    list_sum = sum([len(x) for x in multi_list])\n    list_len = len(multi_list)\n    if list_len < 2:\n        if list_len == 1:\n            return multi_list[0]  # Nothing to do!\n        else:\n            return []\n    # for big matches, use the generalised suffix tree method\n    if ((list_sum / list_len) ** 2) * list_len > 1000:\n        # heuristic: may need to tweak the 1000 in the light of results\n        lcs_dict = suffixtree.multi_lcs(multi_list)\n        # NB suffixtree may be shown as an unresolved reference in the IDE,\n        # but it should work provided it is included in the package\n        if \"error\" not in lcs_dict:\n            if \"response\" in lcs_dict:\n                write_log(\n                        release_id,\n                        'info',\n                        'Longest common string was returned from suffix tree algo')\n                return lcs_dict['response']\n\n     ## If suffix tree fails, write errors to log before proceeding with alternative\n            else:\n                write_log(\n                        release_id,\n                        'error',\n                        'Suffix tree failure for release %s. Error unknown. Using standard lcs algo instead',\n                        release_id)\n        else:\n            write_log(\n                    release_id,\n                    'error',\n                    'Suffix tree failure for release %s. Error message: %s. Using standard lcs algo instead',\n                    release_id,\n                    lcs_dict['error'])\n    # otherwise, or if gst fails, use the standard algorithm\n    first = True\n    common = []\n    for item in multi_list:\n        if first:\n            common = item\n            first = False\n        else:\n            lcs = longest_common_substring(\n                item, common)\n            common = lcs['string']\n    write_log(release_id, 'debug', 'LCS returned from standard algo')\n    return common\n\n\ndef longest_common_substring(s1, s2):\n    \"\"\"\n    Standard lcs algo for short strings, or if suffix tree does not work\n    :param s1: substring 1\n    :param s2: substring 2\n    :return: {'string': the longest common substring,\n        'start': the start position in s1,\n        'length': the length of the common substring}\n    NB this also works on list arguments - i.e. it will find the longest common sub-list\n    \"\"\"\n    m = [[0] * (1 + len(s2)) for i in range(1 + len(s1))]\n    longest, x_longest = 0, 0\n    for x in range(1, 1 + len(s1)):\n        for y in range(1, 1 + len(s2)):\n            if s1[x - 1] == s2[y - 1]:\n                m[x][y] = m[x - 1][y - 1] + 1\n                if m[x][y] > longest:\n                    longest = m[x][y]\n                    x_longest = x\n            else:\n                m[x][y] = 0\n    return {'string': s1[x_longest - longest: x_longest],\n            'start': x_longest - longest, 'length': longest}\n\n\ndef longest_common_sequence(list1, list2, minstart=0, maxstart=0):\n    \"\"\"\n    :param list1: list 1\n    :param list2: list 2\n    :param minstart: the earliest point to start looking for a match\n    :param maxstart: the latest point to start looking for a match\n    :return: {'sequence': the common subsequence, 'length': length of subsequence}\n    maxstart must be >= minstart. If they are equal then the start point is fixed.\n    Note that this only finds subsequences starting at the same position\n    Use longest_common_substring for the more general problem\n    \"\"\"\n    if maxstart < minstart:\n        return None, 0\n    min_len = min(len(list1), len(list2))\n    longest = 0\n    seq = None\n    maxstart = min(maxstart, min_len) + 1\n    for k in range(minstart, maxstart):\n        for i in range(k, min_len + 1):\n            if list1[k:i] == list2[k:i] and i - k > longest:\n                longest = i - k\n                seq = list1[k:i]\n    return {'sequence': seq, 'length': longest}\n\n\ndef substart_finder(mylist, pattern):\n    for i, list_item in enumerate(mylist):\n        if list_item == pattern[0] and mylist[i:i + len(pattern)] == pattern:\n            return i\n    return len(mylist)  # if nothing found\n\n\ndef get_ui_tags():\n## Determine tags for display in ui\n    options = config.setting\n    ui_tags_raw = options['ce_ui_tags']\n    ui_tags = {}\n    ui_tags_split = [x.replace('(','').strip(') ') for x in ui_tags_raw.split('/')]\n    for ui_column in ui_tags_split:\n        if ':'  in ui_column:\n            ui_col_parts = [x.strip() for x in ui_column.split(':')]\n            heading = ui_col_parts[0]\n            tag_names = ui_col_parts[1].split(',')\n            tag_names = [x.strip() for x in tag_names]\n            ui_tags[heading] = tuple(tag_names)\n    return ui_tags\n\n\ndef map_tags(options, release_id, album, tm):\n    \"\"\"\n    Do the common tag processing - including for the genres and tag-mapping sections\n    :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n    :param options: options passed from either Artists or Workparts\n    :param album:\n    :param tm: track metadata\n    :return: None - action is through setting tm contents\n    This is a common function for Artists and Workparts which should only run after both sections have completed for\n    a given track. If, say, Artists calls it and Workparts is not done,\n    then it will not execute until Workparts calls it (and vice versa).\n    \"\"\"\n\n    write_log(release_id, 'debug', 'In map_tags, checking readiness...')\n    if (options['classical_extra_artists'] and '~cea_artists_complete' not in tm) or (\n            options['classical_work_parts'] and '~cea_works_complete' not in tm):\n        write_log(release_id, 'info', '...not ready')\n        return\n    write_log(release_id, 'debug', '... processing tag mapping')\n\n    # blank tags\n    blank_tags = options['cea_blank_tag'].split(\n        \",\") + options['cea_blank_tag_2'].split(\",\")\n    if 'artists_sort' in [x.strip() for x in blank_tags]:\n        blank_tags.append('~artists_sort')\n    for tag in blank_tags:\n        if tag.strip() in tm:\n            # place blanked tags into hidden variables available for\n            # re-use\n            tm['~cea_' + tag.strip()] = tm[tag.strip()]\n            del tm[tag.strip()]\n\n    # album\n    if tm['~cea_album_composer_lastnames']:\n        last_names = str_to_list(tm['~cea_album_composer_lastnames'])\n        if options['cea_composer_album']:\n            # save it as a list to prevent splitting when appending tag\n            tm['~cea_release'] = [tm['album']]\n            new_last_names = []\n            for last_name in last_names:\n                last_name = last_name.strip()\n                new_last_names.append(last_name)\n            if len(new_last_names) > 0:\n                tm['album'] = \"; \".join(new_last_names) + \": \" + tm['album']\n\n    # remove lyricists if no vocals, according to option set\n    if options['cea_no_lyricists'] and not any(\n            [x for x in str_to_list(tm['~cea_performers']) if 'vocals' in x]):\n        if 'lyricist' in tm:\n            del tm['lyricist']\n        for lyricist_tag in ['lyricists', 'librettists', 'translators']:\n            if '~cwp_' + lyricist_tag in tm:\n                del tm['~cwp_' + lyricist_tag]\n\n    # genres\n    if config.setting['folksonomy_tags'] and 'genre' in tm:\n        candidate_genres = str_to_list(tm['genre'])\n        append_tag(release_id, tm, '~cea_candidate_genres', candidate_genres)\n        # to avoid confusion as it will contain unmatched folksonomy tags\n        del tm['genre']\n    else:\n        candidate_genres = []\n    is_classical = False\n    composers_not_found = []\n    composer_found = False\n    composer_born_list = []\n    composer_died_list = []\n    arrangers_not_found = []\n    arranger_found = False\n    arranger_born_list = []\n    arranger_died_list = []\n    no_composer_in_metadata = False\n    if options['cwp_use_muso_refdb'] and options['cwp_muso_classical'] or options['cwp_muso_dates']:\n        if COMPOSER_DICT:\n            composersort_list = []\n            if '~cwp_composer_names' in tm:\n                composer_list = str_to_list(tm['~cwp_composer_names'])\n            else:\n                # maybe there were no works linked,\n                # but it might still a classical track (based on composer name)\n                no_composer_in_metadata = True\n                composer_list = str_to_list(tm['artists'])\n                composersort_list = str_to_list(tm['~artists_sort'])\n                write_log(release_id, 'info', \"No composer metadata for track %s. Using artists %r\", tm['title'],\n                          composer_list)\n            lc_composer_list = [c.lower() for c in composer_list]\n            for ind, composer in enumerate(lc_composer_list):\n                for classical_composer in COMPOSER_DICT:\n                    if composer in classical_composer['lc_name']:\n                        if options['cwp_muso_classical']:\n                            candidate_genres.append('Classical')\n                            is_classical = True\n                        if options['cwp_muso_dates']:\n                            composer_born_list = classical_composer['birth']\n                            composer_died_list = classical_composer['death']\n                        composer_found = True\n                        if no_composer_in_metadata:\n                            composersort = composersort_list[ind]\n                            append_tag(release_id, tm, 'composer', composer_list[ind])\n                            append_tag(release_id, tm, '~cwp_composer_names', composer_list[ind])\n                            append_tag(release_id, tm, 'composersort', composersort)\n                            append_tag(release_id, tm, '~cwp_composers_sort', composersort)\n                            append_tag(release_id, tm, '~cwp_composer_lastnames', composersort.split(', ')[0])\n                        break\n                if not composer_found:\n                    composer_index = lc_composer_list.index(composer)\n                    orig_composer = composer_list[composer_index]\n                    composers_not_found.append(orig_composer)\n                    append_tag(\n                        release_id,\n                        tm,\n                        '~cwp_unrostered_composers',\n                        orig_composer)\n            if composers_not_found:\n                append_tag(\n                    release_id,\n                    tm,\n                    '003_information:composers',\n                    'Composer(s) ' +\n                    list_to_str(composers_not_found) +\n                    ' not found in reference database of classical composers')\n\n            # do the same for arrangers, if required\n            if options['cwp_genres_arranger_as_composer'] or options['cwp_periods_arranger_as_composer']:\n                arranger_list = str_to_list(\n                    tm['~cea_arranger_names']) + str_to_list(tm['~cwp_arranger_names'])\n                lc_arranger_list = [c.lower() for c in arranger_list]\n                for arranger in lc_arranger_list:\n                    for classical_arranger in COMPOSER_DICT:\n                        if arranger in classical_arranger['lc_name']:\n                            if options['cwp_muso_classical'] and options['cwp_genres_arranger_as_composer']:\n                                candidate_genres.append('Classical')\n                                is_classical = True\n                            if options['cwp_muso_dates'] and options['cwp_periods_arranger_as_composer']:\n                                arranger_born_list = classical_arranger['birth']\n                                arranger_died_list = classical_arranger['death']\n                            arranger_found = True\n                            break\n                    if not arranger_found:\n                        arranger_index = lc_arranger_list.index(arranger)\n                        orig_arranger = arranger_list[arranger_index]\n                        arrangers_not_found.append(orig_arranger)\n                        append_tag(\n                            release_id,\n                            tm,\n                            '~cwp_unrostered_arrangers',\n                            orig_arranger)\n                if arrangers_not_found:\n                    append_tag(\n                        release_id,\n                        tm,\n                        '003_information:arrangers',\n                        'Arranger(s) ' +\n                        list_to_str(arrangers_not_found) +\n                        ' not found in reference database of classical composers')\n\n        else:\n            append_tag(\n                release_id,\n                tm,\n                '001_errors:8',\n                '8. No composer reference file. Check log for error messages re path name.')\n\n    if options['cwp_use_muso_refdb'] and options['cwp_muso_genres'] and GENRE_DICT:\n        main_classical_genres_list = [list_to_str(\n            mg['name']).strip() for mg in GENRE_DICT]\n    else:\n        main_classical_genres_list = [\n            sg.strip() for sg in options['cwp_genres_classical_main'].split(',')]\n    sub_classical_genres_list = [\n        sg.strip() for sg in options['cwp_genres_classical_sub'].split(',')]\n    main_other_genres_list = [\n        sg.strip() for sg in options['cwp_genres_other_main'].split(',')]\n    sub_other_genres_list = [sg.strip()\n                             for sg in options['cwp_genres_other_sub'].split(',')]\n    main_classical_genres = []\n    sub_classical_genres = []\n    main_other_genres = []\n    sub_other_genres = []\n    if '~cea_work_type' in tm:\n        candidate_genres += str_to_list(tm['~cea_work_type'])\n    if '~cwp_candidate_genres' in tm:\n        candidate_genres += str_to_list(tm['~cwp_candidate_genres'])\n    write_log(release_id, 'info', \"Candidate genres: %r\", candidate_genres)\n    untagged_genres = []\n    if candidate_genres:\n        main_classical_genres = [\n            val for val in main_classical_genres_list if val.lower() in [\n                genre.lower() for genre in candidate_genres]]\n        sub_classical_genres = [\n            val for val in sub_classical_genres_list if val.lower() in [\n                genre.lower() for genre in candidate_genres]]\n\n        if main_classical_genres or sub_classical_genres or options['cwp_genres_classical_all']:\n            is_classical = True\n            main_classical_genres.append('Classical')\n            candidate_genres.append('Classical')\n            write_log(release_id, 'info', \"Main classical genres for track %s: %r\", tm['title'], main_classical_genres)\n            candidate_genres += str_to_list(tm['~cea_work_type_if_classical'])\n            # next two are repeated statements, but a separate fn would be\n            # clumsy too!\n            main_classical_genres = [\n                val for val in main_classical_genres_list if val.lower() in [\n                    genre.lower() for genre in candidate_genres]]\n            sub_classical_genres = [\n                val for val in sub_classical_genres_list if val.lower() in [\n                    genre.lower() for genre in candidate_genres]]\n        if options['cwp_genres_classical_exclude']:\n            main_classical_genres = [\n                g for g in main_classical_genres if g.lower() != 'classical']\n\n        main_other_genres = [\n            val for val in main_other_genres_list if val.lower() in [\n                genre.lower() for genre in candidate_genres]]\n        sub_other_genres = [\n            val for val in sub_other_genres_list if val.lower() in [\n                genre.lower() for genre in candidate_genres]]\n        all_genres = main_classical_genres + sub_classical_genres + \\\n            main_other_genres + sub_other_genres\n        untagged_genres = [\n            un for un in candidate_genres if un.lower() not in [\n                genre.lower() for genre in all_genres]]\n\n    if options['cwp_genre_tag']:\n        if not options['cwp_genres_filter']:\n            append_tag(\n                release_id,\n                tm,\n                options['cwp_genre_tag'],\n                candidate_genres)\n        else:\n            append_tag(\n                release_id,\n                tm,\n                options['cwp_genre_tag'],\n                main_classical_genres +\n                main_other_genres)\n    if options['cwp_subgenre_tag'] and options['cwp_genres_filter']:\n        append_tag(\n            release_id,\n            tm,\n            options['cwp_subgenre_tag'],\n            sub_classical_genres +\n            sub_other_genres)\n    if is_classical and options['cwp_genres_flag_text'] and options['cwp_genres_flag_tag']:\n        tm[options['cwp_genres_flag_tag']] = options['cwp_genres_flag_text']\n    if not (\n            main_classical_genres +\n            main_other_genres)and options['cwp_genres_filter']:\n        if options['cwp_genres_default']:\n            append_tag(\n                release_id,\n                tm,\n                options['cwp_genre_tag'],\n                options['cwp_genres_default'])\n        else:\n            if options['cwp_genre_tag'] in tm:\n                del tm[options['cwp_genre_tag']]\n    if untagged_genres and options['cwp_genres_filter']:\n        append_tag(\n            release_id,\n            tm,\n            '003_information:genres',\n            'Candidate genres found but not matched: ' +\n            list_to_str(untagged_genres))\n        append_tag(release_id, tm, '~cwp_untagged_genres', untagged_genres)\n\n    # instruments and keys\n    if options['cwp_instruments_MB_names'] and options['cwp_instruments_credited_names'] and tm['~cea_instruments_all']:\n        instruments = str_to_list(tm['~cea_instruments_all'])\n    elif options['cwp_instruments_MB_names'] and tm['~cea_instruments']:\n        instruments = str_to_list(tm['~cea_instruments'])\n    elif options['cwp_instruments_credited_names'] and tm['~cea_instruments_credited']:\n        instruments = str_to_list(tm['~cea_instruments_credited'])\n    else:\n        instruments = None\n    if instruments and options['cwp_instruments_tag']:\n        append_tag(release_id, tm, options['cwp_instruments_tag'], instruments)\n        # need to append rather than over-write as it may be the same as\n        # another tag (e.g. genre)\n    if tm['~cwp_keys'] and options['cwp_key_tag']:\n        append_tag(release_id, tm, options['cwp_key_tag'], tm['~cwp_keys'])\n    # dates\n    if options['cwp_workdate_annotate']:\n        comp = ' (composed)'\n        publ = ' (published)'\n        prem = ' (premiered)'\n    else:\n        comp = ''\n        publ = ''\n        prem = ''\n    tm[options['cwp_workdate_tag']] = ''\n    earliest_date = 9999\n    latest_date = -9999\n    found = False\n    if tm['~cwp_composed_dates']:\n        composed_dates_list = str_to_list(tm['~cwp_composed_dates'])\n        if len(composed_dates_list) > 1:\n            composed_dates_list = str_to_list(\n                composed_dates_list[0])  # use dates of lowest-level work\n        earliest_date = min([int(dates.split(DATE_SEP)[0].strip())\n                             for dates in composed_dates_list])\n        append_tag(\n            release_id,\n            tm,\n            options['cwp_workdate_tag'],\n            list_to_str(composed_dates_list) +\n            comp)\n        found = True\n    if tm['~cwp_published_dates'] and (\n            not found or options['cwp_workdate_use_all']):\n        if not found:\n            published_dates_list = str_to_list(tm['~cwp_published_dates'])\n            if len(published_dates_list) > 1:\n                published_dates_list = str_to_list(\n                    published_dates_list[0])  # use dates of lowest-level work\n            earliest_date = min([int(dates.split(DATE_SEP)[0].strip())\n                                 for dates in published_dates_list])\n            append_tag(\n                release_id,\n                tm,\n                options['cwp_workdate_tag'],\n                list_to_str(published_dates_list) +\n                publ)\n            found = True\n    if tm['~cwp_premiered_dates'] and (\n            not found or options['cwp_workdate_use_all']):\n        if not found:\n            premiered_dates_list = str_to_list(tm['~cwp_premiered_dates'])\n            if len(premiered_dates_list) > 1:\n                premiered_dates_list = str_to_list(\n                    premiered_dates_list[0])  # use dates of lowest-level work\n            earliest_date = min([int(dates.split(DATE_SEP)[0].strip())\n                                 for dates in premiered_dates_list])\n            append_tag(\n                release_id,\n                tm,\n                options['cwp_workdate_tag'],\n                list_to_str(premiered_dates_list) +\n                prem)\n\n    # periods\n    PERIODS = {}\n    if options['cwp_period_map']:\n        if options['cwp_use_muso_refdb'] and options['cwp_muso_periods'] and PERIOD_DICT:\n            for p_item in PERIOD_DICT:\n                if 'start' not in p_item or p_item['start'] == []:\n                    p_item['start'] = [u'-9999']\n                if 'end' not in p_item or p_item['end'] == []:\n                    p_item['end'] = [u'2525']\n                if 'name' not in p_item or p_item['name'] == []:\n                    p_item['name'] = ['NOT SPECIFIED']\n            PERIODS = {list_to_str(mp['name']).strip(): (\n                list_to_str(mp['start']),\n                list_to_str(mp['end']))\n                for mp in PERIOD_DICT}\n            for period in PERIODS:\n                if PERIODS[period][0].lstrip(\n                        '-').isdigit() and PERIODS[period][1].lstrip('-').isdigit():\n                    PERIODS[period] = (int(PERIODS[period][0]),\n                                       int(PERIODS[period][1]))\n                else:\n                    PERIODS[period] = (\n                        9999,\n                        'ERROR - start and/or end of ' +\n                        period +\n                        ' are not integers')\n\n        else:\n            periods = [p.strip() for p in options['cwp_period_map'].split(';')]\n            for p in periods:\n                p = p.split(',')\n                if len(p) == 3:\n                    period = p[0].strip()\n                    start = p[1].strip()\n                    end = p[2].strip()\n                    if start.lstrip(\n                            '-').isdigit() and end.lstrip('-').isdigit():\n                        PERIODS[period] = (int(start), int(end))\n                    else:\n                        PERIODS[period] = (\n                            9999,\n                            'ERROR - start and/or end of ' +\n                            period +\n                            ' are not integers')\n                else:\n                    PERIODS[p[0]] = (\n                        9999, 'ERROR in period map - each item must contain 3 elements')\n    if options['cwp_period_tag'] and PERIODS:\n        if earliest_date == 9999:  # i.e. no work date found\n            if options['cwp_use_muso_refdb'] and options['cwp_muso_dates']:\n                for composer_born in composer_born_list + arranger_born_list:\n                    if composer_born and composer_born.isdigit():\n                        birthdate = int(composer_born)\n                        # productive age is taken as 20->death as per Muso\n                        earliest_date = min(earliest_date, birthdate + 20)\n                        for composer_died in composer_died_list + arranger_died_list:\n                            if composer_died and composer_died.isdigit():\n                                deathdate = int(composer_died)\n                                latest_date = max(latest_date, deathdate)\n                            else:\n                                latest_date = datetime.now().year\n        # sort into start date order before writing tags\n        sorted_periods = collections.OrderedDict(\n            sorted(PERIODS.items(), key=lambda t: t[1]))\n        for period in sorted_periods:\n            if isinstance(\n                    sorted_periods[period][1],\n                    str) and 'ERROR' in sorted_periods[period][1]:\n                tm[options['cwp_period_tag']] = ''\n                append_tag(\n                    release_id,\n                    tm,\n                    '001_errors:9',\n                    '9. ' +\n                    sorted_periods[period])\n                break\n            if earliest_date < 9999:\n                if sorted_periods[period][0] <= earliest_date <= sorted_periods[period][1]:\n                    append_tag(\n                        release_id,\n                        tm,\n                        options['cwp_period_tag'],\n                        period)\n            if latest_date > -9999:\n                if sorted_periods[period][0] <= latest_date <= sorted_periods[period][1]:\n                    append_tag(\n                        release_id,\n                        tm,\n                        options['cwp_period_tag'],\n                        period)\n\n    # generic tag mapping\n    sort_tags = options['cea_tag_sort']\n    if sort_tags:\n        tm['artists_sort'] = str_to_list(tm['~artists_sort'])\n    for i in range(0, 16):\n        tagline = options['cea_tag_' + str(i + 1)].split(\",\")\n        source_group = options['cea_source_' + str(i + 1)].split(\",\")\n        conditional = options['cea_cond_' + str(i + 1)]\n        for item, tagx in enumerate(tagline):\n            tag = tagx.strip()\n            sort = sort_suffix(tag)\n            if not conditional or tm[tag] == \"\":\n                for source_memberx in source_group:\n                    source_member = source_memberx.strip()\n                    sourceline = source_member.split(\"+\")\n                    if len(sourceline) > 1:\n                        source = \"\\\\\"\n                        for source_itemx in sourceline:\n                            source_item = source_itemx.strip()\n                            source_itema = source_itemx.lstrip()\n                            write_log(\n                                    release_id, 'info', \"Source_item: %s\", source_item)\n                            if \"~cea_\" + source_item in tm:\n                                si = tm['~cea_' + source_item]\n                            elif \"~cwp_\" + source_item in tm:\n                                si = tm['~cwp_' + source_item]\n                            elif source_item in tm:\n                                si = tm[source_item]\n                            elif len(source_itema) > 0 and source_itema[0] == \"\\\\\":\n                                si = source_itema[1:]\n                            else:\n                                si = \"\"\n                            if si != \"\" and source != \"\":\n                                source = source + si\n                            else:\n                                source = \"\"\n                    else:\n                        source = sourceline[0]\n                    no_names_source = re.sub('(_names)$', 's', source)\n                    source_sort = sort_suffix(source)\n                    write_log(\n                            release_id,\n                            'info',\n                            \"Tag mapping: Line: %s, Source: %s, Tag: %s, no_names_source: %s, sort: %s, item %s\",\n                            i +\n                            1,\n                            source,\n                            tag,\n                            no_names_source,\n                            sort,\n                            item)\n                    if '~cea_' + source in tm or '~cwp_' + source in tm:\n                        for prefix in ['~cea_', '~cwp_']:\n                            if prefix + source in tm:\n                                write_log(release_id, 'info', prefix)\n                                append_tag(release_id, tm, tag,\n                                           tm[prefix + source], ['; '])\n                                if sort_tags:\n                                    if prefix + no_names_source + source_sort in tm:\n                                        write_log(\n                                                release_id, 'info', prefix + \" sort\")\n                                        append_tag(release_id, tm, tag + sort,\n                                                   tm[prefix + no_names_source + source_sort], ['; '])\n                    elif source in tm or '~' + source in tm:\n                        write_log(release_id, 'info', \"Picard\")\n                        for p in ['', '~']:\n                            if p + source in tm:\n                                append_tag(release_id, tm, tag,\n                                           tm[p + source], ['; ', '/ '])\n                        if sort_tags:\n                            if \"~\" + source + source_sort in tm:\n                                source = \"~\" + source\n                            if source + source_sort in tm:\n                                write_log(\n                                        release_id, 'info', \"Picard sort\")\n                                append_tag(release_id, tm, tag + sort,\n                                           tm[source + source_sort], ['; ', '/ '])\n                    elif len(source) > 0 and source[0] == \"\\\\\":\n                        append_tag(release_id, tm, tag,\n                                   source[1:], ['; ', '/ '])\n                    else:\n                        pass\n\n    # write error messages to tags\n    if options['log_error'] and \"~cea_error\" in tm:\n        for error in str_to_list(tm['~cea_error']):\n            ecode = error[0]\n            append_tag(release_id, tm, '001_errors:' + ecode, error)\n    if options['log_warning'] and \"~cea_warning\" in tm:\n        for warning in str_to_list(tm['~cea_warning']):\n            wcode = warning[0]\n            append_tag(release_id, tm, '002_warnings:' + wcode, warning)\n\n    # delete unwanted tags\n    if not options['log_debug']:\n        if '~cea_works_complete' in tm:\n            del tm['~cea_works_complete']\n        if '~cea_artists_complete' in tm:\n            del tm['~cea_artists_complete']\n        del_list = []\n        for t in tm:\n            if 'ce_tag_cleared' in t:\n                del_list.append(t)\n        for t in del_list:\n            del tm[t]\n\n    # create hidden tags to flag differences\n    if options['ce_show_ui_tags'] and options['ce_ui_tags']:\n        for heading_name, tag_tuple in UI_TAGS:  # UI_TAGS is already iterated in main routine, so no need for .items() method here\n            heading_tag = '~' + heading_name + '_VAL'\n            for tag in tag_tuple:\n                if tag[-5:] != '_DIFF':\n                    append_tag(release_id, tm, heading_tag, tm[tag])\n                else:\n                    tag = '~' + tag\n                    mirror_tags = str_to_list((tm['~ce_mirror_tags']))\n                    for mirror_tag in mirror_tags:\n                        mt = interpret(mirror_tag)\n                        st = str_to_list(mt)\n                        (old_tag, new_tag) = tuple(st)\n                        diff_name = old_tag.replace('OLD', 'DIFF')\n                        if diff_name == tag and  tm[old_tag] != tm[new_tag]:\n                            tm[diff_name] = '*****'\n                            append_tag(release_id, tm, heading_tag, '*****')\n                            break\n\n    # if options over-write enabled, remove it after processing one album\n    options['ce_options_overwrite'] = False\n    config.setting['ce_options_overwrite'] = False\n    # so that options are not retained (in case of refresh with different\n    # options)\n    if '~ce_options' in tm:\n        del tm['~ce_options']\n\n    # remove any unwanted file tags\n    if '~ce_file' in tm and tm['~ce_file'] != \"None\":\n        music_file = tm['~ce_file']\n        orig_metadata = album.tagger.files[music_file].orig_metadata\n        if 'delete_tags' in options and options['delete_tags']:\n            warn = []\n            for delete_item in options['delete_tags']:\n                if delete_item not in tm:  # keep the original for comparison if we have a new version\n                    if delete_item in orig_metadata:\n                        del orig_metadata[delete_item]\n                        if delete_item != '002_warnings:7':  # to avoid circularity!\n                            warn.append(delete_item)\n            if warn and options['log_warning']:\n                append_tag(\n                    release_id,\n                    tm,\n                    '002_warnings:7',\n                    '7. Deleted tags: ' +\n                    ', '.join(warn))\n                write_log(\n                    release_id,\n                    'warning',\n                    'Deleted tags: ' +\n                    ', '.join(warn))\n\n\ndef sort_suffix(tag):\n    \"\"\"To determine what sort suffix is appropriate for a given tag\"\"\"\n    if tag == \"composer\" or tag == \"artist\" or tag == \"albumartist\" or tag == \"trackartist\" or tag == \"~cea_MB_artist\":\n        sort = \"sort\"\n    else:\n        sort = \"_sort\"\n    return sort\n\n\ndef append_tag(release_id, tm, tag, source, separators=None):\n    \"\"\"\n    Update a tag\n    :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n    :param tm: track metadata\n    :param tag: tag to be appended to\n    :param source: item to append to tag\n    :param separators: characters which may be used to split string into a list\n        (any of the characters will be a split point)\n    :return: None. Action is on tm\n    \"\"\"\n    if not separators:\n        separators = []\n    if tag and tag != \"\":\n        if config.setting['log_info']:\n            write_log(\n                release_id,\n                'info',\n                'Appending source: %r to tag: %s (source is type %s) ...',\n                source,\n                tag,\n                type(source))\n            if tag in tm:\n                write_log(\n                    release_id,\n                    'info',\n                    '... existing tag contents = %r',\n                    tm[tag])\n        if source and len(source) > 0:\n            if isinstance(source, str):\n                if separators:\n                    source = re.split('|'.join(separators), source)\n                else:\n                    source = [source]\n            if not isinstance(source, list):\n                source = [source]  # typically for dict items such as saved options\n            if all([isinstance(x, str) for x in source]):  # only append if if source is a list of strings\n                if tag not in tm:\n                    if tag == 'artists_sort':\n                        # There is no artists_sort tag in Picard - just a\n                        # hidden var ~artists_sort, so pick up those into the new tag\n                        hidden = tm['~artists_sort']\n                        if not isinstance(hidden, list):\n                            if separators:\n                                hidden = re.split(\n                                    '|'.join(separators), hidden)\n                                for i, h in enumerate(hidden):\n                                    hidden[i] = h.strip()\n                            else:\n                                hidden = [hidden]\n                        source = add_list_uniquely(source, hidden)\n                    new_tag = True\n                else:\n                    new_tag = False\n\n                for source_item in source:\n                    if isinstance(source_item, str):\n                        source_item = source_item.replace(u'\\u2010', u'-')\n                        source_item = source_item.replace(u'\\u2011', u'-')\n                        source_item = source_item.replace(u'\\u2019', u\"'\")\n                        source_item = source_item.replace(u'\\u2018', u\"'\")\n                        source_item = source_item.replace(u'\\u201c', u'\"')\n                        source_item = source_item.replace(u'\\u201d', u'\"')\n                    if new_tag:\n                        tm[tag] = [source_item]\n                        new_tag = False\n                    else:\n                        if not isinstance(tm[tag], list):\n                            if separators:\n                                tag_list = re.split(\n                                    '|'.join(separators), tm[tag])\n                                for i, t in enumerate(tag_list):\n                                    tag_list[i] = t.strip()\n                            else:\n                                tag_list = [tm[tag]]\n                        else:\n                            tag_list = tm[tag]\n                        if source_item not in tm[tag]:\n                            tag_list.append(source_item)\n                            tm[tag] = tag_list\n                    # NB tag_list is used as metadata object will convert single-item lists to strings\n            else:  # source items are not strings, so just replace\n                tm[tag] = source\n\ndef get_artist_credit(options, release_id, obj):\n    \"\"\"\n    :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n    :param options:\n    :param obj: an XmlNode\n    :return: a list of as-credited names\n    \"\"\"\n    name_credit_list = parse_data(release_id, obj, [], 'artist-credit')\n    credit_list = []\n    if name_credit_list:\n        for name_credits in name_credit_list:\n            for name_credit in name_credits:\n                credited_artist = parse_data(\n                    release_id, name_credit, [], 'name')\n                if credited_artist:\n                    name = parse_data(\n                        release_id, name_credit, [], 'artist', 'name')\n                    sort_name = parse_data(\n                        release_id, name_credit, [], 'artist', 'sort-name')\n                    credit_item = (credited_artist, name, sort_name)\n                    credit_list.append(credit_item)\n        return credit_list\n\n\ndef get_aliases_and_credits(\n        self,\n        options,\n        release_id,\n        album,\n        obj,\n        lang,\n        credited):\n    \"\"\"\n    :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n    :param album:\n    :param self: This relates to the object in the class which called this function\n    :param options:\n    :param obj: an XmlNode\n    :param lang: The language selected in the Picard metadata options\n    :param credited: The options item to determine what as-credited names are being sought\n    :return: None. Sets self.artist_aliases and self.artist_credits[album]\n    \"\"\"\n    name_credit_list = parse_data(release_id, obj, [], 'artist-credit')\n    artist_list = parse_data(release_id, name_credit_list, [], 'artist')\n    for artist in artist_list:\n        sort_names = parse_data(release_id, artist, [], 'sort-name')\n        if sort_names:\n            aliases = parse_data(release_id, artist, [], 'aliases', 'locale:' +\n                                 lang, 'primary:True', 'name')\n            if aliases:\n                self.artist_aliases[sort_names[0]] = aliases[0]\n    if credited:\n        for name_credit in name_credit_list[0]:\n            credited_artist = parse_data(release_id, name_credit, [], 'name')\n            if credited_artist:\n                sort_name = parse_data(\n                    release_id, name_credit, [], 'artist', 'sort-name')\n                if sort_name:\n                    self.artist_credits[album][sort_name[0]\n                                               ] = credited_artist[0]\n\n\ndef get_relation_credits(\n        self,\n        options,\n        release_id,\n        album,\n        obj,\n        lang,\n        credited):\n    \"\"\"\n    :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n    :param self:\n    :param options: UI options\n    :param album: current album\n    :param obj: Xmlnode\n    :param lang: language\n    :param credited: credited-as name\n    :return: None\n    Note that direct recording relationships will over-ride indirect ones (via work)\n    \"\"\"\n\n    rels = parse_data(release_id, obj, [], 'relations', 'target-type:work',\n                      'work', 'relations', 'target-type:artist')\n\n    for artist in rels:\n        sort_names = parse_data(release_id, artist, [], 'artist', 'sort-name')\n        if sort_names:\n            credited_artists = parse_data(\n                release_id, artist, [], 'target-credit')\n            if credited_artists and credited_artists[0] != '' and credited:\n                self.artist_credits[album][sort_names[0]\n                                           ] = credited_artists[0]\n            aliases = parse_data(\n                release_id,\n                artist,\n                [],\n                'artist',\n                'aliases',\n                'locale:' + lang,\n                'primary:True',\n                'name')\n            if aliases:\n                self.artist_aliases[sort_names[0]] = aliases[0]\n\n    rels2 = parse_data(release_id, obj, [], 'relations', 'target-type:artist')\n\n    for artist in rels2:\n        sort_names = parse_data(release_id, artist, [], 'artist', 'sort-name')\n        if sort_names:\n            credited_artists = parse_data(\n                release_id, artist, [], 'target-credit')\n            if credited_artists and credited_artists[0] != '' and credited:\n                self.artist_credits[album][sort_names[0]\n                                           ] = credited_artists[0]\n            aliases = parse_data(\n                release_id,\n                artist,\n                [],\n                'artist',\n                'aliases',\n                'locale:' + lang,\n                'primary:True',\n                'name')\n            if aliases:\n                self.artist_aliases[sort_names[0]] = aliases[0]\n\n\ndef composer_last_names(self, release_id, tm, album):\n    \"\"\"\n    :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n    :param self:\n    :param tm:\n    :param album:\n    :return: None\n    Sets composer last names for album prefixing\n    \"\"\"\n    if '~cea_album_track_composer_lastnames' in tm:\n        if not isinstance(tm['~cea_album_track_composer_lastnames'], list):\n            atc_list = re.split(\n                '|'.join(\n                    self.SEPARATORS),\n                tm['~cea_album_track_composer_lastnames'])\n        else:\n            atc_list = str_to_list(tm['~cea_album_track_composer_lastnames'])\n        for atc_item in atc_list:\n            composer_lastnames = atc_item.strip()\n            if '~length' in tm and tm['~length']:\n                track_length = time_to_secs(tm['~length'])\n            else:\n                track_length = 0\n            if album in self.album_artists:\n                if 'composer_lastnames' in self.album_artists[album]:\n                    if composer_lastnames not in self.album_artists[album]['composer_lastnames']:\n                        self.album_artists[album]['composer_lastnames'][composer_lastnames] = {\n                            'length': track_length}\n                    else:\n                        self.album_artists[album]['composer_lastnames'][composer_lastnames]['length'] += track_length\n                else:\n                    self.album_artists[album]['composer_lastnames'][composer_lastnames] = {\n                        'length': track_length}\n            else:\n                self.album_artists[album]['composer_lastnames'][composer_lastnames] = {\n                    'length': track_length}\n    else:\n        write_log(\n                release_id,\n                'warning',\n                \"No _cea_album_track_composer_lastnames variable available for recording \\\"%s\\\".\",\n                tm['title'])\n        if 'composer' in tm:\n            self.append_tag(\n                release_id,\n                release_id,\n                tm,\n                '~cea_warning',\n                '1. Composer for this track is not in album artists and will not be available to prefix album')\n        else:\n            self.append_tag(\n                release_id,\n                release_id,\n                tm,\n                '~cea_warning',\n                '1. No composer for this track, but checking parent work.')\n\n\ndef add_list_uniquely(list_to, list_from):\n    \"\"\"\n    Adds any items in list_from to list_to, if they are not already present\n    If either arg is a string, it will be converted to a list, e.g. 'abc' -> ['abc']\n    :param list_to:\n    :param list_from:\n    :return: appends only unique elements of list 2 to list 1\n    \"\"\"\n    #\n    if list_to and list_from:\n        if not isinstance(list_to, list):\n            list_to = str_to_list(list_to)\n        if not isinstance(list_from, list):\n            list_from = str_to_list(list_from)\n        for list_item in list_from:\n            if list_item not in list_to:\n                list_to.append(list_item)\n    else:\n        if list_from:\n            list_to = list_from\n    return list_to\n\n\ndef str_to_list(s):\n    \"\"\"\n    :param s:\n    :return: list from string using ; as separator\n    \"\"\"\n    if isinstance(s, list):\n        return s\n    if not isinstance(s, str):\n        try:\n            return list(s)\n        except TypeError:\n            return []\n    else:\n        if s == '':\n            return []\n        else:\n            return s.split('; ')\n\n\ndef list_to_str(l):\n    \"\"\"\n    :param l:\n    :return: string from list using ; as separator\n    \"\"\"\n    if not isinstance(l, list):\n        return l\n    else:\n        return '; '.join(l)\n\n\ndef interpret(tag):\n    \"\"\"\n    :param tag:\n    :return: safe form of eval(tag)\n    \"\"\"\n    if isinstance(tag, str):\n        try:\n            tag = tag.strip(' \\n\\t')\n            return ast.literal_eval(tag)\n        except (SyntaxError, ValueError):\n            return tag\n    else:\n        return tag\n\n\ndef time_to_secs(a):\n    \"\"\"\n    :param a: string x:x:x\n    :return: seconds\n    converts string times to seconds\n    \"\"\"\n    ax = a.split(':')\n    ax = ax[::-1]\n    t = 0\n    for i, x in enumerate(ax):\n        if x.isdigit():\n            t += int(x) * (60 ** i)\n        else:\n            return 0\n    return t\n\n\ndef seq_last_names(self, album):\n    \"\"\"\n    Sequences composer last names for album prefix by the total lengths of their tracks\n    :param self:\n    :param album:\n    :return:\n    \"\"\"\n    ln = []\n    if album in self.album_artists and 'composer_lastnames' in self.album_artists[album]:\n        for x in self.album_artists[album]['composer_lastnames']:\n            if 'length' in self.album_artists[album]['composer_lastnames'][x]:\n                ln.append([x, self.album_artists[album]\n                           ['composer_lastnames'][x]['length']])\n            else:\n                return []\n        ln = sorted(ln, key=lambda a: a[1])\n        ln = ln[::-1]\n    return [a[0] for a in ln]\n\n\ndef year(date):\n    \"\"\"\n    Return YYYY portion of date(s) in YYYY-MM-DD format (may be incomplete, string or list)\n    :param date:\n    :return: YYYY\n    \"\"\"\n    if isinstance(date, list):\n        year_list = [blank_if_none(d).split('-')[0] for d in date]\n        return year_list\n    else:\n        date_list = blank_if_none(date).split('-')\n        return [date_list[0]]\n\n\ndef blank_if_none(val):\n    \"\"\"\n    Make NoneTypes strings\n    :param val: str or None\n    :return: str\n    \"\"\"\n    if not val:\n        return ''\n    else:\n        return val\n\n\ndef strip_excess_punctuation(s):\n    \"\"\"\n    remove orphan punctuation, unmatched quotes and brackets\n    :param s: string\n    :return: string\n    \"\"\"\n    if s:\n        s_prev = ''\n        counter = 0\n        while s != s_prev:\n            if counter > 100:\n                break  # safety valve\n            s_prev = s\n            s = s.replace('  ', ' ')\n            s = s.strip(\"&.-:;, \")\n            s = s.lstrip(\"!)]}\")\n            s = s.rstrip(\"([{\")\n            s = s.lstrip(u\"\\u2019\") # Right single quote\n            s = s.lstrip(u\"\\u201D\") # Right double quote\n            if s.count(u\"\\u201E\") == 0: # u201E is lower double quote (German etc.)\n                s = s.rstrip(u\"\\u201C\") # Left double quote - only strip if there is no German-style lower quote present\n            s = s.rstrip(u\"\\u2018\") # Left single quote\n            if s.count('\"') % 2 != 0:\n                s = s.strip('\"')\n            if s.count(\"'\") % 2 != 0:\n                s = s.strip(\"'\")\n            if len(s) > 0 and s[0] == u\"\\u201C\" and s.count(u\"\\u201D\") == 0:\n                s = s.lstrip(u\"\\u201C\")\n            if len(s) > 0 and s[-1] == u\"\\u201D\" and s.count(u\"\\u201C\") == 0 and s.count(u\"\\u201E\") == 0:  # only strip if there is no German-style lower quote present\n                s = s.rstrip(u\"\\u201D\")\n            if len(s) > 0 and s[0] == u\"\\u2018\" and s.count(u\"\\u2019\") == 0:\n                s = s.lstrip(u\"\\u2018\")\n            if len(s) > 0 and s[-1] == u\"\\u2019\" and s.count(u\"\\u2018\") == 0:\n                s = s.rstrip(u\"\\u2019\")\n            if s:\n                if s.count(\"\\\"\") == 1:\n                    s = s.replace('\"', '')\n                if s.count(\"\\'\") == 1:\n                    s = s.replace(\" '\", \" \")\n                    # s = s.replace(\"' \", \" \") # removed to prevent removal of genuine apostrophes\n                if \"(\" in s and \")\" not in s:\n                    s = s.replace(\"(\", \"\")\n                if \")\" in s and \"(\" not in s:\n                    s = s.replace(\")\", \"\")\n                if \"[\" in s and \"]\" not in s:\n                    s = s.replace(\"[\", \"\")\n                if \"]\" in s and \"[\" not in s:\n                    s = s.replace(\"]\", \"\")\n                if \"{\" in s and \"}\" not in s:\n                    s = s.replace(\"{\", \"\")\n                if \"}\" in s and \"{\" not in s:\n                    s = s.replace(\"}\", \"\")\n            if s:\n                match_chars = [(\"(\", \")\"), (\"[\", \"]\"), (\"{\", \"}\")]\n                last = len(s) - 1\n                for char_pair in match_chars:\n                    if char_pair[0] == s[0] and char_pair[1] == s[last]:\n                        s = s.lstrip(char_pair[0]).rstrip(char_pair[1])\n            counter += 1\n    return s\n\n\n#################\n#################\n# EXTRA ARTISTS #\n#################\n#################\n\n\nclass ExtraArtists():\n\n    # CONSTANTS\n    def __init__(self):\n        self.album_artists = collections.defaultdict(\n            lambda: collections.defaultdict(dict))\n        # collection of artists to be applied at album level\n\n        self.track_listing = collections.defaultdict(list)\n        # collection of tracks - format is {album: [track 1,\n        # track 2, ...]}\n\n        self.options = collections.defaultdict(dict)\n        # collection of Classical Extras options\n\n        self.globals = collections.defaultdict(dict)\n        # collection of global variables for this class\n\n        self.album_performers = collections.defaultdict(\n            lambda: collections.defaultdict(dict))\n        # collection of performers who have release relationships, not track\n        # relationships\n\n        self.album_instruments = collections.defaultdict(\n            lambda: collections.defaultdict(dict))\n        # collection of instruments which have release relationships, not track\n        # relationships\n\n        self.artist_aliases = {}\n        # collection of alias names - format is {sort_name: alias_name, ...}\n\n        self.artist_credits = collections.defaultdict(dict)\n        # collection of credited-as names - format is {album: {sort_name: credit_name,\n        # ...}, ...}\n\n        self.release_artists_sort = collections.defaultdict(list)\n        # collection of release artists - format is {album: [sort_name_1,\n        # sort_name_2, ...]}\n\n        self.lyricist_filled = collections.defaultdict(dict)\n        # Boolean for each track to indicate if lyricist has been found (don't\n        # want to add more from higher levels)\n        # NB this last one is for completeness - not actually used by\n        # ExtraArtists, but here to remove pep8 error\n\n        self.album_series_list = collections.defaultdict(dict)\n        # series relationships - format is {'name_list': series names, 'id_list': series ids, 'number_list': number within series}\n\n    def add_artist_info(\n            self,\n            album,\n            track_metadata,\n            trackXmlNode,\n            releaseXmlNode):\n        \"\"\"\n        Main routine run for each track of release\n        :param album: Current release\n        :param track_metadata: track metadata dictionary\n        :param trackXmlNode: Everything in the track node downwards\n        :param releaseXmlNode: Everything in the release node downwards (so includes all track nodes)\n        :return:\n        \"\"\"\n        release_id = track_metadata['musicbrainz_albumid']\n        if 'start' not in release_status[release_id]:\n            release_status[release_id]['start'] = datetime.now()\n        if 'lookups' not in release_status[release_id]:\n            release_status[release_id]['lookups'] = 0\n        release_status[release_id]['name'] = track_metadata['album']\n        release_status[release_id]['artists'] = True\n        if config.setting['log_debug'] or config.setting['log_info']:\n            write_log(\n                release_id,\n                'debug',\n                'STARTING ARTIST PROCESSING FOR ALBUM %s, DISC %s, TRACK %s',\n                track_metadata['album'],\n                track_metadata['discnumber'],\n                track_metadata['tracknumber'] +\n                ' ' +\n                track_metadata['title'])\n        # write_log(release_id, 'info', 'trackXmlNode = %s', trackXmlNode) # NB can crash Picard\n        # write_log('info', 'releaseXmlNode = %s', releaseXmlNode) # NB can crash Picard\n        # Jump through hoops to get track object!!\n        track = album._new_tracks[-1]\n        tm = track.metadata\n\n        # OPTIONS - OVER-RIDE IF REQUIRED\n        if '~ce_options' not in tm:\n            if config.setting['log_debug'] or config.setting['log_info']:\n                write_log(release_id, 'debug', 'Artists gets track first...')\n            get_options(release_id, album, track)\n        options = interpret(tm['~ce_options'])\n        if not options:\n            if config.setting[\"log_error\"]:\n                write_log(\n                    release_id,\n                    'error',\n                    'Artists. Failure to read saved options for track %s. options = %s',\n                    track,\n                    tm['~ce_options'])\n            options = option_settings(config.setting)\n        self.options[track] = options\n\n        # CONSTANTS\n        self.ERROR = options[\"log_error\"]\n        self.WARNING = options[\"log_warning\"]\n        self.ORCHESTRAS = options[\"cea_orchestras\"].split(',')\n        self.CHOIRS = options[\"cea_choirs\"].split(',')\n        self.GROUPS = options[\"cea_groups\"].split(',')\n        self.ENSEMBLE_TYPES = self.ORCHESTRAS + self.CHOIRS + self.GROUPS\n        self.SEPARATORS = ['; ', '/ ', ';', '/']\n\n        # continue?\n        if not options[\"classical_extra_artists\"]:\n            return\n        # album_files is not used - this is just for logging\n        album_files = album.tagger.get_files_from_objects([album])\n        if options['log_info']:\n            write_log(\n                release_id,\n                'info',\n                'ALBUM FILENAMES for album %r = %s',\n                album,\n                album_files)\n\n        if not (\n            options[\"ce_no_run\"] and (\n                not tm['~ce_file'] or tm['~ce_file'] == \"None\")):\n            # continue\n            write_log(\n                    release_id,\n                    'debug',\n                    \"ExtraArtists - add_artist_info\")\n            if album not in self.track_listing or track not in self.track_listing[album]:\n                self.track_listing[album].append(track)\n            # fix odd hyphens in names for consistency\n            field_types = ['~albumartists', '~albumartists_sort']\n            for field_type in field_types:\n                if field_type in tm:\n                    field = tm[field_type]\n                    if isinstance(field, list):\n                        for x, it in enumerate(field):\n                            field[x] = it.replace(u'\\u2010', u'-')\n                    elif isinstance(field, str):\n                        field = field.replace(u'\\u2010', u'-')\n                    else:\n                        pass\n                    tm[field_type] = field\n\n            # first time for this album (reloads each refresh)\n            if tm['discnumber'] == '1' and tm['tracknumber'] == '1':\n                # get artist aliases - these are cached so can be re-used across\n                # releases, but are reloaded with each refresh\n                get_aliases(self, release_id, album, options, releaseXmlNode)\n\n                # xml_type = 'release'\n                # get performers etc who are related at the release level\n                relation_list = parse_data(\n                    release_id, releaseXmlNode, [], 'relations')\n                album_performerList = get_artists(\n                    options, release_id, tm, relation_list, 'release')['artists']\n                self.album_performers[album] = album_performerList\n                album_instrumentList = get_artists(\n                    options, release_id, tm, relation_list, 'release')['instruments']\n                self.album_instruments[album] = album_instrumentList\n\n                # get series information\n                self.album_series_list = get_series(\n                    options, release_id, relation_list)\n\n            else:\n                if album in self.album_performers:\n                    album_performerList = self.album_performers[album]\n                else:\n                    album_performerList = []\n                if album in self.album_instruments and self.album_instruments[album]:\n                    tm['~cea_instruments'] = self.album_instruments[album][0]\n                    tm['~cea_instruments_credited'] = self.album_instruments[album][1]\n                    tm['~cea_instruments_all'] = self.album_instruments[album][2]\n                    # Should be OK to initialise these here as recording artists\n                    # yet to be processed\n\n            # Fill release info not given by vanilla Picard\n            if self.album_series_list:\n                tm['series'] = self.album_series_list['name_list'] if 'name_list' in self.album_series_list else None\n                tm['musicbrainz_seriesid'] = self.album_series_list['id_list'] if 'id_list' in self.album_series_list else None\n                tm['series_number'] = self.album_series_list['number_list'] if 'number_list' in self.album_series_list else None\n                ## TODO add label id too\n            recording_relation_list = parse_data(\n                release_id, trackXmlNode, [], 'recording', 'relations')\n            recording_series_list = get_series(\n                options, release_id, recording_relation_list)\n            write_log(\n                release_id,\n                'info',\n                'Recording_series_list = %s',\n                recording_series_list)\n\n            track_artist_list = parse_data(\n                release_id, trackXmlNode, [], 'artist-credit')\n            if track_artist_list:\n                track_artist = []\n                track_artistsort = []\n                track_artists = []\n                track_artists_sort = []\n                lang = get_preferred_artist_language(config)\n\n                # Set naming option\n                # Put naming style into preferential list\n\n                # naming as for vanilla Picard for track artists\n\n                if options['translate_artist_names'] and lang:\n                    name_style = ['alias', 'sort']\n                    # documentation indicates that processing should be as below,\n                    # but processing above appears to reflect what vanilla Picard actually does\n                    # if options['standardize_artists']:\n                    #     name_style = ['alias', 'sort']\n                    # else:\n                    #     name_style = ['alias', 'credit', 'sort']\n                else:\n                    if not options['standardize_artists']:\n                        name_style = ['credit']\n                    else:\n                        name_style = []\n                write_log(\n                        release_id,\n                        'info',\n                        'Priority order of naming style for track artists = %s',\n                        name_style)\n                styled_artists = apply_artist_style(\n                    options,\n                    release_id,\n                    lang,\n                    track_artist_list,\n                    name_style,\n                    track_artist,\n                    track_artistsort,\n                    track_artists,\n                    track_artists_sort)\n                tm['artists'] = styled_artists['artists']\n                tm['~artists_sort'] = styled_artists['artists_sort']\n                tm['artist'] = styled_artists['artist']\n                tm['artistsort'] = styled_artists['artistsort']\n\n            if 'recording' in trackXmlNode:\n                self.globals[track]['is_recording'] = True\n                write_log(release_id, 'debug', 'Getting recording details')\n                recording = trackXmlNode['recording']\n                if not isinstance(recording, list):\n                    recording = [recording]\n                for record in recording:\n                    rec_type = type(record)\n                    write_log(release_id, 'info', 'rec-type = %s', rec_type)\n                    write_log(release_id, 'info', record)\n                    # Note that the lists below reflect https://musicbrainz.org/relationships/artist-recording\n                    # Any changes to that DB structure will require changes\n                    # here\n\n                    # get recording artists data\n                    recording_artist_list = parse_data(\n                        release_id, record, [], 'artist-credit')\n                    if recording_artist_list:\n                        recording_artist = []\n                        recording_artistsort = []\n                        recording_artists = []\n                        recording_artists_sort = []\n                        lang = get_preferred_artist_language(config)\n\n                        # Set naming option\n                        # Put naming style into preferential list\n\n                        # naming as for vanilla Picard for track artists (per\n                        # documentation rather than actual?)\n                        if options['cea_ra_trackartist']:\n                            if options['translate_artist_names'] and lang:\n                                if options['standardize_artists']:\n                                    name_style = ['alias', 'sort']\n                                else:\n                                    name_style = ['alias', 'credit', 'sort']\n                            else:\n                                if not options['standardize_artists']:\n                                    name_style = ['credit']\n                                else:\n                                    name_style = []\n                        # naming as for performers in classical extras\n                        elif options['cea_ra_performer']:\n                            if options['cea_aliases']:\n                                if options['cea_alias_overrides']:\n                                    name_style = ['alias', 'credit']\n                                else:\n                                    name_style = ['credit', 'alias']\n                            else:\n                                name_style = ['credit']\n\n                        else:\n                            name_style = []\n                        write_log(\n                                release_id,\n                                'info',\n                                'Priority order of naming style for recording artists = %s',\n                                name_style)\n\n                        styled_artists = apply_artist_style(\n                            options,\n                            release_id,\n                            lang,\n                            recording_artist_list,\n                            name_style,\n                            recording_artist,\n                            recording_artistsort,\n                            recording_artists,\n                            recording_artists_sort)\n                        self.append_tag(\n                            release_id,\n                            tm,\n                            '~cea_recording_artists',\n                            styled_artists['artists'])\n                        self.append_tag(\n                            release_id,\n                            tm,\n                            '~cea_recording_artists_sort',\n                            styled_artists['artists_sort'])\n                        self.append_tag(\n                            release_id,\n                            tm,\n                            '~cea_recording_artist',\n                            styled_artists['artist'])\n                        self.append_tag(\n                            release_id,\n                            tm,\n                            '~cea_recording_artistsort',\n                            styled_artists['artistsort'])\n\n                    else:\n                        tm['~cea_recording_artists'] = ''\n                        tm['~cea_recording_artists_sort'] = ''\n                        tm['~cea_recording_artist'] = ''\n                        tm['~cea_recording_artistsort'] = ''\n\n                    # use recording artist options\n                    tm['~cea_MB_artist'] = str_to_list(tm['artist'])\n                    tm['~cea_MB_artistsort'] = str_to_list(tm['artistsort'])\n                    tm['~cea_MB_artists'] = str_to_list(tm['artists'])\n                    tm['~cea_MB_artists_sort'] = str_to_list(tm['~artists_sort'])\n\n                    if options['cea_ra_use']:\n                        if options['cea_ra_replace_ta']:\n                            if tm['~cea_recording_artist']:\n                                tm['artist'] = str_to_list(tm['~cea_recording_artist'])\n                                tm['artistsort'] = str_to_list(tm['~cea_recording_artistsort'])\n                                tm['artists'] = str_to_list(tm['~cea_recording_artists'])\n                                tm['~artists_sort'] = str_to_list(tm['~cea_recording_artists_sort'])\n                            elif not options['cea_ra_noblank_ta']:\n                                tm['artist'] = ''\n                                tm['artistsort'] = ''\n                                tm['artists'] = ''\n                                tm['~artists_sort'] = ''\n                        elif options['cea_ra_merge_ta']:\n                            if tm['~cea_recording_artist']:\n                                tm['artists'] = add_list_uniquely(\n                                    tm['artists'], tm['~cea_recording_artists'])\n                                tm['~artists_sort'] = add_list_uniquely(\n                                    tm['~artists_sort'], tm['~cea_recording_artists_sort'])\n                                if tm['artist'] != tm['~cea_recording_artist']:\n                                    tm['artist'] = tm['artist'] + \\\n                                        ' (' + tm['~cea_recording_artist'] + ')'\n                                    tm['artistsort'] = tm['artistsort'] + \\\n                                        ' (' + tm['~cea_recording_artistsort'] + ')'\n\n                    # xml_type = 'recording'\n                    relation_list = parse_data(\n                        release_id, record, [], 'relations')\n                    performerList = album_performerList + \\\n                        get_artists(options, release_id, tm, relation_list, 'recording')['artists']\n                    # returns\n                    # [(artist type, instrument or None, artist name, artist sort name, instrument sort, type sort)]\n                    # where instrument sort places solo ahead of additional etc.\n                    # and type sort applies a custom sequencing to the artist\n                    # types\n                    if performerList:\n                        write_log(\n                                release_id, 'info', \"Performers: %s\", performerList)\n                        self.set_performer(\n                            release_id, album, track, performerList, tm)\n                    if not options['classical_work_parts']:\n                        work_artist_list = parse_data(\n                            release_id,\n                            record,\n                            [],\n                            'relations',\n                            'target-type:work',\n                            'type:performance',\n                            'work',\n                            'relations',\n                            'target-type:artist')\n                        work_artists = get_artists(\n                            options, release_id, tm, work_artist_list, 'work')['artists']\n                        set_work_artists(\n                            self, release_id, album, track, work_artists, tm, 0)\n                    # otherwise composers etc. will be set in work parts\n            else:\n                self.globals[track]['is_recording'] = False\n        else:\n            tm['000_major_warning'] = \"WARNING: Classical Extras not run for this track as no file present - \" \\\n                \"deselect the option on the advanced tab to run. If there is a file, then try 'Refresh'.\"\n        if track_metadata['tracknumber'] == track_metadata['totaltracks'] and track_metadata[\n                'discnumber'] == track_metadata['totaldiscs']:  # last track\n            self.process_album(release_id, album)\n            release_status[release_id]['artists-done'] = datetime.now()\n            close_log(release_id, 'artists')\n\n    # Checks for ensembles\n    def ensemble_type(self, performer):\n        \"\"\"\n        Returns ensemble types\n        :param performer:\n        :return:\n        \"\"\"\n        for ensemble_name in self.ORCHESTRAS:\n            ensemble = re.compile(\n                r'(.*)\\b' +\n                ensemble_name +\n                r'\\b(.*)',\n                re.IGNORECASE)\n            if ensemble.search(performer):\n                return 'Orchestra'\n        for ensemble_name in self.CHOIRS:\n            ensemble = re.compile(\n                r'(.*)\\b' +\n                ensemble_name +\n                r'\\b(.*)',\n                re.IGNORECASE)\n            if ensemble.search(performer):\n                return 'Choir'\n        for ensemble_name in self.GROUPS:\n            ensemble = re.compile(\n                r'(.*)\\b' +\n                ensemble_name +\n                r'\\b(.*)',\n                re.IGNORECASE)\n            if ensemble.search(performer):\n                return 'Group'\n        return False\n\n    def process_album(self, release_id, album):\n        \"\"\"\n        Perform final processing after all tracks read\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param album:\n        :return:\n        \"\"\"\n        write_log(\n                release_id,\n                'debug',\n                'ExtraArtists: Starting process_album')\n        # process lyrics tags\n        write_log(release_id, 'debug', 'Starting lyrics processing')\n        common = []\n        tmlyrics_dict = {}\n        tmlyrics_sort = []\n        options = {}\n        for track in self.track_listing[album]:\n            options = self.options[track]\n            if options['cea_split_lyrics'] and options['cea_lyrics_tag']:\n                tm = track.metadata\n                lyrics_tag = options['cea_lyrics_tag']\n                if tm[lyrics_tag]:\n                    # turn text into word lists to speed processing\n                    tmlyrics_dict[track] = tm[lyrics_tag].split()\n        if tmlyrics_dict:\n            tmlyrics_sort = sorted(\n                tmlyrics_dict.items(),\n                key=operator.itemgetter(1))\n            prev = None\n            first_track = None\n            unique_lyrics = []\n            ref_track = {}\n            for lyric_tuple in tmlyrics_sort:  # tuple is (track, lyrics)\n                if lyric_tuple[1] != prev:\n                    unique_lyrics.append(lyric_tuple[1])\n                    first_track = lyric_tuple[0]\n                ref_track[lyric_tuple[0]] = first_track\n                prev = lyric_tuple[1]\n            common = turbo_lcs(\n                release_id,\n                unique_lyrics)\n\n        if common:\n            unique = []\n            for tup in tmlyrics_sort:\n                track = tup[0]\n                ref = ref_track[track]\n                if track == ref:\n                    start = substart_finder(tup[1], common)\n                    length = len(common)\n                    end = min(start + length, len(tup[1]))\n                    unique = tup[1][:start] + tup[1][end:]\n\n                options = self.options[track]\n                if options['cea_split_lyrics'] and options['cea_lyrics_tag']:\n                    tm = track.metadata\n                    if unique:\n                        tm['~cea_track_lyrics'] = ' '.join(unique)\n                    tm['~cea_album_lyrics'] = ' '.join(common)\n                    if options['cea_album_lyrics']:\n                        tm[options['cea_album_lyrics']] = tm['~cea_album_lyrics']\n                    if unique and options['cea_track_lyrics']:\n                        tm[options['cea_track_lyrics']] = tm['~cea_track_lyrics']\n        else:\n            for track in self.track_listing[album]:\n                options = self.options[track]\n                if options['cea_split_lyrics'] and options['cea_lyrics_tag']:\n                    tm['~cea_track_lyrics'] = tm[options['cea_lyrics_tag']]\n                    if options['cea_track_lyrics']:\n                        tm[options['cea_track_lyrics']] = tm['~cea_track_lyrics']\n        write_log(release_id, 'debug', 'Ending lyrics processing')\n\n        for track in self.track_listing[album]:\n            self.write_metadata(release_id, options, album, track)\n        self.track_listing[album] = []\n        write_log(\n                release_id,\n                'info',\n                \"FINISHED Classical Extra Artists. Album: %s\",\n                album)\n\n\n    def write_metadata(self, release_id, options, album, track):\n        \"\"\"\n        Write the metadata for this track\n        :param release_id:\n        :param options:\n        :param album:\n        :param track:\n        :return:\n        \"\"\"\n        options = self.options[track]\n        tm = track.metadata\n        tm['~cea_version'] = PLUGIN_VERSION\n\n        # set inferred genres before any tags are blanked\n        if options['cwp_genres_infer']:\n            self.infer_genres(release_id, options, track, tm)\n\n        # album\n        if not options['classical_work_parts']:\n            if 'composer_lastnames' in self.album_artists[album]:\n                last_names = seq_last_names(self, album)\n                self.append_tag(\n                    release_id,\n                    tm,\n                    '~cea_album_composer_lastnames',\n                    last_names)\n        # otherwise this is done in the workparts class, which has all\n        # composer info\n\n        # process tag mapping\n        tm['~cea_artists_complete'] = \"Y\"\n        map_tags(options, release_id, album, tm)\n\n        # write out options and errors/warnings to tags\n        if options['cea_options_tag'] != \"\":\n            self.cea_options = collections.defaultdict(\n                lambda: collections.defaultdict(\n                    lambda: collections.defaultdict(dict)))\n\n            for opt in plugin_options(\n                    'artists') + plugin_options('tag') + plugin_options('picard'):\n                if 'name' in opt:\n                    if 'value' in opt:\n                        if options[opt['option']]:\n                            self.cea_options['Classical Extras']['Artists options'][opt['name']] = opt['value']\n                    else:\n                        self.cea_options['Classical Extras']['Artists options'][opt['name']\n                        ] = options[opt['option']]\n\n            for opt in plugin_options('tag_detail'):\n                if opt['option'] != \"\":\n                    name_list = opt['name'].split(\"_\")\n                    self.cea_options['Classical Extras']['Artists options'][name_list[0]\n                    ][name_list[1]] = options[opt['option']]\n\n            if options['ce_version_tag'] and options['ce_version_tag'] != \"\":\n                self.append_tag(release_id, tm, options['ce_version_tag'], str(\n                    'Version ' + tm['~cea_version'] + ' of Classical Extras'))\n            if options['cea_options_tag'] and options['cea_options_tag'] != \"\":\n                self.append_tag(\n                    release_id,\n                    tm,\n                    options['cea_options_tag'] +\n                    ':artists_options',\n                    json.loads(\n                        json.dumps(\n                            self.cea_options)))\n\n\n    def infer_genres(self, release_id, options, track, tm):\n        \"\"\"\n        Infer a genre from the artist/instrument metadata\n        :param release_id:\n        :param options:\n        :param track:\n        :param tm: track metadata\n        :return:\n        \"\"\"\n        # Note that this is now mixed in with other sources of genres in def map_tags\n        # ~cea_work_type_if_classical is used for types that are specifically classical\n        # and is only applied in map_tags if the track is deemed to be\n        # classical\n        if (self.globals[track]['is_recording'] and options['classical_work_parts']\n                and '~artists_sort' in tm and 'composersort' in tm\n                and any(x in tm['~artists_sort'] for x in tm['composersort'])\n                and 'writer' not in tm\n                and not any(x in tm['~artists_sort'] for x in tm['~cea_performers_sort'])):\n            self.append_tag(\n                release_id, tm, '~cea_work_type', 'Classical')\n\n        if isinstance(tm['~cea_soloists'], str):\n            soloists = re.split(\n                '|'.join(\n                    self.SEPARATORS),\n                tm['~cea_soloists'])\n        else:\n            soloists = tm['~cea_soloists']\n        if '~cea_vocalists' in tm:\n            if isinstance(tm['~cea_vocalists'], str):\n                vocalists = re.split(\n                    '|'.join(\n                        self.SEPARATORS),\n                    tm['~cea_vocalists'])\n            else:\n                vocalists = tm['~cea_vocalists']\n        else:\n            vocalists = []\n\n        if '~cea_ensembles' in tm:\n            large = False\n            if 'performer:orchestra' in tm:\n                large = True\n                self.append_tag(\n                    release_id, tm, '~cea_work_type_if_classical', 'Orchestral')\n                if '~cea_soloists' in tm:\n                    if 'vocals' in tm['~cea_instruments_all']:\n                        self.append_tag(\n                            release_id, tm, '~cea_work_type', 'Vocal')\n                    if len(soloists) == 1:\n                        if soloists != vocalists:\n                            self.append_tag(\n                                release_id, tm, '~cea_work_type_if_classical', 'Concerto')\n                        else:\n                            self.append_tag(\n                                release_id, tm, '~cea_work_type_if_classical', 'Aria')\n                    elif len(soloists) == 2:\n                        self.append_tag(\n                            release_id, tm, '~cea_work_type_if_classical', 'Duet')\n                        if not vocalists:\n                            self.append_tag(\n                                release_id, tm, '~cea_work_type_if_classical', 'Concerto')\n                    elif len(soloists) == 3:\n                        self.append_tag(\n                            release_id, tm, '~cea_work_type_if_classical', 'Trio')\n                    elif len(soloists) == 4:\n                        self.append_tag(\n                            release_id, tm, '~cea_work_type_if_classical', 'Quartet')\n\n            if 'performer:choir' in tm or 'performer:choir vocals' in tm:\n                large = True\n                self.append_tag(\n                    release_id, tm, '~cea_work_type_if_classical', 'Choral')\n                self.append_tag(\n                    release_id, tm, '~cea_work_type', 'Vocal')\n            else:\n                if large and 'soloists' in tm and tm['soloists'].count(\n                        'vocals') > 1:\n                    self.append_tag(\n                        release_id, tm, '~cea_work_type_if_classical', 'Opera')\n            if not large:\n                if '~cea_soloists' not in tm:\n                    self.append_tag(\n                        release_id, tm, '~cea_work_type_if_classical', 'Chamber music')\n                else:\n                    if vocalists:\n                        self.append_tag(\n                            release_id, tm, '~cea_work_type', 'Song')\n                        self.append_tag(\n                            release_id, tm, '~cea_work_type', 'Vocal')\n                    else:\n                        self.append_tag(\n                            release_id, tm, '~cea_work_type_if_classical', 'Chamber music')\n        else:\n            if len(soloists) == 1:\n                if vocalists != soloists:\n                    self.append_tag(\n                        release_id, tm, '~cea_work_type', 'Instrumental')\n                else:\n                    self.append_tag(\n                        release_id, tm, '~cea_work_type', 'Song')\n                    self.append_tag(\n                        release_id, tm, '~cea_work_type', 'Vocal')\n            elif len(soloists) == 2:\n                self.append_tag(\n                    release_id, tm, '~cea_work_type_if_classical', 'Duet')\n            elif len(soloists) == 3:\n                self.append_tag(\n                    release_id, tm, '~cea_work_type_if_classical', 'Trio')\n            elif len(soloists) == 4:\n                self.append_tag(\n                    release_id, tm, '~cea_work_type_if_classical', 'Quartet')\n            else:\n                if not vocalists:\n                    self.append_tag(\n                        release_id, tm, '~cea_work_type_if_classical', 'Chamber music')\n                else:\n                    self.append_tag(\n                        release_id, tm, '~cea_work_type', 'Song')\n                    self.append_tag(\n                        release_id, tm, '~cea_work_type', 'Vocal')\n\n\n    def append_tag(self, release_id, tm, tag, source):\n        \"\"\"\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param tm:\n        :param tag:\n        :param source:\n        :return:\n        \"\"\"\n        write_log(\n                release_id,\n                'info',\n                \"Extra Artists - appending %s to %s\",\n                source,\n                tag)\n        append_tag(release_id, tm, tag, source, self.SEPARATORS)\n\n    def set_performer(self, release_id, album, track, performerList, tm):\n        \"\"\"\n        Sets the performer-related tags\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param album:\n        :param track:\n        :param performerList: see below\n        :param tm:\n        :return:\n        \"\"\"\n        # performerList is in format [(artist_type, [instrument list],[name list],[sort_name list],\n        # instrument_sort, type_sort),(.....etc]\n        # Sorted by type_sort then sort name then instrument_sort\n        write_log(release_id, 'debug', \"Extra Artists - set_performer\")\n        write_log(release_id, 'info', \"Performer list is:\")\n        write_log(release_id, 'info', performerList)\n        options = self.options[track]\n        # tag strings are a tuple (Picard tag, cea tag, Picard sort tag, cea\n        # sort tag)\n        tag_strings = const.tag_strings('~cea')\n        # insertions lists artist types where names in the main Picard tags may be updated for annotations\n        # (not for performer types as Picard will write performer:inst as Performer name (inst) )\n        insertions = const.INSERTIONS\n\n        # First remove all existing performer tags\n        del_list = []\n        for meta in tm:\n            if 'performer' in meta:\n                del_list.append(meta)\n        for del_item in del_list:\n            del tm[del_item]\n        last_artist = []\n        last_inst_list = []\n        last_instrument = None\n        artist_inst = []\n        artist_inst_list = {}\n        for performer in performerList:\n            artist_type = performer[0]\n            if artist_type not in tag_strings:\n                return None\n            if artist_type in ['instrument', 'vocal', 'performing orchestra']:\n                if performer[1]:\n                    inst_list = performer[1]\n                    attrib_list = []\n                    for attrib in ['solo', 'guest', 'additional']:\n                        if attrib in inst_list:\n                            inst_list.remove(attrib)\n                            attrib_list.append(attrib)\n                    attribs = \" \".join(attrib_list)\n                    instrument = \", \".join(inst_list)\n                    if not options['cea_no_solo'] and attrib_list:\n                        instrument = attribs + \" \" + instrument\n                    if performer[3] == last_artist:\n                        if instrument != last_instrument:\n                            artist_inst.append(instrument)\n                        else:\n                            if inst_list == last_inst_list:\n                                write_log(\n                                    release_id, 'warning', 'Duplicated performer information for %s'\n                                                           ' (may be in Release Relationship as well as Track Relationship).'\n                                                           ' Duplicates have been ignored.', performer[3])\n                                if self.WARNING:\n                                    self.append_tag(\n                                        release_id,\n                                        tm,\n                                        '~cea_warning',\n                                        '2. Duplicated performer information for \"' +\n                                        '; '.join(\n                                            performer[3]) +\n                                        '\" (may be in Release Relationship as well as Track Relationship).'\n                                        ' Duplicates have been ignored.')\n                    else:\n                        artist_inst = [instrument]\n                        last_artist = performer[3]\n                        last_inst_list = inst_list\n                        last_instrument = instrument\n\n                    instrument = \", \".join(artist_inst)\n                else:\n                    instrument = None\n                if artist_type == 'performing orchestra':\n                    instrument = 'orchestra'\n                artist_inst_list[tuple(performer[3])] = instrument\n        for performer in performerList:\n            artist_type = performer[0]\n            if artist_type not in tag_strings:\n                return None\n            performing_artist = False if artist_type in [\n                'arranger', 'instrument arranger', 'orchestrator', 'vocal arranger'] else True\n            if True and artist_type in [\n                'instrument',\n                'vocal',\n                    'performing orchestra']:  # There may be an option here (to replace 'True')\n                # Currently groups instruments by artist - alternative has been\n                # tested if required\n                instrument = artist_inst_list[tuple(performer[3])]\n            else:\n                if performer[1]:\n                    inst_list = performer[1]\n                    if options['cea_no_solo']:\n                        for attrib in ['solo', 'guest', 'additional']:\n                            if attrib in inst_list:\n                                inst_list.remove(attrib)\n                    instrument = \" \".join(inst_list)\n                else:\n                    instrument = None\n                if artist_type == 'performing orchestra':\n                    instrument = 'orchestra'\n            sub_strings = {'instrument': instrument,\n                           'vocal': instrument  # ,\n                           # 'instrument arranger': instrument,\n                           # 'vocal arranger': instrument\n                           }\n            for typ in ['concertmaster']:\n                if options['cea_' + typ] and options['cea_arrangers']:\n                    sub_strings[typ] = ':' + options['cea_' + typ]\n\n            if options['cea_arranger']:\n                if instrument:\n                    arr_inst = options['cea_arranger'] + ' ' + instrument\n                else:\n                    arr_inst = options['cea_arranger']\n            else:\n                arr_inst = instrument\n            annotations = {'instrument': instrument,\n                           'vocal': instrument,\n                           'performing orchestra': instrument,\n                           'chorus master': options['cea_chorusmaster'],\n                           'concertmaster': options['cea_concertmaster'],\n                           'arranger': options['cea_arranger'],\n                           'instrument arranger': arr_inst,\n                           'orchestrator': options['cea_orchestrator'],\n                           'vocal arranger': arr_inst}\n            tag = tag_strings[artist_type][0]\n            cea_tag = tag_strings[artist_type][1]\n            sort_tag = tag_strings[artist_type][2]\n            cea_sort_tag = tag_strings[artist_type][3]\n            cea_names_tag = cea_tag[:-1] + '_names'\n            cea_instrumented_tag = cea_names_tag + '_instrumented'\n            if artist_type in sub_strings:\n                if sub_strings[artist_type]:\n                    tag += sub_strings[artist_type]\n                else:\n                    write_log(\n                            release_id,\n                            'warning',\n                            'No instrument/sub-key available for artist_type %s. Performer = %s. Track is %s',\n                            artist_type,\n                            performer[2],\n                            track)\n\n            if tag:\n                if '~ce_tag_cleared_' + \\\n                        tag not in tm or not tm['~ce_tag_cleared_' + tag] == \"Y\":\n                    if tag in tm:\n                        write_log(release_id, 'info', 'delete tag %s', tag)\n                        del tm[tag]\n                tm['~ce_tag_cleared_' + tag] = \"Y\"\n            if sort_tag:\n                if '~ce_tag_cleared_' + \\\n                        sort_tag not in tm or not tm['~ce_tag_cleared_' + sort_tag] == \"Y\":\n                    if sort_tag in tm:\n                        del tm[sort_tag]\n                tm['~ce_tag_cleared_' + sort_tag] = \"Y\"\n\n            name_list = performer[2]\n            for ind, name in enumerate(name_list):\n                performer_type = ''\n                sort_name = performer[3][ind]\n                no_credit = True\n                # change name to as-credited\n                if (performing_artist and options['cea_performer_credited'] or\n                        not performing_artist and options['cea_composer_credited']):\n                    if sort_name in self.artist_credits[album]:\n                        no_credit = False\n                        name = self.artist_credits[album][sort_name]\n                # over-ride with aliases and use standard MB name (not\n                # as-credited) if no alias\n                if (options['cea_aliases'] or not performing_artist and options['cea_aliases_composer']) and (\n                        no_credit or options['cea_alias_overrides']):\n                    if sort_name in self.artist_aliases:\n                        name = self.artist_aliases[sort_name]\n                # fix cyrillic names if not already fixed\n                if options['cea_cyrillic']:\n                    if not only_roman_chars(name):\n                        name = remove_middle(unsort(sort_name))\n                        # Only remove middle name where the existing\n                        # performer is in non-latin script\n                annotated_name = name\n                if instrument:\n                    instrumented_name = name + ' (' + instrument + ')'\n                else:\n                    instrumented_name = name\n                # add annotations and write performer tags\n                if artist_type in annotations:\n                    if annotations[artist_type]:\n                        annotated_name += ' (' + annotations[artist_type] + ')'\n                    else:\n                        write_log(\n                                release_id,\n                                'warning',\n                                'No annotation (instrument) available for artist_type %s.'\n                                ' Performer = %s. Track is %s',\n                                artist_type,\n                                performer[2],\n                                track)\n                if artist_type in insertions and options['cea_arrangers']:\n                    self.append_tag(release_id, tm, tag, annotated_name)\n                else:\n                    if options['cea_arrangers'] or artist_type == tag:\n                        self.append_tag(release_id, tm, tag, name)\n\n                if options['cea_arrangers'] or artist_type == tag:\n                    if sort_tag:\n                        self.append_tag(release_id, tm, sort_tag, sort_name)\n                        if options['cea_tag_sort'] and '~' in sort_tag:\n                            explicit_sort_tag = sort_tag.replace('~', '')\n                            self.append_tag(\n                                release_id, tm, explicit_sort_tag, sort_name)\n\n                self.append_tag(release_id, tm, cea_tag, annotated_name)\n                self.append_tag(release_id, tm, cea_names_tag, name)\n                if instrumented_name != name:\n                    self.append_tag(\n                        release_id,\n                        tm,\n                        cea_instrumented_tag,\n                        instrumented_name)\n\n                if cea_sort_tag:\n                    self.append_tag(release_id, tm, cea_sort_tag, sort_name)\n\n                # differentiate soloists etc and write related tags\n                if artist_type == 'performing orchestra' or (\n                        instrument and instrument in self.ENSEMBLE_TYPES) or self.ensemble_type(name):\n                    performer_type = 'ensembles'\n                    self.append_tag(\n                        release_id, tm, '~cea_ensembles', instrumented_name)\n                    self.append_tag(\n                        release_id, tm, '~cea_ensemble_names', name)\n                    self.append_tag(\n                        release_id, tm, '~cea_ensembles_sort', sort_name)\n                elif artist_type in ['performer', 'instrument', 'vocal']:\n                    performer_type = 'soloists'\n                    self.append_tag(\n                        release_id, tm, '~cea_soloists', instrumented_name)\n                    self.append_tag(release_id, tm, '~cea_soloist_names', name)\n                    self.append_tag(\n                        release_id, tm, '~cea_soloists_sort', sort_name)\n                    if artist_type == \"vocal\":\n                        self.append_tag(\n                            release_id, tm, '~cea_vocalists', instrumented_name)\n                        self.append_tag(\n                            release_id, tm, '~cea_vocalist_names', name)\n                        self.append_tag(\n                            release_id, tm, '~cea_vocalists_sort', sort_name)\n                    elif instrument:\n                        self.append_tag(\n                            release_id, tm, '~cea_instrumentalists', instrumented_name)\n                        self.append_tag(\n                            release_id, tm, '~cea_instrumentalist_names', name)\n                        self.append_tag(\n                            release_id, tm, '~cea_instrumentalists_sort', sort_name)\n                    else:\n                        self.append_tag(\n                            release_id, tm, '~cea_other_soloists', instrumented_name)\n                        self.append_tag(\n                            release_id, tm, '~cea_other_soloist_names', name)\n                        self.append_tag(\n                            release_id, tm, '~cea_other_soloists_sort', sort_name)\n\n                # set album artists\n                if performer_type or artist_type == 'conductor':\n                    cea_album_tag = cea_tag.replace(\n                        'cea', 'cea_album').replace(\n                        'performers', performer_type)\n                    cea_album_sort_tag = cea_sort_tag.replace(\n                        'cea', 'cea_album').replace(\n                        'performers', performer_type)\n                    if stripsir(name) in tm['~albumartists'] or stripsir(\n                            sort_name) in tm['~albumartists_sort']:\n                        self.append_tag(release_id, tm, cea_album_tag, name)\n                        self.append_tag(\n                            release_id, tm, cea_album_sort_tag, sort_name)\n                    else:\n                        if performer_type:\n                            self.append_tag(\n                                release_id, tm, '~cea_support_performers', instrumented_name)\n                            self.append_tag(\n                                release_id, tm, '~cea_support_performer_names', name)\n                            self.append_tag(\n                                release_id, tm, '~cea_support_performers_sort', sort_name)\n\n##############\n##############\n# WORK PARTS #\n##############\n##############\n\n\nclass PartLevels():\n    # QUEUE-HANDLING\n    class WorksQueue(LockableObject):\n        \"\"\"Object for managing the queue of lookups\"\"\"\n\n        def __init__(self):\n            LockableObject.__init__(self)\n            self.queue = {}\n\n        def __contains__(self, name):\n            return name in self.queue\n\n        def __iter__(self):\n            return self.queue.__iter__()\n\n        def __getitem__(self, name):\n            self.lock_for_read()\n            value = self.queue[name] if name in self.queue else None\n            self.unlock()\n            return value\n\n        def __setitem__(self, name, value):\n            self.lock_for_write()\n            self.queue[name] = value\n            self.unlock()\n\n        def append(self, name, value):\n            self.lock_for_write()\n            if name in self.queue:\n                self.queue[name].append(value)\n                value = False\n            else:\n                self.queue[name] = [value]\n                value = True\n            self.unlock()\n            return value\n\n        def remove(self, name):\n            self.lock_for_write()\n            value = None\n            if name in self.queue:\n                value = self.queue[name]\n                del self.queue[name]\n            self.unlock()\n            return value\n\n        # INITIALISATION\n\n    def __init__(self):\n        self.works_cache = {}\n        # maintains list of parent of each workid, or None if no parent found,\n        # so that XML lookup need only executed if no existing record\n\n        self.partof = collections.defaultdict(dict)\n        # the inverse of the above (immediate children of each parent)\n        # but note that this is specific to the album as children may vary between albums\n        # so format is {album1{parent1: child1, parent2:, child2},\n        # album2{....}}\n\n        self.works_queue = self.WorksQueue()\n        # lookup queue - holds track/album pairs for each queued workid (may be\n        # more than one pair per id, especially for higher-level parts)\n\n        self.parts = collections.defaultdict(\n            lambda: collections.defaultdict(dict))\n        # metadata collection for all parts - structure is {workid: {name: ,\n        # parent: , (track,album): {part_levels}}, etc}\n\n        self.top_works = collections.defaultdict(dict)\n        # metadata collection for top-level works for (track, album) -\n        # structure is {(track, album): {workId: }, etc}\n\n        self.trackback = collections.defaultdict(\n            lambda: collections.defaultdict(dict))\n        # hierarchical iterative work structure - {album: {id: , children:{id:\n        # , children{}, id: etc}, id: etc} }\n\n        self.child_listing = collections.defaultdict(list)\n        # contains list of workIds which are descendants of a given workId, to\n        # prevent recursion when adding new ids\n\n        self.work_listing = collections.defaultdict(list)\n        # contains list of workIds for each album\n\n        self.top = collections.defaultdict(list)\n        # self.top[album] = list of work Ids which are top-level works in album\n\n        self.options = collections.defaultdict(dict)\n        # active Classical Extras options for current track\n\n        self.synonyms = collections.defaultdict(dict)\n        # active synonym options for current track\n\n        self.replacements = collections.defaultdict(dict)\n        # active synonym options for current track\n\n        self.file_works = collections.defaultdict(list)\n        # list of works derived from SongKong-style file tags\n        # structure is {(album, track): [{workid: , name: }, {workid: ....}}\n\n        self.album_artists = collections.defaultdict(\n            lambda: collections.defaultdict(dict))\n        # collection of artists to be applied at album level\n\n        self.artist_aliases = {}\n        # collection of alias names - format is {sort_name: alias_name, ...}\n\n        self.artist_credits = collections.defaultdict(dict)\n        # collection of credited-as names - format is {album: {sort_name: credit_name,\n        # ...}, ...}\n\n        self.release_artists_sort = collections.defaultdict(list)\n        # collection of release artists - format is {album: [sort_name_1,\n        # sort_name_2, ...]}\n\n        self.lyricist_filled = collections.defaultdict(dict)\n        # Boolean for each track to indicate if lyricist has been found (don't\n        # want to add more from higher levels)\n\n        self.orphan_tracks = collections.defaultdict(list)\n        # To keep a list for each album of tracks which do not have works -\n        # format is {album: [track1, track2, ...], etc}\n\n        self.tracks = collections.defaultdict(\n            lambda: collections.defaultdict(dict))\n        # To keep a list of all tracks for the album - format is {album:\n        # {track1: {movement-group: movementgroup, movement-number: movementnumber},\n        #  track2: {}, ..., etc}, album2: etc}\n\n    ########################################\n    # SECTION 1 - Initial track processing #\n    ########################################\n\n    def add_work_info(\n            self,\n            album,\n            track_metadata,\n            trackXmlNode,\n            releaseXmlNode):\n        \"\"\"\n        Main Routine - run for each track\n        :param album:\n        :param track_metadata:\n        :param trackXmlNode:\n        :param releaseXmlNode:\n        :return:\n        \"\"\"\n        release_id = track_metadata['musicbrainz_albumid']\n        if 'start' not in release_status[release_id]:\n            release_status[release_id]['start'] = datetime.now()\n        if 'lookups' not in release_status[release_id]:\n            release_status[release_id]['lookups'] = 0\n        release_status[release_id]['name'] = track_metadata['album']\n        release_status[release_id]['works'] = True\n        if config.setting['log_debug'] or config.setting['log_info']:\n            write_log(\n                release_id,\n                'debug',\n                'STARTING WORKS PROCESSING FOR ALBUM %s, DISC %s, TRACK %s',\n                track_metadata['album'],\n                track_metadata['discnumber'],\n                track_metadata['tracknumber'] +\n                ' ' +\n                track_metadata['title'])\n        # clear the cache if required (if this is not done, then queue count may get out of sync)\n        # Jump through hoops to get track object!!\n        track = album._new_tracks[-1]\n        tm = track.metadata\n        if config.setting['log_debug'] or config.setting['log_info']:\n            write_log(\n                release_id,\n                'debug',\n                'Cache setting for track %s is %s',\n                track,\n                config.setting['use_cache'])\n\n        # OPTIONS - OVER-RIDE IF REQUIRED\n        if '~ce_options' not in tm:\n            if config.setting['log_debug'] or config.setting['log_info']:\n                write_log(release_id, 'debug', 'Workparts gets track first...')\n            get_options(release_id, album, track)\n        options = interpret(tm['~ce_options'])\n\n        if not options:\n            if config.setting['log_error']:\n                write_log(\n                    release_id,\n                    'error',\n                    'Work Parts. Failure to read saved options for track %s. options = %s',\n                    track,\n                    tm['~ce_options'])\n            options = option_settings(config.setting)\n        self.options[track] = options\n\n        # CONSTANTS\n        write_log(release_id, 'basic', 'Options: %s' ,options)\n        self.ERROR = options[\"log_error\"]\n        self.WARNING = options[\"log_warning\"]\n        self.SEPARATORS = ['; ']\n        self.EQ = \"EQ_TO_BE_REVERSED\"  # phrase to indicate that a synonym has been used\n\n        self.get_sk_tags(release_id, album, track, tm, options)\n        self.synonyms[track] = self.get_text_tuples(\n            release_id, track, 'synonyms')  # a list of tuples\n        self.replacements[track] = self.get_text_tuples(\n            release_id, track, 'replacements')  # a list of tuples\n\n        # Continue?\n        if not options[\"classical_work_parts\"]:\n            return\n\n        # OPTION-DEPENDENT CONSTANTS:\n        # Maximum number of XML- lookup retries if error returned from server\n        self.MAX_RETRIES = options[\"cwp_retries\"]\n        self.USE_CACHE = options[\"use_cache\"]\n        if options[\"cwp_partial\"] and options[\"cwp_partial_text\"] and options[\"cwp_level0_works\"]:\n            options[\"cwp_removewords_p\"] = options[\"cwp_removewords\"] + \\\n                \", \" + options[\"cwp_partial_text\"] + ' '\n        else:\n            options[\"cwp_removewords_p\"] = options[\"cwp_removewords\"]\n        # Explanation:\n        # If \"Partial\" is selected then the level 0 work name will have PARTIAL_TEXT appended to it.\n        # If a recording is split across several tracks then each sub-part (quasi-movement) will have the same name\n        # (with the PARTIAL_TEXT added). If level 0 is used to source work names then the level 1 work name will be\n        # changed to be this repeated name and will therefore also include PARTIAL_TEXT.\n        # So we need to add PARTIAL_TEXT to the prefixes list to ensure it is excluded from the level 1 work name.\n        #\n        write_log(\n                release_id,\n                'debug',\n                \"PartLevels - LOAD NEW TRACK: :%s\",\n                track)\n        # write_log(release_id, 'info', \"trackXmlNode:\") # warning - may break Picard\n\n        # first time for this album (reloads each refresh)\n        if tm['discnumber'] == '1' and tm['tracknumber'] == '1':\n            # get artist aliases - these are cached so can be re-used across\n            # releases, but are reloaded with each refresh\n            get_aliases(self, release_id, album, options, releaseXmlNode)\n\n        # fix titles which include composer name\n        composersort =[]\n        if 'compposersort' in tm:\n            composersort = str_to_list(['composersort'])\n        composerlastnames = []\n        for composer in composersort:\n            lname = re.compile(r'(.*),')\n            match = lname.search(composer)\n            if match:\n                composerlastnames.append(match.group(1))\n            else:\n                composerlastnames.append(composer)\n        title = track_metadata['title']\n        colons = title.count(\":\")\n        if colons > 0:\n            title_split = title.split(': ', 1)\n            test = title_split[0]\n            if test in composerlastnames:\n                track_metadata['~cwp_title'] = title_split[1]\n\n        # now process works\n        write_log(\n                release_id,\n                'info',\n                'PartLevels - add_work_info - metadata load = %r',\n                track_metadata)\n        workIds = []\n        if 'musicbrainz_workid' in tm:\n            workIds = str_to_list(tm['musicbrainz_workid'])\n        if workIds and not (options[\"ce_no_run\"] and (\n                not tm['~ce_file'] or tm['~ce_file'] == \"None\")):\n            self.build_work_info(release_id, options, trackXmlNode, album, track, track_metadata, workIds)\n\n        else:  # no work relation\n            write_log(\n                    release_id,\n                    'warning',\n                    \"WARNING - no works for this track: \\\"%s\\\"\",\n                    title)\n            self.append_tag(\n                release_id,\n                track_metadata,\n                '~cwp_warning',\n                '3. No works for this track')\n            if album in self.orphan_tracks:\n                if track not in self.orphan_tracks[album]:\n                    self.orphan_tracks[album].append(track)\n            else:\n                self.orphan_tracks[album] = [track]\n            # Don't publish metadata yet until all album is processed\n\n        # last track\n        write_log(\n                release_id,\n                'debug',\n                'Check for last track. Requests = %s, Tracknumber = %s, Totaltracks = %s,'\n                ' Discnumber = %s, Totaldiscs = %s',\n                album._requests,\n                track_metadata['tracknumber'],\n                track_metadata['totaltracks'],\n                track_metadata['discnumber'],\n                track_metadata['totaldiscs'])\n        if album._requests == 0 and track_metadata['tracknumber'] == track_metadata[\n                'totaltracks'] and track_metadata['discnumber'] == track_metadata['totaldiscs']:\n            self.process_album(release_id, album)\n            release_status[release_id]['works-done'] = datetime.now()\n            close_log(release_id, 'works')\n\n\n    def build_work_info(self, release_id, options, trackXmlNode, album, track, track_metadata, workIds):\n        \"\"\"\n        Construct the work metadata, taking into account partial recordings and medleys\n        :param release_id:\n        :param options:\n        :param trackXmlNode: JSON returned by the webservice\n        :param album:\n        :param track:\n        :param track_metadata:\n        :param workIds: work ids for this track\n        :return:\n        \"\"\"\n        work_list_info = []\n        keyed_workIds = {}\n        for i, workId in enumerate(workIds):\n\n            # sort by ordering_key, if any\n            match_tree = [\n                'recording',\n                'relations',\n                'target-type:work',\n                'work',\n                'id:' + workId]\n            rels = parse_data(release_id, trackXmlNode, [], *match_tree)\n            # for recordings which are ordered within track:-\n            match_tree_1 = [\n                'ordering-key']\n            # for recordings of works which are ordered as part of parent\n            # (may be duplicated by top-down check later):-\n            match_tree_2 = [\n                'relations',\n                'target-type:work',\n                'type:parts',\n                'direction:backward',\n                'ordering-key']\n            parse_result = parse_data(release_id,\n                                      rels,\n                                      [],\n                                      *match_tree_1) + parse_data(release_id,\n                                                                  rels,\n                                                                  [],\n                                                                  *match_tree_2)\n            write_log(\n                release_id,\n                'info',\n                'multi-works - ordering key: %s',\n                parse_result)\n            if parse_result:\n                if isinstance(parse_result[0], int):\n                    key = parse_result[0]\n                elif isinstance(parse_result[0], str) and parse_result[0].isdigit():\n                    key = int(parse_result[0])\n                else:\n                    key = 100 + i\n            else:\n                key = 100 + i\n            keyed_workIds[key] = workId\n        partial = False\n        for key in sorted(keyed_workIds):\n            workId = keyed_workIds[key]\n            work_rels = parse_data(\n                release_id,\n                trackXmlNode,\n                [],\n                'recording',\n                'relations',\n                'target-type:work',\n                'work.id:' + workId)\n            write_log(release_id, 'info', 'work_rels: %s', work_rels)\n            work_attributes = parse_data(\n                release_id, work_rels, [], 'attributes')[0]\n            write_log(\n                release_id,\n                'info',\n                'work_attributes: %s',\n                work_attributes)\n            work_titles = parse_data(\n                release_id, work_rels, [], 'work', 'title')\n            work_list_info_item = {\n                'id': workId,\n                'attributes': work_attributes,\n                'titles': work_titles}\n            work_list_info.append(work_list_info_item)\n            work = []\n            for title in work_titles:\n                work.append(title)\n            if options['cwp_partial']:\n                # treat the recording as work level 0 and the work of which it\n                # is a partial recording as work level 1\n                if 'partial' in work_attributes:\n                    partial = True\n                    parentId = workId\n                    workId = track_metadata['musicbrainz_recordingid']\n\n                    works = []\n                    for w in work:\n                        partwork = w\n                        works.append(partwork)\n\n                    write_log(\n                        release_id,\n                        'info',\n                        \"Id %s is PARTIAL RECORDING OF id: %s, name: %s\",\n                        workId,\n                        parentId,\n                        work)\n                    work_list_info_item = {\n                        'id': workId,\n                        'attributes': [],\n                        'titles': works,\n                        'parent': parentId}\n                    work_list_info.append(work_list_info_item)\n        write_log(\n            release_id,\n            'info',\n            'work_list_info: %s',\n            work_list_info)\n        # we now have a list of items, where the id of each is a work id for the track or\n        #  (multiple instances of) the recording id (for partial works)\n        # we need to turn this into a usable hierarchy - i.e. just one item\n        workId_list = []\n        work_list = []\n        parent_list = []\n        attribute_list = []\n        workId_list_p = []\n        work_list_p = []\n        attribute_list_p = []\n        for w in work_list_info:\n            if 'partial' not in w['attributes'] or not options[\n                'cwp_partial']:  # just do the bottom-level 'works' first\n                workId_list.append(w['id'])\n                work_list += w['titles']\n                attribute_list += w['attributes']\n                if 'parent' in w:\n                    if w['parent'] not in parent_list:  # avoid duplicating parents!\n                        parent_list.append(w['parent'])\n            else:\n                workId_list_p.append(w['id'])\n                work_list_p += w['titles']\n                attribute_list_p += w['attributes']\n        # de-duplicate work names\n        # list(set()) won't work as need to retain order\n        work_list = list(collections.OrderedDict.fromkeys(work_list))\n        work_list_p = list(collections.OrderedDict.fromkeys(work_list_p))\n\n        workId_tuple = tuple(workId_list)\n        workId_tuple_p = tuple(workId_list_p)\n        if workId_tuple not in self.work_listing[album]:\n            self.work_listing[album].append(workId_tuple)\n        if workId_tuple not in self.parts or not self.USE_CACHE:\n            self.parts[workId_tuple]['name'] = work_list\n            if parent_list:\n                if workId_tuple in self.works_cache:\n                    self.works_cache[workId_tuple] += parent_list\n                    self.parts[workId_tuple]['parent'] += parent_list\n                else:\n                    self.works_cache[workId_tuple] = parent_list\n                    self.parts[workId_tuple]['parent'] = parent_list\n                self.parts[workId_tuple_p]['name'] = work_list_p\n                if workId_tuple_p not in self.work_listing[album]:\n                    self.work_listing[album].append(workId_tuple_p)\n\n            if 'medley' in attribute_list_p:\n                self.parts[workId_tuple_p]['medley'] = True\n\n            if 'medley' in attribute_list:\n                self.parts[workId_tuple]['medley'] = True\n\n            if partial:\n                self.parts[workId_tuple]['partial'] = True\n\n        self.trackback[album][workId_tuple]['id'] = workId_list\n        if 'meta' in self.trackback[album][workId_tuple]:\n            if (track,\n                album) not in self.trackback[album][workId_tuple]['meta']:\n                self.trackback[album][workId_tuple]['meta'].append(\n                    (track, album))\n        else:\n            self.trackback[album][workId_tuple]['meta'] = [(track, album)]\n        write_log(\n            release_id,\n            'info',\n            \"Trackback for %s is %s. Partial = %s\",\n            track,\n            self.trackback[album][workId_tuple],\n            partial)\n\n        if workId_tuple in self.works_cache and (\n                self.USE_CACHE or partial):\n            write_log(\n                release_id,\n                'debug',\n                \"GETTING WORK METADATA FROM CACHE, for work %s\",\n                workId_tuple)\n            if workId_tuple not in self.work_listing[album]:\n                self.work_listing[album].append(workId_tuple)\n            not_in_cache = self.check_cache(\n                track_metadata, album, track, workId_tuple, [])\n        else:\n            if partial:\n                not_in_cache = [workId_tuple_p]\n            else:\n                not_in_cache = [workId_tuple]\n        for workId_tuple in not_in_cache:\n            if not self.USE_CACHE:\n                if workId_tuple in self.works_cache:\n                    del self.works_cache[workId_tuple]\n            self.work_not_in_cache(release_id, album, track, workId_tuple)\n\n\n    def get_sk_tags(self, release_id, album, track, tm, options):\n        \"\"\"\n        Get file tags which are consistent with SongKong's metadata usage\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param album:\n        :param track:\n        :param tm:\n        :param options:\n        :return:\n        \"\"\"\n        if options[\"cwp_use_sk\"]:\n            if '~ce_file' in tm and interpret(tm['~ce_file']):\n                music_file = tm['~ce_file']\n                orig_metadata = album.tagger.files[music_file].orig_metadata\n                if 'musicbrainz_work_composition_id' in orig_metadata and 'musicbrainz_workid' in orig_metadata:\n                    if 'musicbrainz_work_composition' in orig_metadata:\n                        if 'musicbrainz_work' in orig_metadata:\n                            if orig_metadata['musicbrainz_work_composition_id'] == orig_metadata[\n                                'musicbrainz_workid'] \\\n                                    and orig_metadata['musicbrainz_work_composition'] != orig_metadata[\n                                        'musicbrainz_work']:\n                                # Picard may have overwritten SongKong tag (top\n                                # work id) with bottom work id\n                                write_log(\n                                    release_id,\n                                    'warning',\n                                    'File tag musicbrainz_workid incorrect? id = %s. Sourcing from MB',\n                                    orig_metadata['musicbrainz_workid'])\n                                if self.WARNING:\n                                    self.append_tag(\n                                        release_id,\n                                        tm,\n                                        '~cwp_warning',\n                                        '4. File tag musicbrainz_workid incorrect? id = ' +\n                                        orig_metadata['musicbrainz_workid'] +\n                                        '. Sourcing from MB')\n                                return None\n                        write_log(\n                                release_id,\n                                'info',\n                                'Read from file tag: musicbrainz_work_composition_id: %s',\n                                orig_metadata['musicbrainz_work_composition_id'])\n                        self.file_works[(album, track)].append({\n                            'workid': orig_metadata['musicbrainz_work_composition_id'].split('; '),\n                            'name': orig_metadata['musicbrainz_work_composition']})\n                    else:\n                        wid = orig_metadata['musicbrainz_work_composition_id']\n                        write_log(\n                            release_id,\n                            'error',\n                            \"No matching work name for id tag %s\",\n                            wid)\n                        if self.ERROR:\n                            self.append_tag(\n                                release_id,\n                                tm,\n                                '~cwp_error',\n                                '2. No matching work name for id tag ' +\n                                wid)\n                        return None\n                    n = 1\n                    while 'musicbrainz_work_part_level' + \\\n                            str(n) + '_id' in orig_metadata:\n                        if 'musicbrainz_work_part_level' + \\\n                                str(n) in orig_metadata:\n                            self.file_works[(album, track)].append({\n                                'workid': orig_metadata[\n                                    'musicbrainz_work_part_level' + str(n) + '_id'].split('; '),\n                                'name': orig_metadata['musicbrainz_work_part_level' + str(n)]})\n                            n += 1\n                        else:\n                            wid = orig_metadata['musicbrainz_work_part_level' +\n                                                str(n) + '_id']\n                            write_log(\n                                release_id, 'error', \"No matching work name for id tag %s\", wid)\n                            if self.ERROR:\n                                self.append_tag(\n                                    release_id,\n                                    tm,\n                                    '~cwp_error',\n                                    '2. No matching work name for id tag ' +\n                                    wid)\n                            break\n                    if orig_metadata['musicbrainz_work_composition_id'] != orig_metadata[\n                            'musicbrainz_workid']:\n                        if 'musicbrainz_work' in orig_metadata:\n                            self.file_works[(album, track)].append({\n                                'workid': orig_metadata['musicbrainz_workid'].split('; '),\n                                'name': orig_metadata['musicbrainz_work']})\n                        else:\n                            wid = orig_metadata['musicbrainz_workid']\n                            write_log(\n                                release_id, 'error', \"No matching work name for id tag %s\", wid)\n                            if self.ERROR:\n                                self.append_tag(\n                                    release_id,\n                                    tm,\n                                    '~cwp_error',\n                                    '2. No matching work name for id tag ' +\n                                    wid)\n                            return None\n                    file_work_levels = len(self.file_works[(album, track)])\n                    write_log(release_id,\n                                  'debug',\n                                  'Loaded works from file tags for track %s. Works: %s: ',\n                                  track,\n                                  self.file_works[(album,\n                                                   track)])\n                    for i, work in enumerate(self.file_works[(album, track)]):\n                        workId = tuple(work['workid'])\n                        if workId not in self.works_cache:  # Use cache in preference to file tags\n                            if workId not in self.work_listing[album]:\n                                self.work_listing[album].append(workId)\n                            self.parts[workId]['name'] = [work['name']]\n                            parentId = None\n                            parent = ''\n                            if i < file_work_levels - 1:\n                                parentId = self.file_works[(\n                                    album, track)][i + 1]['workid']\n                                parent = self.file_works[(\n                                    album, track)][i + 1]['name']\n\n                            if parentId:\n                                self.works_cache[workId] = parentId\n                                self.parts[workId]['parent'] = parentId\n                                self.parts[tuple(parentId)]['name'] = [parent]\n                            else:\n                                # so we remember we looked it up and found none\n                                self.parts[workId]['no_parent'] = True\n                                self.top_works[(track, album)\n                                               ]['workId'] = workId\n                                if workId not in self.top[album]:\n                                    self.top[album].append(workId)\n\n    def check_cache(self, tm, album, track, workId_tuple, not_in_cache):\n        \"\"\"\n        Recursive loop to get cached works\n        :param tm:\n        :param album:\n        :param track:\n        :param workId_tuple:\n        :param not_in_cache:\n        :return:\n        \"\"\"\n        parentId_tuple = tuple(self.works_cache[workId_tuple])\n        if parentId_tuple not in self.work_listing[album]:\n            self.work_listing[album].append(parentId_tuple)\n\n        if parentId_tuple in self.works_cache:\n            self.check_cache(tm, album, track, parentId_tuple, not_in_cache)\n        else:\n            not_in_cache.append(parentId_tuple)\n        return not_in_cache\n\n    def work_not_in_cache(self, release_id, album, track, workId_tuple):\n        \"\"\"\n        Determine actions if work not in cache (is it the top or do we need to look up?)\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param album:\n        :param track:\n        :param workId_tuple:\n        :return:\n        \"\"\"\n\n        write_log(\n                release_id,\n                'debug',\n                'Processing work_not_in_cache for workId %s',\n                workId_tuple)\n        ## NB the first condition below is to prevent the side effect of assigning a dictionary entry in self.parts for workId with no details\n        if workId_tuple in self.parts and 'no_parent' in self.parts[workId_tuple] and (\n                self.USE_CACHE or self.options[track][\"cwp_use_sk\"]) and self.parts[workId_tuple]['no_parent']:\n            write_log(release_id, 'info', '%s is top work', workId_tuple)\n            self.top_works[(track, album)]['workId'] = workId_tuple\n            if album in self.top:\n                if workId_tuple not in self.top[album]:\n                    self.top[album].append(workId_tuple)\n            else:\n                self.top[album] = [workId_tuple]\n        else:\n            write_log(\n                    release_id,\n                    'info',\n                    'Calling work_add_track to look up parents for %s',\n                    workId_tuple)\n            for workId in workId_tuple:\n                self.work_add_track(album, track, workId, 0)\n\n        write_log(\n                release_id,\n                'debug',\n                'End of work_not_in_cache for workId %s',\n                workId_tuple)\n\n    def work_add_track(self, album, track, workId, tries, user_data=True):\n        \"\"\"\n        Add the work to the lookup queue\n        :param user_data:\n        :param album:\n        :param track:\n        :param workId:\n        :param tries: number of lookup attempts\n        :return:\n        \"\"\"\n        release_id = track.metadata['musicbrainz_albumid']\n        write_log(\n                release_id,\n                'debug',\n                \"ADDING WORK TO LOOKUP QUEUE for work %s\",\n                workId)\n        self.album_add_request(release_id, album)\n        # to change the _requests variable to indicate that there are pending\n        # requests for this item and delay Picard from finalizing the album\n        write_log(\n                release_id,\n                'debug',\n                \"Added lookup request for id %s. Requests = %s\",\n                workId,\n                album._requests)\n        if self.works_queue.append(\n                workId,\n                (track,\n                 album)):  # All work combos are queued, but only new workIds are passed to XML lookup\n            host = config.setting[\"server_host\"]\n            port = config.setting[\"server_port\"]\n            path = \"/ws/2/%s/%s\" % ('work', workId)\n            if config.setting['cwp_aliases'] and config.setting['cwp_aliases_tag_text']:\n                if config.setting['cwp_aliases_tags_user'] and user_data:\n                    login = True\n                    tag_type = '+tags +user-tags'\n                else:\n                    login = False\n                    tag_type = '+tags'\n            else:\n                login = False\n                tag_type = ''\n            queryargs = {\n                \"inc\": \"work-rels+artist-rels+label-rels+place-rels+aliases\" +\n                tag_type}\n            write_log(\n                    release_id,\n                    'debug',\n                    \"Initiating XML lookup for %s......\",\n                    workId)\n            if release_id in release_status and 'lookups' in release_status[release_id]:\n                release_status[release_id]['lookups'] += 1\n            return album.tagger.webservice.get(\n                host,\n                port,\n                path,\n                partial(\n                    self.work_process,\n                    workId,\n                    tries),\n                # parse_response_type=\"xml\",\n                priority=True,\n                important=False,\n                mblogin=login,\n                queryargs=queryargs)\n        else:\n            write_log(\n                    release_id,\n                    'debug',\n                    \"Work is already in queue: %s\",\n                    workId)\n\n    ##########################################################################\n    # SECTION 2 - Works processing                                                                     #\n    # NB These functions may operate asynchronously over multiple albums (as well as multiple tracks)  #\n    ##########################################################################\n\n    def work_process(self, workId, tries, response, reply, error):\n        \"\"\"\n        Top routine to process the XML/JSON node response from the lookup\n        NB This function may operate over multiple albums (as well as multiple tracks)\n        :param workId:\n        :param tries:\n        :param response:\n        :param reply:\n        :param error:\n        :return:\n        \"\"\"\n\n        if error:\n            tuples = self.works_queue.remove(workId)\n            for track, album in tuples:\n                release_id = track.metadata['musicbrainz_albumid']\n                write_log(\n                        release_id,\n                        'warning',\n                        \"%r: Network error retrieving work record. Error code %r\",\n                        workId,\n                        error)\n                write_log(\n                        release_id,\n                        'debug',\n                        \"Removed request after network error. Requests = %s\",\n                        album._requests)\n                if tries < self.MAX_RETRIES:\n                    user_data = True\n                    write_log(release_id, 'debug', \"REQUEUEING...\")\n                    if str(error) == '204':  # Authentication error\n                        write_log(\n                                release_id, 'debug', \"... without user authentication\")\n                        user_data = False\n                        self.append_tag(\n                            release_id,\n                            track.metadata,\n                            '~cwp_error',\n                            '3. Authentication failure - data retrieval omits user-specific requests')\n                    self.work_add_track(\n                        album, track, workId, tries + 1, user_data)\n                else:\n                    write_log(\n                        release_id,\n                        'error',\n                        \"EXHAUSTED MAX RE-TRIES for XML lookup for track %s\",\n                        track)\n                    if self.ERROR:\n                        self.append_tag(\n                            release_id,\n                            track.metadata,\n                            '~cwp_error',\n                            \"4. ERROR: MISSING METADATA due to network errors. Re-try or fix manually.\")\n                self.album_remove_request(release_id, album)\n            return\n\n        tuples = self.works_queue.remove(workId)\n        if tuples:\n            new_queue = []\n            prev_album = None\n            album = tuples[0][1] # just added to prevent technical \"reference before assignment\" error\n            release_id = 'No_release_id'\n            for (track, album) in tuples:\n                release_id = track.metadata['musicbrainz_albumid']\n                # Note that this need to be set here as the work may cover\n                # multiple albums\n                if album != prev_album:\n                    write_log(release_id, 'debug',\n                                  \"Work_process. FOUND WORK: %s for album %s\",\n                                  workId, album)\n                    write_log(\n                        release_id,\n                        'debug',\n                        \"Requests for album %s = %s\",\n                        album,\n                        album._requests)\n                prev_album = album\n                write_log(release_id, 'info', \"RESPONSE = %s\", response)\n                # find the id_tuple(s) key with workId in it\n                wid_list = []\n                for w in self.work_listing[album]:\n                    if workId in w and w not in wid_list:\n                        wid_list.append(w)\n                write_log(\n                        release_id,\n                        'info',\n                        'wid_list for %s is %s',\n                        workId,\n                        wid_list)\n                for wid in wid_list:  # wid is a tuple\n                    write_log(\n                            release_id,\n                            'info',\n                            'processing workId tuple: %r',\n                            wid)\n                    metaList = self.work_process_metadata(\n                        release_id, workId, wid, track, response)\n                    parentList = metaList[0]\n                    # returns [[parent id], [parent name], attribute_list] or None if no parent\n                    # found\n                    arrangers = metaList[1]\n                    # not just arrangers - also composers, lyricists etc.\n                    if wid in self.parts:\n\n                        if arrangers:\n                            if 'arrangers' in self.parts[wid]:\n                                self.parts[wid]['arrangers'] += arrangers\n                            else:\n                                self.parts[wid]['arrangers'] = arrangers\n\n                        if parentList:\n                            # first fix the sort order of multi-works at the prev level\n                            # so that recordings of multiple movements of the same parent work will have the\n                            # movements listed in the correct order (i.e.\n                            # ordering-key, if available)\n                            if len(wid) > 1:\n                                for idx in wid:\n                                    if idx == workId:\n                                        match_tree = [\n                                            'relations',\n                                            'target-type:work',\n                                            'direction:backward',\n                                            'ordering-key']\n                                        parse_result = parse_data(\n                                            release_id, response, [], *match_tree)\n                                        write_log(\n                                                release_id,\n                                                'info',\n                                                'multi-works - ordering key for id %s is %s',\n                                                idx,\n                                                parse_result)\n                                        if parse_result:\n                                            if isinstance(\n                                                    parse_result[0], str) and parse_result[0].isdigit():\n                                                key = int(parse_result[0])\n                                            elif isinstance(parse_result[0], int):\n                                                key = parse_result[0]\n                                            else:\n                                                key = 9999\n                                            self.parts[wid]['order'][idx] = key\n\n                            parentIds = parentList[0]\n                            parents = parentList[1]\n                            parent_attributes = parentList[2]\n                            write_log(\n                                    release_id,\n                                    'info',\n                                    'Parents - ids: %s, names: %s',\n                                    parentIds,\n                                    parents)\n                            # remove any parents that are descendants of wid as\n                            # they will result in circular references\n                            del_list = []\n                            for i, parentId in enumerate(parentIds):\n                                for work_item in wid:\n                                    if work_item in self.child_listing and parentId in self.child_listing[\n                                            work_item]:\n                                        del_list.append(i)\n                            for i in list(set(del_list)):\n                                removed_id = parentIds.pop(i)\n                                removed_name = parents.pop(i)\n                                write_log(\n                                        release_id, 'error', \"Found parent which is descendant of child - \"\n                                        \"not using, to prevent circular references. id = %s,\"\n                                        \" name = %s\", removed_id, removed_name)\n                                tm = track.metadata\n                                self.append_tag(\n                                    release_id,\n                                    tm,\n                                    '~cwp_error',\n                                    '5. Found parent which which is descendant of child - not using '\n                                    'to prevent circular references. id = ' +\n                                    removed_id +\n                                    ', name = ' +\n                                    removed_name)\n                            is_collection = False\n                            for attribute in parent_attributes:\n                                if attribute['collection']:\n                                    is_collection = True\n                                    break\n                            # de-dup parent ids before we start\n                            parentIds = list(\n                                collections.OrderedDict.fromkeys(parentIds))\n\n                            # add descendants to checklist to prevent recursion\n                            for p in parentIds:\n                                for w in wid:\n                                    self.child_listing[p].append(w)\n                                    if w in self.child_listing:\n                                        self.child_listing[p] += self.child_listing[w]\n\n                            if parentIds:\n                                if wid in self.works_cache:\n                                    # Make sure we haven't done this\n                                    # relationship before, perhaps for another\n                                    # album\n\n                                    if not (set(\n                                            self.works_cache[wid]) >= set(parentIds)):\n                                        prev_ids = tuple(self.works_cache[wid])\n                                        prev_name = self.parts[prev_ids]['name']\n                                        self.works_cache[wid] = add_list_uniquely(\n                                            self.works_cache[wid], parentIds)\n                                        self.parts[wid]['parent'] = add_list_uniquely(\n                                            self.parts[wid]['parent'], parentIds)\n                                        index = self.work_listing[album].index(\n                                            prev_ids)\n                                        new_id_list = add_list_uniquely(\n                                            list(prev_ids), parentIds)\n                                        new_ids = tuple(new_id_list)\n                                        self.work_listing[album][index] = new_ids\n                                        self.parts[new_ids] = self.parts[prev_ids]\n                                        #del self.parts[prev_ids]  # Removed from here to deal with multi-parent parts. De-dup now takes place in process_albums.\n                                        self.parts[new_ids]['name'] = add_list_uniquely(\n                                            prev_name, parents)\n                                        parentIds = new_id_list\n                                        write_log(\n                                            release_id,\n                                            'debug',\n                                            \"In work_process. Changed wid in self.part: prev_ids = %s, new_ids = %s, prev_name = %s, new name = %s\",\n                                            prev_ids,\n                                            new_ids,\n                                            prev_name,\n                                            self.parts[new_ids]['name'])\n\n\n                                else:\n                                    self.works_cache[wid] = parentIds\n                                    self.parts[wid]['parent'] = parentIds\n                                    self.parts[tuple(parentIds)\n                                               ]['name'] = parents\n                                    self.work_listing[album].append(\n                                        tuple(parentIds))\n                                # de-duplicate the parent names\n                                # self.parts[tuple(parentIds)]['name'] = list(\n                                #     collections.OrderedDict.fromkeys(self.parts[tuple(parentIds)]['name']))\n                                # list(set()) won't work as need to retain order\n                                self.parts[tuple(parentIds)]['is_collection'] = is_collection\n                                write_log(\n                                    release_id,\n                                    'debug',\n                                    \"In work_process. self.parts[%s]['is_collection']: %s\",\n                                    tuple(parentIds),\n                                    self.parts[tuple(parentIds)]['is_collection'])\n                                # de-duplicate the parent ids also, otherwise they will be treated as a separate parent\n                                # in the trackback structure\n                                self.parts[wid]['parent'] = list(\n                                    collections.OrderedDict.fromkeys(\n                                        self.parts[wid]['parent']))\n                                self.works_cache[wid] = list(\n                                    collections.OrderedDict.fromkeys(\n                                        self.works_cache[wid]))\n                                write_log(\n                                        release_id,\n                                        'info',\n                                        'Added parent ids to work_listing: %s, [Requests = %s]',\n                                        parentIds,\n                                        album._requests)\n                                write_log(\n                                        release_id,\n                                        'info',\n                                        'work_listing after adding parents: %s',\n                                        self.work_listing[album])\n                                # the higher-level work might already be in\n                                # cache from another album\n                                if tuple(\n                                        parentIds) in self.works_cache and self.USE_CACHE:\n                                    not_in_cache = self.check_cache(\n                                        track.metadata, album, track, tuple(parentIds), [])\n                                    for workId_tuple in not_in_cache:\n                                        new_queue.append(\n                                            (release_id, album, track, workId_tuple))\n\n                                else:\n                                    if not self.USE_CACHE:\n                                        if tuple(\n                                                parentIds) in self.works_cache:\n                                            del self.works_cache[tuple(\n                                                parentIds)]\n                                    for parentId in parentIds:\n                                        new_queue.append(\n                                            (release_id, album, track, (parentId,)))\n\n                            else:\n                                # so we remember we looked it up and found none\n                                self.parts[wid]['no_parent'] = True\n                                self.top_works[(track, album)]['workId'] = wid\n                                if wid not in self.top[album]:\n                                    self.top[album].append(wid)\n                                write_log(\n                                        release_id, 'info', \"TOP[album]: %s\", self.top[album])\n                        else:\n                            # so we remember we looked it up and found none\n                            self.parts[wid]['no_parent'] = True\n                            self.top_works[(track, album)]['workId'] = wid\n                            self.top[album].append(wid)\n\n                write_log(\n                        release_id,\n                        'debug',\n                        \"End of tuple processing for workid %s in album %s, track %s,\"\n                        \" requests remaining  = %s, new queue is %r\",\n                        workId,\n                        album,\n                        track,\n                        album._requests,\n                        new_queue)\n                self.album_remove_request(release_id, album)\n                for queued_item in new_queue:\n                    write_log(\n                            release_id,\n                            'info',\n                            'Have a new queue: queued_item = %r',\n                            queued_item)\n            write_log(\n                    release_id,\n                    'debug',\n                    'Penultimate end of work_process for %s (subject to parent lookups in \"new_queue\")',\n                    workId)\n            for queued_item in new_queue:\n                self.work_not_in_cache(\n                    queued_item[0],\n                    queued_item[1],\n                    queued_item[2],\n                    queued_item[3])\n            write_log(release_id, 'debug',\n                          'Ultimate end of work_process for %s', workId)\n\n            if album._requests == 0:\n                self.process_album(release_id, album)\n                album._finalize_loading(None)\n                release_status[release_id]['works-done'] = datetime.now()\n                close_log(release_id, 'works')\n\n    def work_process_metadata(self, release_id, workId, wid, track, response):\n        \"\"\"\n        Process XML node\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        NB release_id may be from a different album than the original, if works lookups are identical\n        :param workId:\n        :param wid: The work id tuple of which workId is a member\n        :param track:\n        :param response:\n        :return:\n        \"\"\"\n        write_log(release_id, 'debug', \"In work_process_metadata\")\n        all_tags = parse_data(release_id, response, [], 'tags', 'name')\n        self.parts[wid]['folks_genres'] = all_tags\n        self.parts[wid]['worktype_genres'] = parse_data(\n            release_id, response, [], 'type')\n        key = parse_data(\n            release_id,\n            response,\n            [],\n            'attributes',\n            'type:Key',\n            'value')\n        self.parts[wid]['key'] = key\n        composed_begin_dates = year(\n            parse_data(\n                release_id,\n                response,\n                [],\n                'relations',\n                'target-type:artist',\n                'type:composer',\n                'begin'))\n        composed_end_dates = year(\n            parse_data(\n                release_id,\n                response,\n                [],\n                'relations',\n                'target-type:artist',\n                'type:composer',\n                'end'))\n        if composed_begin_dates == composed_end_dates:\n            composed_dates = composed_begin_dates\n        else:\n            composed_dates = list(\n                zip(composed_begin_dates, composed_end_dates))\n            composed_dates = [y + DATE_SEP + z if y != z else y for y, z in composed_dates]\n        self.parts[wid]['composed_dates'] = composed_dates\n        published_begin_dates = year(\n            parse_data(\n                release_id,\n                response,\n                [],\n                'relations',\n                'target-type:label',\n                'type:publishing',\n                'begin'))\n        published_end_dates = year(\n            parse_data(\n                release_id,\n                response,\n                [],\n                'relations',\n                'target-type:label',\n                'type:publishing',\n                'end'))\n        if published_begin_dates == published_end_dates:\n            published_dates = published_begin_dates\n        else:\n            published_dates = list(\n                zip(published_begin_dates, published_end_dates))\n            published_dates = [x + DATE_SEP + y for x, y in published_dates]\n        self.parts[wid]['published_dates'] = published_dates\n\n        premiered_begin_dates = year(\n            parse_data(\n                release_id,\n                response,\n                [],\n                'relations',\n                'target-type:place',\n                'type:premiere',\n                'begin'))\n        premiered_end_dates = year(\n            parse_data(\n                release_id,\n                response,\n                [],\n                'relations',\n                'target-type:place',\n                'type:premiere',\n                'end'))\n        if premiered_begin_dates == premiered_end_dates:\n            premiered_dates = premiered_begin_dates\n        else:\n            premiered_dates = list(\n                zip(premiered_begin_dates, premiered_end_dates))\n            premiered_dates = [x + DATE_SEP + y for x, y in premiered_dates]\n        self.parts[wid]['premiered_dates'] = premiered_dates\n\n        lang = get_preferred_artist_language(config)\n        if lang:\n            alias = parse_data(release_id, response, [], 'aliases',\n                               'locale:' + lang, 'primary:True', 'name')\n            user_tags = parse_data(\n                release_id, response, [], 'user-tags', 'name')\n            if config.setting['cwp_aliases_tags_user']:\n                tags = user_tags\n            else:\n                tags = all_tags\n            if alias:\n                self.parts[wid]['alias'] = self.parts[wid]['name'][:]\n                self.parts[wid]['tags'] = tags\n                for ind, w in enumerate(wid):\n                    if w == workId:\n                        # alias should be a one item list but just in case it isn't...\n                        if len(self.parts[wid]['alias']) > ind:\n                            # The condition here is just to trap errors caused by database inconsistencies\n                            # (e.g. a part is shown as a recording of two works, one of which is an arrangement\n                            # of the other - this can create a two-item wid with a one-item self.parts[wid]['name']\n                            self.parts[wid]['alias'][ind] = '; '.join(\n                                alias)\n        relation_list = parse_data(release_id, response, [], 'relations')\n        return self.work_process_relations(\n            release_id, track, workId, wid, relation_list)\n\n    def work_process_relations(\n            self,\n            release_id,\n            track,\n            workId,\n            wid,\n            relations):\n        \"\"\"\n        Find the parents etc.\n        NB track is just the last album/track for this work - used as being\n        representative for options identification. If this is inconsistent (e.g. different collections\n        option for albums with the same works) then the latest added track will over-ride others' settings).\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param track:\n        :param workId:\n        :param wid:\n        :param relations:\n        :return:\n        \"\"\"\n        write_log(\n                release_id,\n                'debug',\n                \"In work_process_relations. Relations--> %s\",\n                relations)\n        if track:\n            options = self.options[track]\n        else:\n            options = config.setting\n        new_workIds = []\n        new_works = []\n        attributes_list = []\n        relation_attributes = parse_data(\n            release_id,\n            relations,\n            [],\n            'target-type:work',\n            'type:parts',\n            'direction:backward',\n            'attributes')\n        new_work_list = []\n        write_log(\n            release_id,\n            'debug',\n            \"relation_attributes--> %s\",\n            relation_attributes)\n        for relation_attribute in relation_attributes:\n            if (\n                    'part of collection' not in relation_attribute) or options['cwp_collections']:\n                new_work_list += parse_data(release_id,\n                                            relations,\n                                            [],\n                                            'target-type:work',\n                                            'type:parts',\n                                            'direction:backward',\n                                            'work')\n            attributes_dict = {'collection' : ('part of collection' in relation_attribute),\n                               'movements' : ('movement' in relation_attribute),\n                               'acts' : ('act' in relation_attribute),\n                               'numbers' : ('number' in relation_attribute)}\n            attributes_list += [attributes_dict]\n            if (\n                    'part of collection' in relation_attribute) and not options['cwp_collections']:\n                write_log(\n                    release_id,\n                    'info',\n                    'Not getting parent work because relationship is \"part of collection\" and option not selected')\n        if new_work_list:\n            write_log(\n                    release_id,\n                    'info',\n                    'new_work_list: %s',\n                    new_work_list)\n            new_workIds = parse_data(release_id, new_work_list, [], 'id')\n            new_works = parse_data(release_id, new_work_list, [], 'title')\n        else:\n            arrangement_of = parse_data(\n                release_id,\n                relations,\n                [],\n                'target-type:work',\n                'type:arrangement',\n                'direction:backward',\n                'work')\n            if arrangement_of and options['cwp_arrangements']:\n                new_workIds = parse_data(release_id, arrangement_of, [], 'id')\n                new_works = parse_data(release_id, arrangement_of, [], 'title')\n                self.parts[wid]['arrangement'] = True\n            else:\n                medley_of = parse_data(\n                    release_id,\n                    relations,\n                    [],\n                    'target-type:work',\n                    'type:medley',\n                    'work')\n                direction = parse_data(\n                    release_id,\n                    relations,\n                    [],\n                    'target-type:work',\n                    'type:medley',\n                    'direction')\n                if 'backward' not in direction:\n                    write_log(\n                            release_id, 'info', 'Medley_of: %s', medley_of)\n                    if medley_of and options['cwp_medley']:\n                        medley_list = []\n                        medley_id_list = []\n                        for medley_item in medley_of:\n                            medley_list = medley_list + \\\n                                parse_data(release_id, medley_item, [], 'title')\n                            medley_id_list = medley_id_list + \\\n                                parse_data(release_id, medley_item, [], 'id')\n                            # (parse_data is a list...)\n                            new_workIds = medley_id_list\n                            new_works = medley_list\n                            write_log(\n                                    release_id, 'info', 'Medley_list: %s', medley_list)\n                        self.parts[wid]['medley_list'] = medley_list\n\n        write_log(\n                release_id,\n                'info',\n                'New works: ids: %s, names: %s, attributes: %s',\n                new_workIds,\n                new_works,\n                attributes_list)\n\n        artists = get_artists(\n            options,\n            release_id,\n            {},\n            relations,\n            'work')['artists']\n        # artist_types = ['arranger', 'instrument arranger', 'orchestrator', 'composer', 'writer', 'lyricist',\n        #                 'librettist', 'revised by', 'translator', 'reconstructed by', 'vocal arranger']\n\n        write_log(release_id, 'info', \"ARTISTS %s\", artists)\n\n        workItems = (new_workIds, new_works, attributes_list)\n        itemsFound = [workItems, artists]\n        return itemsFound\n\n    @staticmethod\n    def album_add_request(release_id, album):\n        \"\"\"\n        To keep track as to whether all lookups have been processed\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param album:\n        :return:\n        \"\"\"\n        album._requests += 1\n        write_log(\n                release_id,\n                'debug',\n                \"Added album request - requests: %s\",\n                album._requests)\n\n    @staticmethod\n    def album_remove_request(release_id, album):\n        \"\"\"\n        To keep track as to whether all lookups have been processed\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param album:\n        :return:\n        \"\"\"\n        album._requests -= 1\n        write_log(\n                release_id,\n                'debug',\n                \"Removed album request - requests: %s\",\n                album._requests)\n\n    ##################################################\n    # SECTION 3 - Organise tracks and works in album #\n    ##################################################\n\n    def process_album(self, release_id, album):\n        \"\"\"\n        Top routine to run end-of-album processes\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param album:\n        :return:\n        \"\"\"\n        write_log(release_id, 'debug', \"PROCESS ALBUM %s\", album)\n        release_status[release_id]['done-lookups'] = datetime.now()\n        # De-duplicate names in self.parts, maintaining order (in case part names have been arrived at via multiple paths)\n        for part_item in self.parts:\n            if 'name' in self.parts[part_item]:\n                self.parts[part_item]['name'] = list(collections.OrderedDict.fromkeys(str_to_list(self.parts[part_item]['name'])))\n        # populate the inverse hierarchy\n        write_log(release_id, 'info', \"Cache: %s\", self.works_cache)\n        write_log(release_id, 'info', \"Work listing %s\", self.work_listing)\n        alias_tag_list = config.setting['cwp_aliases_tag_text'].split(',')\n        for i, tag_item in enumerate(alias_tag_list):\n            alias_tag_list[i] = tag_item.strip()\n        for workId in self.work_listing[album]:\n            if workId in self.parts:\n                write_log(\n                    release_id,\n                    'info',\n                    'Processing workid: %s',\n                    workId)\n                write_log(\n                    release_id,\n                    'info',\n                    'self.work_listing[album]: %s',\n                    self.work_listing[album])\n                if len(workId) > 1:\n                    # fix the order of names using ordering keys gathered in\n                    # work_process\n                    if 'order' in self.parts[workId]:\n                        seq = []\n                        for idx in workId:\n                            if idx in self.parts[workId]['order']:\n                                seq.append(self.parts[workId]['order'][idx])\n                            else:\n                                # for the possibility of workids not part of\n                                # the same parent and not all ordered\n                                seq.append(999)\n                        zipped_names = zip(self.parts[workId]['name'], seq)\n                        sorted_tups = sorted(zipped_names, key=lambda x: x[1])\n                        self.parts[workId]['name'] = [x[0]\n                                                      for x in sorted_tups]\n                # use aliases where appropriate\n                # name is a list - need a string to test for Latin chars\n                name_string = '; '.join(self.parts[workId]['name'])\n                if config.setting['cwp_aliases']:\n                    if config.setting['cwp_aliases_all'] or (\n                        config.setting['cwp_aliases_greek'] and not only_roman_chars(name_string)) or (\n                        'tags' in self.parts[workId] and any(\n                            x in self.parts[workId]['tags'] for x in alias_tag_list)):\n                        if 'alias' in self.parts[workId] and self.parts[workId]['alias']:\n                            self.parts[workId]['name'] = self.parts[workId]['alias'][:]\n                topId = None\n                write_log(\n                        release_id,\n                        'info',\n                        'Works_cache: %s',\n                        self.works_cache)\n                if workId in self.works_cache:\n                    parentIds = tuple(self.works_cache[workId])\n                    # for parentId in parentIds:\n                    write_log(\n                            release_id,\n                            'debug',\n                            \"Create inverses: %s, %s\",\n                            workId,\n                            parentIds)\n                    if parentIds in self.partof[album]:\n                        if workId not in self.partof[album][parentIds]:\n                            self.partof[album][parentIds].append(workId)\n                    else:\n                        self.partof[album][parentIds] = [workId]\n                    write_log(release_id, 'info', \"Partof: %s\",\n                                  self.partof[album][parentIds])\n                    if 'no_parent' in self.parts[parentIds]:\n                        # to handle case if album includes works already in\n                        # cache from a different album\n                        if self.parts[parentIds]['no_parent']:\n                            topId = parentIds\n                else:\n                    topId = workId\n                if topId:\n                    if album in self.top:\n                        if topId not in self.top[album]:\n                            self.top[album].append(topId)\n                    else:\n                        self.top[album] = [topId]\n        # work out the full hierarchy and part levels\n        height = 0\n        write_log(\n                release_id,\n                'info',\n                \"TOP: %s, \\nALBUM: %s, \\nTOP[ALBUM]: %s\",\n                self.top,\n                album,\n                self.top[album])\n        if len(self.top[album]) > 1:\n            single_work_album = 0\n        else:\n            single_work_album = 1\n        for topId in self.top[album]:\n            self.create_trackback(release_id, album, topId)\n            write_log(\n                    release_id,\n                    'info',\n                    \"Top id = %s, Name = %s\",\n                    topId,\n                    self.parts[topId]['name'])\n            write_log(\n                    release_id,\n                    'info',\n                    \"Trackback before levels: %s\",\n                    self.trackback[album][topId])\n            work_part_levels = self.level_calc(\n                release_id, self.trackback[album][topId], height)\n            write_log(\n                    release_id,\n                    'info',\n                    \"Trackback after levels: %s\",\n                    self.trackback[album][topId])\n            # determine the level which will be the principal 'work' level\n            if work_part_levels >= 3:\n                ref_level = work_part_levels - single_work_album\n            else:\n                ref_level = work_part_levels\n            # extended metadata scheme won't display more than 3 work levels\n            # ref_level = min(3, ref_level)\n            ref_height = work_part_levels - ref_level\n            top_info = {\n                'levels': work_part_levels,\n                'id': topId,\n                'name': self.parts[topId]['name'],\n                'single': single_work_album}\n            # set the metadata in sequence defined by the work structure\n            answer = self.process_trackback(\n                release_id,\n                album,\n                self.trackback[album][topId],\n                ref_height,\n                top_info)\n            ##\n            #     trackback is a tree in the form {album: {id: , children:{id: , children{},\n            #                                                             id: etc},\n            #                                             id: etc} }\n            #     process_trackback uses the trackback tree to derive title and level_0 based hierarchies\n            #     from the structure. It also returns a tuple (id, tracks), where tracks has the structure\n            #     {'track': [(track, height), (track, height), ...tuples...]\n            #     'work': [[worknames], [worknames], ...lists...]\n            #     'tracknumber': [num, num, ...floats of form n.nnn = disc.track...]\n            #     'title':  [title, title, ...strings...]}\n            #     each list is the same length - i.e. the number of tracks for the top work\n            #     there can be more than one workname for a track\n            #     height is the number of part levels for the related track\n            ##\n            if answer:\n                tracks = sorted(zip(answer[1]['track'], answer[1]['tracknumber']), key=lambda x: x[1])\n                # need them in tracknumber sequence for the movement numbers to be correct\n                write_log(release_id, 'info', \"TRACKS: %s\", tracks)\n                # work_part_levels = self.trackback[album][topId]['depth']\n                movement_count = 0\n                prev_movementgroup = None\n                for track, _ in tracks:\n                    movement_count += 1\n                    track_meta = track[0]\n                    tm = track_meta.metadata\n                    if '~cwp_workid_0' in tm:\n                        workIds = tuple(str_to_list(tm['~cwp_workid_0']))\n                        if workIds:\n                            count = 0\n                            self.process_work_artists(\n                                release_id, album, track_meta, workIds, tm, count)\n                    title_work_levels = 0\n                    if '~cwp_title_work_levels' in tm:\n                        title_work_levels = int(tm['~cwp_title_work_levels'])\n                    movementgroup = self.extend_metadata(\n                        release_id,\n                        top_info,\n                        track_meta,\n                        ref_height,\n                        title_work_levels)  # revise for new data\n                    if track_meta not in self.tracks[album]:\n                        self.tracks[album][track_meta] = {}\n                    if movementgroup:\n                        if movementgroup != prev_movementgroup:\n                            movement_count = 1\n                        write_log(\n                            release_id,\n                            'debug',\n                            \"processing movements for track: %s - movement-group is %s\",\n                            track, movementgroup)\n                        self.tracks[album][track_meta]['movement-group'] = movementgroup\n                        self.tracks[album][track_meta]['movement-number'] = movement_count\n                        self.parts[tuple(movementgroup)]['movement-total'] = movement_count\n                    prev_movementgroup = movementgroup\n\n                write_log(\n                        release_id,\n                        'debug',\n                        \"FINISHED TRACK PROCESSING FOR Top work id: %s\",\n                        topId)\n        # Need to redo the loop so that all album-wide tm is updated before\n        # publishing\n        for track, movement_info in self.tracks[album].items():\n            self.publish_metadata(release_id, album, track, movement_info)\n        # #\n        # The messages below are normally commented out as they get VERY long if there are a lot of albums loaded\n        # For extreme debugging, remove the comments and just run one or a few albums\n        # Do not forget to comment out again.\n        # #\n        # write_log(release_id, 'info', 'Self.parts: %s', self.parts)\n        # write_log(release_id, 'info', 'Self.trackback: %s', self.trackback)\n\n        # tidy up\n        self.trackback[album].clear()\n        # Finally process the orphan tracks\n        if album in self.orphan_tracks:\n            for track in self.orphan_tracks[album]:\n                tm = track.metadata\n                options = self.options[track]\n                if options['cwp_derive_works_from_title']:\n                    work, movt, inter_work = self.derive_from_title(release_id, track, tm['title'])\n                    tm['~cwp_extended_work'] = tm['~cwp_extended_groupheading'] = tm['~cwp_title_work'] = \\\n                        tm['~cwp_title_groupheading'] = tm['~cwp_work'] = tm['~cwp_groupheading']= work\n                    tm['~cwp_part'] = tm['~cwp_extended_part'] = tm['~cwp_title_part_0'] = movt\n                    tm['~cwp_inter_work'] = tm['~cwp_extended_inter_work'] = tm['~cwp_inter_title_work'] = inter_work\n                self.publish_metadata(release_id, album, track)\n        write_log(release_id, 'debug', \"PROCESS ALBUM function complete\")\n\n    def create_trackback(self, release_id, album, parentId):\n        \"\"\"\n        Create an inverse listing of the work-parent relationships\n        :param release_id:\n        :param album:\n        :param parentId:\n        :return: trackback for a given parentId\n        \"\"\"\n        write_log(release_id, 'debug', \"Create trackback for %s\", parentId)\n        if parentId in self.partof[album]:  # NB parentId is a tuple\n            for child in self.partof[album][parentId]:  # NB child is a tuple\n                if child in self.partof[album]:\n                    child_trackback = self.create_trackback(\n                        release_id, album, child)\n                    self.append_trackback(\n                        release_id, album, parentId, child_trackback)\n                else:\n                    self.append_trackback(\n                        release_id, album, parentId, self.trackback[album][child])\n            return self.trackback[album][parentId]\n        else:\n            return self.trackback[album][parentId]\n\n    def append_trackback(self, release_id, album, parentId, child):\n        \"\"\"\n        Recursive process to populate trackback\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param album:\n        :param parentId:\n        :param child:\n        :return:\n        \"\"\"\n        write_log(release_id, 'debug', \"In append_trackback...\")\n        if parentId in self.trackback[album]:  # NB parentId is a tuple\n            if 'children' in self.trackback[album][parentId]:\n                if child not in self.trackback[album][parentId]['children']:\n                    write_log(release_id, 'info', \"TRYING TO APPEND...\")\n                    self.trackback[album][parentId]['children'].append(child)\n                    write_log(\n                            release_id,\n                            'info',\n                            \"...PARENT %s - ADDED %s as child\",\n                            self.parts[parentId]['name'],\n                            child)\n                else:\n                    write_log(\n                            release_id,\n                            'info',\n                            \"Parent %s already has %s as child\",\n                            parentId,\n                            child)\n            else:\n                self.trackback[album][parentId]['children'] = [child]\n                write_log(\n                        release_id,\n                        'info',\n                        \"Existing PARENT %s - ADDED %s as child\",\n                        self.parts[parentId]['name'],\n                        child)\n        else:\n            self.trackback[album][parentId]['id'] = parentId\n            self.trackback[album][parentId]['children'] = [child]\n            write_log(\n                release_id,\n                'info',\n                \"New PARENT %s - ADDED %s as child\",\n                self.parts[parentId]['name'],\n                child)\n            write_log(\n                release_id,\n                'info',\n                \"APPENDED TRACKBACK: %s\",\n                self.trackback[album][parentId])\n        return self.trackback[album][parentId]\n\n    def level_calc(self, release_id, trackback, height):\n        \"\"\"\n        Recursive process to determine the max level for a work\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param trackback:\n        :param height: number of levels above this one\n        :return:\n        \"\"\"\n        write_log(release_id, 'debug', 'In level_calc process')\n        if 'children' not in trackback:\n            write_log(release_id, 'info', \"Got to bottom\")\n            trackback['height'] = height\n            trackback['depth'] = 0\n            return 0\n        else:\n            trackback['height'] = height\n            height += 1\n            max_depth = 0\n            for child in trackback['children']:\n                write_log(release_id, 'info', \"CHILD: %s\", child)\n                depth = self.level_calc(release_id, child, height) + 1\n                write_log(release_id, 'info', \"DEPTH: %s\", depth)\n                max_depth = max(depth, max_depth)\n            trackback['depth'] = max_depth\n            return max_depth\n\n        ###########################################\n        # SECTION 4 - Process tracks within album #\n        ###########################################\n\n    def process_trackback(\n            self,\n            release_id,\n            album_req,\n            trackback,\n            ref_height,\n            top_info):\n        \"\"\"\n        Set work structure metadata & govern other metadata-setting processes\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param album_req:\n        :param trackback:\n        :param ref_height:\n        :param top_info:\n        :return:\n        \"\"\"\n        write_log(\n                release_id,\n                'debug',\n                \"IN PROCESS_TRACKBACK. Trackback = %s\",\n                trackback)\n        tracks = collections.defaultdict(dict)\n        process_now = False\n        if 'meta' in trackback:\n            for track, album in trackback['meta']:\n                if album_req == album:\n                    process_now = True\n        if process_now or 'children' not in trackback:\n            if 'meta' in trackback and 'id' in trackback and 'depth' in trackback and 'height' in trackback:\n                write_log(release_id, 'info', \"Processing level 0\")\n                depth = trackback['depth']\n                height = trackback['height']\n                workId = tuple(trackback['id'])\n                if depth != 0:\n                    if 'children' in trackback:\n                        child_response = self.process_trackback_children(\n                            release_id, album_req, trackback, ref_height, top_info, tracks)\n                        tracks = child_response[1]\n                    write_log(\n                            release_id,\n                            'info',\n                            'Bottom level for this trackback is higher level elsewhere - adjusting levels')\n                    depth = 0\n                write_log(release_id, 'info', \"WorkId: %s, Work name: %s\", workId, self.parts[workId]['name'])\n                for track, album in trackback['meta']:\n                    if album == album_req:\n                        write_log(release_id, 'info', \"Track: %s\", track)\n                        tm = track.metadata\n                        write_log(\n                                release_id, 'info', \"Track metadata = %s\", tm)\n                        tm['~cwp_workid_' + str(depth)] = workId\n                        self.write_tags(release_id, track, tm, workId)\n                        self.make_annotations(release_id, track, workId)\n                        # strip leading and trailing spaces from work names\n                        if isinstance(self.parts[workId]['name'], str):\n                            worktemp = self.parts[workId]['name'].strip()\n                        else:\n                            for index, it in enumerate(\n                                    self.parts[workId]['name']):\n                                self.parts[workId]['name'][index] = it.strip()\n                            worktemp = self.parts[workId]['name']\n                        if isinstance(top_info['name'], str):\n                            toptemp = top_info['name'].strip()\n                        else:\n                            for index, it in enumerate(top_info['name']):\n                                top_info['name'][index] = it.strip()\n                            toptemp = top_info['name']\n                        tm['~cwp_work_' + str(depth)] = worktemp\n                        tm['~cwp_part_levels'] = str(height)\n                        tm['~cwp_work_part_levels'] = str(top_info['levels'])\n                        tm['~cwp_workid_top'] = top_info['id']\n                        tm['~cwp_work_top'] = toptemp\n                        tm['~cwp_single_work_album'] = top_info['single']\n                        write_log(\n                                release_id, 'info', \"Track metadata = %s\", tm)\n                        if 'track' in tracks:\n                            tracks['track'].append((track, height))\n                        else:\n                            tracks['track'] = [(track, height)]\n                        tracks['tracknumber'] = [int(tm['discnumber']) + (int(tm['tracknumber']) / 1000)]\n                        # Hopefully no more than 999 tracks per disc!\n                        write_log(release_id, 'info', \"Tracks: %s\", tracks)\n\n                response = (workId, tracks)\n                write_log(release_id, 'debug', \"LEAVING PROCESS_TRACKBACK\")\n                write_log(\n                        release_id,\n                        'info',\n                        \"depth %s Response = %s\",\n                        depth,\n                        response)\n                return response\n            else:\n                return None\n        else:\n            response = self.process_trackback_children(\n                release_id, album_req, trackback, ref_height, top_info, tracks)\n            return response\n\n    def process_trackback_children(\n            self,\n            release_id,\n            album_req,\n            trackback,\n            ref_height,\n            top_info,\n            tracks):\n        \"\"\"\n        TODO add some better documentation!\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param album_req:\n        :param trackback:\n        :param ref_height:\n        :param top_info:\n        :param tracks:\n        :return:\n        \"\"\"\n        if 'id' in trackback and 'depth' in trackback and 'height' in trackback:\n            write_log(\n                    release_id,\n                    'debug',\n                    'In process_children_trackback for trackback %s',\n                    trackback)\n            depth = trackback['depth']\n            height = trackback['height']\n            parentId = tuple(trackback['id'])\n            parent = self.parts[parentId]['name']\n            width = 0\n            for child in trackback['children']:\n                width += 1\n                write_log(\n                        release_id,\n                        'info',\n                        \"child trackback = %s\",\n                        child)\n                answer = self.process_trackback(\n                    release_id, album_req, child, ref_height, top_info)\n                if answer:\n                    workId = answer[0]\n                    child_tracks = answer[1]['track']\n                    for track in child_tracks:\n                        track_meta = track[0]\n                        track_height = track[1]\n                        part_level = track_height - height\n                        write_log(\n                                release_id,\n                                'debug',\n                                \"Calling set metadata %s\",\n                                (part_level,\n                                 workId,\n                                 parentId,\n                                 parent,\n                                 track_meta))\n                        self.set_metadata(\n                            release_id, part_level, workId, parentId, parent, track_meta)\n                        if 'track' in tracks:\n                            tracks['track'].append(\n                                (track_meta, track_height))\n                        else:\n                            tracks['track'] = [(track_meta, track_height)]\n                        tm = track_meta.metadata\n                        # ~cwp_title if composer had to be removed\n                        title = tm['~cwp_title'] or tm['title']\n                        if 'title' in tracks:\n                            tracks['title'].append(title)\n                        else:\n                            tracks['title'] = [title]\n                        # to make sure we get it as a list\n                        work = tm.getall('~cwp_work_0')\n                        if 'work' in tracks:\n                            tracks['work'].append(work)\n                        else:\n                            tracks['work'] = [work]\n                        if 'tracknumber' not in tm:\n                            tm['tracknumber'] = 0\n                        if 'discnumber' not in tm:\n                            tm['discnumber'] = 0\n                        if 'tracknumber' in tracks:\n                            tracks['tracknumber'].append(\n                                int(tm['discnumber']) + (int(tm['tracknumber']) / 1000))\n                        else:\n                            tracks['tracknumber'] = [\n                                int(tm['discnumber']) + (int(tm['tracknumber']) / 1000)]\n            if tracks and 'track' in tracks:\n                track = tracks['track'][0][0]\n                # NB this will only be the first track of tracks, but its\n                # options will be used for the structure\n                self.derive_from_structure(\n                    release_id, top_info, tracks, height, depth, width, 'title')\n                if self.options[track][\"cwp_level0_works\"]:\n                    # replace hierarchical works with those from work_0 (for\n                    # consistency)\n                    self.derive_from_structure(\n                        release_id, top_info, tracks, height, depth, width, 'work')\n\n                write_log(\n                        release_id,\n                        'info',\n                        \"Trackback result for %s = %s\",\n                        parentId,\n                        tracks)\n                response = parentId, tracks\n                write_log(\n                        release_id,\n                        'debug',\n                        \"LEAVING PROCESS_CHILD_TRACKBACK depth %s Response = %s\",\n                        depth,\n                        response)\n                return response\n            else:\n                return None\n        else:\n            return None\n\n    def derive_from_structure(\n            self,\n            release_id,\n            top_info,\n            tracks,\n            height,\n            depth,\n            width,\n            name_type):\n        \"\"\"\n        Derive title (or work level-0) components from MB hierarchical work structure\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param top_info:\n         {'levels': work_part_levels,'id': topId,'name': self.parts[topId]['name'],'single': single_work_album}\n        :param tracks:\n         {'track':[(track1, height1), (track2, height2), ...], 'work': [work1, work2,...],\n          'title': [title1, title2, ...], 'tracknumber': [tracknumber1, tracknumber2, ...]}\n          where height is the number of levels in total in the branch for that track (i.e. height 1 => work_0 & work_1)\n        :param height: number of levels above the current one\n        :param depth: maximum number of levels\n        :param width: number of siblings\n        :param name_type: work or title\n        :return:\n        \"\"\"\n        if 'track' in tracks:\n            track = tracks['track'][0][0]\n            # NB this will only be the first track of tracks, but its\n            # options will be used for the structure\n            single_work_track = False  # default\n            write_log(\n                release_id,\n                'debug',\n                \"Deriving info for %s from structure for tracks %s\",\n                name_type,\n                tracks['track'])\n            write_log(\n                release_id,\n                'info',\n                '%ss are %r',\n                name_type,\n                tracks[name_type])\n            if 'tracknumber' in tracks:\n                sorted_tracknumbers = sorted(tracks['tracknumber'])\n            else:\n                sorted_tracknumbers = None\n            write_log(\n                    release_id,\n                    'info',\n                    \"SORTED TRACKNUMBERS: %s\",\n                    sorted_tracknumbers)\n            common_len = 0\n            if name_type in tracks:\n                meta_str = \"_title\" if name_type == 'title' else \"_X0\"\n                # in case of works, could be a list of lists\n                name_list = tracks[name_type]\n                write_log(\n                        release_id,\n                        'info',\n                        \"%s list %s\",\n                        name_type,\n                        name_list)\n                if len(name_list) == 1:  # only one track in this work so try and extract using colons\n                    single_work_track = True\n                    track_height = tracks['track'][0][1]\n                    if track_height - height > 0:  # track_height - height == part_level\n                        if name_type == 'title':\n                            write_log(\n                                    release_id,\n                                    'debug',\n                                    \"Single track work. Deriving directly from title text: %s\",\n                                    track)\n                            ti = name_list[0]\n                            common_subset = self.derive_from_title(\n                                release_id, track, ti)[0]\n                        else:\n                            common_subset = \"\"\n                    else:\n                        common_subset = name_list[0]\n                    write_log(\n                            release_id,\n                            'info',\n                            \"%s is single-track work. common_subset is set to %s\",\n                            tracks['track'][0][0],\n                            common_subset)\n                    if common_subset:\n                        common_len = len(common_subset)\n                    else:\n                        common_len = 0\n                else:  # NB if names are lists of lists, we'll assume they all start the same way\n                    if isinstance(name_list[0], list):\n                        compare = name_list[0][0].split()\n                    else:\n                        # a list of the words in the first name\n                        compare = name_list[0].split()\n                    for name_item in name_list:\n                        if isinstance(name_item, list):\n                            name = name_item[0]\n                        else:\n                            name = name_item\n                        lcs = longest_common_sequence(compare, name.split())\n                        compare = lcs['sequence']\n                        if not compare:\n                            common_len = 0\n                            break\n                        if lcs['length'] > 0:\n                            common_subset = \" \".join(compare)\n                            write_log(\n                                    release_id,\n                                    'info',\n                                    \"Common subset from %ss at level %s, item name %s ..........\",\n                                    name_type,\n                                    tracks['track'][0][1] -\n                                    height,\n                                    name)\n                            write_log(\n                                    release_id, 'info', \"..........is %s\", common_subset)\n                            common_len = len(common_subset)\n\n                write_log(\n                        release_id,\n                        'info',\n                        \"checked for common sequence - length is %s\",\n                        common_len)\n            for track_index, track_item in enumerate(tracks['track']):\n                track_meta = track_item[0]\n                tm = track_meta.metadata\n                top_level = int(tm['~cwp_part_levels'])\n                part_level = track_item[1] - height\n                if common_len > 0:\n                    self.create_work_levels(release_id, name_type, tracks, track, track_index,\n                                            track_meta, tm, meta_str, part_level, depth, width, common_len)\n\n                else:  # (no common substring at this level)\n                    if name_type == 'work':\n                        write_log(release_id, 'info',\n                                  'single track work - indicator = %s. track = %s, part_level = %s, top_level = %s',\n                                  single_work_track, track_item, part_level, top_level)\n                        if part_level >= top_level:  # so it won't be covered by top-down action\n                            for level in range(\n                                    0, part_level + 1):  # fill in the missing work names from the canonical list\n                                if '~cwp' + meta_str + '_work_' + \\\n                                        str(level) not in tm:\n                                    tm['~cwp' +\n                                       meta_str +\n                                       '_work_' +\n                                       str(level)] = tm['~cwp_work_' +\n                                                        str(level)]\n                                    if level > 0:\n                                        self.level0_warn(release_id, tm, level)\n                                if '~cwp' + meta_str + '_part_' + \\\n                                        str(level) not in tm and '~cwp_part_' + str(level) in tm:\n                                    tm['~cwp' +\n                                       meta_str +\n                                       '_part_' +\n                                       str(level)] = tm['~cwp_part_' +\n                                                        str(level)]\n                                    if level > 0:\n                                        self.level0_warn(release_id, tm, level)\n\n\n    def create_work_levels(self, release_id, name_type, tracks, track, track_index,\n                           track_meta, tm, meta_str, part_level, depth, width, common_len):\n        \"\"\"\n        For a group of tracks with common metadata in the title/level0 work, create the work structure\n        for that metadata, using the structure in the MB database\n        :param release_id:\n        :param name_type: title or work\n        :param tracks: {'track':[(track1, height1), (track2, height2), ...], 'work': [work1, work2,...],\n          'title': [title1, title2, ...], 'tracknumber': [tracknumber1, tracknumber2, ...]}\n          where height is the number of levels in total in the branch for that track (i.e. height 1 => work_0 & work_1)\n        :param track:\n        :param track_index: index of track in tracks\n        :param track_meta:\n        :param tm: track meta (dup?)\n        :param meta_str: string created from name_type\n        :param part_level: The level of the current item in the works hierarchy\n        :param depth: The number of levels below the current item\n        :param width: The number of children of the current item\n        :param common_len: length of the common text\n        :return:\n        \"\"\"\n        allow_repeats = True\n        write_log(\n            release_id,\n            'info',\n            \"Use %s info for track: %s at level %s\",\n            name_type,\n            track_meta,\n            part_level)\n        name = tracks[name_type][track_index]\n        if isinstance(name, list):\n            work = name[0][:common_len]\n        else:\n            work = name[:common_len]\n        work = work.rstrip(\":,.;- \")\n        if self.options[track][\"cwp_removewords_p\"]:\n            removewords = self.options[track][\"cwp_removewords_p\"].split(\n                ',')\n        else:\n            removewords = []\n        write_log(\n            release_id,\n            'info',\n            \"Prefixes (in %s) = %s\",\n            name_type,\n            removewords)\n        for prefix in removewords:\n            prefix2 = str(prefix).lower().rstrip()\n            if prefix2[0] != \" \":\n                prefix2 = \" \" + prefix2\n            write_log(\n                release_id, 'info', \"checking prefix %s\", prefix2)\n            if work.lower().endswith(prefix2):\n                if len(prefix2) > 0:\n                    work = work[:-len(prefix2)]\n                    common_len = len(work)\n                    work = work.rstrip(\":,.;- \")\n            if work.lower() == prefix2.strip():\n                work = ''\n                common_len = 0\n        write_log(\n            release_id,\n            'info',\n            \"work after prefix strip %s\",\n            work)\n        write_log(release_id, 'info', \"Prefixes checked\")\n\n        tm['~cwp' + meta_str + '_work_' +\n           str(part_level)] = work\n\n        if part_level > 0 and name_type == \"work\":\n            write_log(\n                release_id,\n                'info',\n                'checking if %s is repeated name at part_level = %s',\n                work,\n                part_level)\n            write_log(release_id, 'info', 'lower work name is %s',\n                      tm['~cwp' + meta_str + '_work_' + str(part_level - 1)])\n        # fill in missing names caused by no common string at lower levels\n        # count the missing levels and push the current name\n        # down to the lowest missing level\n        missing_levels = 0\n        fill_level = part_level - 1\n        while '~cwp' + meta_str + '_work_' + \\\n                str(fill_level) not in tm:\n            missing_levels += 1\n            fill_level -= 1\n            if fill_level < 0:\n                break\n        write_log(\n            release_id,\n            'info',\n            'there is/are %s missing level(s)',\n            missing_levels)\n        if missing_levels > 0:\n            allow_repeats = True\n        for lev in range(\n                part_level - missing_levels, part_level):\n\n            if lev > 0:  # not filled_lowest and lev > 0:\n                tm['~cwp' + meta_str +\n                   '_work_' + str(lev)] = work\n                tm['~cwp' +\n                   meta_str +\n                   '_part_' +\n                   str(lev - 1)] = self.strip_parent_from_work(track,\n                                                               release_id,\n                                                               interpret(tm['~cwp' + meta_str + '_work_'\n                                                                            + str(lev - 1)]),\n                                                               tm['~cwp' + meta_str + '_work_' + str(lev)],\n                                                               lev - 1, False)[0]\n            else:\n                tm['~cwp' + meta_str + '_work_' + str(lev)] = tm['~cwp_work_' + str(lev)]\n\n        if missing_levels > 0:\n            write_log(release_id, 'info', 'lower work name is now %r', tm.getall(\n                '~cwp' + meta_str + '_work_' + str(part_level - 1)))\n        # now fix the repeated work name at this level\n        if work == tm['~cwp' + meta_str + '_work_' +\n                      str(part_level - 1)] and not allow_repeats:\n            tm['~cwp' +\n               meta_str +\n               '_work_' +\n               str(part_level)] = tm['~cwp_work_' +\n                                     str(part_level)]\n            self.level0_warn(release_id, tm, part_level)\n        tm['~cwp' +\n           meta_str +\n           '_part_' +\n           str(part_level -\n               1)] = self.strip_parent_from_work(track,\n                                                 release_id,\n                                                 tm.getall('~cwp' + meta_str + '_work_' + str(part_level - 1)),\n                                                 tm['~cwp' + meta_str + '_work_' + str(part_level)],\n                                                 part_level - 1, False)[0]\n        if part_level == 1:\n            if isinstance(name, list):\n                movt = [x[common_len:].strip().lstrip(\":,.;- \")\n                        for x in name]\n            else:\n                movt = name[common_len:].strip().lstrip(\":,.;- \")\n            write_log(\n                release_id, 'info', \"%s - movt = %s\", name_type, movt)\n            tm['~cwp' + meta_str + '_part_0'] = movt\n        write_log(\n            release_id,\n            'info',\n            \"%s Work part_level = %s\",\n            name_type,\n            part_level)\n        if name_type == 'title':\n            if '~cwp_title_work_' + str(part_level - 1) in tm and tm['~cwp_title_work_' + str(\n                    part_level)] == tm['~cwp_title_work_' + str(part_level - 1)] and width == 1:\n                pass  # don't count higher part-levels which are not distinct from lower ones\n                #  when the parent work has only one child\n            else:\n                tm['~cwp_title_work_levels'] = depth\n                tm['~cwp_title_part_levels'] = part_level\n        write_log(\n            release_id,\n            'info',\n            \"Set new metadata for %s OK\",\n            name_type)\n\n    def level0_warn(self, release_id, tm, level):\n        \"\"\"\n        Issue warnings if inadequate level 0 data\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param tm:\n        :param level:\n        :return:\n        \"\"\"\n        write_log(\n            release_id,\n            'warning',\n            'Unable to use level 0 as work name source in level %s - using hierarchy instead',\n            level)\n        if self.WARNING:\n            self.append_tag(\n                release_id,\n                tm,\n                '~cwp_warning',\n                '5. Unable to use level 0 as work name source in level ' +\n                str(level) +\n                ' - using hierarchy instead')\n\n    def set_metadata(\n            self,\n            release_id,\n            part_level,\n            workId,\n            parentId,\n            parent,\n            track):\n        \"\"\"\n        Set the names of works and parts\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param part_level:\n        :param workId:\n        :param parentId:\n        :param parent:\n        :param track:\n        :return:\n        \"\"\"\n        write_log(\n                release_id,\n                'debug',\n                \"SETTING METADATA FOR TRACK = %r, parent = %s, part_level = %s\",\n                track,\n                parent,\n                part_level)\n        tm = track.metadata\n        if parentId:\n            self.write_tags(release_id, track, tm, parentId)\n            self.make_annotations(release_id, track, parentId)\n            if 'annotations' in self.parts[workId]:\n                work_annotations = self.parts[workId]['annotations']\n                self.parts[workId]['stripped_annotations'] = work_annotations\n            else:\n                work_annotations = []\n            if 'annotations' in self.parts[parentId]:\n                parent_annotations = self.parts[parentId]['annotations']\n            else:\n                parent_annotations = []\n            if parent_annotations:\n                work_annotations = [\n                    z for z in work_annotations if z not in parent_annotations]\n                self.parts[workId]['stripped_annotations'] = work_annotations\n\n            tm['~cwp_workid_' + str(part_level)] = parentId\n            tm['~cwp_work_' + str(part_level)] = parent\n            # maybe more than one work name\n            work = self.parts[workId]['name']\n            write_log(release_id, 'info', \"Set work name to: %s\", work)\n            works = []\n            # in case there is only one and it isn't in a list\n            if isinstance(work, str):\n                works.append(work)\n            else:\n                works = work[:]\n            stripped_works = []\n            for work in works:\n                extend = True\n                strip = self.strip_parent_from_work(\n                    track, release_id, work, parent, part_level, extend, parentId, workId)\n\n                stripped_works.append(strip[0])\n                write_log(\n                        release_id,\n                        'info',\n                        \"Parent: %s, Stripped works = %s\",\n                        parent,\n                        stripped_works)\n                # now == parent, after removing full_parent logic\n                full_parent = strip[1]\n                if full_parent != parent:\n                    tm['~cwp_work_' +\n                       str(part_level)] = full_parent.strip()\n                    self.parts[parentId]['name'] = full_parent\n                    if 'no_parent' in self.parts[parentId]:\n                        if self.parts[parentId]['no_parent']:\n                            tm['~cwp_work_top'] = full_parent.strip()\n            tm['~cwp_part_' + str(part_level - 1)] = stripped_works\n            self.parts[workId]['stripped_name'] = stripped_works\n        write_log(release_id, 'debug', \"GOT TO END OF SET_METADATA\")\n\n    def write_tags(self, release_id, track, tm, workId):\n        \"\"\"\n        write genre-related tags from internal variables\n        :param track:\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param tm: track metadata\n        :param workId: MBID of current work\n        :return: None - just writes tags\n        \"\"\"\n        options = self.options[track]\n        candidate_genres = []\n        if options['cwp_genres_use_folks'] and 'folks_genres' in self.parts[workId]:\n            candidate_genres += self.parts[workId]['folks_genres']\n        if options['cwp_genres_use_worktype'] and 'worktype_genres' in self.parts[workId]:\n            candidate_genres += self.parts[workId]['worktype_genres']\n            self.append_tag(\n                release_id,\n                tm,\n                '~cwp_worktype_genres',\n                self.parts[workId]['worktype_genres'])\n        self.append_tag(\n            release_id,\n            tm,\n            '~cwp_candidate_genres',\n            candidate_genres)\n        self.append_tag(release_id, tm, '~cwp_keys', self.parts[workId]['key'])\n        self.append_tag(release_id, tm, '~cwp_composed_dates',\n                        self.parts[workId]['composed_dates'])\n        self.append_tag(release_id, tm, '~cwp_published_dates',\n                        self.parts[workId]['published_dates'])\n        self.append_tag(release_id, tm, '~cwp_premiered_dates',\n                        self.parts[workId]['premiered_dates'])\n\n    def make_annotations(self, release_id, track, wid):\n        \"\"\"\n        create an 'annotations' entry in the 'parts' dict, as dictated by options, from dates and keys\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param track: the current track\n        :param wid: the current work MBID\n        :return:\n        \"\"\"\n        write_log(\n                release_id,\n                'debug',\n                \"Starting module %s\",\n                'make_annotations')\n        options = self.options[track]\n        if options['cwp_workdate_include']:\n            if options['cwp_workdate_source_composed'] and 'composed_dates' in self.parts[wid] and self.parts[wid]['composed_dates']:\n                workdates = self.parts[wid]['composed_dates']\n            elif options['cwp_workdate_source_published'] and 'published_dates' in self.parts[wid] and self.parts[wid]['published_dates']:\n                workdates = self.parts[wid]['published_dates']\n            elif options['cwp_workdate_source_premiered'] and 'premiered_dates' in self.parts[wid] and self.parts[wid]['premiered_dates']:\n                workdates = self.parts[wid]['premiered_dates']\n            else:\n                workdates = []\n        else:\n            workdates = []\n        keys = []\n        if options['cwp_key_include'] and 'key' in self.parts[wid] and self.parts[wid]['key']:\n            keys = self.parts[wid]['key']\n        elif options['cwp_key_contingent_include'] and 'key' in self.parts[wid] and self.parts[wid]['key']\\\n                and 'name' in self.parts[wid]:\n            write_log(\n                    release_id,\n                    'info',\n                    'checking for key. keys = %s, names = %s',\n                    self.parts[wid]['key'],\n                    self.parts[wid]['name'])\n            # add all the parent names to the string for checking -\n            work_name = list_to_str(self.parts[wid]['name'])\n            work_chk = wid\n            while work_chk in self.works_cache:\n                parent_chk = tuple(self.works_cache[work_chk])\n                if parent_chk in self.parts and self.parts[parent_chk] and 'name' in self.parts[parent_chk] and self.parts[parent_chk]['name']:\n                    parent_name = list_to_str(self.parts[parent_chk]['name'])\n                    p_name_orig = self.parts[parent_chk]['name']\n                    p_chk = self.parts[parent_chk]\n                    work_name = parent_name + ': ' + work_name\n                work_chk = parent_chk\n            # now see if the key has been mentioned in the work or its parents\n            for key in self.parts[wid]['key']:\n                # if not any([key.lower() in x.lower() for x in\n                # str_to_list(work_name)]): #  TODO remove\n                if not key.lower() in work_name.lower():\n                    keys.append(key)\n        annotations = keys + workdates\n        if annotations:\n            self.parts[wid]['annotations'] = annotations\n        else:\n            if 'annotations' in self.parts[wid]:\n                del self.parts[wid]['annotations']\n        write_log(\n                release_id,\n                'info',\n                'make annotations has set id %s on track %s with annotation %s',\n                wid,\n                track,\n                annotations)\n        write_log(\n                release_id,\n                'debug',\n                \"Ending module %s\",\n                'make_annotations')\n\n    @staticmethod\n    def derive_from_title(release_id, track, title):\n        \"\"\"\n        Attempt to parse title to get components\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param track:\n        :param title:\n        :return:\n        \"\"\"\n        write_log(\n                release_id,\n                'info',\n                \"DERIVING METADATA FROM TITLE for track: %s\",\n                track)\n        tm = track.metadata\n        movt = title\n        work = \"\"\n        colons = title.count(\": \")\n        inter_work = None\n        if '~cwp_part_levels' in tm:\n            part_levels = int(tm['~cwp_part_levels'])\n            if int(tm['~cwp_work_part_levels']\n                   ) > 0:  # we have a work with movements\n                if colons > 0:\n                    title_split = title.split(': ', 1)\n                    title_rsplit = title.rsplit(': ', 1)\n                    if part_levels >= colons:\n                        work = title_rsplit[0]\n                        movt = title_rsplit[1]\n                    else:\n                        work = title_split[0]\n                        movt = title_split[1]\n        else:\n            # No works found so try and just get parts from title\n            if colons > 0:\n                title_split = title.rsplit(': ', 1)\n                work = title_split[0]\n                if colons > 1:\n                    colon_ind = work.rfind(':')\n                    inter_work = work[colon_ind + 1:].strip()\n                    work = work[:colon_ind]\n                movt = title_split[1]\n        write_log(release_id, 'info', \"Work %s, Movt %s\", work, movt)\n        return work, movt, inter_work\n\n    def process_work_artists(\n            self,\n            release_id,\n            album,\n            track,\n            workIds,\n            tm,\n            count):\n        \"\"\"\n        Carry out the artist processing that needs to be done in the PartLevels class\n        as it requires XML lookups of the works\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param album:\n        :param track:\n        :param workIds:\n        :param tm:\n        :param count:\n        :return:\n        \"\"\"\n        if not self.options[track]['classical_extra_artists']:\n            write_log(\n                    release_id,\n                    'debug',\n                    'Not processing work_artists as ExtraArtists not selected to be run')\n            return None\n        write_log(\n                release_id,\n                'debug',\n                'In process_work_artists for track: %s, workIds: %s',\n                track,\n                workIds)\n        write_log(\n                release_id,\n                'debug',\n                'In process_work_artists for track: %s, self.parts: %s',\n                track,\n                self.parts)\n        if workIds in self.parts and 'arrangers' in self.parts[workIds]:\n            write_log(\n                    release_id,\n                    'info',\n                    'Arrangers = %s',\n                    self.parts[workIds]['arrangers'])\n            set_work_artists(\n                self,\n                release_id,\n                album,\n                track,\n                self.parts[workIds]['arrangers'],\n                tm,\n                count)\n        if workIds in self.works_cache:\n            count += 1\n            self.process_work_artists(release_id, album, track, tuple(\n                self.works_cache[workIds]), tm, count)\n\n    #################################################\n    # SECTION 5 - Extend work metadata using titles #\n    #################################################\n\n    def extend_metadata(self, release_id, top_info, track, ref_height, depth):\n        \"\"\"\n        Combine MB work and title data according to user options\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param top_info:\n        :param track:\n        :param ref_height:\n        :param depth:\n        :return:\n        \"\"\"\n        write_log(release_id, 'debug', 'IN EXTEND_METADATA')\n        tm = track.metadata\n        options = self.options[track]\n        movementgroup = ()\n        if '~cwp_part_levels' not in tm:\n            write_log(\n                    release_id,\n                    'debug',\n                    'NO PART LEVELS. Metadata = %s',\n                    tm)\n            return None\n        part_levels = int(tm['~cwp_part_levels'])\n        write_log(\n                release_id,\n                'debug',\n                \"Extending metadata for track: %s, ref_height: %s, depth: %s, part_levels: %s\",\n                track,\n                ref_height,\n                depth,\n                part_levels)\n        write_log(release_id, 'info', \"Metadata = %s\", tm)\n\n        # previously: ref_height = work_part_levels - ref_level,\n        # where this ref-level is the level for the top-named work\n        # so ref_height is effectively the \"single work album\" indicator (1 or 0) -\n        #   i.e. where all tracks are part of one work which is implicitly the album\n        #   without there being a groupheading for it\n        ref_level = part_levels - ref_height\n        # work_ref_level = work_part_levels - ref_height # not currently used\n\n        # replace works and parts by those derived from the level 0 work, where\n        # required, available and appropriate, but only use work names based on\n        # level 0 text if it doesn't cause ambiguity\n\n        # before embellishing with partial / arrangement etc\n        vanilla_part = tm['~cwp_part_0']\n\n        # Fix text for arrangements, partials and medleys (Done here so that\n        # cache can be used)\n        if options['cwp_arrangements'] and options[\"cwp_arrangements_text\"]:\n            for lev in range(\n                    0,\n                    ref_level):  # top level will not be an arrangement else there would be a higher level\n                # needs to be a tuple to match\n                if '~cwp_workid_' + str(lev) in tm:\n                    tup_id = tuple(str_to_list(tm['~cwp_workid_' + str(lev)]))\n                    if 'arrangement' in self.parts[tup_id] and self.parts[tup_id]['arrangement']:\n                        update_list = ['~cwp_work_', '~cwp_part_']\n                        if options[\"cwp_level0_works\"] and '~cwp_X0_work_' + \\\n                                str(lev) in tm:\n                            update_list += ['~cwp_X0_work_', '~cwp_X0_part_']\n                        for item in update_list:\n                            tm[item + str(lev)] = options[\"cwp_arrangements_text\"] + \\\n                                ' ' + tm[item + str(lev)]\n\n        if options['cwp_partial'] and options[\"cwp_partial_text\"]:\n            if '~cwp_workid_0' in tm:\n                work0_id = tuple(str_to_list(tm['~cwp_workid_0']))\n                if 'partial' in self.parts[work0_id] and self.parts[work0_id]['partial']:\n                    update_list = ['~cwp_work_0', '~cwp_part_0']\n                    if options[\"cwp_level0_works\"] and '~cwp_X0_work_0' in tm:\n                        update_list += ['~cwp_X0_work_0', '~cwp_X0_part_0']\n                    for item in update_list:\n                        meta_item = tm.getall(item)\n                        if isinstance(\n                                meta_item, list):  # it should be a list as I think getall always returns a list\n                            if meta_item == []:\n                                meta_item.append(options[\"cwp_partial_text\"])\n                            else:\n                                for ind, w in enumerate(meta_item):\n                                    meta_item[ind] = options[\"cwp_partial_text\"] + ' ' + w\n                            write_log(\n                                release_id, 'info', 'now meta item is %s', meta_item)\n                            tm[item] = meta_item\n                        else:\n                            tm[item] = options[\"cwp_partial_text\"] + \\\n                                ' ' + tm[item]\n                            write_log(\n                                release_id, 'info', 'meta item is not a list')\n\n        # fix \"type 1\" medley text\n        if options['cwp_medley']:\n            for lev in range(0, ref_level + 1):\n                if '~cwp_workid_' + str(lev) in tm:\n                    tup_id = tuple(str_to_list(tm['~cwp_workid_' + str(lev)]))\n                    if 'medley_list' in self.parts[tup_id] and self.parts[tup_id]['medley_list']:\n                        medley_list = self.parts[tup_id]['medley_list']\n                        tm['~cwp_work_' + str(lev)] += \" (\" + options[\"cwp_medley_text\"] + \\\n                            ': ' + ', '.join(medley_list) + \")\"\n                        if '~cwp_part_' + str(lev) in tm:\n                            tm['~cwp_part_' + str(\n                                lev)] = \"(\" + options[\"cwp_medley_text\"] + \") \" + tm['~cwp_part_' + str(lev)]\n\n        # add any annotations for dates and keys\n        if options['cwp_workdate_include'] or options['cwp_key_include'] or options['cwp_key_contingent_include']:\n            if options[\"cwp_titles\"] and part_levels == 0:\n                # ~cwp_title_work_0 will not have been set, but need it to hold any annotations\n                tm['~cwp_title_work_0'] = tm['~cwp_title'] or tm['title']\n            for lev in range(0, part_levels + 1):\n                if '~cwp_workid_' + str(lev) in tm:\n                    tup_id = tuple(str_to_list(tm['~cwp_workid_' + str(lev)]))\n                    if 'annotations' in self.parts[tup_id]:\n                        write_log(\n                                release_id,\n                                'info',\n                                'in extend_metadata, annotations for id %s on track %s are %s',\n                                tup_id,\n                                track,\n                                self.parts[tup_id]['annotations'])\n                        tm['~cwp_work_' + str(lev)] += \" (\" + \\\n                            ', '.join(self.parts[tup_id]['annotations']) + \")\"\n                        if options[\"cwp_level0_works\"] and '~cwp_X0_work_' + \\\n                                str(lev) in tm:\n                            tm['~cwp_X0_work_' + str(lev)] += \" (\" + ', '.join(\n                                self.parts[tup_id]['annotations']) + \")\"\n                        if options[\"cwp_titles\"] and '~cwp_title_work_' + \\\n                                str(lev) in tm:\n                            tm['~cwp_title_work_' + str(lev)] += \" (\" + ', '.join(\n                                self.parts[tup_id]['annotations']) + \")\"\n                        if lev < part_levels:\n                            if 'stripped_annotations' in self.parts[tup_id]:\n                                if self.parts[tup_id]['stripped_annotations']:\n                                    tm['~cwp_part_' + str(lev)] += \" (\" + ', '.join(\n                                        self.parts[tup_id]['stripped_annotations']) + \")\"\n                                    if options[\"cwp_level0_works\"] and '~cwp_X0_part_' + \\\n                                            str(lev) in tm:\n                                        tm['~cwp_X0_part_' + str(lev)] += \" (\" + ', '.join(\n                                            self.parts[tup_id]['stripped_annotations']) + \")\"\n                                    if options[\"cwp_titles\"] and '~cwp_title_part_' + \\\n                                            str(lev) in tm:\n                                        tm['~cwp_title_part' + str(lev)] += \" (\" + ', '.join(\n                                            self.parts[tup_id]['stripped_annotations']) + \")\"\n\n        part = []\n        work = []\n        for level in range(0, part_levels):\n            part.append(tm['~cwp_part_' + str(level)])\n            work.append(tm['~cwp_work_' + str(level)])\n        work.append(tm['~cwp_work_' + str(part_levels)])\n\n        # Use level_0-derived names if applicable\n        if options[\"cwp_level0_works\"]:\n            for level in range(0, part_levels + 1):\n                if '~cwp_X0_work_' + str(level) in tm:\n                    work[level] = tm['~cwp_X0_work_' + str(level)]\n                else:\n                    if level != 0:\n                        work[level] = ''\n                if part and len(part) > level:\n                    if '~cwp_X0_part_' + str(level) in tm:\n                        part[level] = tm['~cwp_X0_part_' + str(level)]\n                    else:\n                        if level != 0:\n                            part[level] = ''\n\n        # set up group heading and part\n        if part_levels > 0:\n            groupheading = work[1]\n            work_main = work[ref_level]\n            inter_work = None\n            work_titles = tm['~cwp_title_work_' + str(ref_level)]\n            if ref_level > 1:\n                for r in range(1, ref_level):\n                    if inter_work:\n                        inter_work = ': ' + inter_work\n                    inter_work = part[r] + (inter_work or '')\n                groupheading = work[ref_level] + ':: ' + (inter_work or '')\n        else:\n            groupheading = work[0]\n            work_main = groupheading\n            inter_work = None\n            work_titles = None\n\n        # determine movement grouping (highest level that is not a collection)\n        if '~cwp_workid_top' in tm:\n            movementgroup = tuple(str_to_list(tm['~cwp_workid_top']))\n            n = part_levels\n            write_log(\n                    release_id,\n                    'debug',\n                    \"In extend. self.parts[%s]['is_collection']: %s\",\n                    movementgroup,\n                    self.parts[movementgroup]['is_collection'])\n            while self.parts[movementgroup]['is_collection']:\n                n -= 1\n                if n < 0:\n                    # shouldn't happen in theory as bottom level can't be a collection, but just in case...\n                    break\n                if '~cwp_workid_'  + str(n) in tm:\n                    movementgroup = tuple(str_to_list(tm['~cwp_workid_'  + str(n)]))\n                else:\n                    break\n\n        # set part text (initially)\n        if part:\n            part_main = part[0]\n        else:\n            part_main = work[0]\n        tm['~cwp_part'] = part_main\n\n        # fix medley text for \"type 2\" medleys\n        type2_medley = False\n        if self.parts[tuple(str_to_list(tm['~cwp_workid_0']))\n                      ]['medley'] and options['cwp_medley']:\n            if options[\"cwp_medley_text\"]:\n                if part_levels > 0:\n                    medleyheading = groupheading + ':: ' + part[0]\n                else:\n                    medleyheading = groupheading\n                groupheading = medleyheading + \\\n                    ' (' + options[\"cwp_medley_text\"] + ')'\n            type2_medley = True\n\n        tm['~cwp_groupheading'] = groupheading\n        tm['~cwp_work'] = work_main\n        tm['~cwp_inter_work'] = inter_work\n        tm['~cwp_title_work'] = work_titles\n        write_log(\n                release_id,\n                'debug',\n                \"Groupheading set to: %s\",\n                groupheading)\n        # extend group heading from title metadata\n        if groupheading:\n            ext_groupheading = groupheading\n            title_groupheading = None\n            ext_work = work_main\n            ext_inter_work = inter_work\n            inter_title_work = \"\"\n\n            if '~cwp_title_work_levels' in tm:\n\n                title_depth = int(tm['~cwp_title_work_levels'])\n                write_log(\n                        release_id,\n                        'info',\n                        \"Title_depth: %s\",\n                        title_depth)\n                diff_work = [\"\"] * ref_level\n                diff_part = [\"\"] * ref_level\n                title_tag = [\"\"]\n                # level 0 work for title # was 'x'  # to avoid errors, reset\n                # before used\n                tw_str_lower = 'title'\n                max_d = min(ref_level, title_depth) + 1\n                for d in range(1, max_d):\n                    tw_str = '~cwp_title_work_' + str(d)\n                    write_log(release_id, 'info', \"TW_STR = %s\", tw_str)\n                    if tw_str in tm:\n                        title_tag.append(tm[tw_str])\n                        title_work = title_tag[d]\n                        work_main = ''\n                        for w in range(d, ref_level + 1):\n                            work_main += (work[w] + ' ')\n                        diff_work[d - 1] = self.diff_pair(\n                            release_id, track, tm, work_main, title_work)\n                        if diff_work[d - 1]:\n                            diff_work[d - 1] = diff_work[d - 1].strip('.;:-,')\n                            if diff_work[d - 1] == '…':\n                                diff_work[d - 1] = ''\n                        if d > 1 and tw_str_lower in tm:\n                            title_part = self.strip_parent_from_work(\n                                track, release_id, tm[tw_str_lower], tm[tw_str], 0, False)[0]\n                            if title_part:\n                                title_part = title_part.strip(' .;:-,')\n                            tm['~cwp_title_part_' +\n                                str(d - 1)] = title_part\n                            part_n = part[d - 1]\n                            diff_part[d - 1] = self.diff_pair(\n                                release_id, track, tm, part_n, title_part) or \"\"\n                            if diff_part[d - 1] == '…':\n                                diff_part[d - 1] = ''\n                    else:\n                        title_tag.append('')\n                    tw_str_lower = tw_str\n                # remove duplicate items at lower levels in diff_work:\n                for w in range(ref_level - 2, -1, -1):\n                    for higher in range(1, ref_level - w):\n                        if diff_work[w] and diff_work[w + higher]:\n                            diff_work[w] = diff_work[w].replace(\n                                diff_work[w + higher], '').strip(' .;:-,\\u2026')\n                            # if diff_work[w] == '…':\n                            #     diff_work[w] = ''\n                write_log(\n                        release_id,\n                        'info',\n                        \"diff list for works: %s\",\n                        diff_work)\n                write_log(\n                        release_id,\n                        'info',\n                        \"diff list for parts: %s\",\n                        diff_part)\n                if not diff_work or len(diff_work) == 0:\n                    if part_levels > 0:\n                        ext_groupheading = groupheading\n                else:\n                    write_log(\n                            release_id,\n                            'debug',\n                            \"Now calc extended groupheading...\")\n                    write_log(\n                            release_id,\n                            'info',\n                            \"depth = %s, ref_level = %s, title_depth = %s\",\n                            depth,\n                            ref_level,\n                            title_depth)\n                    write_log(\n                            release_id,\n                            'info',\n                            \"diff_work = %s, diff_part = %s\",\n                            diff_work,\n                            diff_part)\n                    # remove duplications:\n                    for lev in range(1, ref_level):\n                        for diff_list in [diff_work, diff_part]:\n                            if diff_list[lev] and diff_list[lev - 1]:\n                                diff_list[lev - 1] = self.diff_pair(\n                                    release_id, track, tm, diff_list[lev], diff_list[lev - 1])\n                                if diff_list[lev - 1] == '…':\n                                    diff_list[lev - 1] = ''\n                    write_log(\n                            release_id,\n                            'info',\n                            \"Removed duplication. Revised diff_work = %s, diff_part = %s\",\n                            diff_work,\n                            diff_part)\n                    if part_levels > 0 and depth >= 1:\n                        addn_work = []\n                        addn_part = []\n                        for stripped_work in diff_work:\n                            if stripped_work:\n                                write_log(\n                                        release_id, 'info', \"Stripped work = %s\", stripped_work)\n                                addn_work.append(\" {\" + stripped_work + \"}\")\n                            else:\n                                addn_work.append(\"\")\n                        for stripped_part in diff_part:\n                            if stripped_part and stripped_part != \"\":\n                                write_log(release_id, 'info', \"Stripped part = %s\", stripped_part)\n                                addn_part.append(\" {\" + stripped_part + \"}\")\n                            else:\n                                addn_part.append(\"\")\n                        write_log(\n                                release_id,\n                                'info',\n                                \"addn_work = %s, addn_part = %s\",\n                                addn_work,\n                                addn_part)\n                        ext_groupheading = work[1] + addn_work[0]\n                        ext_work = work[ref_level] + addn_work[ref_level - 1]\n                        ext_inter_work = \"\"\n                        inter_title_work = \"\"\n                        title_groupheading = tm['~cwp_title_work_1']\n                        if ref_level > 1:\n                            for r in range(1, ref_level):\n                                if ext_inter_work:\n                                    ext_inter_work = ': ' + ext_inter_work\n                                ext_inter_work = part[r] + \\\n                                    addn_work[r - 1] + ext_inter_work\n                            ext_groupheading = work[ref_level] + \\\n                                addn_work[ref_level - 1] + ':: ' + ext_inter_work\n                        if title_depth > 1 and ref_level > 1:\n                            for r in range(1, min(title_depth, ref_level)):\n                                if inter_title_work:\n                                    inter_title_work = ': ' + inter_title_work\n                                inter_title_work = tm['~cwp_title_part_' +\n                                                      str(r)] + inter_title_work\n                            title_groupheading = tm['~cwp_title_work_' + str(\n                                min(title_depth, ref_level))] + ':: ' + inter_title_work\n\n                    else:\n                        ext_groupheading = groupheading  # title will be in part\n                        ext_work = work_main\n                        ext_inter_work = inter_work\n                        inter_title_work = \"\"\n\n                    write_log(release_id, 'debug', \".... ext_groupheading done\")\n\n            if ext_groupheading:\n                write_log(\n                        release_id,\n                        'info',\n                        \"EXTENDED GROUPHEADING: %s\",\n                        ext_groupheading)\n                tm['~cwp_extended_groupheading'] = ext_groupheading\n                tm['~cwp_extended_work'] = ext_work\n                if ext_inter_work:\n                    tm['~cwp_extended_inter_work'] = ext_inter_work\n                if inter_title_work:\n                    tm['~cwp_inter_title_work'] = inter_title_work\n                if title_groupheading:\n                    tm['~cwp_title_groupheading'] = title_groupheading\n                    write_log(\n                            release_id,\n                            'info',\n                            \"title_groupheading = %s\",\n                            title_groupheading)\n        # extend part from title metadata\n        write_log(\n                release_id,\n                'debug',\n                \"NOW EXTEND PART...(part = %s)\",\n                part_main)\n        if part_main:\n            if '~cwp_title_part_0' in tm:\n                movement = tm['~cwp_title_part_0']\n            else:\n                movement = tm['~cwp_title_part_0'] or tm['~cwp_title'] or tm['title']\n            if '~cwp_extended_groupheading' in tm:\n                work_compare = tm['~cwp_extended_groupheading'] + \\\n                    ': ' + part_main\n            elif '~cwp_work_1' in tm:\n                work_compare = work[1] + ': ' + part_main\n            else:\n                work_compare = work[0]\n            diff = self.diff_pair(\n                release_id, track, tm, work_compare, movement)\n            # compare with the fullest possible work name, not the stripped one\n            #  - to maximise the duplication elimination\n            reverse_diff = self.diff_pair(\n                release_id, track, tm, movement, vanilla_part)\n            # for the reverse comparison use the part name without any work details or annotation\n            if diff and reverse_diff and self.parts[tuple(str_to_list(tm['~cwp_workid_0']))]['partial']:\n                diff = movement\n            # for partial tracks, do not eliminate the title text as it is\n            # frequently deliberately a component of the the overall work txt\n            # (unless it is identical)\n            fill_part = options['cwp_fill_part']\n            # To fill part with title text if it\n            # would otherwise have no text other than arrangement or partial\n            # annotations\n            if not diff and not vanilla_part and part_levels > 0 and fill_part:\n                # In other words the movement will have no text other than\n                # arrangement or partial annotations\n                diff = movement\n            write_log(release_id, 'info', \"DIFF PART - MOVT. ti =%s\", diff)\n            write_log(release_id,\n                          'info',\n                          'medley indicator for %s is %s',\n                          tm['~cwp_workid_0'],\n                          self.parts[tuple(str_to_list(tm['~cwp_workid_0']))]['medley'])\n\n            if type2_medley:\n                tm['~cwp_extended_part'] = \"{\" + movement + \"}\"\n            else:\n                if diff:\n                    tm['~cwp_extended_part'] = part_main + \\\n                        \" {\" + diff.strip() + \"}\"\n                else:\n                    tm['~cwp_extended_part'] = part_main\n                if part_levels == 0:\n                    if tm['~cwp_extended_groupheading']:\n                        del tm['~cwp_extended_groupheading']\n\n        # remove unwanted groupheadings (needed them up to now for adding\n        # extensions)\n        if '~cwp_groupheading' in tm and tm['~cwp_groupheading'] == tm['~cwp_part']:\n            del tm['~cwp_groupheading']\n        if '~cwp_title_groupheading' in tm and tm['~cwp_title_groupheading'] == tm['~cwp_title_part']:\n            del tm['~cwp_title_groupheading']\n        # clean up groupheadings (may be stray separators if level 0  or title\n        # options used)\n        if '~cwp_groupheading' in tm:\n            tm['~cwp_groupheading'] = tm['~cwp_groupheading'].strip(\n                ':').strip(\n                options['cwp_single_work_sep']).strip(\n                options['cwp_multi_work_sep'])\n        if '~cwp_extended_groupheading' in tm:\n            tm['~cwp_extended_groupheading'] = tm['~cwp_extended_groupheading'].strip(\n                ':').strip(\n                options['cwp_single_work_sep']).strip(\n                options['cwp_multi_work_sep'])\n        if '~cwp_title_groupheading' in tm:\n            tm['~cwp_title_groupheading'] = tm['~cwp_title_groupheading'].strip(\n                ':').strip(\n                options['cwp_single_work_sep']).strip(\n                options['cwp_multi_work_sep'])\n        write_log(release_id, 'debug', \"....done\")\n        return movementgroup\n\n    ##########################################################\n    # SECTION 6- Write metadata to tags according to options #\n    ##########################################################\n\n    def publish_metadata(self, release_id, album, track, movement_info={}):\n        \"\"\"\n        Write out the metadata according to user options\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param album:\n        :param track:\n        :param movement_info: format is {'movement-group': movementgroup, 'movement-number': movementnumber}\n        :return:\n        \"\"\"\n        write_log(release_id, 'debug', \"IN PUBLISH METADATA for %s\", track)\n        options = self.options[track]\n        tm = track.metadata\n        tm['~cwp_version'] = PLUGIN_VERSION\n\n        # set movement grouping tags (hidden vars)\n        if movement_info:\n            movementtotal = self.parts[tuple(movement_info['movement-group'])]['movement-total']\n            if movementtotal > 1:\n                tm['~cwp_movt_num'] = movement_info['movement-number']\n                tm['~cwp_movt_tot'] = movementtotal\n\n        # album composers needed by map_tags (set in set_work_artists)\n        if 'composer_lastnames' in self.album_artists[album]:\n            last_names = seq_last_names(self, album)\n            self.append_tag(\n                release_id,\n                tm,\n                '~cea_album_composer_lastnames',\n                last_names)\n\n        write_log(release_id, 'info', \"Check options\")\n        if options[\"cwp_titles\"]:\n            write_log(release_id, 'info', \"titles\")\n            part = tm['~cwp_title_part_0'] or tm['~cwp_title_work_0']or tm['~cwp_title'] or tm['title']\n            # for multi-level work display\n            groupheading = tm['~cwp_title_groupheading'] or \"\"\n            # for single-level work display\n            work = tm['~cwp_title_work'] or \"\"\n            inter_work = tm['~cwp_inter_title_work'] or \"\"\n        elif options[\"cwp_works\"]:\n            write_log(release_id, 'info', \"works\")\n            part = tm['~cwp_part']\n            groupheading = tm['~cwp_groupheading'] or \"\"\n            work = tm['~cwp_work'] or \"\"\n            inter_work = tm['~cwp_inter_work'] or \"\"\n        else:\n            # options[\"cwp_extended\"]\n            write_log(release_id, 'info', \"extended\")\n            part = tm['~cwp_extended_part']\n            groupheading = tm['~cwp_extended_groupheading'] or \"\"\n            work = tm['~cwp_extended_work'] or \"\"\n            inter_work = tm['~cwp_extended_inter_work'] or \"\"\n        write_log(release_id, 'info', \"Done options\")\n        p1 = RE_ROMANS_AT_START\n        # Matches positive integers with punctuation\n        p2 = re.compile(r'^\\W*\\d+[.):-]')\n        movt = part\n        for _ in range(\n                0, 5):  # in case of multiple levels\n            movt = p2.sub('', p1.sub('', movt)).strip()\n        write_log(release_id, 'info', \"Done movt\")\n        movt_inc_tags = options[\"cwp_movt_tag_inc\"].split(\",\")\n        movt_inc_tags = [x.strip(' ') for x in movt_inc_tags]\n        movt_exc_tags = options[\"cwp_movt_tag_exc\"].split(\",\")\n        movt_exc_tags = [x.strip(' ') for x in movt_exc_tags]\n        movt_inc_1_tags = options[\"cwp_movt_tag_inc1\"].split(\",\")\n        movt_inc_1_tags = [x.strip(' ') for x in movt_inc_1_tags]\n        movt_exc_1_tags = options[\"cwp_movt_tag_exc1\"].split(\",\")\n        movt_exc_1_tags = [x.strip(' ') for x in movt_exc_1_tags]\n        movt_no_tags = options[\"cwp_movt_no_tag\"].split(\",\")\n        movt_no_tags = [x.strip(' ') for x in movt_no_tags]\n        movt_no_sep = options[\"cwp_movt_no_sep\"]\n        movt_tot_tags = options[\"cwp_movt_tot_tag\"].split(\",\")\n        movt_tot_tags = [x.strip(' ') for x in movt_tot_tags]\n        gh_tags = options[\"cwp_work_tag_multi\"].split(\",\")\n        gh_tags = [x.strip(' ') for x in gh_tags]\n        gh_sep = options[\"cwp_multi_work_sep\"]\n        work_tags = options[\"cwp_work_tag_single\"].split(\",\")\n        work_tags = [x.strip(' ') for x in work_tags]\n        work_sep = options[\"cwp_single_work_sep\"]\n        top_tags = options[\"cwp_top_tag\"].split(\",\")\n        top_tags = [x.strip(' ') for x in top_tags]\n\n        write_log(\n                release_id,\n                'info',\n                \"Done splits. gh_tags: %s, work_tags: %s, movt_inc_tags: %s, movt_exc_tags: %s, movt_no_tags: %s\",\n                gh_tags,\n                work_tags,\n                movt_inc_tags,\n                movt_exc_tags,\n                movt_no_tags)\n\n        for tag in gh_tags + work_tags + movt_inc_tags + movt_exc_tags + movt_no_tags:\n            tm[tag] = \"\"\n        for tag in gh_tags:\n            if tag in movt_inc_tags + movt_exc_tags + movt_no_tags:\n                self.append_tag(release_id, tm, tag, groupheading, gh_sep)\n            else:\n                self.append_tag(release_id, tm, tag, groupheading)\n        for tag in work_tags:\n            if tag in movt_inc_1_tags + movt_exc_1_tags + movt_no_tags:\n                self.append_tag(release_id, tm, tag, work, work_sep)\n            else:\n                self.append_tag(release_id, tm, tag, work)\n            if '~cwp_part_levels' in tm and int(tm['~cwp_part_levels']) > 0:\n                self.append_tag(\n                    release_id,\n                    tm,\n                    'show work movement',\n                    '1')  # original tag for iTunes, kept for backwards compatibility\n                self.append_tag(\n                    release_id,\n                    tm,\n                    'showmovement',\n                    '1')  # new tag for iTunes & MusicBee, consistent with Picard tag docs\n        for tag in top_tags:\n            if '~cwp_work_top' in tm:\n                self.append_tag(release_id, tm, tag, tm['~cwp_work_top'])\n\n        if '~cwp_movt_num' in tm and len(tm['~cwp_movt_num']) > 0:\n            movt_num_punc = tm['~cwp_movt_num'] + movt_no_sep + ' '\n        else:\n            movt_num_punc = ''\n\n        for tag in movt_no_tags:\n            if tag not in movt_inc_tags + movt_exc_tags + movt_inc_1_tags + movt_exc_1_tags:\n                self.append_tag(release_id, tm, tag, tm['~cwp_movt_num'])\n\n        for tag in movt_tot_tags:\n            self.append_tag(release_id, tm, tag, tm['~cwp_movt_tot'])\n\n        for tag in movt_exc_tags:\n            if tag in movt_no_tags:\n                movt = movt_num_punc + movt\n            self.append_tag(release_id, tm, tag, movt)\n\n        for tag in movt_inc_tags:\n            if tag in movt_no_tags:\n                part = movt_num_punc + part\n            self.append_tag(release_id, tm, tag, part)\n\n\n        for tag in movt_inc_1_tags + movt_exc_1_tags:\n            if tag in movt_inc_1_tags:\n                pt = part\n            else:\n                pt = movt\n            if tag in movt_no_tags:\n                pt = movt_num_punc + pt\n            if inter_work and inter_work != \"\":\n                if tag in movt_exc_tags + movt_inc_tags and tag != \"\":\n                    write_log(\n                        release_id,\n                        'warning',\n                        \"Tag %s will have multiple contents\",\n                        tag)\n                    if self.WARNING:\n                        self.append_tag(release_id, tm, '~cwp_warning', '6. Tag ' +\n                                    tag +\n                                    ' has multiple contents')\n                self.append_tag(\n                    release_id,\n                    tm,\n                    tag,\n                    inter_work +\n                    work_sep +\n                    \" \" +\n                    pt)\n            else:\n                self.append_tag(release_id, tm, tag, pt)\n\n        for tag in movt_exc_tags + movt_inc_tags + movt_exc_1_tags + movt_inc_1_tags:\n            if tag in movt_no_tags:\n                # i.e treat as one item, not multiple\n                tm[tag] = \"\".join(re.split('|'.join(self.SEPARATORS), tm[tag]))\n\n        # write \"SongKong\" tags\n        if options['cwp_write_sk']:\n            write_log(release_id, 'debug', \"Writing SongKong work tags\")\n            if '~cwp_part_levels' in tm:\n                part_levels = int(tm['~cwp_part_levels'])\n                for n in range(0, part_levels + 1):\n                    if '~cwp_work_' + \\\n                            str(n) in tm and '~cwp_workid_' + str(n) in tm:\n                        source = tm['~cwp_work_' + str(n)]\n                        source_id = list(\n                            tuple(str_to_list(tm['~cwp_workid_' + str(n)])))\n                        if n == 0:\n                            self.append_tag(\n                                release_id, tm, 'musicbrainz_work_composition', source)\n                            for source_id_item in source_id:\n                                self.append_tag(\n                                    release_id, tm, 'musicbrainz_work_composition_id', source_id_item)\n                        if n == part_levels:\n                            self.append_tag(\n                                release_id, tm, 'musicbrainz_work', source)\n                            if 'musicbrainz_workid' in tm:\n                                del tm['musicbrainz_workid']\n                            # Delete the Picard version of this tag before\n                            # replacing it with the SongKong version\n                            for source_id_item in source_id:\n                                self.append_tag(\n                                    release_id, tm, 'musicbrainz_workid', source_id_item)\n                        if n != 0 and n != part_levels:\n                            self.append_tag(\n                                release_id, tm, 'musicbrainz_work_part_level' + str(n), source)\n                            for source_id_item in source_id:\n                                self.append_tag(\n                                    release_id,\n                                    tm,\n                                    'musicbrainz_work_part_level' +\n                                    str(n) +\n                                    '_id',\n                                    source_id_item)\n\n        # carry out tag mapping\n        tm['~cea_works_complete'] = \"Y\"\n        map_tags(options, release_id, album, tm)\n\n        write_log(release_id, 'debug', \"Published metadata for %s\", track)\n        if options['cwp_options_tag'] != \"\":\n            self.cwp_options = collections.defaultdict(\n                lambda: collections.defaultdict(dict))\n\n            for opt in plugin_options('workparts') + plugin_options('genres'):\n                if 'name' in opt:\n                    if 'value' in opt:\n                        if options[opt['option']]:\n                            self.cwp_options['Classical Extras']['Works options'][opt['name']] = opt['value']\n                    else:\n                        self.cwp_options['Classical Extras']['Works options'][opt['name']\n                                                                              ] = options[opt['option']]\n\n            write_log(release_id, 'info', \"Options %s\", self.cwp_options)\n            if options['ce_version_tag'] and options['ce_version_tag'] != \"\":\n                self.append_tag(release_id, tm, options['ce_version_tag'], str(\n                    'Version ' + tm['~cwp_version'] + ' of Classical Extras'))\n            if options['cwp_options_tag'] and options['cwp_options_tag'] != \"\":\n                self.append_tag(release_id, tm, options['cwp_options_tag'] +\n                                ':workparts_options', json.loads(\n                    json.dumps(\n                        self.cwp_options)))\n        if self.ERROR and \"~cwp_error\" in tm:\n            for error in str_to_list(tm['~cwp_error']):\n                code = error[0]\n                self.append_tag(release_id, tm, '001_errors:' + code, error)\n        if self.WARNING and \"~cwp_warning\" in tm:\n            for warning in str_to_list(tm['~cwp_warning']):\n                wcode = warning[0]\n                self.append_tag(release_id, tm, '002_warnings:' + wcode, warning)\n\n\n    def append_tag(self, release_id, tm, tag, source, sep=None):\n        \"\"\"\n        pass to main append routine\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param tm:\n        :param tag:\n        :param source:\n        :param sep: separators may be used to split string into list on appending\n        :return:\n        \"\"\"\n        write_log(\n                release_id,\n                'info',\n                \"In append_tag (Work parts). tag = %s, source = %s, sep =%s\",\n                tag,\n                source,\n                sep)\n        append_tag(release_id, tm, tag, source, self.SEPARATORS)\n        write_log(\n                release_id,\n                'info',\n                \"Appended. Resulting contents of tag: %s are: %s\",\n                tag,\n                tm[tag])\n\n    ################################################\n    # SECTION 7 - Common string handling functions #\n    ################################################\n\n    def strip_parent_from_work(\n            self,\n            track,\n            release_id,\n            work,\n            parent,\n            part_level,\n            extend,\n            parentId=None,\n            workId=None):\n        \"\"\"\n        Remove common text\n        :param track:\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param work: could be a list of works, all of which require stripping\n        :param parent:\n        :param part_level:\n        :param extend:\n        :param parentId:\n        :param workId:\n        :return:\n        \"\"\"\n        # extend=True is used [ NO LONGER to find \"full_parent\" names] + (with parentId)\n        #  to trigger recursion if unable to strip parent name from work and also to look for common subsequences\n        # extend=False is used when this routine is called for other purposes\n        # than strict work: parent relationships\n        options = self.options[track]\n        write_log(\n            release_id,\n            'debug',\n            \"STRIPPING HIGHER LEVEL WORK TEXT FROM PART NAMES\")\n        write_log(\n            release_id,\n            'info',\n            'PARAMS: WORK = %r, PARENT = %s, PART_LEVEL = %s, EXTEND= %s',\n            work,\n            parent,\n            part_level,\n            extend)\n        if isinstance(work, list):\n            result = []\n            for w, work_item in enumerate(work):\n                if workId and isinstance(workId, list):\n                    sub_workId = workId[w]\n                else:\n                    sub_workId = workId\n                result.append(\n                    self.strip_parent_from_work(\n                        track,\n                        release_id,\n                        work_item,\n                        parent,\n                        part_level,\n                        extend,\n                        parentId,\n                        sub_workId)[0])\n            return result, parent\n        if not isinstance(parent, str):\n            # in case it is a list - make sure it is a string\n            parent = '; '.join(parent)\n        if not isinstance(work, str):\n            work = '; '.join(work)\n\n        # replace any punctuation or numbers, with a space (to remove any\n        # inconsistent punctuation and numbering) - (?u) specifies the\n        # re.UNICODE flag in sub\n        clean_parent = re.sub(\"(?u)[\\W]\", ' ', parent)\n        # now allow the spaces to be filled with up to 2 non-letters\n        pattern_parent = clean_parent.replace(\" \", \"\\W{0,2}\")\n        pattern_parent = \"(^|.*?\\s)(\\W*\" + pattern_parent + \"\\W?)(.*)\"\n        # (removed previous alternative pattern for extend=true, owing to catastrophic backtracking)\n        write_log(\n                release_id,\n                'info',\n                \"Pattern parent: %s, Work: %s\",\n                pattern_parent,\n                work)\n        p = re.compile(pattern_parent, re.IGNORECASE | re.UNICODE)\n        m = p.search(work)\n        if m:\n            write_log(release_id, 'info', \"Matched...\")\n            if m.group(1):\n                stripped_work = m.group(1) + u\"\\u2026\" + m.group(3)\n            else:\n                stripped_work = m.group(3)\n            # may not have a full work name in the parent (missing op. no.\n            # etc.)\n            stripped_work = stripped_work.lstrip(\":;,.- \")\n        else:\n            write_log(release_id, 'info', \"No match...\")\n            stripped_work = work\n\n            if extend and options['cwp_common_chars'] > 0:\n                # try stripping out a common substring (multiple times until\n                # nothing more stripped)\n                prev_stripped_work = ''\n                counter = 1\n                while prev_stripped_work != stripped_work:\n                    if counter > 20:\n                        break  # in case something went awry\n                    prev_stripped_work = stripped_work\n                    parent_tuples = self.listify(release_id, track, parent)\n                    parent_words = parent_tuples['s_tuple']\n                    clean_parent_words = list(parent_tuples['s_test_tuple'])\n                    for w, word in enumerate(clean_parent_words):\n                        clean_parent_words[w] = self.boil(release_id, word)\n                    work_tuples = self.listify(\n                        release_id, track, stripped_work)\n                    work_words = work_tuples['s_tuple']\n                    clean_work_words = list(work_tuples['s_test_tuple'])\n                    for w, word in enumerate(clean_work_words):\n                        clean_work_words[w] = self.boil(release_id, word)\n                    common_dets = longest_common_substring(\n                        clean_work_words, clean_parent_words)\n                    # this is actually a list, not a string, since list\n                    # arguments were supplied\n                    common_seq = common_dets['string']\n                    seq_length = common_dets['length']\n                    seq_start = common_dets['start']\n                    # the original items (before 'cleaning')\n                    full_common_seq = [\n                        x.group() for x in work_words[seq_start:seq_start + seq_length]]\n                    # number of words in common_seq\n                    full_seq_length = sum([len(x.split())\n                                           for x in full_common_seq])\n                    write_log(\n                        release_id,\n                        'info',\n                        'Checking common sequence between parent and work, iteration %s ... parent_words = %s',\n                        counter,\n                        parent_words)\n                    write_log(\n                        release_id,\n                        'info',\n                        '... longest common sequence = %s',\n                        common_seq)\n                    if full_seq_length > 0:\n                        potential_stripped_work = stripped_work\n                        if seq_start > 0:\n                            ellipsis = ' ' + u\"\\u2026\" + ' '\n                        else:\n                            ellipsis = ''\n                        if counter > 1:\n                            potential_stripped_work = stripped_work.rstrip(\n                                ' :,-\\u2026')\n                            potential_stripped_work = potential_stripped_work.replace(\n                                '(\\u2026)', '').rstrip()\n                        potential_stripped_work = potential_stripped_work[:work_words[seq_start].start(\n                        )] + ellipsis + potential_stripped_work[work_words[seq_start + seq_length - 1].end():]\n                        potential_stripped_work = potential_stripped_work.lstrip(\n                            ' :,-')\n                        potential_stripped_work = re.sub(\n                            r'(\\W*…\\W*)(\\W*…\\W*)', ' … ', potential_stripped_work)\n                        potential_stripped_work = strip_excess_punctuation(\n                            potential_stripped_work)\n\n                        if full_seq_length >= options['cwp_common_chars'] \\\n                                or potential_stripped_work == '' and options['cwp_allow_empty_parts']:\n                            # Make sure it is more than the required min (it will be > 0 anyway)\n                            # unless a full strip will result anyway (and blank\n                            # part names are allowed)\n                            stripped_work = potential_stripped_work\n                            if not stripped_work or stripped_work == '':\n                                if workId and \\\n                                        ('arrangement' in self.parts[workId] and self.parts[workId]['arrangement']\n                                         and options['cwp_arrangements'] and options['cwp_arrangements_text']) \\\n                                        or ('partial' in self.parts[workId] and self.parts[workId]['partial']\n                                            and options['cwp_partial'] and options['cwp_partial_text']) \\\n                                        and options['cwp_allow_empty_parts']:\n                                    pass\n                                else:\n                                    stripped_work = prev_stripped_work  # do not allow empty parts\n                    counter += 1\n            stripped_work = strip_excess_punctuation(stripped_work)\n            write_log(\n                    release_id,\n                    'info',\n                    'stripped_work = %s',\n                    stripped_work)\n            if extend and parentId and parentId in self.works_cache:\n                write_log(\n                        release_id,\n                        'info',\n                        \"Looking for match at next level up\")\n                grandparentIds = tuple(self.works_cache[parentId])\n                grandparent = self.parts[grandparentIds]['name']\n                stripped_work = self.strip_parent_from_work(\n                    track,\n                    release_id,\n                    stripped_work,\n                    grandparent,\n                    part_level,\n                    True,\n                    grandparentIds,\n                    workId)[0]\n\n        write_log(\n                release_id,\n                'info',\n                \"Finished strip_parent_from_work, Work: %s\",\n                work)\n        write_log(release_id, 'info', \"Stripped work: %s\", stripped_work)\n        # Changed full_parent to parent after removal of 'extend' logic above\n        stripped_work = strip_excess_punctuation(stripped_work)\n        write_log(release_id, 'info', \"Stripped work after punctuation removal: %s\", stripped_work)\n        return stripped_work, parent\n\n    def diff_pair(\n            self,\n            release_id,\n            track,\n            tm,\n            mb_item,\n            title_item,\n            remove_numbers=True):\n        \"\"\"\n        Removes common text (or synonyms) from title item\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param track:\n        :param tm:\n        :param mb_item:\n        :param title_item:\n        :param remove_numbers: remove movement numbers when comparing (not currently called with False by anything)\n        :return: Reduced title item\n        \"\"\"\n        write_log(release_id, 'debug', \"Inside DIFF_PAIR\")\n        mb = mb_item.strip()\n        write_log(release_id, 'info', \"mb = %s\", mb)\n        write_log(release_id, 'info', \"title_item = %s\", title_item)\n        if not mb:\n            write_log(\n                    release_id,\n                    'info',\n                    'End of DIFF_PAIR. Returning %s',\n                    None)\n            return None\n        ti = title_item.strip(\" :;-.,\")\n        if ti.count('\"') == 1:\n            ti = ti.strip('\"')\n        if ti.count(\"'\") == 1:\n            ti = ti.strip(\"'\")\n        write_log(release_id, 'info', \"ti (amended) = %s\", ti)\n        if not ti:\n            write_log(\n                    release_id,\n                    'info',\n                    'End of DIFF_PAIR. Returning %s',\n                    None)\n            return None\n\n        if self.options[track][\"cwp_removewords_p\"]:\n            removewords = self.options[track][\"cwp_removewords_p\"].split(',')\n        else:\n            removewords = []\n        write_log(release_id, 'info', \"Prefixes = %s\", removewords)\n        # remove numbers, roman numerals, part etc and punctuation from the\n        # start\n        write_log(release_id, 'info', \"checking prefixes\")\n        found_prefix = True\n        i = 0\n        while found_prefix:\n            if i > 20:\n                break  # safety valve\n            found_prefix = False\n            for prefix in removewords:\n                if prefix[0] != \" \":\n                    prefix2 = str(prefix).lower().lstrip()\n                    write_log(\n                            release_id, 'info', \"checking prefix %s\", prefix2)\n                    if mb.lower().startswith(prefix2):\n                        found_prefix = True\n                        mb = mb[len(prefix2):]\n                    if ti.lower().startswith(prefix2):\n                        found_prefix = True\n                        ti = ti[len(prefix2):]\n            mb = mb.strip()\n            ti = ti.strip()\n            i += 1\n            write_log(\n                    release_id,\n                    'info',\n                    \"pairs after prefix strip iteration %s. mb = %s, ti = %s\",\n                    i,\n                    mb,\n                    ti)\n        write_log(release_id, 'info', \"Prefixes checked\")\n\n        #  replacements\n        replacements = self.replacements[track]\n        write_log(release_id, 'info', \"Replacement: %s\", replacements)\n        for tup in replacements:\n            for ind in range(0, len(tup) - 1):\n                ti = re.sub(tup[ind], tup[-1], ti, flags=re.IGNORECASE)\n        write_log(\n                release_id,\n                'debug',\n                'Looking for any new words in the title')\n\n        write_log(\n                release_id,\n                'info',\n                \"Check before splitting: mb = %s, ti = %s\",\n                mb,\n                ti)\n\n        ti_tuples = self.listify(release_id, track, ti)\n        ti_tuple = ti_tuples['s_tuple']\n        ti_test_tuple = ti_tuples['s_test_tuple']\n\n        mb_tuples = self.listify(release_id, track, mb)\n        mb_test_tuple = mb_tuples['s_test_tuple']\n\n        write_log(\n                release_id,\n                'info',\n                \"Check after splitting: mb_test = %s, ti = %s, ti_test = %s\",\n                mb_test_tuple,\n                ti_tuple,\n                ti_test_tuple)\n\n        ti_stencil = self.stencil(release_id, ti_tuple, ti)\n        ti_list = ti_stencil['match list']\n        ti_list_punc = ti_stencil['gap list']\n        ti_test_list = list(ti_test_tuple)\n        if ti_stencil['dummy']:\n            # to deal with case where stencil has added a dummy item at the\n            # start\n            ti_test_list.insert(0, '')\n        write_log(release_id, 'info', 'ti_test_list = %r', ti_test_list)\n        # zip is an iterable, not a list in Python 3, so make it re-usable\n        ti_zip_list = list(zip(ti_list, ti_list_punc))\n\n        # len(ti_list) should be = len(ti_test_list) as only difference should\n        # be synonyms which are each one 'word'\n        # However, because of the grouping of some words via regex, it is possible that inconsistencies might arise\n        # Therefore, there is a test here to check for equality and produce an\n        # error message (but continue processing)\n        if len(ti_list) != len(ti_test_list):\n            write_log(\n                    release_id,\n                    'error',\n                    'Mismatch in title list after canonization/synonymization')\n            write_log(\n                release_id,\n                'error',\n                'Orig. title list = %r. Test list = %r',\n                ti_list,\n                ti_test_list)\n        # mb_test_tuple = self.listify(release_id, track, mb_test)\n        mb_list2 = list(mb_test_tuple)\n        for index, mb_bit2 in enumerate(mb_list2):\n            mb_list2[index] = self.boil(release_id, mb_bit2)\n            write_log(\n                    release_id,\n                    'info',\n                    \"mb_list2[%s] = %s\",\n                    index,\n                    mb_list2[index])\n        ti_new = []\n        ti_rich_list = []\n        for i, ti_bit_test in enumerate(ti_test_list):\n            if i <= len(ti_list) - 1:\n                ti_bit = ti_zip_list[i]\n                # NB ti_bit is a tuple where the word (1st item) is grouped\n                # with its following punctuation (2nd item)\n            else:\n                ti_bit = ('', '')\n            write_log(\n                    release_id,\n                    'info',\n                    \"i = %s, ti_bit_test = %s, ti_bit = %s\",\n                    i,\n                    ti_bit_test,\n                    ti_bit)\n            ti_rich_list.append((ti_bit, True))\n            # Boolean to indicate whether ti_bit is a new word\n\n            if ti_bit_test == '':\n                ti_rich_list[i] = (ti_bit, False)\n            else:\n                if self.boil(release_id, ti_bit_test) in mb_list2:\n                    ti_rich_list[i] = (ti_bit, False)\n\n        if remove_numbers:  # Only remove numbers at the start if they are not new items\n            p0 = re.compile(r'\\b\\w+\\b')\n            p1 = RE_ROMANS\n            p2 = re.compile(r'^\\d+')  # Matches positive integers\n            starts_with_numeral = True\n            while starts_with_numeral:\n                starts_with_numeral = False\n                if ti_rich_list and p0.match(ti_rich_list[0][0][0]):\n                    start_word = p0.match(ti_rich_list[0][0][0]).group()\n                    if p1.match(start_word) or p2.match(start_word):\n                        if not ti_rich_list[0][1]:\n                            starts_with_numeral = True\n                            ti_rich_list.pop(0)\n                            ti_test_list.pop(0)\n\n        write_log(\n                release_id,\n                'info',\n                \"ti_rich_list before removing singletons = %s. length = %s\",\n                ti_rich_list,\n                len(ti_rich_list))\n\n        s = 0\n        index = 0\n        change = ()\n        for i, (t, n) in enumerate(ti_rich_list):\n            if n:\n                s += 1\n                index = i\n                change = t  # NB this is a tuple\n\n        p = self.options[track][\"cwp_proximity\"]\n        ep = self.options[track][\"cwp_end_proximity\"]\n        # NB these may be modified later\n\n        if s == 1:\n            if 0 < index < len(ti_rich_list) - 1:\n                # ignore singleton new words in middle of title unless they are\n                # within \"cwp_end_proximity\" from the start or end\n                write_log(\n                    release_id, 'info', 'item length is %s', len(\n                        change[0].split()))\n                # also make sure that the item is just one word before\n                # eliminating\n                if ep < index < len(ti_rich_list) - ep - \\\n                        1 and len(change[0].split()) == 1:\n                    ti_rich_list[index] = (change, False)\n                    s = 0\n\n        # remove prepositions\n        write_log(\n                release_id,\n                'info',\n                \"ti_rich_list before removing prepositions = %s. length = %s\",\n                ti_rich_list,\n                len(ti_rich_list))\n        if self.options[track][\"cwp_prepositions\"]:\n            prepositions_fat = self.options[track][\"cwp_prepositions\"].split(\n                ',')\n            prepositions = [w.strip() for w in prepositions_fat]\n            for i, ti_bit_test in enumerate(\n                    reversed(ti_test_list)):  # Need to reverse it to check later prepositions first\n                if ti_bit_test.lower().strip() in prepositions:\n                    # NB i is counting up while traversing the list backwards\n                    j = len(ti_rich_list) - i - 1\n                    if i == 0 or not ti_rich_list[j + 1][1]:\n                        # Don't make it false if it is preceded by a\n                        # non-preposition new word\n                        if not (j > 0 and ti_rich_list[j -\n                                                       1][1] and ti_test_list[j -\n                                                                              1].lower() not in prepositions):\n                            ti_rich_list[j] = (ti_rich_list[j][0], False)\n\n        # create comparison for later usage\n        compare_string = ''\n        for item in ti_rich_list:\n            if item[1]:\n                compare_string += item[0][0]\n        ti_compare = self.boil(release_id, compare_string)\n        compare_length = len(ti_compare)\n\n        write_log(\n                release_id,\n                'info',\n                \"ti_rich_list before gapping (True indicates a word in title not in MB work) = %s. length = %s\",\n                ti_rich_list,\n                len(ti_rich_list))\n        if s > 0:\n            d = p - ep\n            start = True  # To keep track of new words at the start of the title\n            for i, (ti_bit, new) in enumerate(ti_rich_list):\n                if not new:\n                    write_log(\n                            release_id,\n                            'info',\n                            \"item(i = %s) val = %s - not new. proximity param = %s, end_proximity param = %s\",\n                            i,\n                            ti_bit,\n                            p,\n                            ep)\n                    if start:\n                        prox_test = ep\n                    else:\n                        prox_test = p\n                    if prox_test > 0:\n                        for j in range(0, prox_test + 1):\n                            write_log(release_id, 'info', \"item(i) = %s, look-ahead(j) = %s\", i, j)\n                            if i + j < len(ti_rich_list):\n                                if ti_rich_list[i + j][1]:\n                                    write_log(\n                                            release_id, 'info', \"Set to true..\")\n                                    ti_rich_list[i] = (ti_bit, True)\n                                    write_log(\n                                            release_id, 'info', \"...set OK\")\n                            else:\n                                if j <= p - d:\n                                    ti_rich_list[i] = (ti_bit, True)\n                else:\n                    p = self.options[track][\"cwp_proximity\"]\n                    start = False\n                if not ti_rich_list[i][1]:\n                    p -= 1\n                    ep -= 1\n        write_log(\n                release_id,\n                'info',\n                \"ti_rich_list after gapping (True indicates new words plus infills) = %s\",\n                ti_rich_list)\n        nothing_new = True\n        for (ti_bit, new) in ti_rich_list:\n            if new:\n                nothing_new = False\n                new_prev = True\n                break\n        if nothing_new:\n            write_log(\n                    release_id,\n                    'info',\n                    'End of DIFF_PAIR. Returning %s',\n                    None)\n            return None\n        else:\n            new_prev = False\n            for i, (ti_bit, new) in enumerate(ti_rich_list):\n                write_log(release_id, 'info', \"Create new for %s?\", ti_bit)\n                if new:\n                    write_log(release_id, 'info', \"Yes for %s\", ti_bit)\n                    if not new_prev:\n                        if i > 0:\n                            # check to see if the last char of the prev\n                            # punctuation group needs to be added first\n                            if len(ti_rich_list[i - 1][0][1]) > 1:\n                                # i.e. ti_bit[1][-1] of previous loop\n                                ti_new.append(ti_rich_list[i - 1][0][1][-1])\n                    ti_new.append(ti_bit[0])\n                    if len(ti_bit[1]) > 1:\n                        if i < len(ti_rich_list) - 1:\n                            if ti_rich_list[i + 1][1]:\n                                ti_new.append(ti_bit[1])\n                            else:\n                                ti_new.append(ti_bit[1][:-1])\n                        else:\n                            ti_new.append(ti_bit[1])\n                    else:\n                        ti_new.append(ti_bit[1])\n                    write_log(\n                            release_id,\n                            'info',\n                            \"appended %s. ti_new is now %s\",\n                            ti_bit,\n                            ti_new)\n                else:\n                    write_log(release_id, 'info', \"Not for %s\", ti_bit)\n                    if new != new_prev:\n                        ti_new.append(u\"\\u2026\" + ' ')\n\n                new_prev = new\n        if ti_new:\n            write_log(release_id, 'info', \"ti_new %s\", ti_new)\n            ti = ''.join(ti_new)\n            write_log(release_id, 'info', \"New text from title = %s\", ti)\n        else:\n            write_log(release_id, 'info', \"New text empty\")\n            write_log(\n                    release_id,\n                    'info',\n                    'End of DIFF_PAIR. Returning %s',\n                    None)\n            return None\n        # see if there is any significant difference between the strings\n        if ti:\n            nopunc_ti = ti_compare  # was  = self.boil(release_id, ti)\n            # not necessary as already set?\n            nopunc_mb = self.boil(release_id, mb)\n            # ti_len = len(nopunc_ti) use compare_length instead (= len before\n            # removals and additions)\n            substring_proportion = float(\n                self.options[track][\"cwp_substring_match\"]) / 100\n            sub_len = compare_length * substring_proportion\n            if substring_proportion < 1:\n                write_log(release_id, 'info', \"test sub....\")\n                lcs = longest_common_substring(nopunc_mb, nopunc_ti)['string']\n                write_log(\n                        release_id,\n                        'info',\n                        \"Longest common substring is: %s. Threshold length is %s\",\n                        lcs,\n                        sub_len)\n                if len(lcs) >= sub_len:\n                    write_log(\n                            release_id,\n                            'info',\n                            'End of DIFF_PAIR. Returning %s',\n                            None)\n                    return None\n            write_log(release_id, 'info', \"...done, ti =%s\", ti)\n        # remove duplicate successive words (and remove first word of title\n        # item if it duplicates last word of mb item)\n        if ti:\n            ti_list_new = re.split(' ', ti)\n            ti_list_ref = ti_list_new\n            ti_bit_prev = None\n            for i, ti_bit in enumerate(ti_list_ref):\n                if ti_bit != \"...\":\n\n                    if i > 1:\n                        if self.boil(\n                                release_id, ti_bit) == self.boil(\n                                release_id, ti_bit_prev):\n                            dup = ti_list_new.pop(i)\n                            write_log(release_id, 'info', \"...removed dup %s\", dup)\n\n                ti_bit_prev = ti_bit\n            if ti_list_new and mb_list2:\n                write_log(release_id,\n                          'info',\n                          \"1st word of ti = %s. Last word of mb = %s\",\n                          ti_list_new[0],\n                          mb_list2[-1])\n                if self.boil(release_id, ti_list_new[0]) == mb_list2[-1]:\n                    write_log(release_id, 'info', \"Removing 1st word from ti...\")\n                    first = ti_list_new.pop(0)\n                    write_log(release_id, 'info', \"...removed %s\", first)\n            else:\n                write_log(\n                        release_id,\n                        'info',\n                        'End of DIFF_PAIR. Returning %s',\n                        None)\n                return None\n            if ti_list_new:\n                ti = ' '.join(ti_list_new)\n            else:\n                write_log(\n                        release_id,\n                        'info',\n                        'End of DIFF_PAIR. Returning %s',\n                        None)\n                return None\n        # remove excess brackets and punctuation\n        if ti:\n            ti = strip_excess_punctuation(ti)\n            write_log(release_id, 'info', \"stripped punc ok. ti = %s\", ti)\n        write_log(\n                release_id,\n                'debug',\n                \"DIFF_PAIR is returning ti = %s\",\n                ti)\n        if ti and len(ti) > 0:\n            write_log(\n                    release_id,\n                    'info',\n                    'End of DIFF_PAIR. Returning %s',\n                    ti)\n            return ti\n        else:\n            write_log(\n                    release_id,\n                    'info',\n                    'End of DIFF_PAIR. Returning %s',\n                    None)\n            return None\n\n\n    @staticmethod\n    def canonize_opus(release_id, track, s):\n        \"\"\"\n        make opus numbers etc. into one-word items\n        :param release_id:\n        :param track:\n        :param s: A string\n        :return:\n        \"\"\"\n        write_log(release_id, 'debug', 'Canonizing: %s', s)\n        # Canonize catalogue & opus numbers (e.g. turn K. 126 into K126 or K\n        # 345a into K345a or op. 144 into op144):\n        regex = re.compile(\n            r'\\b((?:op|no|k|kk|kv|L|B|Hob|S|D|M)|\\w+WV)\\W?\\s?(\\d+\\-?\\u2013?\\u2014?\\d*\\w*)\\b',\n            re.IGNORECASE)\n        regex_match = regex.search(s)\n        s_canon = s\n        if regex_match and len(regex_match.groups()) == 2:\n            pt1 = regex_match.group(1) or ''\n            pt2 = regex_match.group(2) or ''\n            if regex_match.group(1) and regex_match.group(2):\n                pt1 = re.sub(\n                    r'^\\W*no\\b',\n                    '',\n                    regex_match.group(1),\n                    flags=re.IGNORECASE)\n            s_canon = pt1 + pt2\n        write_log(release_id, 'info', 'canonized item = %s', s_canon)\n        return s_canon\n\n    @staticmethod\n    def canonize_key(release_id, track, s):\n        \"\"\"\n        make keys into standardized one-word items\n        :param release_id:\n        :param track:\n        :param s: A string\n        :return:\n        \"\"\"\n        write_log(release_id, 'debug', 'Canonizing: %s', s)\n        match = RE_KEYS.search(s)\n        s_canon = s\n        if match:\n            if match.group(2):\n                k2 = re.sub(\n                    r'\\-sharp|\\u266F',\n                    'sharp',\n                    match.group(2),\n                    flags=re.IGNORECASE)\n                k2 = re.sub(r'\\-flat|\\u266D', 'flat', k2, flags=re.IGNORECASE)\n                k2 = k2.replace('-', '')\n            else:\n                k2 = ''\n            if not match.group(3) or match.group(\n                    3).strip() == '':  # if the scale is not given, assume it is the major key\n                if match.group(1).isupper(\n                ) or k2 != '':  # but only if it is upper case or has an accent\n                    k3 = 'major'\n                else:\n                    k3 = ''\n            else:\n                k3 = match.group(3).strip()\n            s_canon = match.group(1).strip() + k2.strip() + k3\n        write_log(release_id, 'info', 'canonized item = %s', s_canon)\n        return s_canon\n\n    @staticmethod\n    def canonize_synonyms(release_id, tuples, s):\n        \"\"\"\n        make synonyms equal\n        :param release_id:\n        :param tuples\n        :param s: A string\n        :return:\n        \"\"\"\n        write_log(release_id, 'debug', 'Canonizing: %s', s)\n        s_canon = s\n        syn_patterns = []\n        syn_subs = []\n        for syn_tup in tuples:\n            syn_pattern = r'((?:^|\\W)' + \\\n                r'(?:$|\\W)|(?:^|\\W)'.join(syn_tup) + r'(?:$|\\W))'\n            syn_patterns.append(syn_pattern)\n            # to get the last synonym in the tuple - the canonical form\n            syn_sub = syn_tup[-1:][0]\n            syn_subs.append(syn_sub)\n        for syn_ind, pattern in enumerate(syn_patterns):\n            regex = re.compile(pattern, re.IGNORECASE)\n            regex_match = regex.search(s)\n            if regex_match:\n                test_reg = regex_match.group().strip()\n                s_canon = s_canon.replace(test_reg, syn_subs[syn_ind])\n\n        write_log(release_id, 'info', 'canonized item = %s', s_canon)\n        return s_canon\n\n    def find_synonyms(self, release_id, track, reg_item):\n        \"\"\"\n        extend regex item to include synonyms\n        :param release_id:\n        :param track:\n        :param reg_item: A regex portion\n        :return: reg_new: A replacement for reg_item that includes all its synonyms\n         (if reg_item matches the last in a synonym tuple)\n        \"\"\"\n        write_log(release_id, 'debug', 'Finding synonyms of: %s', reg_item)\n        syn_others = []\n        syn_all = []\n        for syn_tup in self.synonyms[track]:\n            # to get the last synonym in the tuple - the canonical form\n            syn_last = syn_tup[-1:][0]\n            if re.match(r'^\\s*' + reg_item + r'\\s*$', syn_last, re.IGNORECASE):\n                syn_others += syn_tup[:-1]\n                syn_all += syn_tup\n        if syn_others:\n            reg_item = '(?:' + ')|(?:'.join(syn_others) + \\\n                ')|(?:' + reg_item + ')'\n\n        write_log(release_id, 'info', 'new regex item = %s', reg_item)\n        return reg_item, syn_all\n\n    def listify(self, release_id, track, s):\n        \"\"\"\n        Turn a string into a list of 'words', where words may also be phrases which\n        are then 'canonized' - i.e. turned into equivalents for comparison purposes\n        :param release_id:\n        :param track:\n        :param s: string\n        :return: s_tuple: a tuple of all the **match objects** (re words and defined phrases)\n                 s_test_tuple: a tuple of the matched and canonized words and phrases (i.e. a tuple of strings, not objects)\n        \"\"\"\n        tuples = self.synonyms[track]\n        # just list anything that is a synonym (with word boundary markers)\n        syn_pattern = '|'.join(\n            [r'(?:^|\\W|\\b)' + x + r'(?:$|\\W)' for y in self.synonyms[track] for x in y])\n        op = self.find_synonyms(\n            release_id,\n            track,\n            r'(?:op|no|k|kk|kv|L|B|Hob|S|D|M|\\w+WV)')\n        op_groups = op[0]\n        op_all = op[1]\n        notes = self.find_synonyms(release_id, track, r'[ABCDEFG]')\n        notes_groups = notes[0]\n        notes_all = notes[1]\n        sharp = self.find_synonyms(release_id, track, r'sharp')\n        sharp_groups = sharp[0]\n        sharp_all = sharp[1]\n        flat = self.find_synonyms(release_id, track, r'flat')\n        flat_groups = flat[0]\n        flat_all = flat[1]\n        major = self.find_synonyms(release_id, track, r'major')\n        major_groups = major[0]\n        major_all = major[1]\n        minor = self.find_synonyms(release_id, track, r'minor')\n        minor_groups = minor[0]\n        minor_all = minor[1]\n        opus_pattern = r\"(?:\\b((?:(\" + op_groups + \\\n            r\"))\\W?\\s?\\d+\\-?\\u2013?\\u2014?\\d*\\w*)\\b)\"\n        note_pattern = r\"(\\b\" + notes_groups + r\")\"\n        accent_pattern = r\"(?:\\-(\" + sharp_groups + r\")(?:\\s+|\\b)|\\-(\" + flat_groups + r\")(?:\\s+|\\b)|\\s(\" + sharp_groups + \\\n                         r\")(?:\\s+|\\b)|\\s(\" + flat_groups + r\")(?:\\s+|\\b)|\\u266F(?:\\s+|\\b)|\\u266D(?:\\s+|\\b)|(?:[:,.]?\\s+|$|\\-))\"\n        scale_pattern = r\"(?:((\" + major_groups + \\\n            r\")|(\" + minor_groups + r\"))?\\b)\"\n        key_pattern = note_pattern + accent_pattern + scale_pattern\n        hyphen_split_pattern = r\"(?:\\b|\\\"|\\')(\\w+['’]?\\w*)|(?:\\b\\w+\\b)|(\\B\\&\\B)\"\n        # treat em-dash and en-dash as hyphens\n        hyphen_embed_pattern = r\"(?:\\b|\\\"|\\')(\\w+['’\\-\\u2013\\u2014]?\\w*)|(?:\\b\\w+\\b)|(\\B\\&\\B)\"\n\n        # The regex is split into two iterations as putting it all together can have unpredictable consequences\n        # - may match synonyms before op's even though that is later in the string\n\n        # First match the op's and keys\n        regex_1 = opus_pattern + r\"|(\" + key_pattern + r\")\"\n        matches_1 = re.finditer(regex_1, s, re.UNICODE | re.IGNORECASE)\n        s_list = []\n        s_test_list = []\n        s_scrubbed = s\n        all_synonyms_lists = [\n            op_all,\n            notes_all,\n            sharp_all,\n            flat_all,\n            sharp_all,\n            flat_all,\n            major_all,\n            minor_all]\n        matches_list = [2, 4, 5, 6, 7, 8, 10, 11]\n        for match in matches_1:\n            test_a = match.group()\n            match_a = []\n            match_a.append(match.group())\n            for j in range(1, 12):\n                match_a.append(match.group(j))\n            # 0. overall match\n            # 1. overall opus match\n            # 2. 2-char op match\n            # 3. overall key match\n            # 4. note match\n            # 5. hyphenated sharp match\n            # 6. hyphenated flat match\n            # 7. non-hyphenated sharp match\n            # 8. non-hyphenated flat match\n            # 9. overall scale match\n            # 10. major match\n            # 11. minor match\n            for i, all_synonyms_list in enumerate(all_synonyms_lists):\n                if all_synonyms_list and match_a[matches_list[i]]:\n                    match_regex = [re.match(pattern, match_a[matches_list[i]], re.IGNORECASE).group()\n                                   for pattern in all_synonyms_list\n                                   if re.match(pattern, match_a[matches_list[i]], re.IGNORECASE)]\n                    if match_regex:\n                        match_a[matches_list[i]] = self.canonize_synonyms(\n                            release_id, tuples, match_a[matches_list[i]])\n                        test_a = re.sub(r\"\\b\" + match_regex[0] + r\"(?:\\b|$|\\s|\\.)\",\n                                        match_a[matches_list[i]],\n                                        test_a, flags=re.IGNORECASE)\n            if match_a[1]:\n                clean_opus = test_a.strip(' ,.:;/-?\"')\n                test_a = re.sub(\n                    re.escape(clean_opus),\n                    self.canonize_opus(\n                        release_id,\n                        track,\n                        clean_opus),\n                    test_a,\n                    flags=re.IGNORECASE)\n            if match_a[3]:\n                clean_key = test_a.strip(' ,.:;/-?\"')\n                test_a = re.sub(\n                    re.escape(clean_key),\n                    self.canonize_key(\n                        release_id,\n                        track,\n                        clean_key),\n                    test_a,\n                    flags=re.IGNORECASE)\n\n            s_test_list.append(test_a)\n            s_list.append(match)\n            s_scrubbed_list = list(s_scrubbed)\n            for char in range(match.start(), match.end()):\n                if len(s_scrubbed_list) >= match.end():  # belt and braces\n                    s_scrubbed_list[char] = '#'\n            s_scrubbed = ''.join(s_scrubbed_list)\n\n        # Then match the synonyms and remaining words\n        if self.options[track][\"cwp_split_hyphenated\"]:\n            regex_2 = r\"(\" + syn_pattern + r\")|\" + hyphen_split_pattern\n            # allow ampersands and non-latin characters as word characters. Treat apostrophes as part of words.\n            # Treat opus and catalogue entries - e.g. K. 657 or OP.5 or op. 35a or CD 144 or BWV 243a - as one word\n            # also treat ranges of opus numbers (connected by dash, en dash or\n            # em dash) as one word\n        else:\n            regex_2 = r\"(\" + syn_pattern + r\")|\" + hyphen_embed_pattern\n            # as previous but also treat embedded hyphens as part of words.\n        matches_2 = re.finditer(\n            regex_2, s_scrubbed, re.UNICODE | re.IGNORECASE)\n        for match in matches_2:\n            if match.group(1) and match.group(1) == match.group():\n                s_test_list.append(\n                    self.canonize_synonyms(\n                        release_id,\n                        tuples,\n                        match.group(1)))  # synonym\n            else:\n                s_test_list.append(match.group())\n            s_list.append(match)\n        if s_list:\n            s_zip = list(zip(s_list, s_test_list))\n            s_list, s_test_list = zip(\n                *sorted(s_zip, key=lambda tup: tup[0].start()))\n        s_tuple = tuple(s_list)\n        s_test_tuple = tuple(s_test_list)\n        return {'s_tuple': s_tuple, 's_test_tuple': s_test_tuple}\n\n    def get_text_tuples(self, release_id, track, text_type):\n        \"\"\"\n        Return synonym or 'replacement' tuples\n        :param release_id:\n        :param track:\n        :param text_type: 'replacements' or 'synonyms'\n        Note that code in this method refers to synonyms (as that was written first), but applies equally to replacements and ui_tags\n        :return:\n        \"\"\"\n        tm = track.metadata\n        strsyns = re.split(r'(?<!\\\\)/',\n                           self.options[track][\"cwp_\" + text_type])\n        synonyms = []\n        for syn in strsyns:\n            tup_match = re.search(r'\\((.*)\\)', syn)\n            if tup_match:\n                # to ignore escaped commas\n                tup = re.split(r'(?<!\\\\),', tup_match.group(1))\n            else:\n                tup = ''\n            if len(tup) >= 2:\n                for i, ts in enumerate(tup):\n                    tup[i] = ts.strip(\"' \").strip('\"')\n                    if len(\n                            tup[i]) > 4 and tup[i][0] == \"!\" and tup[i][1] == \"!\" and tup[i][-1] == \"!\" and tup[i][-2] == \"!\":\n                        # we have a reg ex inside - this deals with legacy\n                        # replacement text where enclosure in double-shouts was\n                        # required\n                        tup[i] = tup[i][2:-2]\n                    if (i < len(tup) - 1 or text_type ==\n                            'synonyms') and not tup[i]:\n                        write_log(\n                            release_id,\n                            'warning',\n                            '%s: entries must not be blank - error in %s',\n                            text_type,\n                            syn)\n                        if self.WARNING:\n                            self.append_tag(\n                            release_id,\n                            tm,\n                            '~cwp_warning',\n                            '7. ' + text_type + ': entries must not be blank - error in ' + syn)\n                        tup[i] = \"**BAD**\"\n                    elif [tup for t in synonyms if tup[i] in t]:\n                        write_log(\n                            release_id,\n                            'warning',\n                            '%s: keys cannot duplicate any in existing %s - error in %s '\n                            '- omitted from %s. To fix, place all %s in one tuple.',\n                            text_type,\n                            text_type,\n                            syn,\n                            text_type,\n                            text_type)\n                        if self.WARNING:\n                            self.append_tag(release_id, tm, '~cwp_warning',\n                                        '7. ' + text_type + ': keys cannot duplicate any in existing ' + text_type + ' - error in ' +\n                                        syn + ' - omitted from ' + text_type + '. To fix, place all ' + text_type + ' in one tuple.')\n                        tup[i] = \"**BAD**\"\n                if \"**BAD**\" in tup:\n                    continue\n                else:\n                    synonyms.append(tup)\n            else:\n                write_log(\n                    release_id,\n                    'warning',\n                    'Error in %s format for %s',\n                    text_type,\n                    syn)\n                if self.WARNING:\n                    self.append_tag(\n                    release_id,\n                    tm,\n                    '~cwp_warning',\n                    '7. Error in ' +\n                    text_type +\n                    ' format for ' +\n                    syn)\n        write_log(release_id, 'info', \"%s: %s\", text_type, synonyms)\n        return synonyms\n\n    @staticmethod\n    def stencil(release_id, matches_tuple, test_string):\n        \"\"\"\n        Produce lists of matching items, AND the items in between, in equal length lists\n        :param release_id:\n        :param matches_tuple: tuple of regex matches\n        :param test_string: original string used in regex\n        :return: 'match list' - list of matched strings, 'gap list' - list of strings in gaps between matches\n        \"\"\"\n        match_items = []\n        gap_items = []\n        dummy = False\n        pointer = 0\n        write_log(\n                release_id,\n                'debug',\n                'In fn stencil. test_string = %s. matches_tuple = %s',\n                test_string,\n                matches_tuple)\n        for match_num, match in enumerate(matches_tuple):\n            start = match.start()\n            end = match.end()\n            if start > pointer:\n                if pointer == 0:\n                    # add a null word item at start to keep the lists the same\n                    # length\n                    match_items.append('')\n                    dummy = True\n                gap_items.append(test_string[pointer:start])\n            else:\n                if pointer > 0:\n                    # shouldn't happen, but just in case there are two word\n                    # items with no gap\n                    gap_items.append('')\n            match_items.append(test_string[start:end])\n            pointer = end\n            if match_num + 1 == len(matches_tuple):\n                # pick up any punc items at end\n                gap_items.append(test_string[pointer:])\n        return {\n            'match list': match_items,\n            'gap list': gap_items,\n            'dummy': dummy}\n\n    def boil(self, release_id, s):\n        \"\"\"\n        Remove punctuation, spaces, capitals and accents for string comparisons\n        :param release_id: name for log file - usually =musicbrainz_albumid\n        unless called outside metadata processor\n        :param s:\n        :return:\n        \"\"\"\n        write_log(release_id, 'debug', \"boiling %s\", s)\n        s = s.lower()\n        s = replace_roman_numerals(s)\n        s = s.replace('sch', 'sh')\\\n            .replace(u'\\xdf', 'ss')\\\n            .replace('sz', 'ss')\\\n            .replace(u'\\u0153', 'oe')\\\n            .replace('oe', 'o')\\\n            .replace(u'\\u00fc', 'ue')\\\n            .replace('ue', 'u')\\\n            .replace(u'\\u00e6', 'ae')\\\n            .replace('ae', 'a')\\\n            .replace(u'\\u266F', 'sharp')\\\n            .replace(u'\\u266D', 'flat')\\\n            .replace(u'\\u2013', '-')\\\n            .replace(u'\\u2014', '-')\n        # first term above is to remove the markers used for synonyms, to\n        # enable a true comparison\n        punc = re.compile(r'\\W*', re.ASCII)\n        s = ''.join(\n            c for c in unicodedata.normalize(\n                'NFD',\n                s) if unicodedata.category(c) != 'Mn')\n        boiled = punc.sub('', s).strip().lower().rstrip(\"s'\")\n        write_log(release_id, 'debug', \"boiled result = %s\", boiled)\n        return boiled\n\n\n################\n# OPTIONS PAGE #\n################\n\nclass ClassicalExtrasOptionsPage(OptionsPage):\n    NAME = \"classical_extras\"\n    TITLE = \"Classical Extras\"\n    PARENT = \"plugins\"\n    HELP_URL = \"http://music.highmossergate.co.uk/symphony/tagging/classical-extras/\"\n    opts = plugin_options('artists') + plugin_options('tag') + plugin_options('tag_detail') +\\\n        plugin_options('workparts') + plugin_options('genres') + plugin_options('other')\n\n    options = [\n        IntOption(\"persist\", 'ce_tab', 0)\n    ]\n    # custom logging for non-album-related messages is written to session.log\n    for opt in opts:\n        if 'type' in opt:\n            if 'default' in opt:\n                default = opt['default']\n            else:\n                default = \"\"\n            if opt['type'] == 'Boolean':\n                options.append(BoolOption(\"setting\", opt['option'], default))\n            elif opt['type'] == 'Text' or opt['type'] == 'Combo' or opt['type'] == 'PlainText':\n                options.append(TextOption(\"setting\", opt['option'], default))\n            elif opt['type'] == 'Integer':\n                options.append(IntOption(\"setting\", opt['option'], default))\n            else:\n                write_log(\n                    \"session\",\n                    'error',\n                    \"Error in setting options for option = %s\",\n                    opt['option'])\n\n    def __init__(self, parent=None):\n        super(ClassicalExtrasOptionsPage, self).__init__(parent)\n        self.ui = Ui_ClassicalExtrasOptionsPage()\n        self.ui.setupUi(self)\n\n    def load(self):\n        \"\"\"\n        Load the options - NB all options are set in plugin_options, so this just parses that\n        :return:\n        \"\"\"\n        opts = plugin_options('artists') + plugin_options('tag') + plugin_options('tag_detail') +\\\n            plugin_options('workparts') + plugin_options('genres') + plugin_options('other')\n\n        # To force a toggle so that signal given\n        toggle_list = ['use_cwp',\n                       'use_cea',\n                       'cea_override',\n                       'cwp_override',\n                       'cea_ra_use',\n                       'cea_split_lyrics',\n                       'cwp_partial',\n                       'cwp_arrangements',\n                       'cwp_medley',\n                       'cwp_use_muso_refdb',\n                       'ce_show_ui_tags',]\n\n        # open at last used tab\n        if 'ce_tab' in config.persist:\n            cfg_val = config.persist['ce_tab'] or 0\n            if 0 <= cfg_val <= 5:\n                self.ui.tabWidget.setCurrentIndex(cfg_val)\n        else:\n            self.ui.tabWidget.setCurrentIndex(0)\n\n        for opt in opts:\n            if opt['option'] == 'classical_work_parts':\n                ui_name = 'use_cwp'\n            elif opt['option'] == 'classical_extra_artists':\n                ui_name = 'use_cea'\n            else:\n                ui_name = opt['option']\n            if ui_name in toggle_list:\n                not_setting = not self.config.setting[opt['option']]\n                self.ui.__dict__[ui_name].setChecked(not_setting)\n\n            if opt['type'] == 'Boolean':\n                self.ui.__dict__[ui_name].setChecked(\n                    self.config.setting[opt['option']])\n            elif opt['type'] == 'Text':\n                self.ui.__dict__[ui_name].setText(\n                    self.config.setting[opt['option']])\n            elif opt['type'] == 'PlainText':\n                self.ui.__dict__[ui_name].setPlainText(\n                    self.config.setting[opt['option']])\n            elif opt['type'] == 'Combo':\n                self.ui.__dict__[ui_name].setEditText(\n                    self.config.setting[opt['option']])\n            elif opt['type'] == 'Integer':\n                self.ui.__dict__[ui_name].setValue(\n                    self.config.setting[opt['option']])\n            else:\n                write_log(\n                    'session',\n                    'error',\n                    \"Error in loading options for option = %s\",\n                    opt['option'])\n\n    def save(self):\n        opts = plugin_options('artists') + plugin_options('tag') + plugin_options('tag_detail') +\\\n            plugin_options('workparts') + plugin_options('genres') + plugin_options('other')\n\n        # save tab setting\n        config.persist['ce_tab'] = self.ui.tabWidget.currentIndex()\n\n        for opt in opts:\n            if opt['option'] == 'classical_work_parts':\n                ui_name = 'use_cwp'\n            elif opt['option'] == 'classical_extra_artists':\n                ui_name = 'use_cea'\n            else:\n                ui_name = opt['option']\n            if opt['type'] == 'Boolean':\n                self.config.setting[opt['option']] = self.ui.__dict__[\n                    ui_name].isChecked()\n            elif opt['type'] == 'Text':\n                self.config.setting[opt['option']] = str(\n                    self.ui.__dict__[ui_name].text())\n            elif opt['type'] == 'PlainText':\n                self.config.setting[opt['option']] = str(\n                    self.ui.__dict__[ui_name].toPlainText())\n            elif opt['type'] == 'Combo':\n                self.config.setting[opt['option']] = str(\n                    self.ui.__dict__[ui_name].currentText())\n            elif opt['type'] == 'Integer':\n                self.config.setting[opt['option']\n                                    ] = self.ui.__dict__[ui_name].value()\n            else:\n                write_log(\n                    'session',\n                    'error',\n                    \"Error in saving options for option = %s\",\n                    opt['option'])\n\n\n#################\n# MAIN ROUTINE  #\n#################\n\n# custom logging for non-album-related messages is written to session.log\nwrite_log('session', 'basic', 'Loading ' + PLUGIN_NAME)\n\n# SET UI COLUMNS FOR PICARD RHS\nif config.setting['ce_show_ui_tags'] and config.setting['ce_ui_tags']:\n    from picard.ui.itemviews import MainPanel\n    UI_TAGS = get_ui_tags().items()\n    for heading, tag_names in UI_TAGS:\n        heading_tag = '~' + heading + '_VAL'\n        MainPanel.columns.append((N_(heading), heading_tag))\n    write_log('session', 'info', 'UI_TAGS')\n    write_log('session', 'info', UI_TAGS)\n\n\n# set defaults for certain options that MUST be manually changed by the\n# user each time they are to be over-ridden\nconfig.setting['use_cache'] = True\nconfig.setting['ce_options_overwrite'] = False\nconfig.setting['track_ars'] = True\nconfig.setting['release_ars'] = True\n\n\n# REFERENCE DATA\nREF_DICT = get_references_from_file(\n    'session',\n    config.setting['cwp_muso_path'],\n    config.setting['cwp_muso_refdb'])\nwrite_log('session', 'info', 'External references (Muso):')\nwrite_log('session', 'info', REF_DICT)\nCOMPOSER_DICT = REF_DICT['composers']\nif config.setting['cwp_muso_classical'] and not COMPOSER_DICT:\n    write_log('session', 'error', 'No composer roster found')\nfor cd in COMPOSER_DICT:\n    cd['lc_name'] = [c.lower() for c in cd['name']]\n    cd['lc_sort'] = [c.lower() for c in cd['sort']]\nPERIOD_DICT = REF_DICT['periods']\nif (config.setting['cwp_muso_dates']\n        or config.setting['cwp_muso_periods']) and not PERIOD_DICT:\n    write_log('session', 'error', 'No period map found')\nGENRE_DICT = REF_DICT['genres']\nif config.setting['cwp_muso_genres'] and not GENRE_DICT:\n    write_log('session', 'error', 'No classical genre list found')\n\n# API CALLS\nregister_track_metadata_processor(PartLevels().add_work_info)\nregister_track_metadata_processor(ExtraArtists().add_artist_info)\nregister_options_page(ClassicalExtrasOptionsPage)\n\n# END\nwrite_log('session', 'basic', 'Finished intialisation')\n"
  },
  {
    "path": "plugins/classical_extras/const.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nDeclare constants for Picard Classical Extras plugin\nv2.0.2\n\"\"\"\n# Copyright (C) 2018 Mark Evens\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nRELATION_TYPES = {\n    'work': [\n        'arranger',\n        'instrument arranger',\n        'orchestrator',\n        'composer',\n        'writer',\n        'lyricist',\n        'librettist',\n        'revised by',\n        'translator',\n        'reconstructed by',\n        'vocal arranger'],\n    'release': [\n        'instrument',\n        'performer',\n        'vocal',\n        'performing orchestra',\n        'conductor',\n        'chorus master',\n        'concertmaster',\n        'arranger',\n        'instrument arranger',\n        'orchestrator',\n        'vocal arranger'],\n    'recording': [\n        'instrument',\n        'performer',\n        'vocal',\n        'performing orchestra',\n        'conductor',\n        'chorus master',\n        'concertmaster',\n        'arranger',\n        'instrument arranger',\n        'orchestrator',\n        'vocal arranger']}\n\nARTISTS_OPTIONS = [\n    {'option': 'classical_extra_artists',\n     'name': 'run extra artists',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_orchestras',\n     'name': 'orchestra strings',\n     'type': 'Text',\n     'default': 'orchestra, philharmonic, philharmonica, philharmoniker, musicians, academy, symphony, orkester'\n     },\n    {'option': 'cea_choirs',\n     'name': 'choir strings',\n     'type': 'Text',\n     'default': 'choir, choir vocals, chorus, singers, domchors, domspatzen, koor, kammerkoor'\n     },\n    {'option': 'cea_groups',\n     'name': 'group strings',\n     'type': 'Text',\n     'default': 'ensemble, band, group, trio, quartet, quintet, sextet, septet, octet, chamber, consort, players, '\n                'les ,the , quartett'\n     },\n    {'option': 'cea_aliases',\n     'name': 'replace artist name with alias?',\n     'value': 'replace',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_aliases_composer',\n     'name': 'replace artist name with alias?',\n     'value': 'composer',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cea_no_aliases',\n     'name': 'replace artist name with alias?',\n     'value': 'no replace',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cea_alias_overrides',\n     'name': 'alias vs credited-as',\n     'value': 'alias over-rides',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_credited_overrides',\n     'name': 'alias vs credited-as',\n     'value': 'credited-as over-rides',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cea_ra_use',\n     'name': 'use recording artist',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cea_ra_trackartist',\n     'name': 'recording artist name style',\n     'value': 'track artist',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cea_ra_performer',\n     'name': 'recording artist name style',\n     'value': 'performer',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_ra_replace_ta',\n     'name': 'recording artist effect on track artist',\n     'value': 'replace',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cea_ra_noblank_ta',\n     'name': 'disallow blank recording artist',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cea_ra_merge_ta',\n     'name': 'recording artist effect on track artist',\n     'value': 'merge',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_composer_album',\n     'name': 'Album prefix',\n     # 'value': 'Composer', # Can't use 'value' if there is only one option, otherwise False will revert to default\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_arrangers',\n     'name': 'include arrangers',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_no_lyricists',\n     'name': 'exclude lyricists if no vocals',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_cyrillic',\n     'name': 'fix cyrillic',\n     'type': 'Boolean',\n     'default': True\n     },\n    # {'option': 'cea_genres',\n    #  'name': 'infer work types',\n    #  'type': 'Boolean',\n    #  'default': True\n    #  },\n    # Note that the above is no longer used - replaced by cwp_genres_infer from v0.9.2\n    {'option': 'cea_credited',\n     'name': 'use release credited-as name',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_release_relationship_credited',\n     'name': 'use release relationship credited-as name',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_group_credited',\n     'name': 'use release-group credited-as name',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_recording_credited',\n     'name': 'use recording credited-as name',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cea_recording_relationship_credited',\n     'name': 'use recording relationship credited-as name',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_track_credited',\n     'name': 'use track credited-as name',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_performer_credited',\n     'name': 'use credited-as name for performer',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_composer_credited',\n     'name': 'use credited-as name for composer',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cea_inst_credit',\n     'name': 'use credited instrument',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_no_solo',\n     'name': 'exclude solo',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_chorusmaster',\n     'name': 'chorusmaster',\n     'type': 'Text',\n     'default': 'choirmaster'\n     },\n    {'option': 'cea_orchestrator',\n     'name': 'orchestrator',\n     'type': 'Text',\n     'default': 'orch.'\n     },\n    {'option': 'cea_concertmaster',\n     'name': 'concertmaster',\n     'type': 'Text',\n     'default': 'leader'\n     },\n    {'option': 'cea_lyricist',\n     'name': 'lyricist',\n     'type': 'Text',\n     'default': 'lyrics'\n     },\n    {'option': 'cea_librettist',\n     'name': 'librettist',\n     'type': 'Text',\n     'default': 'libretto'\n     },\n    {'option': 'cea_writer',\n     'name': 'writer',\n     'type': 'Text',\n     'default': 'writer'\n     },\n    {'option': 'cea_arranger',\n     'name': 'arranger',\n     'type': 'Text',\n     'default': 'arr.'\n     },\n    {'option': 'cea_reconstructed',\n     'name': 'reconstructed by',\n     'type': 'Text',\n     'default': 'reconstructed'\n     },\n    {'option': 'cea_revised',\n     'name': 'revised by',\n     'type': 'Text',\n     'default': 'revised'\n     },\n    {'option': 'cea_translator',\n     'name': 'translator',\n     'type': 'Text',\n     'default': 'trans.'\n     },\n    {'option': 'cea_split_lyrics',\n     'name': 'split lyrics',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cea_lyrics_tag',\n     'name': 'lyrics',\n     'type': 'Text',\n     'default': 'lyrics'\n     },\n    {'option': 'cea_album_lyrics',\n     'name': 'album lyrics',\n     'type': 'Text',\n     'default': 'albumnotes'\n     },\n    {'option': 'cea_track_lyrics',\n     'name': 'track lyrics',\n     'type': 'Text',\n     'default': 'tracknotes'\n     }\n]\n\nTAG_OPTIONS = [\n    {'option': 'cea_blank_tag',\n     'name': 'Tags to blank',\n     'type': 'Text',\n     'default': 'artist, artistsort'\n     },\n    {'option': 'cea_blank_tag_2',\n     'name': 'Tags to blank 2',\n     'type': 'Text',\n     'default': 'performer:orchestra, performer:choir, performer:choir vocals'\n     },\n    {'option': 'cea_keep',\n     'name': 'File tags to keep',\n     'type': 'Text',\n     'default': ''\n     },\n    {'option': 'cea_clear_tags',\n     'name': 'Clear previous tags',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cea_tag_sort',\n     'name': 'populate sort tags',\n     'type': 'Boolean',\n     'default': True\n     }\n]\n#  (tag mapping detail lines)\ndefault_list = [\n    ('album_soloists, album_ensembles, album_conductors', 'artist, artists', False),\n    ('recording_artists', 'artist, artists', True),\n    ('soloist_names, ensemble_names, conductors', 'artist, artists', True),\n    ('soloists', 'soloists, trackartist, involved people', False),\n    ('release', 'release_name', False),\n    ('ensemble_names', 'band', False),\n    ('composers', 'artist', True),\n    ('MB_artists', 'composer', True),\n    ('arranger', 'composer', True)\n]\nTAG_DETAIL_OPTIONS = []\nfor i in range(0, 16):\n    if i < len(default_list):\n        default_source, default_tag, default_cond = default_list[i]\n    else:\n        default_source = ''\n        default_tag = ''\n        default_cond = False\n    TAG_DETAIL_OPTIONS.append({'option': 'cea_source_' + str(i + 1),\n                               'name': 'line ' + str(i + 1) + '_source',\n                               'type': 'Combo',\n                               'default': default_source\n                               })\n    TAG_DETAIL_OPTIONS.append({'option': 'cea_tag_' + str(i + 1),\n                               'name': 'line ' + str(i + 1) + '_tag',\n                               'type': 'Text',\n                               'default': default_tag\n                               })\n    TAG_DETAIL_OPTIONS.append({'option': 'cea_cond_' + str(i + 1),\n                               'name': 'line ' + str(i + 1) + '_conditional',\n                               'type': 'Boolean',\n                               'default': default_cond\n                               })\n\nWORKPARTS_OPTIONS = [\n    {'option': 'classical_work_parts',\n     'name': 'run work parts',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_collections',\n     'name': 'include collection relations',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_allow_empty_parts',\n     # allow parts to be blank if there is arrangement or partial text label\n     # checked = split\n     'name': 'allow-empty-parts',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_common_chars',\n     # for use in strip_parent_from_work\n     # where no match exists and substring elimination is used\n     # this sets the minimum number of matching 'words' (words followed by punctuation/spaces or EOL)\n     #  required before they will be eliminated\n     # 0 => no elimination\n     # default is 2 words\n     'name': 'min common words to eliminate',\n     'type': 'Integer',\n     'default': 2\n     },\n    {'option': 'cwp_proximity',\n     # proximity of new words in title comparison which will result in\n     # infill words being included as well. 2 means 2-word 'gaps' of\n     # existing words between new words will be treated as 'new'\n     'name': 'in-string proximity trigger',\n     'type': 'Integer',\n     'default': 2\n     },\n    {'option': 'cwp_end_proximity',\n     # proximity measure to be used when infilling to the end of the title\n     'name': 'end-string proximity trigger',\n     'type': 'Integer',\n     'default': 1\n     },\n    {'option': 'cwp_split_hyphenated',\n     # splitting of hyphenated words for matching purposes\n     # checked = split\n     'name': 'hyphen-splitting',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_substring_match',\n     # Proportion of a string to be matched to a (usually larger) string for\n     # it to be considered essentially similar\n     'name': 'similarity threshold',\n     'type': 'Integer',\n     'default': 100\n     },\n    {'option': 'cwp_fill_part',\n     # Fill part name with title text if it would otherwise\n     # have no text other than arrangement or partial annotations\n     'name': 'disallow empty part names',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_prepositions',\n     'name': 'prepositions',\n     'type': 'Text',\n     'default': \"a, the, in, on, at, of, after, and, de, d'un, d'une, la, le, no, from, &, e, ed, et, un,\"\n                \" une, al, ala, alla\"\n     },\n    {'option': 'cwp_removewords',\n     'name': 'ignore prefixes',\n     'type': 'Text',\n     'default': ' part , act , scene, movement, movt, no. , no , n., n , nr., nr , book , the , a , la , le , un ,'\n                ' une , el , il , tableau, from , KV ,Concerto in, Concerto'\n     },\n    {'option': 'cwp_synonyms',\n     'name': 'synonyms',\n     'type': 'PlainText',\n     'default': '(1, one) / (2, two) / (3, three) / (&, and) / (Rezitativ, Recitativo, Recitative) / '\n                '(Sinfonia, Sinfonie, Symphonie, Symphony) / (Arie, Aria) / '\n                '(Minuetto, Menuetto, Minuetta, Menuet, Minuet) / (Bourée, Bouree , Bourrée)'\n     },\n    {'option': 'cwp_replacements',\n     'name': 'replacements',\n     'type': 'Text',\n     'default': '(words to be replaced, replacement words) / (please blank me, ) / (etc, etc)'\n     },\n    {'option': 'cwp_titles',\n     'name': 'Style',\n     'value': 'Titles',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_works',\n     'name': 'Style',\n     'value': 'Works',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_extended',\n     'name': 'Style',\n     'value': 'Extended',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_hierarchical_works',\n     'name': 'Work source',\n     'value': 'Hierarchy',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_level0_works',\n     'name': 'Work source',\n     'value': 'Level_0',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_derive_works_from_title',\n     'name': 'Derive works from title',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_movt_tag_inc',\n     'name': 'movement tag inc num',\n     'type': 'Text',\n     'default': 'part, movement, subtitle'\n     },\n    {'option': 'cwp_movt_tag_exc',\n     'name': 'movement tag exc num',\n     'type': 'Text',\n     'default': ''\n     },\n    {'option': 'cwp_movt_tag_inc1',\n     'name': '1-level movement tag inc num',\n     'type': 'Text',\n     'default': 'movement'\n     },\n    {'option': 'cwp_movt_tag_exc1',\n     'name': '1-level movement tag exc num',\n     'type': 'Text',\n     'default': ''\n     },\n    {'option': 'cwp_movt_no_tag',\n     'name': 'movement num tag',\n     'type': 'Text',\n     'default': 'movementnumber'\n     },\n    {'option': 'cwp_movt_tot_tag',\n     'name': 'movement tot tag',\n     'type': 'Text',\n     'default': 'movementtotal'\n     },\n    {'option': 'cwp_work_tag_multi',\n     'name': 'multi-level work tag',\n     'type': 'Text',\n     'default': 'groupheading, work'\n     },\n    {'option': 'cwp_work_tag_single',\n     'name': 'single level work tag',\n     'type': 'Text',\n     'default': ''\n     },\n    {'option': 'cwp_top_tag',\n     'name': 'top level work tag',\n     'type': 'Text',\n     'default': 'top_work, style, grouping'\n     },\n    {'option': 'cwp_multi_work_sep',\n     'name': 'multi-level work separator',\n     'type': 'Combo',\n     'default': ':'\n     },\n    {'option': 'cwp_single_work_sep',\n     'name': 'single level work separator',\n     'type': 'Combo',\n     'default': ':'\n     },\n    {'option': 'cwp_movt_no_sep',\n     'name': 'movement number separator',\n     'type': 'Combo',\n     'default': '.'\n     },\n    {'option': 'cwp_partial',\n     'name': 'show partial recordings',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_partial_text',\n     'name': 'partial text',\n     'type': 'Text',\n     'default': '(part)'\n     },\n    {'option': 'cwp_arrangements',\n     'name': 'include arrangement of',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_arrangements_text',\n     'name': 'arrangements text',\n     'type': 'Text',\n     'default': 'Arrangement:'\n     },\n    {'option': 'cwp_medley',\n     'name': 'list medleys',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_medley_text',\n     'name': 'medley text',\n     'type': 'Text',\n     'default': 'Medley'\n     }\n]\n# Options on \"Genres etc.\" tab\n\nGENRE_OPTIONS = [\n    {'option': 'cwp_genre_tag',\n     'name': 'main genre tag',\n     'type': 'Text',\n     'default': 'genre'\n     },\n    {'option': 'cwp_subgenre_tag',\n     'name': 'sub-genre tag',\n     'type': 'Text',\n     'default': 'sub-genre'\n     },\n    {'option': 'cwp_genres_use_file',\n     'name': 'source genre from file',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_genres_use_folks',\n     'name': 'source genre from folksonomy tags',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_genres_use_worktype',\n     'name': 'source genre from work-type(s)',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_genres_infer',\n     'name': 'infer genre from artist details(s)',\n     'type': 'Boolean',\n     'default': False\n     },\n    # Note that the \"infer from artists\" option was in  the \"artists\"\n    # section - legacy from v0.9.1 & prior\n    {'option': 'cwp_genres_filter',\n     'name': 'apply filter to genres',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_genres_classical_main',\n     'name': 'classical main genres',\n     'type': 'PlainText',\n     'default': 'Classical, Chamber music, Concerto, Symphony, Opera, Orchestral, Sonata, Choral, Aria, Ballet, '\n                'Oratorio, Motet, Symphonic poem, Suite, Partita, Song-cycle, Overture, '\n                'Mass, Cantata'\n     },\n    {'option': 'cwp_genres_classical_sub',\n     'name': 'classical sub-genres',\n     'type': 'PlainText',\n     'default': 'Chant, Classical crossover, Minimalism, Avant-garde, Impressionist, Aria, Duet, Trio, Quartet'\n     },\n    {'option': 'cwp_genres_other_main',\n     'name': 'general main genres',\n     'type': 'PlainText',\n     'default': 'Alternative music, Blues, Country, Dance, Easy listening, Electronic music, Folk, Folk / pop, '\n                'Hip hop / rap, Indie,  Religious, Asian, Jazz, Latin, New age, Pop, R&B / Soul, Reggae, Rock, '\n                'World music, Celtic folk, French Medieval'\n     },\n    {'option': 'cwp_genres_other_sub',\n     'name': 'general sub-genres',\n     'type': 'PlainText',\n     'default': 'Song, Vocal, Christmas, Instrumental'\n     },\n    {'option': 'cwp_genres_arranger_as_composer',\n     'name': 'treat arranger as for composer for genre-setting',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_genres_classical_all',\n     'name': 'make tracks classical',\n     'value': 'all',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_genres_classical_selective',\n     'name': 'make tracks classical',\n     'value': 'selective',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_genres_classical_exclude',\n     'name': 'exclude \"classical\" from main genre tag',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_genres_flag_text',\n     'name': 'classical flag',\n     'type': 'Text',\n     'default': '1'\n     },\n    {'option': 'cwp_genres_flag_tag',\n     'name': 'classical flag tag',\n     'type': 'Text',\n     'default': 'is_classical'\n     },\n    {'option': 'cwp_genres_default',\n     'name': 'default genre',\n     'type': 'Text',\n     'default': 'Other'\n     },\n    {'option': 'cwp_instruments_tag',\n     'name': 'instruments tag',\n     'type': 'Text',\n     'default': 'instrument'\n     },\n    {'option': 'cwp_instruments_MB_names',\n     'name': 'use MB instrument names',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_instruments_credited_names',\n     'name': 'use credited instrument names',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_key_tag',\n     'name': 'key tag',\n     'type': 'Text',\n     'default': 'key'\n     },\n    {'option': 'cwp_key_contingent_include',\n     'name': 'contingent include key in workname',\n     'value': 'contingent',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_key_never_include',\n     'name': 'never include key in workname',\n     'value': 'never',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_key_include',\n     'name': 'include key in workname',\n     'value': 'always',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_workdate_tag',\n     'name': 'workdate tag',\n     'type': 'Text',\n     'default': 'work_year'\n     },\n    {'option': 'cwp_workdate_source_composed',\n     'name': 'use composed for workdate',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_workdate_source_published',\n     'name': 'use published for workdate',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_workdate_source_premiered',\n     'name': 'use premiered for workdate',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_workdate_use_first',\n     'name': 'use workdate sources sequentially',\n     'value': 'sequence',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_workdate_use_all',\n     'name': 'use all workdate sources',\n     'value': 'all',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_workdate_annotate',\n     'name': 'annotate dates',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_workdate_include',\n     'name': 'include workdate in workname',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_period_tag',\n     'name': 'period tag',\n     'type': 'Text',\n     'default': 'period'\n     },\n    {'option': 'cwp_periods_arranger_as_composer',\n     'name': 'treat arranger as for composer for period-setting',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_period_map',\n     'name': 'period map',\n     'type': 'PlainText',\n     'default': 'Early, -3000,800; Medieval, 800,1400; Renaissance, 1400, 1600; Baroque, 1600,1750; '\n                'Classical, 1750,1820; Early Romantic, 1800,1850; Late Romantic, 1850,1910; '\n                '20th Century, 1910,1975; Contemporary, 1975,2525'\n     }\n]\n# Picard options which are also saved (NB only affects plugin processing - not main Picard processing)\nPICARD_OPTIONS = [\n    {'option': 'standardize_artists',\n     'name': 'standardize artists',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'translate_artist_names',\n     'name': 'translate artist names',\n     'type': 'Boolean',\n     'default': True\n     },\n]\n\n# other options (not saved in file tags)\nOTHER_OPTIONS = [\n    {'option': 'use_cache',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_aliases',\n     'name': 'replace with alias?',\n     'value': 'replace',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_no_aliases',\n     'name': 'replace with alias?',\n     'value': 'no replace',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_aliases_all',\n     'name': 'alias replacement type',\n     'value': 'all',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_aliases_greek',\n     'name': 'alias replacement type',\n     'value': 'non-latin',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_aliases_tagged',\n     'name': 'alias replacement type',\n     'value': 'tagged works',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_aliases_tag_text',\n     'name': 'use_alias tag text',\n     'type': 'Text',\n     'default': 'use_alias'\n     },\n    {'option': 'cwp_aliases_tags_all',\n     'name': 'use_alias tags all',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'cwp_aliases_tags_user',\n     'name': 'use_alias tags user',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_use_sk',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_write_sk',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_retries',\n     'type': 'Integer',\n     'default': 6\n     },\n    {'option': 'cwp_use_muso_refdb',\n     'name': 'use Muso ref database',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_muso_genres',\n     'name': 'use Muso classical genres',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_muso_classical',\n     'name': 'use Muso classical composers',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_muso_dates',\n     'name': 'use Muso composer dates',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_muso_periods',\n     'name': 'use Muso periods',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_muso_path',\n     'name': 'path to Muso database',\n     'type': 'Text',\n     'default': 'C:\\\\Users\\\\Public\\\\Music\\\\muso\\\\database'\n     },\n    {'option': 'cwp_muso_refdb',\n     'name': 'name of Muso reference database',\n     'type': 'Text',\n     'default': 'Reference.xml'\n     },\n    {'option': 'log_error',\n     'type': 'Boolean',\n             'default': True\n     },\n    {'option': 'log_warning',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'log_debug',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'log_basic',\n     'type': 'Boolean',\n     'default': True\n     },\n    {'option': 'log_info',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'ce_version_tag',\n     'type': 'Text',\n     'default': 'stamp'\n     },\n    {'option': 'cea_options_tag',\n     'type': 'Text',\n     'default': 'comment'\n     },\n    {'option': 'cwp_options_tag',\n     'type': 'Text',\n     'default': 'comment'\n     },\n    {'option': 'cea_override',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'ce_tagmap_override',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'cwp_override',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'ce_genres_override',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'ce_options_overwrite',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'ce_no_run',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'ce_show_ui_tags',\n     'type': 'Boolean',\n     'default': False\n     },\n    {'option': 'ce_ui_tags',\n     ## Note that this is not just for work parts (although that is the main use),\n     # but cwp prefix is to make use of code for synonyms\n     'name': 'tags for ui columns',\n     'type': 'PlainText',\n     'default': 'Work diff: (groupheading_DIFF, work_DIFF, top_work_DIFF, grouping_DIFF) / Part diff: (part_DIFF, movement_DIFF) / Missing file metadata: 002_important_warning'\n     }\n]\n\nARTIST_TYPE_ORDER = {'vocal': 1,\n                  'instrument': 1,\n                  'performer': 0,\n                  'performing orchestra': 2,\n                  'concertmaster': 3,\n                  'conductor': 4,\n                  'chorus master': 5,\n                  'composer': 6,\n                  'writer': 7,\n                  'reconstructed by': 8,\n                  'instrument arranger': 9,\n                  'vocal arranger': 9,\n                  'arranger': 11,\n                  'orchestrator': 12,\n                  'revised by': 13,\n                  'lyricist': 14,\n                  'librettist': 15,\n                  'translator': 16\n                  }\n\nCYRILLIC_UPPER = {\n        u'А': u'A',\n        u'Б': u'B',\n        u'В': u'V',\n        u'Г': u'G',\n        u'Д': u'D',\n        u'Е': u'E',\n        u'Ё': u'E',\n        u'Ж': u'Zh',\n        u'З': u'Z',\n        u'И': u'I',\n        u'Й': u'Y',\n        u'К': u'K',\n        u'Л': u'L',\n        u'М': u'M',\n        u'Н': u'N',\n        u'О': u'O',\n        u'П': u'P',\n        u'Р': u'R',\n        u'С': u'S',\n        u'Т': u'T',\n        u'У': u'U',\n        u'Ф': u'F',\n        u'Х': u'H',\n        u'Ц': u'Ts',\n        u'Ч': u'Ch',\n        u'Ш': u'Sh',\n        u'Щ': u'Sch',\n        u'Ъ': u'',\n        u'Ы': u'Y',\n        u'Ь': u'',\n        u'Э': u'E',\n        u'Ю': u'Yu',\n        u'Я': u'Ya'\n    }\nCYRILLIC_LOWER = {\n    u'а': u'a',\n    u'б': u'b',\n    u'в': u'v',\n    u'г': u'g',\n    u'д': u'd',\n    u'е': u'e',\n    u'ё': u'e',\n    u'ж': u'zh',\n    u'з': u'z',\n    u'и': u'i',\n    u'й': u'y',\n    u'к': u'k',\n    u'л': u'l',\n    u'м': u'm',\n    u'н': u'n',\n    u'о': u'o',\n    u'п': u'p',\n    u'р': u'r',\n    u'с': u's',\n    u'т': u't',\n    u'у': u'u',\n    u'ф': u'f',\n    u'х': u'h',\n    u'ц': u'ts',\n    u'ч': u'ch',\n    u'ш': u'sh',\n    u'щ': u'sch',\n    u'ъ': u'',\n    u'ы': u'y',\n    u'ь': u'',\n    u'э': u'e',\n    u'ю': u'yu',\n    u'я': u'ya'\n}\n\ndef tag_strings(pre):\n    TAG_STRINGS = {\n            'writer': (\n                'composer',\n                pre + '_writers',\n                'composersort',\n                pre + '_writers_sort'),\n            'composer': (\n                'composer',\n                pre + '_composers',\n                'composersort',\n                pre + '_composers_sort'),\n            'lyricist': (\n                'lyricist',\n                pre + '_lyricists',\n                '~lyricists_sort',\n                pre + '_lyricists_sort'),\n            'librettist': (\n                'lyricist',\n                pre + '_librettists',\n                '~lyricists_sort',\n                pre + '_librettists_sort'),\n            'revised by': (\n                'arranger',\n                pre + '_revisors',\n                '~arranger_sort',\n                pre + '_revisors_sort'),\n            'translator': (\n                'lyricist',\n                pre + '_translators',\n                '~lyricists_sort',\n                pre + '_translators_sort'),\n            'reconstructed by': (\n                'arranger',\n                pre + '_reconstructors',\n                '~arranger_sort',\n                pre + '_reconstructors_sort'),\n            'arranger': (\n                'arranger',\n                pre + '_arrangers',\n                '~arranger_sort',\n                pre + '_arrangers_sort'),\n            'instrument arranger': (\n                'arranger',\n                pre + '_arrangers',\n                '~arranger_sort',\n                pre + '_arrangers_sort'),\n            'orchestrator': (\n                'arranger',\n                pre + '_orchestrators',\n                '~arranger_sort',\n                pre + '_orchestrators_sort'),\n            'vocal arranger': (\n                'arranger',\n                pre + '_arrangers',\n                '~arranger_sort',\n                pre + '_arrangers_sort'),\n            'performer': (\n                'performer:',\n                pre + '_performers',\n                '~performer_sort',\n                pre + '_performers_sort'),\n            'instrument': (\n                'performer:',\n                pre + '_performers',\n                '~performer_sort',\n                pre + '_performers_sort'),\n            'vocal': (\n                'performer:',\n                pre + '_performers',\n                '~performer_sort',\n                pre + '_performers_sort'),\n            'performing orchestra': (\n                'performer:orchestra',\n                pre + '_ensembles',\n                '~performer_sort',\n                pre + '_ensembles_sort'),\n            'conductor': (\n                'conductor',\n                pre + '_conductors',\n                '~conductor_sort',\n                pre + '_conductors_sort'),\n            'chorus master': (\n                'conductor',\n                pre + '_chorusmasters',\n                '~conductor_sort',\n                pre + '_chorusmasters_sort'),\n            'concertmaster': (\n                'performer',\n                pre + '~_leaders',\n                '~performer_sort',\n                pre + '_leaders_sort')}\n    return TAG_STRINGS\n\nINSERTIONS = ['writer',\n              'lyricist',\n              'librettist',\n              'revised by',\n              'translator',\n              'arranger',\n              'reconstructed by',\n              'orchestrator',\n              'instrument arranger',\n              'vocal arranger',\n              'chorus master']"
  },
  {
    "path": "plugins/classical_extras/options_classical_extras.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>ClassicalExtrasOptionsPage</class>\n <widget class=\"QWidget\" name=\"ClassicalExtrasOptionsPage\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>1145</width>\n    <height>918</height>\n   </rect>\n  </property>\n  <property name=\"sizePolicy\">\n   <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n    <horstretch>0</horstretch>\n    <verstretch>0</verstretch>\n   </sizepolicy>\n  </property>\n  <property name=\"contextMenuPolicy\">\n   <enum>Qt::DefaultContextMenu</enum>\n  </property>\n  <property name=\"acceptDrops\">\n   <bool>false</bool>\n  </property>\n  <layout class=\"QVBoxLayout\">\n   <property name=\"spacing\">\n    <number>6</number>\n   </property>\n   <property name=\"margin\">\n    <number>9</number>\n   </property>\n   <item>\n    <widget class=\"QTabWidget\" name=\"tabWidget\">\n     <property name=\"sizePolicy\">\n      <sizepolicy hsizetype=\"MinimumExpanding\" vsizetype=\"MinimumExpanding\">\n       <horstretch>0</horstretch>\n       <verstretch>0</verstretch>\n      </sizepolicy>\n     </property>\n     <property name=\"maximumSize\">\n      <size>\n       <width>1200</width>\n       <height>1200</height>\n      </size>\n     </property>\n     <property name=\"palette\">\n      <palette>\n       <active/>\n       <inactive/>\n       <disabled/>\n      </palette>\n     </property>\n     <property name=\"autoFillBackground\">\n      <bool>false</bool>\n     </property>\n     <property name=\"styleSheet\">\n      <string notr=\"true\"/>\n     </property>\n     <property name=\"currentIndex\">\n      <number>4</number>\n     </property>\n     <widget class=\"QWidget\" name=\"Artists\">\n      <property name=\"toolTip\">\n       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n      </property>\n      <attribute name=\"title\">\n       <string>Artists</string>\n      </attribute>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_10\">\n       <item>\n        <widget class=\"QScrollArea\" name=\"scrollArea\">\n         <property name=\"sizePolicy\">\n          <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n           <horstretch>0</horstretch>\n           <verstretch>0</verstretch>\n          </sizepolicy>\n         </property>\n         <property name=\"focusPolicy\">\n          <enum>Qt::WheelFocus</enum>\n         </property>\n         <property name=\"frameShape\">\n          <enum>QFrame::NoFrame</enum>\n         </property>\n         <property name=\"frameShadow\">\n          <enum>QFrame::Plain</enum>\n         </property>\n         <property name=\"lineWidth\">\n          <number>1</number>\n         </property>\n         <property name=\"widgetResizable\">\n          <bool>true</bool>\n         </property>\n         <widget class=\"QWidget\" name=\"scrollAreaWidgetContents\">\n          <property name=\"geometry\">\n           <rect>\n            <x>0</x>\n            <y>0</y>\n            <width>1086</width>\n            <height>1071</height>\n           </rect>\n          </property>\n          <property name=\"acceptDrops\">\n           <bool>false</bool>\n          </property>\n          <layout class=\"QVBoxLayout\" name=\"verticalLayout_13\">\n           <item>\n            <widget class=\"QFrame\" name=\"artists_run_frame\">\n             <property name=\"palette\">\n              <palette>\n               <active>\n                <colorrole role=\"Button\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n                <colorrole role=\"Base\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n                <colorrole role=\"Window\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n               </active>\n               <inactive>\n                <colorrole role=\"Button\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n                <colorrole role=\"Base\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n                <colorrole role=\"Window\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n               </inactive>\n               <disabled>\n                <colorrole role=\"Button\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n                <colorrole role=\"Base\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n                <colorrole role=\"Window\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n               </disabled>\n              </palette>\n             </property>\n             <property name=\"autoFillBackground\">\n              <bool>false</bool>\n             </property>\n             <property name=\"styleSheet\">\n              <string notr=\"true\">background-color: rgb(255, 255, 222);</string>\n             </property>\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QHBoxLayout\" name=\"horizontalLayout_9\">\n              <item>\n               <widget class=\"QCheckBox\" name=\"use_cea\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>255</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>255</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>255</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>255</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>255</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>255</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>255</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>255</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>255</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"toolTip\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;should be selected otherwise this section will not run&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\"/>\n                </property>\n                <property name=\"text\">\n                 <string>Create extra artist metadata (MUST BE TICKED FOR THIS SECTION TO RUN)</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QLabel\" name=\"infer_worktypes_old_label\">\n                <property name=\"text\">\n                 <string>(Note that the &quot;infer work-types&quot; option has moved to the &quot;genres&quot; tab)</string>\n                </property>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"Line\" name=\"line_4\">\n             <property name=\"orientation\">\n              <enum>Qt::Horizontal</enum>\n             </property>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QLabel\" name=\"naming_style_note_label\">\n             <property name=\"text\">\n              <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;The naming style for 'artist' tags is set in the main Picard Options-&amp;gt;Metadata section&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n             </property>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"naming_options_frame_2\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_36\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"naming_options_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(176, 220, 192);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Work-artist / performer naming options&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QFrame\" name=\"naming_options_frame\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"cursor\">\n                 <cursorShape>UpArrowCursor</cursorShape>\n                </property>\n                <property name=\"whatsThis\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&amp;quot;Work-artists&amp;quot; are types such as composer, writer, arranger and lyricist who belong to the MusicBrainz Work-Artist relationship&lt;/p&gt;&lt;p&gt;&amp;quot;Performers&amp;quot; are types such as performer and conductor who belong to the MusicBrainz Recording-Artist relationship&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(211, 248, 224);</string>\n                </property>\n                <property name=\"frameShape\">\n                 <enum>QFrame::NoFrame</enum>\n                </property>\n                <layout class=\"QFormLayout\" name=\"formLayout_3\">\n                 <property name=\"fieldGrowthPolicy\">\n                  <enum>QFormLayout::AllNonFixedFieldsGrow</enum>\n                 </property>\n                 <property name=\"verticalSpacing\">\n                  <number>6</number>\n                 </property>\n                 <property name=\"leftMargin\">\n                  <number>9</number>\n                 </property>\n                 <property name=\"topMargin\">\n                  <number>0</number>\n                 </property>\n                 <property name=\"rightMargin\">\n                  <number>9</number>\n                 </property>\n                 <item row=\"2\" column=\"0\" colspan=\"2\">\n                  <widget class=\"QLabel\" name=\"label_22\">\n                   <property name=\"text\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This section does not change the contents of &amp;quot;artist&amp;quot; or &amp;quot;album artist&amp;quot; tags - it only affects writer (composer etc.) and peformer tags, by using as-credited/alias names from the artist data for the release.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"3\" column=\"1\">\n                  <widget class=\"QGroupBox\" name=\"credited_as_options_box\">\n                   <property name=\"toolTip\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Credited-as options:-</string>\n                   </property>\n                   <layout class=\"QHBoxLayout\" name=\"horizontalLayout_18\">\n                    <item>\n                     <widget class=\"QGroupBox\" name=\"names_to_use_box\">\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select the source for 'as-credited' names - whether these are applied depends on the sub-options choices.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"autoFillBackground\">\n                       <bool>false</bool>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                      <property name=\"title\">\n                       <string>Names to use...</string>\n                      </property>\n                      <layout class=\"QVBoxLayout\" name=\"verticalLayout_26\">\n                       <item>\n                        <widget class=\"QCheckBox\" name=\"cea_recording_credited\">\n                         <property name=\"layoutDirection\">\n                          <enum>Qt::RightToLeft</enum>\n                         </property>\n                         <property name=\"text\">\n                          <string>Use &quot;credited as&quot; name for work-artists/performers who are recording artists</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QCheckBox\" name=\"cea_group_credited\">\n                         <property name=\"layoutDirection\">\n                          <enum>Qt::RightToLeft</enum>\n                         </property>\n                         <property name=\"autoFillBackground\">\n                          <bool>false</bool>\n                         </property>\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\"/>\n                         </property>\n                         <property name=\"text\">\n                          <string>and/or release group artists</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QCheckBox\" name=\"cea_credited\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"layoutDirection\">\n                          <enum>Qt::RightToLeft</enum>\n                         </property>\n                         <property name=\"text\">\n                          <string>and/or release artists</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QCheckBox\" name=\"cea_release_relationship_credited\">\n                         <property name=\"layoutDirection\">\n                          <enum>Qt::RightToLeft</enum>\n                         </property>\n                         <property name=\"text\">\n                          <string>and/or release relationship artists</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QCheckBox\" name=\"cea_recording_relationship_credited\">\n                         <property name=\"layoutDirection\">\n                          <enum>Qt::RightToLeft</enum>\n                         </property>\n                         <property name=\"text\">\n                          <string>and/or recording relationship artists</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QCheckBox\" name=\"cea_track_credited\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"layoutDirection\">\n                          <enum>Qt::RightToLeft</enum>\n                         </property>\n                         <property name=\"text\">\n                          <string>and/or track artists</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_24\">\n                         <property name=\"layoutDirection\">\n                          <enum>Qt::RightToLeft</enum>\n                         </property>\n                         <property name=\"text\">\n                          <string>The above are applied in sequence - e.g. track artist credit will over-ride release artist credit.</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_88\">\n                         <property name=\"text\">\n                          <string>Names are cached. A restart is necessary if any of the above name sources are removed.</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QGroupBox\" name=\"places_to_use_them_box\">\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select the tag types where any 'as-credited' names will be applied - whether these are applied depends on the sub-options choices.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                      <property name=\"title\">\n                       <string>Places to use them ...</string>\n                      </property>\n                      <layout class=\"QVBoxLayout\" name=\"verticalLayout_27\">\n                       <property name=\"sizeConstraint\">\n                        <enum>QLayout::SetDefaultConstraint</enum>\n                       </property>\n                       <property name=\"leftMargin\">\n                        <number>9</number>\n                       </property>\n                       <item>\n                        <widget class=\"QCheckBox\" name=\"cea_performer_credited\">\n                         <property name=\"layoutDirection\">\n                          <enum>Qt::RightToLeft</enum>\n                         </property>\n                         <property name=\"text\">\n                          <string>Use for performing artists</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QCheckBox\" name=\"cea_composer_credited\">\n                         <property name=\"layoutDirection\">\n                          <enum>Qt::RightToLeft</enum>\n                         </property>\n                         <property name=\"text\">\n                          <string>Use for work-artists</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item row=\"4\" column=\"0\" colspan=\"2\">\n                  <widget class=\"QGroupBox\" name=\"naming_sub_options_box\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Sub-options</string>\n                   </property>\n                   <layout class=\"QHBoxLayout\" name=\"horizontalLayout_11\">\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"cea_alias_overrides\">\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Alias (if it exists) will over-ride as-credited&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Alias over-rides credited-as</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"cea_credited_overrides\">\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;As-credited (if it exists) will over-ride alias&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Credited-as over-rides MB/Alias</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"cea_cyrillic\">\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Will be based on sort names. For cyrillic script names, patronyms will be removed.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Fix non-Latin text in names (where possible and if not fixed by other naming options)</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item row=\"3\" column=\"0\">\n                  <widget class=\"QGroupBox\" name=\"MB_std_names_aliases_box_outer\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(211, 248, 224);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>MusicBrainz standard names and Aliases</string>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_24\">\n                    <item>\n                     <widget class=\"QGroupBox\" name=\"MB_std_names_aliases_box_inner\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                      <property name=\"title\">\n                       <string/>\n                      </property>\n                      <layout class=\"QVBoxLayout\" name=\"verticalLayout_40\">\n                       <item>\n                        <widget class=\"QRadioButton\" name=\"cea_no_aliases\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Do not use aliases (but may be replaced by as-credited name)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Use MB standard names</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QRadioButton\" name=\"cea_aliases\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Alias will only be available for use if the work-artist/performer is also a release artist, recording artist or track artist.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Use alias for all work-artists/performers</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QRadioButton\" name=\"cea_aliases_composer\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Only use alias (if available) for work-artists (writers, composers, arrangers, lyricists etc.)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Use alias for work-artists only</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"recording_artists_options_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_37\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"recording_artist_options_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(176, 220, 192);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Recording artist options&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"recording_artists_options_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"toolTip\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select recording artist options (see &amp;quot;What's this&amp;quot;)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"whatsThis\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In MusicBrainz, the recording artist may be different from the track artist. For classical music, the MusicBrainz guidelines state that the track artist should be the composer; however the recording artist(s) is/are usually the principal performer(s).&lt;/p&gt;&lt;p&gt;Classical Extras puts the recording artists into 'hidden variables' (as a minimum) using the chosen naming convention.&lt;/p&gt;&lt;p&gt;There is also option to allow you to replace the track artist by the recording artist (or to merge them). The chosen action will be applied to the 'artist', 'artists', 'artistsort' and 'artists_sort' tags. Note that 'artist' is a single-valued string whereas 'artists' is a list and may be multi-valued. Lists are properly merged, but because the 'artist' string may have different join-phrases etc, a merged tag may have the recording artist(s) in brackets after the track artist(s).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(211, 248, 224);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QHBoxLayout\" name=\"horizontalLayout_24\">\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"naming_convention_box\">\n                   <property name=\"whatsThis\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;In classical music (in MusicBrainz), recording artists will usually be performers whereas track artists are composers. By default, the naming convention for performers (set in the previous section) will be used (although only the as-credited name set for the recording artist will be applied). Alternatively, the naming convention for track artists can be used - which is determined by the main Picard metadat options.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Naming convention as for ...</string>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_31\">\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"cea_ra_trackartist\">\n                      <property name=\"text\">\n                       <string>...track artist (set in Picard options)</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"cea_ra_performer\">\n                      <property name=\"text\">\n                       <string>... perfomers (set above)</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"Line\" name=\"line_7\">\n                   <property name=\"orientation\">\n                    <enum>Qt::Vertical</enum>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QCheckBox\" name=\"cea_ra_use\">\n                   <property name=\"text\">\n                    <string>Use recording artists to update track artists -&gt;</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"ra_replace_merge_options_box\">\n                   <property name=\"sizePolicy\">\n                    <sizepolicy hsizetype=\"MinimumExpanding\" vsizetype=\"Preferred\">\n                     <horstretch>0</horstretch>\n                     <verstretch>0</verstretch>\n                    </sizepolicy>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Replace/merge options</string>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_29\">\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"cea_ra_replace_ta\">\n                      <property name=\"text\">\n                       <string>Replace track artist by recording artist</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"cea_ra_noblank_ta\">\n                      <property name=\"sizePolicy\">\n                       <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Minimum\">\n                        <horstretch>0</horstretch>\n                        <verstretch>0</verstretch>\n                       </sizepolicy>\n                      </property>\n                      <property name=\"layoutDirection\">\n                       <enum>Qt::LeftToRight</enum>\n                      </property>\n                      <property name=\"text\">\n                       <string>Only replace if rec. artist exists</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"cea_ra_merge_ta\">\n                      <property name=\"text\">\n                       <string>Merge track artist and recording artist</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"other_artist_options_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_38\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"other_artist_options_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(176, 220, 192);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Other artist options&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"other_artist_options_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>211</red>\n                      <green>248</green>\n                      <blue>224</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(211, 248, 224);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QGridLayout\" name=\"gridLayout\">\n                 <property name=\"verticalSpacing\">\n                  <number>6</number>\n                 </property>\n                 <item row=\"0\" column=\"1\">\n                  <widget class=\"QGroupBox\" name=\"annotations_lh_box\">\n                   <property name=\"toolTip\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter text to appear in annotations. Do not use any quotation marks.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgba(250, 250, 250, 250);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Annotations - performers and lyricists</string>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_15\">\n                    <item>\n                     <widget class=\"QFrame\" name=\"chorusmaster_frame\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(211, 248, 224);</string>\n                      </property>\n                      <property name=\"frameShape\">\n                       <enum>QFrame::StyledPanel</enum>\n                      </property>\n                      <property name=\"frameShadow\">\n                       <enum>QFrame::Raised</enum>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_8\">\n                       <property name=\"spacing\">\n                        <number>6</number>\n                       </property>\n                       <property name=\"sizeConstraint\">\n                        <enum>QLayout::SetDefaultConstraint</enum>\n                       </property>\n                       <property name=\"topMargin\">\n                        <number>1</number>\n                       </property>\n                       <property name=\"bottomMargin\">\n                        <number>9</number>\n                       </property>\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_44\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Annotation to include with &amp;quot;chorus master&amp;quot; in conductor tag.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Chorus Master</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLineEdit\" name=\"cea_chorusmaster\">\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QFrame\" name=\"concertmaster_frame\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(211, 248, 224);</string>\n                      </property>\n                      <property name=\"frameShape\">\n                       <enum>QFrame::StyledPanel</enum>\n                      </property>\n                      <property name=\"frameShadow\">\n                       <enum>QFrame::Raised</enum>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_6\">\n                       <property name=\"topMargin\">\n                        <number>1</number>\n                       </property>\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_46\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Annotation to include for &amp;quot;concert master&amp;quot; in performer tag.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Concert Master</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLineEdit\" name=\"cea_concertmaster\">\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QFrame\" name=\"lyricist_frame\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(211, 248, 224);</string>\n                      </property>\n                      <property name=\"frameShape\">\n                       <enum>QFrame::StyledPanel</enum>\n                      </property>\n                      <property name=\"frameShadow\">\n                       <enum>QFrame::Raised</enum>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_23\">\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_34\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Annotation for lyricist, to include in lyricist tag&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Lyricist</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLineEdit\" name=\"cea_lyricist\">\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QFrame\" name=\"librettist_frame\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(211, 248, 224);</string>\n                      </property>\n                      <property name=\"frameShape\">\n                       <enum>QFrame::StyledPanel</enum>\n                      </property>\n                      <property name=\"frameShadow\">\n                       <enum>QFrame::Raised</enum>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_19\">\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_26\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Annotation for librettist, to include in lyricist tag&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Librettist</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLineEdit\" name=\"cea_librettist\">\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QFrame\" name=\"translator_frame\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(211, 248, 224);</string>\n                      </property>\n                      <property name=\"frameShape\">\n                       <enum>QFrame::StyledPanel</enum>\n                      </property>\n                      <property name=\"frameShadow\">\n                       <enum>QFrame::Raised</enum>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_21\">\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_30\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Annotation for translator, to include in lyricist tag&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Translator</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLineEdit\" name=\"cea_translator\">\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item row=\"0\" column=\"0\">\n                  <widget class=\"QGroupBox\" name=\"other_artist_checkboxes_box\">\n                   <property name=\"toolTip\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select as required. See &amp;quot;What's this&amp;quot; for details.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgba(250, 250, 250, 250);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string/>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_14\">\n                    <property name=\"spacing\">\n                     <number>6</number>\n                    </property>\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"cea_arrangers\">\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"whatsThis\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This will gather together, for example, any arranger-type information from the recording, work or parent works and place it in the &amp;quot;arranger&amp;quot; tag ('host' tag), with the annotation (see details to right of this box) in brackets. All arranger types will also be put in a hidden variable, e.g. _cwp_orchestrators. The table below shows the artist types, host tag and hidden variable for each artist type.&lt;/p&gt;&lt;p&gt;| Artist type | Host tag | Hidden variable |&lt;/p&gt;&lt;p&gt;| ----------------- | ------------------| -------------------------------------- |&lt;/p&gt;&lt;p&gt;| writer | composer | writers |&lt;/p&gt;&lt;p&gt;| lyricist | lyricist | lyricists |&lt;/p&gt;&lt;p&gt;| librettist | lyricist | librettists |&lt;/p&gt;&lt;p&gt;| revised by | arranger | revisors |&lt;/p&gt;&lt;p&gt;| translator | lyricist | translators |&lt;/p&gt;&lt;p&gt;| arranger | arranger | arrangers |&lt;/p&gt;&lt;p&gt;| reconstructed by | arranger | reconstructors |&lt;/p&gt;&lt;p&gt;| orchestrator | arranger | orchestrators |&lt;/p&gt;&lt;p&gt;| instrument arranger | arranger | arrangers (with instrument type in brackets) |&lt;/p&gt;&lt;p&gt;| vocal arranger | arranger | arrangers (with voice type in brackets) |&lt;/p&gt;&lt;p&gt;| chorus master | conductor | chorusmasters |&lt;/p&gt;&lt;p&gt;| concertmaster | performer (with annotation as a sub-key) | leaders |&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Modify host tags and include annotations (see =&gt;)</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"cea_composer_album\">\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"whatsThis\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This will add the composer(s) last name(s) before the album name, if they are listed as album artists. If there is more than one composer, they will be listed in the descending order of the length of their music on the release.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Name album as &quot;Composer Last Name(s): Album Name&quot;</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"cea_no_lyricists\">\n                      <property name=\"whatsThis\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This applies to both the Picard 'lyricist' tag and the related internal plugin hidden variables '_cwp_lyricists' etc.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Do not write 'lyricist' tag if no vocal performers</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"cea_inst_credit\">\n                      <property name=\"text\">\n                       <string>Use &quot;credited-as&quot; name for instrument</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"cea_no_solo\">\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select to eliminate &amp;quot;additional&amp;quot;, &amp;quot;solo&amp;quot; or &amp;quot;guest&amp;quot; from instrument description&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"whatsThis\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;MusicBrainz permits the use of &amp;quot;solo&amp;quot;, &amp;quot;guest&amp;quot; and &amp;quot;additional&amp;quot; as instrument attributes although, for classical music, its use should be fairly rare - usually only if explicitly stated as a &amp;quot;solo&amp;quot; on the the sleevenotes. Classical Extras provides the option to exclude these attributes (the default), but you may wish to enable them for certain releases or non-Classical / cross-over releases.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Do not include attributes (e.g. 'solo') in an instrument type</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item row=\"0\" column=\"2\">\n                  <widget class=\"QGroupBox\" name=\"annotations_rh_box\">\n                   <property name=\"toolTip\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter text to appear in annotations. Do not use any quotation marks.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Annotations - writers and arrangers</string>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_21\">\n                    <item>\n                     <widget class=\"QFrame\" name=\"writer_frame\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(211, 248, 224);</string>\n                      </property>\n                      <property name=\"frameShape\">\n                       <enum>QFrame::StyledPanel</enum>\n                      </property>\n                      <property name=\"frameShadow\">\n                       <enum>QFrame::Raised</enum>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_30\">\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_56\">\n                         <property name=\"text\">\n                          <string>Writer</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLineEdit\" name=\"cea_writer\">\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QFrame\" name=\"arranger_frame\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(211, 248, 224);</string>\n                      </property>\n                      <property name=\"frameShape\">\n                       <enum>QFrame::StyledPanel</enum>\n                      </property>\n                      <property name=\"frameShadow\">\n                       <enum>QFrame::Raised</enum>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_29\">\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_54\">\n                         <property name=\"text\">\n                          <string>Arranger</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLineEdit\" name=\"cea_arranger\">\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QFrame\" name=\"orchestrator_frame\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(211, 248, 224);</string>\n                      </property>\n                      <property name=\"frameShape\">\n                       <enum>QFrame::StyledPanel</enum>\n                      </property>\n                      <property name=\"frameShadow\">\n                       <enum>QFrame::Raised</enum>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_7\">\n                       <property name=\"topMargin\">\n                        <number>1</number>\n                       </property>\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_45\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Text with which to annotate orchestrator in the arranger tag.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Orchestrator</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLineEdit\" name=\"cea_orchestrator\">\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QFrame\" name=\"reconstructed_frame\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(211, 248, 224);</string>\n                      </property>\n                      <property name=\"frameShape\">\n                       <enum>QFrame::StyledPanel</enum>\n                      </property>\n                      <property name=\"frameShadow\">\n                       <enum>QFrame::Raised</enum>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_22\">\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_32\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Annotation for &amp;quot;reconstructed by&amp;quot;, to include in arranger tag&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Reconstructed by</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLineEdit\" name=\"cea_reconstructed\">\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QFrame\" name=\"revised_frame\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(211, 248, 224);</string>\n                      </property>\n                      <property name=\"frameShape\">\n                       <enum>QFrame::StyledPanel</enum>\n                      </property>\n                      <property name=\"frameShadow\">\n                       <enum>QFrame::Raised</enum>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_20\">\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_28\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Annotation for &amp;quot;revised by&amp;quot;, to include in arranger tag tag&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Revised by</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLineEdit\" name=\"cea_revised\">\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"Line\" name=\"line_3\">\n             <property name=\"lineWidth\">\n              <number>1</number>\n             </property>\n             <property name=\"orientation\">\n              <enum>Qt::Horizontal</enum>\n             </property>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"lyrics_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_39\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"lyrics_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(204, 168, 161);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Lyrics&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"lyrics_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>230</red>\n                      <green>215</green>\n                      <blue>211</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>230</red>\n                      <green>215</green>\n                      <blue>211</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>230</red>\n                      <green>215</green>\n                      <blue>211</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>230</red>\n                      <green>215</green>\n                      <blue>211</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>230</red>\n                      <green>215</green>\n                      <blue>211</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>230</red>\n                      <green>215</green>\n                      <blue>211</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>230</red>\n                      <green>215</green>\n                      <blue>211</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>230</red>\n                      <green>215</green>\n                      <blue>211</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>230</red>\n                      <green>215</green>\n                      <blue>211</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"whatsThis\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Please note that this section operates on the underlying input file tags, not the Picard-generated tags (MusicBrainz does not have lyrics)&lt;/span&gt;&lt;/p&gt;&lt;p&gt;  Sometimes &amp;quot;lyrics&amp;quot; tags can contain album notes (repeated for every track in an album) as well as track notes and lyrics. This section will filter out the common text and place it in a different tag from the text which is unique to each track.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(230, 215, 211);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QHBoxLayout\" name=\"horizontalLayout_25\">\n                 <item>\n                  <widget class=\"QCheckBox\" name=\"cea_split_lyrics\">\n                   <property name=\"toolTip\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;enables this section&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"text\">\n                    <string>Split lyrics tag into track and album levels</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"lyrics_and_notes_tags_frame\">\n                   <property name=\"toolTip\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter a valid tag name (no spaces, punctuation or special charcaters other than underline; use lower case)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"title\">\n                    <string/>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_30\">\n                    <item>\n                     <widget class=\"QFrame\" name=\"lyrics_tags_frame\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(204, 168, 161);</string>\n                      </property>\n                      <property name=\"frameShape\">\n                       <enum>QFrame::StyledPanel</enum>\n                      </property>\n                      <property name=\"frameShadow\">\n                       <enum>QFrame::Raised</enum>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_26\">\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_50\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The name of the lyrics file tag in the input file (normally just 'lyrics')&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Incoming lyrics tag (i.e. file tag)</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLineEdit\" name=\"cea_lyrics_tag\">\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QFrame\" name=\"album_notes_tags_frame\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(204, 168, 161);</string>\n                      </property>\n                      <property name=\"frameShape\">\n                       <enum>QFrame::StyledPanel</enum>\n                      </property>\n                      <property name=\"frameShadow\">\n                       <enum>QFrame::Raised</enum>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_27\">\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_51\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The name of the tag where common text should be placed&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Tag for album notes / lyrics</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLineEdit\" name=\"cea_album_lyrics\">\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QFrame\" name=\"track_notes_tags_frame\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(204, 168, 161);</string>\n                      </property>\n                      <property name=\"frameShape\">\n                       <enum>QFrame::StyledPanel</enum>\n                      </property>\n                      <property name=\"frameShadow\">\n                       <enum>QFrame::Raised</enum>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_28\">\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_52\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The name of the tag where notes/lyrics unique to a track should be placed&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Tag for track notes / lyrics</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLineEdit\" name=\"cea_track_lyrics\">\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <spacer name=\"verticalSpacer\">\n             <property name=\"orientation\">\n              <enum>Qt::Vertical</enum>\n             </property>\n             <property name=\"sizeHint\" stdset=\"0\">\n              <size>\n               <width>20</width>\n               <height>40</height>\n              </size>\n             </property>\n            </spacer>\n           </item>\n          </layout>\n         </widget>\n        </widget>\n       </item>\n      </layout>\n     </widget>\n     <widget class=\"QWidget\" name=\"Works\">\n      <attribute name=\"title\">\n       <string>Works and parts</string>\n      </attribute>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_4\">\n       <item>\n        <widget class=\"QScrollArea\" name=\"scrollArea_3\">\n         <property name=\"sizePolicy\">\n          <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n           <horstretch>0</horstretch>\n           <verstretch>0</verstretch>\n          </sizepolicy>\n         </property>\n         <property name=\"widgetResizable\">\n          <bool>true</bool>\n         </property>\n         <widget class=\"QWidget\" name=\"scrollAreaWidgetContents_3\">\n          <property name=\"geometry\">\n           <rect>\n            <x>0</x>\n            <y>0</y>\n            <width>1084</width>\n            <height>1086</height>\n           </rect>\n          </property>\n          <property name=\"sizePolicy\">\n           <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n            <horstretch>0</horstretch>\n            <verstretch>0</verstretch>\n           </sizepolicy>\n          </property>\n          <layout class=\"QVBoxLayout\" name=\"verticalLayout_25\">\n           <item>\n            <widget class=\"QFrame\" name=\"works_run_frame\">\n             <property name=\"sizePolicy\">\n              <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Maximum\">\n               <horstretch>0</horstretch>\n               <verstretch>0</verstretch>\n              </sizepolicy>\n             </property>\n             <property name=\"palette\">\n              <palette>\n               <active>\n                <colorrole role=\"Button\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n                <colorrole role=\"Base\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n                <colorrole role=\"Window\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n               </active>\n               <inactive>\n                <colorrole role=\"Button\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n                <colorrole role=\"Base\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n                <colorrole role=\"Window\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n               </inactive>\n               <disabled>\n                <colorrole role=\"Button\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n                <colorrole role=\"Base\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n                <colorrole role=\"Window\">\n                 <brush brushstyle=\"SolidPattern\">\n                  <color alpha=\"255\">\n                   <red>255</red>\n                   <green>255</green>\n                   <blue>222</blue>\n                  </color>\n                 </brush>\n                </colorrole>\n               </disabled>\n              </palette>\n             </property>\n             <property name=\"autoFillBackground\">\n              <bool>false</bool>\n             </property>\n             <property name=\"styleSheet\">\n              <string notr=\"true\">background-color: rgb(255, 255, 222);</string>\n             </property>\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QHBoxLayout\" name=\"horizontalLayout_3\">\n              <item>\n               <widget class=\"QCheckBox\" name=\"use_cwp\">\n                <property name=\"toolTip\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&amp;quot;Include all work levels&amp;quot; should be selected otherwise this section will not run.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"text\">\n                 <string>Include all work levels (MUST BE TICKED FOR THIS SECTION TO RUN)*</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QCheckBox\" name=\"cwp_collections\">\n                <property name=\"toolTip\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This will include parent works where the relationship has the attribute 'part of collection'.&lt;br/&gt;PLEASE BE CONSISTENT and do not use different options on albums with the same works, or the results may be unexpected.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"text\">\n                 <string>Include collection relationships (but not &quot;series&quot;)</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QCheckBox\" name=\"use_cache\">\n                <property name=\"toolTip\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select to use cached works. Deselect to refesh from MusicBrainz.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"whatsThis\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&amp;quot;Use cache&amp;quot; prevents excessive look-ups of the MB database. Every look-up of a parent work needs to be performed separately (hopefully the MB database might make this easier some day). Network usage constraints by MB means that each look-up takes a minimum of 1 second. Once a release has been looked-up, the works are retained in cache, significantly reducing the time required if, say, the options are changed and the data refreshed. However, if the user edits the works in the MB database then the cache will need to be turned off temporarily for the refresh to find the new/changed works. Also some types of work (e.g. arrangements) will require a full look-up if options have been changed.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"text\">\n                 <string>Use cache (if available)*</string>\n                </property>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"work_style_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_44\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"work_style_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(205, 230, 255);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Tagging style&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"work_style_box\">\n                <property name=\"sizePolicy\">\n                 <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Maximum\">\n                  <horstretch>0</horstretch>\n                  <verstretch>0</verstretch>\n                 </sizepolicy>\n                </property>\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>225</red>\n                      <green>240</green>\n                      <blue>255</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>225</red>\n                      <green>240</green>\n                      <blue>255</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>225</red>\n                      <green>240</green>\n                      <blue>255</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>225</red>\n                      <green>240</green>\n                      <blue>255</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>225</red>\n                      <green>240</green>\n                      <blue>255</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>225</red>\n                      <green>240</green>\n                      <blue>255</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>225</red>\n                      <green>240</green>\n                      <blue>255</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>225</red>\n                      <green>240</green>\n                      <blue>255</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>225</red>\n                      <green>240</green>\n                      <blue>255</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"whatsThis\">\n                 <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;\n&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;\np, li { white-space: pre-wrap; }\n&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&amp;quot;Tagging style&amp;quot;. This section determines how the hierarchy of works will be sourced. &lt;/p&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Works source&lt;/span&gt;: There are 3 options for determing the principal source of the works metadata &lt;/p&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&amp;quot;Use only metadata from title text&amp;quot;. The plugin will attempt to extract the hierarchy of works from the track title by looking for repetitions and patterns. If the title does not contain all the work names in the hierarchy then obviously this will limit what can be provided. &lt;/p&gt;\n&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&amp;quot;Use only metadata from canonical works&amp;quot;. The hierarchy in the MB database will be used. Assuming the work is correctly entered in MB, this should provide all the data. However the text may differ from the track titles and will be the same for all recordings. It may also be in the language of the composer whereas the titles will probably be in the language of the release. (This language issue can also be addressed by using aliases - see below). &lt;/p&gt;\n&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&amp;quot;Use canonical work metadata enhanced with title text&amp;quot;. This supplements the canonical data with text from the titles &lt;span style=&quot; font-weight:600;&quot;&gt;where it is significantly different&lt;/span&gt;. The supplementary title data will be in curly brackets. This is clearly the most complete metadata style of the three but may lead to long descriptions. It is particularly useful for providing translations.&lt;/p&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Source of canonical work text&lt;/span&gt;. Where either of the second two options above are chosen, there is a further choice to be made: &lt;/p&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&amp;quot;Full MusicBrainz work hierarchy&amp;quot;. The names of each level of work are used to populate the relevant tags. E.g. if &amp;quot;Má vlast: I. Vyšehrad, JB 1:112/1&amp;quot; (level 0) is part of &amp;quot;Má vlast, JB 1:112&amp;quot; (level 1) then the parent work will be tagged as &amp;quot;Má vlast, JB 1:112&amp;quot;, not &amp;quot;Má vlast&amp;quot;. So, while accurate, this option might be more verbose. &lt;/p&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&amp;quot;Consistent with lowest level work description&amp;quot;. The names of the level 0 work are used to populate the relevant tags. I.e. if &amp;quot;Má vlast: I. Vyšehrad, JB 1:112/1&amp;quot; (level 0) is part of &amp;quot;Má vlast, JB 1:112&amp;quot; (level 1) then the parent work will be tagged as &amp;quot;Má vlast&amp;quot;, not &amp;quot;Má vlast, JB 1:112&amp;quot;. This frequently looks better, but not always, &lt;span style=&quot; font-weight:600;&quot;&gt;particularly if the level 0 work name does not contain all the parent work detail&lt;/span&gt;. If the full structure is not implicit in the level 0 name then a warning will be logged and written to the &amp;quot;warning&amp;quot; tag. &lt;/p&gt;\n&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Strategy for setting style:&lt;/span&gt; &lt;span style=&quot; font-style:italic;&quot;&gt;It is suggested that you start with &amp;quot;extended/enhanced&amp;quot; style and the &amp;quot;Consistent with lowest level work description&amp;quot; as the source (this is the default). If this does not give acceptable results, try switching to &amp;quot;Full MusicBrainz work hierarchy&amp;quot;. If the &amp;quot;enhanced&amp;quot; details in curly brackets (from the track title) give odd results then switch the style to &amp;quot;canonical works&amp;quot; only. Any remaining oddities are then probably in the MusicBrainz data, which may require editing.&lt;/span&gt; &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(225, 240, 255);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QFormLayout\" name=\"formLayout_2\">\n                 <item row=\"0\" column=\"0\">\n                  <widget class=\"QGroupBox\" name=\"works_source_box\">\n                   <property name=\"whatsThis\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;There are 3 options for determing the principal source of the works metadata&lt;/p&gt;&lt;p&gt;      - &amp;quot;Use only metadata from title text&amp;quot;. The plugin will attempt to extract the hierarchy of works from the track title by looking for repetitions and patterns. If the title does not contain all the work names in the hierarchy then obviously this will limit what can be provided.&lt;/p&gt;&lt;p&gt;      - &amp;quot;Use only metadata from canonical works&amp;quot;. The hierarchy in the MB database will be used. Assuming the work is correctly entered in MB, this should provide all the data. However the text may differ from the track titles and will be the same for all recordings. It may also be in the language of the composer whereas the titles will be in the language of the release.&lt;/p&gt;&lt;p&gt;      - &amp;quot;Use canonical work metadata enhanced with title text&amp;quot;. This supplements the canonical data with text from the titles &lt;span style=&quot; font-weight:600;&quot;&gt;where it is significantly different&lt;/span&gt;. The supplementary data will be in curly brackets. This is clearly the most complete metadata style of the three but may lead to long descriptions. It is particularly useful for providing translations - see image below for an example (using the Muso library manager).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Works source</string>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_11\">\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"cwp_titles\">\n                      <property name=\"whatsThis\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&amp;quot;Use only metadata from title text&amp;quot;. The plugin will attempt to extract the hierarchy of works from the track title by looking for repetitions and patterns. If the title does not contain all the work names in the hierarchy then obviously this will limit what can be provided.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Use only metadata from title text</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"cwp_works\">\n                      <property name=\"whatsThis\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&amp;quot;Use only metadata from canonical works&amp;quot;. The hierarchy in the MB database will be used. Assuming the work is correctly entered in MB, this should provide all the data. However the text may differ from the track titles and will be the same for all recordings. It may also be in the language of the composer whereas the titles will probably be in the language of the release. (This language issue can also be addressed by using aliases - see below).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Use only metadata from canonical works</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"cwp_extended\">\n                      <property name=\"whatsThis\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&amp;quot;Use canonical work metadata enhanced with title text&amp;quot;. This supplements the canonical data with text from the titles **where it is significantly different**. The supplementary title data will be in curly brackets. This is clearly the most complete metadata style of the three but may lead to long descriptions. It is particularly useful for providing translations&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Use canonical work metadata enhanced with title text</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item row=\"0\" column=\"1\">\n                  <widget class=\"QGroupBox\" name=\"source_of_canonical_box\">\n                   <property name=\"enabled\">\n                    <bool>true</bool>\n                   </property>\n                   <property name=\"whatsThis\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Where either of the second two options above are chosen, there is a further choice to be made:&lt;/p&gt;&lt;p&gt;      - &amp;quot;Full MusicBrainz work hierarchy&amp;quot;. The names of each level of work are used to populate the relevant tags. I.e. if &amp;quot;Má vlast: I. Vyšehrad, JB 1:112/1&amp;quot; (level 0) is part of &amp;quot;Má vlast, JB 1:112&amp;quot; (level 1) then the parent work will be tagged as &amp;quot;Má vlast, JB 1:112&amp;quot;, not &amp;quot;Má vlast&amp;quot;. So, while accurate, this option might be more verbose.&lt;/p&gt;&lt;p&gt;      - &amp;quot;Consistent with lowest level work description&amp;quot;. The names of the level 0 work are used to populate the relevant tags. I.e. if &amp;quot;Má vlast: I. Vyšehrad, JB 1:112/1&amp;quot; (level 0) is part of &amp;quot;Má vlast, JB 1:112&amp;quot; (level 1) then the parent work will be tagged as &amp;quot;Má vlast&amp;quot;, not &amp;quot;Má vlast, JB 1:112&amp;quot;. This frequently looks better, but not always, &lt;span style=&quot; font-weight:600;&quot;&gt;particularly if the level 0 work name does not contain all the parent work detail&lt;/span&gt;. If the full structure is not implicit in the level 0 name then a warning will be logged and written to the &amp;quot;warning&amp;quot; tag.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Source of canonical work text (if applicable)</string>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_6\">\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"cwp_hierarchical_works\">\n                      <property name=\"whatsThis\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&amp;quot;Full MusicBrainz work hierarchy&amp;quot;. The names of each level of work are used to populate the relevant tags. E.g. if &amp;quot;Má vlast: I. Vyšehrad, JB 1:112/1&amp;quot; (level 0) is part of &amp;quot;Má vlast, JB 1:112&amp;quot; (level 1) then the parent work will be tagged as &amp;quot;Má vlast, JB 1:112&amp;quot;, not &amp;quot;Má vlast&amp;quot;. So, while accurate, this option might be more verbose.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Full MusicBrainz work hierarchy (may be more verbose)</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"cwp_level0_works\">\n                      <property name=\"whatsThis\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&amp;quot;Consistent with lowest level work description&amp;quot;. The names of the level 0 work are used to populate the relevant tags. I.e. if &amp;quot;Má vlast: I. Vyšehrad, JB 1:112/1&amp;quot; (level 0) is part of &amp;quot;Má vlast, JB 1:112&amp;quot; (level 1) then the parent work will be tagged as &amp;quot;Má vlast&amp;quot;, not &amp;quot;Má vlast, JB 1:112&amp;quot;. This frequently looks better, but not always, &lt;span style=&quot; font-weight:600;&quot;&gt;particularly if the level 0 work name does not contain all the parent work detail&lt;/span&gt;. If the full structure is not implicit in the level 0 name then a warning will be logged and written to the &amp;quot;warning&amp;quot; tag.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Consistent with lowest level work description (may be less verbose, but not always complete)</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item row=\"1\" column=\"0\" colspan=\"2\">\n                  <widget class=\"QCheckBox\" name=\"cwp_derive_works_from_title\">\n                   <property name=\"text\">\n                    <string>Attempt to get works and movement info from title if there are no work relationships? (Requires title in form &quot;work: movement&quot;)</string>\n                   </property>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"work_aliases_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_45\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"work_aliases_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(255, 186, 189);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Aliases&lt;/span&gt; (NB Use a consistent approach throughout the library otherwise duplicate works may occur - see the Readme)*&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"work_aliases_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>220</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>220</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>220</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>220</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>220</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>220</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>220</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>220</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>220</green>\n                      <blue>222</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"whatsThis\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&amp;quot;Replace work names by aliases&amp;quot; will use &lt;span style=&quot; font-weight:600;&quot;&gt;primary&lt;/span&gt; aliases for the chosen locale instead of standard MusicBrainz work names. To choose the locale, use the drop-down under &amp;quot;translate artist names&amp;quot; in the main Picard Options--&amp;gt;Metadata page. Note that this option is not saved as a file tag since, if different choices are made for different releases, different work names may be stored and therefore cannot be grouped together in your player/library manager. &lt;/p&gt;&lt;p&gt;The sub-options allow either the replacement of all work names, where a primary alias exists, just the replacement of work names which are in non-Latin script, or only replace those which are flagged with user &amp;quot;Folksonomy&amp;quot; tags. The tag text needs to be included in the text box, in which case flagged works will be 'aliased' as well as non-Latin script works, if the second sub-option is chosen. Note that the tags may either be anyone's tags (&amp;quot;Look in all tags&amp;quot;) or the user's own tags. If selecting &amp;quot;Look in user's own tags only&amp;quot; you &lt;span style=&quot; font-weight:600;&quot;&gt;must&lt;/span&gt; be logged in to your MusicBrainz user account (in the Picard Options-&amp;gt;General page), otherwise repeated dialogue boxes may be generated and you may need to force restart Picard.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(255, 220, 222);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QHBoxLayout\" name=\"horizontalLayout_16\">\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"replace_MBworknames_box\">\n                   <property name=\"title\">\n                    <string>Replace MB work names?</string>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_22\">\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"cwp_aliases\">\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use primary aliases for the chosen locale instead of standard MusicBrainz work names.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Replace work names by aliases. Select method --&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"cwp_no_aliases\">\n                      <property name=\"text\">\n                       <string>Do not replace work names</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"what_to_replace_outer_box\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\"/>\n                   </property>\n                   <property name=\"title\">\n                    <string>What to replace?</string>\n                   </property>\n                   <layout class=\"QHBoxLayout\" name=\"horizontalLayout_17\">\n                    <item>\n                     <widget class=\"QGroupBox\" name=\"what_to_replace_radios_box\">\n                      <property name=\"title\">\n                       <string/>\n                      </property>\n                      <layout class=\"QVBoxLayout\" name=\"verticalLayout_23\">\n                       <item>\n                        <widget class=\"QRadioButton\" name=\"cwp_aliases_all\">\n                         <property name=\"text\">\n                          <string>All work names</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QRadioButton\" name=\"cwp_aliases_greek\">\n                         <property name=\"text\">\n                          <string>Non-latin work names</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QRadioButton\" name=\"cwp_aliases_tagged\">\n                         <property name=\"text\">\n                          <string>Only tagged works</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QGroupBox\" name=\"works_alias_tags_box\">\n                      <property name=\"title\">\n                       <string>Tags (&quot;Folksonomy&quot;) identifying works to be replaced by aliases</string>\n                      </property>\n                      <layout class=\"QFormLayout\" name=\"formLayout\">\n                       <item row=\"1\" column=\"0\">\n                        <widget class=\"QRadioButton\" name=\"cwp_aliases_tags_all\">\n                         <property name=\"text\">\n                          <string>Look in all tags</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item row=\"1\" column=\"1\">\n                        <widget class=\"QRadioButton\" name=\"cwp_aliases_tags_user\">\n                         <property name=\"text\">\n                          <string>Look in user's own tags only (MUST BE LOGGED IN!)</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item row=\"0\" column=\"0\" colspan=\"2\">\n                        <widget class=\"QLineEdit\" name=\"cwp_aliases_tag_text\">\n                         <property name=\"toolTip\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Separate multiple tags by commas&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"styleSheet\">\n                          <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"works_parts_tags_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_46\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"work_parts_tags_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(255, 194, 158);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Tags to create&lt;/span&gt; - Use commas to separate multiple tags or leave blank to omit&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"works_parts_tags_box\">\n                <property name=\"sizePolicy\">\n                 <sizepolicy hsizetype=\"Preferred\" vsizetype=\"MinimumExpanding\">\n                  <horstretch>0</horstretch>\n                  <verstretch>0</verstretch>\n                 </sizepolicy>\n                </property>\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>221</green>\n                      <blue>201</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>221</green>\n                      <blue>201</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>221</green>\n                      <blue>201</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>221</green>\n                      <blue>201</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>221</green>\n                      <blue>201</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>221</green>\n                      <blue>201</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>221</green>\n                      <blue>201</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>221</green>\n                      <blue>201</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>221</green>\n                      <blue>201</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"toolTip\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Separate multiple tags by commas.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"whatsThis\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&amp;quot;Tags to create&amp;quot; sets the names of the tags that will be created from the sources described above. All these tags will be blanked before filling as specified. Tags specified against more than one source will have later sources appended in the sequence specified, separated by separators as specified.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(255, 221, 201);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QVBoxLayout\" name=\"verticalLayout_9\">\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"works_tags_box\">\n                   <property name=\"palette\">\n                    <palette>\n                     <active>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </active>\n                     <inactive>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </inactive>\n                     <disabled>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </disabled>\n                    </palette>\n                   </property>\n                   <property name=\"autoFillBackground\">\n                    <bool>false</bool>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(255, 209, 182);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Work tags</string>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_8\">\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_40\">\n                      <property name=\"text\">\n                       <string>Separator</string>\n                      </property>\n                      <property name=\"alignment\">\n                       <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <layout class=\"QHBoxLayout\" name=\"_8\">\n                      <property name=\"spacing\">\n                       <number>6</number>\n                      </property>\n                      <property name=\"margin\">\n                       <number>0</number>\n                      </property>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_11\">\n                        <property name=\"sizePolicy\">\n                         <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                          <horstretch>0</horstretch>\n                          <verstretch>0</verstretch>\n                         </sizepolicy>\n                        </property>\n                        <property name=\"whatsThis\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Some software (notably Muso) can display a 2-level work hierarchy as well as the work-movement hierarchy. This tag can be use to store the 2-level work name (a double colon :: is used to separate the levels within the tag).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"text\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Tags for Work - for software with 2-level capability (e.g. Muso)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QLineEdit\" name=\"cwp_work_tag_multi\">\n                        <property name=\"styleSheet\">\n                         <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QComboBox\" name=\"cwp_multi_work_sep\">\n                        <property name=\"editable\">\n                         <bool>true</bool>\n                        </property>\n                        <item>\n                         <property name=\"text\">\n                          <string/>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>; </string>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>: </string>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>. </string>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>, </string>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>- </string>\n                         </property>\n                        </item>\n                       </widget>\n                      </item>\n                     </layout>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"label\">\n                      <property name=\"text\">\n                       <string>(In this format, intermediate works will be displayed after a double colon  :: )</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <layout class=\"QHBoxLayout\" name=\"_12\">\n                      <property name=\"spacing\">\n                       <number>6</number>\n                      </property>\n                      <property name=\"margin\">\n                       <number>0</number>\n                      </property>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_15\">\n                        <property name=\"sizePolicy\">\n                         <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                          <horstretch>0</horstretch>\n                          <verstretch>0</verstretch>\n                         </sizepolicy>\n                        </property>\n                        <property name=\"whatsThis\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Software which can display a movement and work (but no higher levels) could use any tags specified here. Note that if there are multiple work levels, the intermediate levels will not be tagged. Users wanting all the information should use the tags from the previous option (but it may cause some breaks in the display if levels change) - alternatively the missing work levels can be included in a movement tag (see below).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"text\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Tags for Work - for software with 1-level capability (e.g. iTunes)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QLineEdit\" name=\"cwp_work_tag_single\">\n                        <property name=\"styleSheet\">\n                         <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QComboBox\" name=\"cwp_single_work_sep\">\n                        <property name=\"editable\">\n                         <bool>true</bool>\n                        </property>\n                        <item>\n                         <property name=\"text\">\n                          <string/>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>; </string>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>: </string>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>. </string>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>, </string>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>- </string>\n                         </property>\n                        </item>\n                       </widget>\n                      </item>\n                     </layout>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_2\">\n                      <property name=\"text\">\n                       <string>(Intermediate works will not be displayed:- Either 1. use the 2-level format if you wish to display them, but note that this will ceate new work, or 2. include them in the movement [see below])</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <layout class=\"QHBoxLayout\" name=\"_9\">\n                      <property name=\"spacing\">\n                       <number>6</number>\n                      </property>\n                      <property name=\"margin\">\n                       <number>0</number>\n                      </property>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_12\">\n                        <property name=\"sizePolicy\">\n                         <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                          <horstretch>0</horstretch>\n                          <verstretch>0</verstretch>\n                         </sizepolicy>\n                        </property>\n                        <property name=\"whatsThis\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This is the top-level work held in MB. This can be useful for cataloguing and searching (if the library software is capable). Note that this will always be the &amp;quot;canonical&amp;quot; MB name, not one derived from titles or the lowest level work name and that no annotations (e.g. key or work year) will be added. However, if &amp;quot;replace work names by aliases&amp;quot; has been selected and is applicable, the relevant alias will be used.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"text\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Tags for top-level (canonical) work (for capable library managers)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QLineEdit\" name=\"cwp_top_tag\">\n                        <property name=\"styleSheet\">\n                         <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_10\">\n                        <property name=\"text\">\n                         <string>  N/A    </string>\n                        </property>\n                       </widget>\n                      </item>\n                     </layout>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"parts_tags_box\">\n                   <property name=\"palette\">\n                    <palette>\n                     <active>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </active>\n                     <inactive>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </inactive>\n                     <disabled>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>255</red>\n                         <green>209</green>\n                         <blue>182</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </disabled>\n                    </palette>\n                   </property>\n                   <property name=\"autoFillBackground\">\n                    <bool>false</bool>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(255, 209, 182);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Movement/Part tags</string>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_7\">\n                    <item>\n                     <layout class=\"QHBoxLayout\" name=\"_10\">\n                      <property name=\"spacing\">\n                       <number>6</number>\n                      </property>\n                      <property name=\"margin\">\n                       <number>0</number>\n                      </property>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_13\">\n                        <property name=\"sizePolicy\">\n                         <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                          <horstretch>0</horstretch>\n                          <verstretch>0</verstretch>\n                         </sizepolicy>\n                        </property>\n                        <property name=\"toolTip\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The Picard standard tag is  'movementnumber' - include that or other(s) of your choice&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"whatsThis\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This is not necessarily the embedded movt/part number, but is the sequence number of the movement within its parent work &lt;span style=&quot; font-weight:600;&quot;&gt;on the current release&lt;/span&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"text\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Tags for (computed) movement number (Picard std tag is movementnumber)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QLineEdit\" name=\"cwp_movt_no_tag\">\n                        <property name=\"styleSheet\">\n                         <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QComboBox\" name=\"cwp_movt_no_sep\">\n                        <property name=\"editable\">\n                         <bool>true</bool>\n                        </property>\n                        <item>\n                         <property name=\"text\">\n                          <string/>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>; </string>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>: </string>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>. </string>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>, </string>\n                         </property>\n                        </item>\n                        <item>\n                         <property name=\"text\">\n                          <string>- </string>\n                         </property>\n                        </item>\n                       </widget>\n                      </item>\n                     </layout>\n                    </item>\n                    <item>\n                     <layout class=\"QHBoxLayout\" name=\"_24\">\n                      <property name=\"spacing\">\n                       <number>6</number>\n                      </property>\n                      <property name=\"margin\">\n                       <number>0</number>\n                      </property>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_43\">\n                        <property name=\"sizePolicy\">\n                         <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                          <horstretch>0</horstretch>\n                          <verstretch>0</verstretch>\n                         </sizepolicy>\n                        </property>\n                        <property name=\"toolTip\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The Picard tag 'movementtotal' will be populated in any case - no need to specify it&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"whatsThis\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This is not necessarily the total number of movements in the parent work, but is the total number of movement tracks within the parent work &lt;span style=&quot; font-weight:600;&quot;&gt;on the current release&lt;/span&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"text\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Tags for (computed) total number of movements (Picard std tag is movementtotal)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QLineEdit\" name=\"cwp_movt_tot_tag\">\n                        <property name=\"styleSheet\">\n                         <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_79\">\n                        <property name=\"text\">\n                         <string>  N/A    </string>\n                        </property>\n                       </widget>\n                      </item>\n                     </layout>\n                    </item>\n                    <item>\n                     <widget class=\"QFrame\" name=\"frame\">\n                      <property name=\"frameShape\">\n                       <enum>QFrame::StyledPanel</enum>\n                      </property>\n                      <property name=\"frameShadow\">\n                       <enum>QFrame::Raised</enum>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_5\">\n                       <property name=\"leftMargin\">\n                        <number>0</number>\n                       </property>\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_49\">\n                         <property name=\"text\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Movement name tags (Picard std tag is movement)&lt;br/&gt;Use different movement tags if required for different level systems ==&amp;gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_47\">\n                         <property name=\"text\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;for use with multi-level work tags&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QLabel\" name=\"label_48\">\n                         <property name=\"text\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;for use with1-level work tags (intermediate works will prefix movement)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                    <item>\n                     <layout class=\"QHBoxLayout\" name=\"_11\">\n                      <property name=\"spacing\">\n                       <number>6</number>\n                      </property>\n                      <property name=\"margin\">\n                       <number>0</number>\n                      </property>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_14\">\n                        <property name=\"sizePolicy\">\n                         <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                          <horstretch>0</horstretch>\n                          <verstretch>0</verstretch>\n                         </sizepolicy>\n                        </property>\n                        <property name=\"toolTip\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The Picard standard tag is  'movement' - include that or other(s) of your choice&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"whatsThis\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;As below, but without the movement part/number prefix (if applicable)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"text\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Tags for Movement - excluding embedded movt/part numbers&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QLineEdit\" name=\"cwp_movt_tag_exc\">\n                        <property name=\"styleSheet\">\n                         <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QLineEdit\" name=\"cwp_movt_tag_exc1\">\n                        <property name=\"styleSheet\">\n                         <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_39\">\n                        <property name=\"text\">\n                         <string>  N/A    </string>\n                        </property>\n                       </widget>\n                      </item>\n                     </layout>\n                    </item>\n                    <item>\n                     <layout class=\"QHBoxLayout\" name=\"_6\">\n                      <property name=\"spacing\">\n                       <number>6</number>\n                      </property>\n                      <property name=\"margin\">\n                       <number>0</number>\n                      </property>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_9\">\n                        <property name=\"sizePolicy\">\n                         <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                          <horstretch>0</horstretch>\n                          <verstretch>0</verstretch>\n                         </sizepolicy>\n                        </property>\n                        <property name=\"toolTip\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The Picard standard tag is  'movement' - include that or other(s) of your choice&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"whatsThis\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This tag(s) will contain the full lowest-level part name extracted from the lowest-level work name, according to the chosen tagging style.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"text\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Tags for Movement - including embedded movt/part numbers&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QLineEdit\" name=\"cwp_movt_tag_inc\">\n                        <property name=\"styleSheet\">\n                         <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QLineEdit\" name=\"cwp_movt_tag_inc1\">\n                        <property name=\"styleSheet\">\n                         <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_37\">\n                        <property name=\"text\">\n                         <string>  N/A    </string>\n                        </property>\n                       </widget>\n                      </item>\n                     </layout>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"partial_arrangements_medleys_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_47\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"partial_arrangements_medleys_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(195, 168, 179);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Partial recordings, arrangements and medleys&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"partial_arrangements_medleys_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>221</red>\n                      <green>209</green>\n                      <blue>221</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>221</red>\n                      <green>209</green>\n                      <blue>221</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>221</red>\n                      <green>209</green>\n                      <blue>221</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>221</red>\n                      <green>209</green>\n                      <blue>221</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>221</red>\n                      <green>209</green>\n                      <blue>221</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>221</red>\n                      <green>209</green>\n                      <blue>221</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>221</red>\n                      <green>209</green>\n                      <blue>221</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>221</red>\n                      <green>209</green>\n                      <blue>221</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>221</red>\n                      <green>209</green>\n                      <blue>221</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"toolTip\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter text - do not use any quotation marks&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(221, 209, 221);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QVBoxLayout\" name=\"verticalLayout_19\">\n                 <item>\n                  <widget class=\"QLabel\" name=\"label_20\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">font: 75 8pt &quot;MS Shell Dlg 2&quot;;\ntext-decoration: underline;</string>\n                   </property>\n                   <property name=\"text\">\n                    <string>N.B. If these options are selected or deselected, quit and restart Picard before proceeding</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"partial_box\">\n                   <property name=\"whatsThis\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If this option is selected, partial recordings will be treated as a sub-part of the whole recording and will have the related text included in its name. Note that this text is at the end of the canonical name, but the latter will probably be stripped from the sub-part as it duplicates the recording work name; any title text will be appended to the whole. Note that, if &amp;quot;Consistent with lowest level work description&amp;quot; is chosen in section 2, the text may be treated as a &amp;quot;prefix&amp;quot; similar to those in the &amp;quot;Advanced&amp;quot; tab. If this eliminates other similar prefixes and has unwanted effects, then either change the desired text slightly (e.g. surround with brackets) or use the &amp;quot;Full MusicBrainz work hierarchy&amp;quot; option in section 2.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Partial recordings</string>\n                   </property>\n                   <layout class=\"QHBoxLayout\" name=\"horizontalLayout_13\">\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"cwp_partial\">\n                      <property name=\"text\">\n                       <string>Show partial recordings as separate sub-part, labelled with -&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"cwp_partial_text\">\n                      <property name=\"enabled\">\n                       <bool>true</bool>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"arrangements_box\">\n                   <property name=\"whatsThis\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If this option is selected, works which are arrangements of other works will have the latter treated in the same manner as &amp;quot;parent&amp;quot; works, except that the arrangement work name will be prefixed by the text provided.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Arrangements</string>\n                   </property>\n                   <layout class=\"QHBoxLayout\" name=\"horizontalLayout_15\">\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"cwp_arrangements\">\n                      <property name=\"text\">\n                       <string>Show arrangements as parts of original works, labelled with -&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"cwp_arrangements_text\">\n                      <property name=\"enabled\">\n                       <bool>true</bool>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                      <property name=\"dragEnabled\">\n                       <bool>false</bool>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"medleys_box\">\n                   <property name=\"title\">\n                    <string>Medleys</string>\n                   </property>\n                   <layout class=\"QHBoxLayout\" name=\"horizontalLayout_12\">\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"cwp_medley\">\n                      <property name=\"whatsThis\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Medleys&lt;/span&gt;&lt;/p&gt;&lt;p&gt;      These can occur in two ways in MusicBrainz: (a) the recording is described as a &amp;quot;medley of&amp;quot; a number of works and (b) the track is described as (more than one) &amp;quot;medley including a recording of&amp;quot; a work. In the first case, the specified text will be included in brackets after the work name, whereas in the second case, the track will be treated as a recording of multiple works and the specified text will appear in the parent work name.&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Include medley list, labelled with -&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"cwp_medley_text\">\n                      <property name=\"enabled\">\n                       <bool>true</bool>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"songkong_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_48\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"leftMargin\">\n               <number>0</number>\n              </property>\n              <property name=\"topMargin\">\n               <number>0</number>\n              </property>\n              <property name=\"bottomMargin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"songkong_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(182, 182, 62);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;SongKong-compatible tag usage&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"songkong_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>227</red>\n                      <green>227</green>\n                      <blue>143</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>227</red>\n                      <green>227</green>\n                      <blue>143</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>227</red>\n                      <green>227</green>\n                      <blue>143</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>227</red>\n                      <green>227</green>\n                      <blue>143</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>227</red>\n                      <green>227</green>\n                      <blue>143</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>227</red>\n                      <green>227</green>\n                      <blue>143</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>227</red>\n                      <green>227</green>\n                      <blue>143</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>227</red>\n                      <green>227</green>\n                      <blue>143</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>227</red>\n                      <green>227</green>\n                      <blue>143</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"toolTip\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;See &amp;quot;What's this&amp;quot;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"whatsThis\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt; &amp;quot;Use work tags on file (no look up on MB) if Use Cache selected&amp;quot;: This will enable the existing work tags on the file to be used in preference to looking up on MusicBrainz, if those tags are SongKong-compatible (which should be the case if SongKong has been used or if the SongKong tags have been previously written by this plugin). If present, this can speed up processing considerably, but obviously any new data on MusicBrainz will be missed. For the option to operate, &amp;quot;Use cache&amp;quot; also needs to be selected. Although faster, some of the subtleties of a full look-up will be missed - for example, parent works which are arrangements will not be highlighted as such, some arrangers or composers of original works may be omitted and some medley information may be missed. **In general, therefore, the use of this option will result in poorer metadata than allowing the full database look-up to run. It is not recommended unless speed is more important than quality.**&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;    &amp;quot;Write SongKong-compatible work tags&amp;quot; does what it says. These can then be used by the previous option, if the release is subsequently reloaded into Picard, to speed things up (assuming the reload was not to pick up new work data).&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;Note that Picard and SongKong use the tag musicbrainz_workid to mean different things. If Picard has overwritten the SongKong tag (not a problem if this plugin is used) then a warning will be given and the works will be looked up on MusicBrainz. Also note that once a release is loaded, subsequent refreshes will use the cache (if option is ticked) in preference to the file tags.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(227, 227, 143);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QHBoxLayout\" name=\"horizontalLayout_14\">\n                 <item>\n                  <widget class=\"QCheckBox\" name=\"cwp_use_sk\">\n                   <property name=\"text\">\n                    <string>Use work tags on file (no look up on MB) if Use Cache selected* (NOT RECOMMENDED - SEE README)</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QCheckBox\" name=\"cwp_write_sk\">\n                   <property name=\"text\">\n                    <string>Write SongKong-compatible work tags*</string>\n                   </property>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QLabel\" name=\"label_82\">\n             <property name=\"text\">\n              <string>* ASTERISKED OPTIONS ARE NOT SAVED IN FILE TAGS</string>\n             </property>\n            </widget>\n           </item>\n          </layout>\n         </widget>\n        </widget>\n       </item>\n      </layout>\n     </widget>\n     <widget class=\"QWidget\" name=\"Genres\">\n      <attribute name=\"title\">\n       <string>Genres etc.</string>\n      </attribute>\n      <layout class=\"QGridLayout\" name=\"gridLayout_2\">\n       <item row=\"1\" column=\"0\">\n        <widget class=\"QScrollArea\" name=\"scrollArea_6\">\n         <property name=\"widgetResizable\">\n          <bool>true</bool>\n         </property>\n         <widget class=\"QWidget\" name=\"scrollAreaWidgetContents_5\">\n          <property name=\"geometry\">\n           <rect>\n            <x>0</x>\n            <y>-26</y>\n            <width>1084</width>\n            <height>1310</height>\n           </rect>\n          </property>\n          <layout class=\"QGridLayout\" name=\"gridLayout_13\">\n           <item row=\"2\" column=\"0\">\n            <widget class=\"QFrame\" name=\"genre_tag_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_52\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"genre_tag_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(138, 222, 187);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Genre tags&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"genre_tag_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(207, 236, 225);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QGridLayout\" name=\"gridLayout_5\">\n                 <item row=\"0\" column=\"0\">\n                  <widget class=\"QLabel\" name=\"label_73\">\n                   <property name=\"text\">\n                    <string>Name of genre tag</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"0\" column=\"2\">\n                  <widget class=\"QLabel\" name=\"label_74\">\n                   <property name=\"text\">\n                    <string>Name of sub-genre tag</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"0\" column=\"3\">\n                  <widget class=\"QLineEdit\" name=\"cwp_subgenre_tag\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(250,250,250);</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"0\" column=\"1\">\n                  <widget class=\"QLineEdit\" name=\"cwp_genre_tag\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(250,250,250);</string>\n                   </property>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item row=\"5\" column=\"0\">\n            <widget class=\"QFrame\" name=\"classical_genre_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_51\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"classical_genre_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(138, 222, 187);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;&amp;quot;Classical&amp;quot; genre &lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"classical_genre_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(207, 236, 225);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QGridLayout\" name=\"gridLayout_3\">\n                 <item row=\"1\" column=\"3\" colspan=\"2\">\n                  <widget class=\"QCheckBox\" name=\"cwp_genres_classical_exclude\">\n                   <property name=\"text\">\n                    <string>Exclude the text &quot;classical&quot; from main genre tag even if listed above</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"0\" column=\"2\" colspan=\"2\">\n                  <widget class=\"QRadioButton\" name=\"cwp_genres_classical_selective\">\n                   <property name=\"text\">\n                    <string>Make track &quot;classical&quot; only if there is a classical-specific genre (or do nothing if there is no filter)</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"1\" column=\"0\" colspan=\"3\">\n                  <widget class=\"QCheckBox\" name=\"cwp_muso_classical\">\n                   <property name=\"font\">\n                    <font>\n                     <weight>75</weight>\n                     <bold>true</bold>\n                    </font>\n                   </property>\n                   <property name=\"text\">\n                    <string>Use Muso composer list to determine if classical*</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"0\" column=\"0\" colspan=\"2\">\n                  <widget class=\"QRadioButton\" name=\"cwp_genres_classical_all\">\n                   <property name=\"text\">\n                    <string>Make all tracks &quot;classical&quot;</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"3\" column=\"0\">\n                  <widget class=\"QLabel\" name=\"label_64\">\n                   <property name=\"text\">\n                    <string>Write a flag with text =</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"3\" column=\"1\" colspan=\"3\">\n                  <widget class=\"QLineEdit\" name=\"cwp_genres_flag_text\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"3\" column=\"4\">\n                  <widget class=\"QLabel\" name=\"label_71\">\n                   <property name=\"text\">\n                    <string> in the following tag if the track is classical</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"3\" column=\"5\">\n                  <widget class=\"QLineEdit\" name=\"cwp_genres_flag_tag\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"2\" column=\"0\">\n                  <widget class=\"QCheckBox\" name=\"cwp_genres_arranger_as_composer\">\n                   <property name=\"text\">\n                    <string>(Treat arrangers as for composers)</string>\n                   </property>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item row=\"8\" column=\"0\">\n            <widget class=\"QLabel\" name=\"label_81\">\n             <property name=\"text\">\n              <string>* ASTERISKED OPTIONS ARE NOT SAVED IN FILE TAGS</string>\n             </property>\n            </widget>\n           </item>\n           <item row=\"1\" column=\"0\">\n            <widget class=\"QCheckBox\" name=\"cwp_use_muso_refdb\">\n             <property name=\"font\">\n              <font>\n               <weight>75</weight>\n               <bold>true</bold>\n              </font>\n             </property>\n             <property name=\"text\">\n              <string>Use Muso reference database (default path is set on &quot;advanced&quot; tab)*</string>\n             </property>\n            </widget>\n           </item>\n           <item row=\"6\" column=\"0\">\n            <widget class=\"QFrame\" name=\"instruments_keys_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_53\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"instruments_keys_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(225, 168, 171);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Instruments and keys&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"instruments_keys_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>245</red>\n                      <green>224</green>\n                      <blue>226</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>245</red>\n                      <green>224</green>\n                      <blue>226</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>245</red>\n                      <green>224</green>\n                      <blue>226</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>245</red>\n                      <green>224</green>\n                      <blue>226</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>245</red>\n                      <green>224</green>\n                      <blue>226</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>245</red>\n                      <green>224</green>\n                      <blue>226</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>245</red>\n                      <green>224</green>\n                      <blue>226</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>245</red>\n                      <green>224</green>\n                      <blue>226</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>245</red>\n                      <green>224</green>\n                      <blue>226</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(245, 224, 226);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QVBoxLayout\" name=\"verticalLayout_34\">\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"instruments_box\">\n                   <property name=\"palette\">\n                    <palette>\n                     <active>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </active>\n                     <inactive>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </inactive>\n                     <disabled>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </disabled>\n                    </palette>\n                   </property>\n                   <property name=\"autoFillBackground\">\n                    <bool>false</bool>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(245, 210, 213);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Instruments</string>\n                   </property>\n                   <layout class=\"QGridLayout\" name=\"gridLayout_11\">\n                    <item row=\"0\" column=\"0\">\n                     <widget class=\"QLabel\" name=\"label_66\">\n                      <property name=\"text\">\n                       <string>Tag name for instruments (will hold all instruments for a track)</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"0\" column=\"1\">\n                     <widget class=\"QLineEdit\" name=\"cwp_instruments_tag\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"1\" column=\"0\" colspan=\"2\">\n                     <widget class=\"QGroupBox\" name=\"instruments_source_box\">\n                      <property name=\"title\">\n                       <string>Name sources to use for instruments (select at least one, otherwise no instruments will be included in tag)</string>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_33\">\n                       <item>\n                        <widget class=\"QCheckBox\" name=\"cwp_instruments_MB_names\">\n                         <property name=\"text\">\n                          <string>MusicBrainz standard names</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QCheckBox\" name=\"cwp_instruments_credited_names\">\n                         <property name=\"text\">\n                          <string>&quot;Credited-as&quot; names</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"keys_box\">\n                   <property name=\"palette\">\n                    <palette>\n                     <active>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </active>\n                     <inactive>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </inactive>\n                     <disabled>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>245</red>\n                         <green>210</green>\n                         <blue>213</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </disabled>\n                    </palette>\n                   </property>\n                   <property name=\"autoFillBackground\">\n                    <bool>false</bool>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(245, 210, 213);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Keys</string>\n                   </property>\n                   <layout class=\"QGridLayout\" name=\"gridLayout_4\">\n                    <item row=\"0\" column=\"0\">\n                     <widget class=\"QLabel\" name=\"label_72\">\n                      <property name=\"text\">\n                       <string>Tag name for key(s)</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"0\" column=\"2\">\n                     <widget class=\"QLineEdit\" name=\"cwp_key_tag\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"1\" column=\"0\" colspan=\"2\">\n                     <widget class=\"QGroupBox\" name=\"keys_include_box\">\n                      <property name=\"title\">\n                       <string>Include key(s) in work name?</string>\n                      </property>\n                      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_35\">\n                       <item>\n                        <widget class=\"QRadioButton\" name=\"cwp_key_never_include\">\n                         <property name=\"text\">\n                          <string>Never</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QRadioButton\" name=\"cwp_key_contingent_include\">\n                         <property name=\"text\">\n                          <string>Only if key not already mentioned in work name</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item>\n                        <widget class=\"QRadioButton\" name=\"cwp_key_include\">\n                         <property name=\"whatsThis\">\n                          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&amp;quot;Include key(s) in work names&amp;quot; gives the option to include the key signature for a work in brackets after the name of the work in the metadata. Keys will be added in the appropriate levels: e.g. Dvořák's New World Symphony will get (E minor) at the work level, but only movements with different keys will be annotated viz. &amp;quot;II. Largo (D-flat major, C-Sharp minor)&amp;quot;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                         </property>\n                         <property name=\"text\">\n                          <string>Always</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item row=\"4\" column=\"0\">\n            <widget class=\"QFrame\" name=\"allowed_genres_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <property name=\"lineWidth\">\n              <number>1</number>\n             </property>\n             <property name=\"midLineWidth\">\n              <number>0</number>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_50\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"allowed_filters_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(138, 222, 187);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Allowed genres (filter)&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QCheckBox\" name=\"cwp_genres_filter\">\n                <property name=\"layoutDirection\">\n                 <enum>Qt::LeftToRight</enum>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(138, 222, 187);</string>\n                </property>\n                <property name=\"text\">\n                 <string>Only apply genres to tags if they match pre-defined names:</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"genre_filters_frame\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"whatsThis\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Explanation of genre-matching:&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;Only genres matching those in the boxes will be placed in the genre or sub-genre tags.&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;If  there is a matching genre found in the &amp;quot;classical main genres&amp;quot; or &amp;quot;classical sub-genres&amp;quot; box, then the track will be treated as being classical.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(207, 236, 225);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QGridLayout\" name=\"gridLayout_6\">\n                 <item row=\"2\" column=\"0\" colspan=\"2\">\n                  <widget class=\"QGroupBox\" name=\"classical_genres_box\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(190, 236, 219);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Classical genres (i.e. specific to classical music) - List separated by commas</string>\n                   </property>\n                   <layout class=\"QGridLayout\" name=\"gridLayout_9\">\n                    <item row=\"0\" column=\"0\">\n                     <widget class=\"QLabel\" name=\"label_60\">\n                      <property name=\"text\">\n                       <string>Main genres:</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"0\" column=\"1\" rowspan=\"3\">\n                     <widget class=\"QPlainTextEdit\" name=\"cwp_genres_classical_main\">\n                      <property name=\"maximumSize\">\n                       <size>\n                        <width>16777215</width>\n                        <height>50</height>\n                       </size>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250,250,250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"0\" column=\"2\">\n                     <widget class=\"QLabel\" name=\"label_75\">\n                      <property name=\"text\">\n                       <string>Sub-genres:</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"0\" column=\"3\" rowspan=\"3\">\n                     <widget class=\"QPlainTextEdit\" name=\"cwp_genres_classical_sub\">\n                      <property name=\"sizePolicy\">\n                       <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Expanding\">\n                        <horstretch>0</horstretch>\n                        <verstretch>0</verstretch>\n                       </sizepolicy>\n                      </property>\n                      <property name=\"maximumSize\">\n                       <size>\n                        <width>16777215</width>\n                        <height>50</height>\n                       </size>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250,250,250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"1\" column=\"0\">\n                     <widget class=\"QCheckBox\" name=\"cwp_muso_genres\">\n                      <property name=\"font\">\n                       <font>\n                        <weight>75</weight>\n                        <bold>true</bold>\n                       </font>\n                      </property>\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select this to use the &amp;quot;classical genres&amp;quot; in Muso options as the &amp;quot;Main classical genres&amp;quot; here.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Use Muso classical genres*</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item row=\"3\" column=\"0\" colspan=\"2\">\n                  <widget class=\"QGroupBox\" name=\"general_genres_box\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(190, 236, 219);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>General genres (may be associated with classical music, but not necessarily, e.g. &quot;instrumental&quot;) - List separated by commas</string>\n                   </property>\n                   <layout class=\"QHBoxLayout\" name=\"horizontalLayout_36\">\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_62\">\n                      <property name=\"text\">\n                       <string>Main genres</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QPlainTextEdit\" name=\"cwp_genres_other_main\">\n                      <property name=\"maximumSize\">\n                       <size>\n                        <width>16777215</width>\n                        <height>50</height>\n                       </size>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250,250,250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_76\">\n                      <property name=\"text\">\n                       <string>Sub-genres</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QPlainTextEdit\" name=\"cwp_genres_other_sub\">\n                      <property name=\"maximumSize\">\n                       <size>\n                        <width>16777215</width>\n                        <height>50</height>\n                       </size>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250,250,250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                   <zorder>cwp_genres_other_main</zorder>\n                   <zorder>cwp_genres_other_sub</zorder>\n                   <zorder>label_62</zorder>\n                   <zorder>label_76</zorder>\n                  </widget>\n                 </item>\n                 <item row=\"4\" column=\"0\">\n                  <widget class=\"QLabel\" name=\"label_80\">\n                   <property name=\"text\">\n                    <string>Genre name to use if none of the above main genres apply (leave blank if not required)</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"4\" column=\"1\">\n                  <widget class=\"QLineEdit\" name=\"cwp_genres_default\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(250,250,250);</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"0\" column=\"0\" colspan=\"2\">\n                  <widget class=\"QLabel\" name=\"label_77\">\n                   <property name=\"text\">\n                    <string>List genres, separated by commas. Only those genres listed will be included in tags.</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"1\" column=\"0\">\n                  <widget class=\"QLabel\" name=\"label_78\">\n                   <property name=\"text\">\n                    <string>See &quot;what's this&quot; for more details.</string>\n                   </property>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item row=\"3\" column=\"0\">\n            <widget class=\"QFrame\" name=\"source_of_genres_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_49\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"source_of_genres_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(138, 222, 187);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Source of genres&lt;/span&gt; - Note: if &amp;quot;existing file tag&amp;quot; is selected, information from the tag &amp;quot;genre&amp;quot; and the genre tag name specified above (if different) will be used&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"source_of_genres_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>207</red>\n                      <green>236</green>\n                      <blue>225</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(207, 236, 225);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QHBoxLayout\" name=\"horizontalLayout_32\">\n                 <item>\n                  <widget class=\"QCheckBox\" name=\"cwp_genres_use_file\">\n                   <property name=\"toolTip\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;NB: This will use the contents of the file tag with the name given above (usually 'genre').&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"text\">\n                    <string>Existing file tag (see note above)</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QCheckBox\" name=\"cwp_genres_use_folks\">\n                   <property name=\"whatsThis\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This will use the folksonomy tags for &lt;span style=&quot; font-weight:600;&quot;&gt;works&lt;/span&gt; as a possible source of genres (if they match one of the lists below).&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;To use the folksonomy tags for &lt;span style=&quot; font-weight:600;&quot;&gt;releases/tracks&lt;/span&gt;, select the main Picard option in Options-&amp;gt;Metadata-&amp;gt;&amp;quot;Use folksonomy tags as genre&amp;quot;. Again (unlike vanilla Picard) they will only be used by this plugin if they match one of the lists below.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"text\">\n                    <string>Folksonomy work tags</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QCheckBox\" name=\"cwp_genres_use_worktype\">\n                   <property name=\"text\">\n                    <string>Work-type</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QCheckBox\" name=\"cwp_genres_infer\">\n                   <property name=\"text\">\n                    <string>Infer from artist metadata</string>\n                   </property>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item row=\"7\" column=\"0\">\n            <widget class=\"QFrame\" name=\"periods_dates_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_54\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"label_109\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(195, 183, 213);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Periods and dates&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"periods_dates_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>224</green>\n                      <blue>236</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>224</green>\n                      <blue>236</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>224</green>\n                      <blue>236</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>224</green>\n                      <blue>236</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>224</green>\n                      <blue>236</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>224</green>\n                      <blue>236</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>224</green>\n                      <blue>236</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>224</green>\n                      <blue>236</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>224</green>\n                      <blue>236</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(229, 224, 236);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QVBoxLayout\" name=\"verticalLayout_32\">\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"dates_box\">\n                   <property name=\"palette\">\n                    <palette>\n                     <active>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </active>\n                     <inactive>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </inactive>\n                     <disabled>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </disabled>\n                    </palette>\n                   </property>\n                   <property name=\"autoFillBackground\">\n                    <bool>false</bool>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(222, 212, 236);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Work dates</string>\n                   </property>\n                   <layout class=\"QGridLayout\" name=\"gridLayout_10\">\n                    <item row=\"1\" column=\"0\" colspan=\"3\">\n                     <widget class=\"QGroupBox\" name=\"dates_box_inner\">\n                      <property name=\"title\">\n                       <string>Source of work dates for above tag</string>\n                      </property>\n                      <layout class=\"QGridLayout\" name=\"gridLayout_7\">\n                       <item row=\"0\" column=\"0\">\n                        <widget class=\"QCheckBox\" name=\"cwp_workdate_source_composed\">\n                         <property name=\"text\">\n                          <string>Composed date (or parent composed date)</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item row=\"0\" column=\"1\">\n                        <widget class=\"QCheckBox\" name=\"cwp_workdate_source_published\">\n                         <property name=\"text\">\n                          <string>Published date</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item row=\"0\" column=\"2\">\n                        <widget class=\"QCheckBox\" name=\"cwp_workdate_source_premiered\">\n                         <property name=\"text\">\n                          <string>Premiered date</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item row=\"1\" column=\"0\" colspan=\"3\">\n                        <widget class=\"Line\" name=\"line_9\">\n                         <property name=\"orientation\">\n                          <enum>Qt::Horizontal</enum>\n                         </property>\n                        </widget>\n                       </item>\n                       <item row=\"2\" column=\"0\">\n                        <widget class=\"QRadioButton\" name=\"cwp_workdate_use_first\">\n                         <property name=\"text\">\n                          <string>Use first available of above (in listed order)</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item row=\"2\" column=\"1\">\n                        <widget class=\"QRadioButton\" name=\"cwp_workdate_use_all\">\n                         <property name=\"text\">\n                          <string>Include all sources</string>\n                         </property>\n                        </widget>\n                       </item>\n                       <item row=\"2\" column=\"2\">\n                        <widget class=\"QCheckBox\" name=\"cwp_workdate_annotate\">\n                         <property name=\"text\">\n                          <string>Annotate dates using source name</string>\n                         </property>\n                        </widget>\n                       </item>\n                      </layout>\n                     </widget>\n                    </item>\n                    <item row=\"0\" column=\"0\">\n                     <widget class=\"QLabel\" name=\"label_68\">\n                      <property name=\"text\">\n                       <string>Tag name for work date</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"2\" column=\"0\" colspan=\"2\">\n                     <widget class=\"QCheckBox\" name=\"cwp_workdate_include\">\n                      <property name=\"whatsThis\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&amp;quot;Includeworkdate in work names&amp;quot; gives the option to include the 'work year' for a work in brackets after the name of the work in the metadata. Dates (years) will be added in the appropriate levels: e.g. Smetana's 'Má vlast' will get (1874-1879) at the work level, but the movements with different dates will be annotated viz. &amp;quot;Vyšehrad, JB 1:112/1 (1874)&amp;quot;. If the dates are the same, there should be no repitetion at the movement level. (Work dates will be used in preference order, i.e. composed - published - premiered, with only the first available date being shown).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Include workdate in work name (in preference order listed above, with no annotation)</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"0\" column=\"1\" colspan=\"2\">\n                     <widget class=\"QLineEdit\" name=\"cwp_workdate_tag\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"Line\" name=\"line_5\">\n                   <property name=\"midLineWidth\">\n                    <number>0</number>\n                   </property>\n                   <property name=\"orientation\">\n                    <enum>Qt::Horizontal</enum>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"periods_box\">\n                   <property name=\"palette\">\n                    <palette>\n                     <active>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </active>\n                     <inactive>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </inactive>\n                     <disabled>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>222</red>\n                         <green>212</green>\n                         <blue>236</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </disabled>\n                    </palette>\n                   </property>\n                   <property name=\"autoFillBackground\">\n                    <bool>false</bool>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(222, 212, 236);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Periods</string>\n                   </property>\n                   <layout class=\"QGridLayout\" name=\"gridLayout_12\">\n                    <item row=\"0\" column=\"0\">\n                     <widget class=\"QLabel\" name=\"label_69\">\n                      <property name=\"text\">\n                       <string>Tag name for period</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"4\" column=\"0\" colspan=\"2\">\n                     <widget class=\"QCheckBox\" name=\"cwp_muso_periods\">\n                      <property name=\"font\">\n                       <font>\n                        <weight>75</weight>\n                        <bold>true</bold>\n                       </font>\n                      </property>\n                      <property name=\"text\">\n                       <string>Use Muso map*</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"1\" column=\"0\" colspan=\"3\">\n                     <widget class=\"QCheckBox\" name=\"cwp_muso_dates\">\n                      <property name=\"font\">\n                       <font>\n                        <weight>75</weight>\n                        <bold>true</bold>\n                       </font>\n                      </property>\n                      <property name=\"text\">\n                       <string>Use Muso composer dates (if no work date) to determine period*</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"0\" column=\"1\" colspan=\"2\">\n                     <widget class=\"QLineEdit\" name=\"cwp_period_tag\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"3\" column=\"0\">\n                     <widget class=\"QLabel\" name=\"label_70\">\n                      <property name=\"text\">\n                       <string>Period map:</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"3\" column=\"2\" rowspan=\"2\">\n                     <widget class=\"QPlainTextEdit\" name=\"cwp_period_map\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"5\" column=\"2\">\n                     <widget class=\"QLabel\" name=\"period_map_annotation_label\">\n                      <property name=\"text\">\n                       <string> (Period name, Start year, End year; Period name2, ... etc.) - periods may overlap [Do not use commas or semi-colons within period name]</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item row=\"2\" column=\"0\">\n                     <widget class=\"QCheckBox\" name=\"cwp_periods_arranger_as_composer\">\n                      <property name=\"text\">\n                       <string>(Treat arrangers as for composers)</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item row=\"0\" column=\"0\">\n            <widget class=\"QLabel\" name=\"label_119\">\n             <property name=\"text\">\n              <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:9pt; font-weight:600;&quot;&gt;N.B. At least one of the first two tabs (Artists: &amp;quot;Create extra artist metadata&amp;quot;, or Works and parts: &amp;quot;Include all work levels&amp;quot;) &lt;/span&gt;&lt;span style=&quot; font-size:9pt; font-weight:600; text-decoration: underline;&quot;&gt;must&lt;/span&gt;&lt;span style=&quot; font-size:9pt; font-weight:600;&quot;&gt; be enabled for this section to run.&lt;br/&gt;&lt;/span&gt;&lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;(Functionality will be reduced unless both the first two tabs are enabled.) &lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n             </property>\n            </widget>\n           </item>\n          </layout>\n         </widget>\n        </widget>\n       </item>\n      </layout>\n     </widget>\n     <widget class=\"QWidget\" name=\"Tag_mapping\">\n      <attribute name=\"title\">\n       <string>Tag mapping</string>\n      </attribute>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_28\">\n       <item>\n        <widget class=\"QScrollArea\" name=\"scrollArea_4\">\n         <property name=\"sizePolicy\">\n          <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n           <horstretch>0</horstretch>\n           <verstretch>0</verstretch>\n          </sizepolicy>\n         </property>\n         <property name=\"verticalScrollBarPolicy\">\n          <enum>Qt::ScrollBarAsNeeded</enum>\n         </property>\n         <property name=\"widgetResizable\">\n          <bool>true</bool>\n         </property>\n         <widget class=\"QWidget\" name=\"scrollAreaWidgetContents_8\">\n          <property name=\"geometry\">\n           <rect>\n            <x>0</x>\n            <y>0</y>\n            <width>1084</width>\n            <height>917</height>\n           </rect>\n          </property>\n          <layout class=\"QVBoxLayout\" name=\"verticalLayout_33\">\n           <item>\n            <widget class=\"QLabel\" name=\"label_18\">\n             <property name=\"styleSheet\">\n              <string notr=\"true\"/>\n             </property>\n             <property name=\"text\">\n              <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:9pt; font-weight:600;&quot;&gt;N.B. At least one of the first two tabs (Artists: &amp;quot;Create extra artist metadata&amp;quot;, or Works and parts: &amp;quot;Include all work levels&amp;quot;) &lt;/span&gt;&lt;span style=&quot; font-size:9pt; font-weight:600; text-decoration: underline;&quot;&gt;must&lt;/span&gt;&lt;span style=&quot; font-size:9pt; font-weight:600;&quot;&gt; be enabled for this section to run. &lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n             </property>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"initial_tag_processing_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_41\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"label_97\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(209, 171, 222);\n</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Initial tag processing&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"initial_tag_processing_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>242</red>\n                      <green>221</green>\n                      <blue>245</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>242</red>\n                      <green>221</green>\n                      <blue>245</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>242</red>\n                      <green>221</green>\n                      <blue>245</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>242</red>\n                      <green>221</green>\n                      <blue>245</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>242</red>\n                      <green>221</green>\n                      <blue>245</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>242</red>\n                      <green>221</green>\n                      <blue>245</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>242</red>\n                      <green>221</green>\n                      <blue>245</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>242</red>\n                      <green>221</green>\n                      <blue>245</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>242</red>\n                      <green>221</green>\n                      <blue>245</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"whatsThis\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(242, 221, 245);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QVBoxLayout\" name=\"verticalLayout_17\">\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"tags_to_blank\">\n                   <property name=\"palette\">\n                    <palette>\n                     <active>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>242</red>\n                         <green>221</green>\n                         <blue>245</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>242</red>\n                         <green>221</green>\n                         <blue>245</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>242</red>\n                         <green>221</green>\n                         <blue>245</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </active>\n                     <inactive>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>242</red>\n                         <green>221</green>\n                         <blue>245</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>242</red>\n                         <green>221</green>\n                         <blue>245</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>242</red>\n                         <green>221</green>\n                         <blue>245</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </inactive>\n                     <disabled>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>242</red>\n                         <green>221</green>\n                         <blue>245</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>242</red>\n                         <green>221</green>\n                         <blue>245</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>242</red>\n                         <green>221</green>\n                         <blue>245</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </disabled>\n                    </palette>\n                   </property>\n                   <property name=\"whatsThis\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Any tags specified in the next two rows will be blanked before applying the tag sources described in the following section. NB this applies only to Picard-generated tags, not to other tags which might pre-exist on the file: to blank those, use the main Options-&amp;gt;Tags page. Comma-separate the tag names within the rows and note that these names are case-sensitive.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"autoFillBackground\">\n                    <bool>false</bool>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\"/>\n                   </property>\n                   <property name=\"title\">\n                    <string>Remove Picard-generated tags before applying subsequent actions? (NB existing LOCAL FILE tags will remain unless cleared using standard Picard options - to remove these, overwrite them in the next section)</string>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_12\">\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_3\">\n                      <property name=\"text\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; text-decoration: underline;&quot;&gt;Picard-generated&lt;/span&gt; tags to blank (comma-separated, case-sensitive):&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"cea_blank_tag\">\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter file tag names, separated by commas&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"cea_blank_tag_2\">\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter file tag names, separated by commas&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QLabel\" name=\"label_19\">\n                   <property name=\"whatsThis\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"text\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;List &lt;span style=&quot; text-decoration: underline;&quot;&gt;existing file tags&lt;/span&gt; which will be appended to rather than over-written by tag mapping (this will keep tags even if &amp;quot;Clear existing tags&amp;quot; is selected on main options)&lt;br/&gt;NB To allow appending to happen, &lt;span style=&quot; text-decoration: underline;&quot;&gt;do not also include these tags in &amp;quot;Preserve tags&amp;quot;&lt;/span&gt; on the main options.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QLineEdit\" name=\"cea_keep\">\n                   <property name=\"toolTip\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter file tag names, separated by commas&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"whatsThis\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This refers to the tags which already exist on files which have been matched to MusicBrainz in the right-hand panel, not the tags generated by Picard from the MusicBrainz database. Normally, Picard cannot process these tags - either it will overwrite them (if it creates a similarly named tag), clear them (if 'Clear existing tags' is specified in the main Options-&amp;gt;Tags screen) or keep them (if 'Preserve these tags...' is specified after the 'Clear existing tags' option). Classical Extras allows a further option - for the tags to be appended to in the tag mapping section (see below). List file tags which will be appended to rather than over-written by tag mapping (NB this will keep tags even if &amp;quot;Clear existing tags&amp;quot; is selected on main options). In addition, certain existing tags may be used by Classical Extras - in particular genre-related tags (see the Genres etc. options tab for more).&lt;/p&gt;&lt;p&gt;Note that if &amp;quot;Split lyrics tag&amp;quot; is specified (see the Artists tab), then the tag named there will be included in the 'Keep file tags' list and does not need to be added in this section.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QCheckBox\" name=\"cea_clear_tags\">\n                   <property name=\"toolTip\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Note that the main Picard option &amp;quot;Clear existing tags&amp;quot; should be &lt;span style=&quot; text-decoration: underline;&quot;&gt;unchecked&lt;/span&gt; for this option to operate in preference to that Picard option. The difference is that this option &lt;span style=&quot; text-decoration: underline;&quot;&gt;will not intefere with cover art&lt;/span&gt;, whereas the main Picard option will remove previous cover art.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"whatsThis\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If selected: the bottom pane of Picard will only show tags which have been generated from the MusicBrainz lookups plus any existing file tags which are listed above or in the main options &amp;quot;Preserve tags...&amp;quot;.&lt;/p&gt;&lt;p&gt;This does not mean that the file tags will be removed when saving the file. For that to happen, &amp;quot;Clear existing tags&amp;quot; needs to be selected in the main options.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"text\">\n                    <string>Do not show any file tags that are NOT listed above AND NOT listed in the main Picard &quot;Preserve tags...&quot; option (Options-&gt;Tags), even if &quot;Clear existing tags&quot; is not selected.</string>\n                   </property>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"tagmap_details_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_3\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"label_98\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(229, 174, 142);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Tag map details&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"tagmap_details_box\">\n                <property name=\"sizePolicy\">\n                 <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Expanding\">\n                  <horstretch>0</horstretch>\n                  <verstretch>0</verstretch>\n                 </sizepolicy>\n                </property>\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>229</green>\n                      <blue>214</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>229</green>\n                      <blue>214</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>229</green>\n                      <blue>214</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>229</green>\n                      <blue>214</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>229</green>\n                      <blue>214</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>229</green>\n                      <blue>214</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>229</green>\n                      <blue>214</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>229</green>\n                      <blue>214</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>255</red>\n                      <green>229</green>\n                      <blue>214</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"focusPolicy\">\n                 <enum>Qt::NoFocus</enum>\n                </property>\n                <property name=\"toolTip\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter tags, separated by commas.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">font: 75 8pt &quot;MS Shell Dlg 2&quot;;\nbackground-color: rgb(255, 229, 214);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QGridLayout\" name=\"gridLayout_14\">\n                 <item row=\"0\" column=\"0\">\n                  <widget class=\"QTextBrowser\" name=\"textBrowser\">\n                   <property name=\"sizePolicy\">\n                    <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Minimum\">\n                     <horstretch>0</horstretch>\n                     <verstretch>0</verstretch>\n                    </sizepolicy>\n                   </property>\n                   <property name=\"maximumSize\">\n                    <size>\n                     <width>16777215</width>\n                     <height>100</height>\n                    </size>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(249, 215, 187);</string>\n                   </property>\n                   <property name=\"html\">\n                    <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;\n&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;\np, li { white-space: pre-wrap; }\n&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:72; font-style:normal;&quot;&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Notes: &lt;/span&gt;&lt;/p&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Click &amp;quot;Source from:&amp;quot; button to edit source tags.&lt;/p&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Any valid Picard-generated tag can be entered in the &amp;quot;source&amp;quot; box, as well as Classical Extras sources, and mapped into other tags - not just restricted to artists.&lt;/p&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;To put a constant in a tag, type it into the source box preceded by a backslash \\.&lt;/p&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;In all cases, the source will be APPENDED to the Picard tag. To replace the standard tag, first blank it in the section above - add it back later in the list below if required (e.g. artist -&amp;gt; artist).&lt;/p&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;BUT note that any existing LOCAL FILE tag will be replaced by (not appended with) any Picard/Classical Extras tag UNLESS specified in the list box above.&lt;/p&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;These tag-mapping options may be omitted from the over-riding of artist options - see advanced tab&lt;/p&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;For more help seethe readme.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"1\" column=\"0\">\n                  <widget class=\"QFrame\" name=\"frame_25\">\n                   <layout class=\"QHBoxLayout\" name=\"_14\">\n                    <property name=\"spacing\">\n                     <number>6</number>\n                    </property>\n                    <property name=\"sizeConstraint\">\n                     <enum>QLayout::SetDefaultConstraint</enum>\n                    </property>\n                    <property name=\"margin\">\n                     <number>1</number>\n                    </property>\n                    <item>\n                     <widget class=\"QToolButton\" name=\"toolButton_1\">\n                      <property name=\"toolTip\">\n                       <string>Click to edit sources</string>\n                      </property>\n                      <property name=\"autoFillBackground\">\n                       <bool>false</bool>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\"/>\n                      </property>\n                      <property name=\"text\">\n                       <string>Source from:</string>\n                      </property>\n                      <property name=\"checkable\">\n                       <bool>true</bool>\n                      </property>\n                      <property name=\"toolButtonStyle\">\n                       <enum>Qt::ToolButtonIconOnly</enum>\n                      </property>\n                      <property name=\"autoRaise\">\n                       <bool>false</bool>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QComboBox\" name=\"cea_source_1\">\n                      <property name=\"enabled\">\n                       <bool>false</bool>\n                      </property>\n                      <property name=\"sizePolicy\">\n                       <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Fixed\">\n                        <horstretch>0</horstretch>\n                        <verstretch>0</verstretch>\n                       </sizepolicy>\n                      </property>\n                      <property name=\"mouseTracking\">\n                       <bool>false</bool>\n                      </property>\n                      <property name=\"focusPolicy\">\n                       <enum>Qt::StrongFocus</enum>\n                      </property>\n                      <property name=\"toolTip\">\n                       <string>Click button to edit. See notes above.</string>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                      <property name=\"editable\">\n                       <bool>true</bool>\n                      </property>\n                      <property name=\"sizeAdjustPolicy\">\n                       <enum>QComboBox::AdjustToContents</enum>\n                      </property>\n                      <item>\n                       <property name=\"text\">\n                        <string/>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>album_soloists, album_conductors, album_ensembles</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>soloists, conductors, ensembles, album_composers, composers</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>album_soloists</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>album_conductors</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>album_ensembles</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>album_composers</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>album_composer_lastnames</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>soloists</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>soloist_names</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>ensembles</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>ensemble_names</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>composers</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>arrangers</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>orchestrators</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>conductors</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>chorusmasters</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>leaders</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>support_performers</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>work_type</string>\n                       </property>\n                      </item>\n                      <item>\n                       <property name=\"text\">\n                        <string>release</string>\n                       </property>\n                      </item>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_21\">\n                      <property name=\"text\">\n                       <string>into tags:</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"cea_tag_1\">\n                      <property name=\"toolTip\">\n                       <string>Enter comma-separated list of tags</string>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"cea_cond_1\">\n                      <property name=\"layoutDirection\">\n                       <enum>Qt::RightToLeft</enum>\n                      </property>\n                      <property name=\"text\">\n                       <string>Conditional?</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item row=\"2\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_15\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_2\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_2\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_23\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_2\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_2\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"3\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_16\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_3\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_3\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_25\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_3\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_3\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"4\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_17\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_4\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_4\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_27\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_4\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_4\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"5\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_18\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_5\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_5\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_29\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_5\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_5\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"6\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_19\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_6\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_6\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_31\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_6\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_6\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"7\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_20\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_7\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_7\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_33\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_7\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_7\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"8\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_21\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_8\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_8\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_35\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_8\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_8\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"9\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_30\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_9\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_9\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_53\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_9\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_9\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"10\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_31\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_10\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_10\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_55\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_10\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_10\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"11\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_32\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_11\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_11\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_57\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_11\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_11\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"12\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_33\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_12\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_12\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_59\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_12\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_12\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"13\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_34\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_13\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_13\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_61\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_13\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_13\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"14\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_35\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_14\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_14\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_63\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_14\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_14\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"15\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_36\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_15\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_15\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_65\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_15\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_15\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"16\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_37\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QToolButton\" name=\"toolButton_16\">\n                     <property name=\"toolTip\">\n                      <string>Click to edit sources</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\"/>\n                     </property>\n                     <property name=\"text\">\n                      <string>Source from:</string>\n                     </property>\n                     <property name=\"checkable\">\n                      <bool>true</bool>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QComboBox\" name=\"cea_source_16\">\n                     <property name=\"enabled\">\n                      <bool>false</bool>\n                     </property>\n                     <property name=\"toolTip\">\n                      <string>Click button to edit. See notes above.</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"editable\">\n                      <bool>true</bool>\n                     </property>\n                     <item>\n                      <property name=\"text\">\n                       <string/>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists, album_conductors, album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists, conductors, ensembles, album_composers, composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>album_composer_lastnames</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloists</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>soloist_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensembles</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>ensemble_names</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>composers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>arrangers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>orchestrators</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>conductors</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>chorusmasters</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>leaders</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>support_performers</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>work_type</string>\n                      </property>\n                     </item>\n                     <item>\n                      <property name=\"text\">\n                       <string>release</string>\n                      </property>\n                     </item>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_67\">\n                     <property name=\"text\">\n                      <string>into tags:</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLineEdit\" name=\"cea_tag_16\">\n                     <property name=\"toolTip\">\n                      <string>Enter comma-separated list of tags</string>\n                     </property>\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cea_cond_16\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>Conditional?</string>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"17\" column=\"0\">\n                  <layout class=\"QHBoxLayout\" name=\"_38\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_42\">\n                     <property name=\"sizePolicy\">\n                      <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Preferred\">\n                       <horstretch>0</horstretch>\n                       <verstretch>0</verstretch>\n                      </sizepolicy>\n                     </property>\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::LeftToRight</enum>\n                     </property>\n                     <property name=\"lineWidth\">\n                      <number>1</number>\n                     </property>\n                     <property name=\"text\">\n                      <string>(If source is empty, tag will be left unchanged)                                                                                                                                        </string>\n                     </property>\n                     <property name=\"alignment\">\n                      <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_17\">\n                     <property name=\"sizePolicy\">\n                      <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Preferred\">\n                       <horstretch>0</horstretch>\n                       <verstretch>0</verstretch>\n                      </sizepolicy>\n                     </property>\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string>(Conditional tags will only be filled if previously empty)</string>\n                     </property>\n                     <property name=\"alignment\">\n                      <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item row=\"18\" column=\"0\">\n                  <widget class=\"QCheckBox\" name=\"cea_tag_sort\">\n                   <property name=\"toolTip\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select to include sort-tags, where available. See&lt;span style=&quot; font-style:italic;&quot;&gt; &amp;quot;What's this?&amp;quot;&lt;/span&gt;  for more details.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"whatsThis\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If a sort tag is associated with the source tag then the sort names will be placed in a sort tag corresponding to the destination tag. Note that the only explicit sort tags written by Picard are for artist, albumartist and composer. Piacrd also writes hidden variables '_artists_sort' and 'albumartists_sort' (note the plurals - these are the sort tags for multi-valued alternatives 'artists' and '_albumartists'). To be consistent with this approach, the plugin writes hidden variables for other tags - e.g. '_arranger_sort'. The plugin also writes hidden sort variables for the various hidden artist variables - e.g. '_cwp_librettists' has a matching sort variable '_cwp_librettists_sort'. Therefore most artist-type sources &lt;span style=&quot; font-weight:600;&quot;&gt;will&lt;/span&gt; have a sort tag/variable associated with them and these will be placed in a destination sort tag if this option is selected -&lt;span style=&quot; font-weight:600;&quot;&gt; in other words, selecting this option will cause most destination tags to have associated sort tags&lt;/span&gt;.&lt;span style=&quot; font-weight:600;&quot;&gt; Furthermore, any hidden sort variables associated with tags which are not listed explicitly in the tag mapping section will also be written out as tags&lt;/span&gt; (i.e. even if the related tags are not included as destination tags). Note, however, that composite sources (e.g. &amp;quot; ensemble_names + \\;  + conductors&amp;quot;) do not have sort tags associated with them.&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;If this option is not selected, no additional sort tags will be written, but the hidden variables will still be available, so if a sort tag is required explicitly, just map the sort tag directly - e.g. map 'conductors_sort' to 'conductor_sort'.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"layoutDirection\">\n                    <enum>Qt::LeftToRight</enum>\n                   </property>\n                   <property name=\"text\">\n                    <string>Also populate sort tags</string>\n                   </property>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n          </layout>\n         </widget>\n        </widget>\n       </item>\n      </layout>\n     </widget>\n     <widget class=\"QWidget\" name=\"Advanced\">\n      <attribute name=\"title\">\n       <string>Advanced</string>\n      </attribute>\n      <layout class=\"QHBoxLayout\" name=\"horizontalLayout_31\">\n       <item>\n        <widget class=\"QScrollArea\" name=\"scrollArea_2\">\n         <property name=\"sizePolicy\">\n          <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n           <horstretch>0</horstretch>\n           <verstretch>0</verstretch>\n          </sizepolicy>\n         </property>\n         <property name=\"widgetResizable\">\n          <bool>true</bool>\n         </property>\n         <widget class=\"QWidget\" name=\"scrollAreaWidgetContents_2\">\n          <property name=\"geometry\">\n           <rect>\n            <x>0</x>\n            <y>-853</y>\n            <width>1084</width>\n            <height>1707</height>\n           </rect>\n          </property>\n          <layout class=\"QVBoxLayout\" name=\"verticalLayout_18\">\n           <item>\n            <widget class=\"QFrame\" name=\"advanced_general_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_55\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"label_110\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(208, 208, 156);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;General&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"advanced_general_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(229, 229, 197);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QVBoxLayout\" name=\"verticalLayout_5\">\n                 <item>\n                  <widget class=\"QCheckBox\" name=\"ce_no_run\">\n                   <property name=\"text\">\n                    <string>Do not run Classical Extras for tracks where no pre-existing file is detected (warning tag will be written)</string>\n                   </property>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"advanced_artists_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_56\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"advanced_artists_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(208, 208, 156);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Artists (only effective if &quot;Artists&quot; section enabled)&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"advanced_artists_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"toolTip\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Separate multiple names by commas. Do not use any quotation marks.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"whatsThis\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Permits the listing of strings by which ensembles of different types may be identified. This is used by the plugin to place performer details in the relevant hidden variables and thus make them available for use in the &amp;quot;Tag mapping&amp;quot; tab as sources for any required tags. &lt;/p&gt;&lt;p&gt;If it is important that only whole words are to be matched, be sure to include a space after the string.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(229, 229, 197);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QVBoxLayout\" name=\"verticalLayout_20\">\n                 <item>\n                  <widget class=\"QLabel\" name=\"ensemble_strings_label\">\n                   <property name=\"text\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Ensemble strings&lt;/span&gt; (separate names by commas)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"ensemble_strings_box\">\n                   <property name=\"title\">\n                    <string/>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"_13\">\n                    <property name=\"spacing\">\n                     <number>2</number>\n                    </property>\n                    <property name=\"margin\">\n                     <number>9</number>\n                    </property>\n                    <item>\n                     <widget class=\"QLabel\" name=\"cea_orchestras_2\">\n                      <property name=\"text\">\n                       <string>Orchestras</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"cea_orchestras\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"cea_choirs_2\">\n                      <property name=\"text\">\n                       <string>Choirs</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"cea_choirs\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"cea_groups_2\">\n                      <property name=\"text\">\n                       <string>Groups (i.e. other ensembles such as quartets etc.)</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"cea_groups\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"advanced_workparts_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_57\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"advanced_workparts_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(208, 208, 156);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Works and parts (only effective if &quot;Works and parts&quot; section enabled)&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"advanced_workparts_box\">\n                <property name=\"sizePolicy\">\n                 <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n                  <horstretch>0</horstretch>\n                  <verstretch>0</verstretch>\n                 </sizepolicy>\n                </property>\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(229, 229, 197);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n                 <item>\n                  <layout class=\"QHBoxLayout\" name=\"_2\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_4\">\n                     <property name=\"sizePolicy\">\n                      <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                       <horstretch>0</horstretch>\n                       <verstretch>0</verstretch>\n                      </sizepolicy>\n                     </property>\n                     <property name=\"whatsThis\">\n                      <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Sometimes MB lookups fail. Unfortunately Picard (currently) has no automatic &amp;quot;retry&amp;quot; function. The plugin will attempt to retry for the specified number of attempts. If it still fails, the hidden variable _cwp_error will be set with a message; if error logging is checked in section 4, an error message will be written to the log and the contents of _cwp_error will be written out to a special tag called &amp;quot;An_error_has_occurred&amp;quot; which should appear prominently in the bottom pane of Picard. The problem may be resolved by refreshing, otherwise there may be a problem with the MB database availability. It is unlikely to be a software problem with the plugin.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                     </property>\n                     <property name=\"text\">\n                      <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Max number of re-tries to access works (in case of server errors)*&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                     </property>\n                     <property name=\"buddy\">\n                      <cstring>cwp_retries</cstring>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QSpinBox\" name=\"cwp_retries\">\n                     <property name=\"styleSheet\">\n                      <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                     </property>\n                     <property name=\"suffix\">\n                      <string/>\n                     </property>\n                     <property name=\"minimum\">\n                      <number>0</number>\n                     </property>\n                     <property name=\"maximum\">\n                      <number>20</number>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item>\n                  <layout class=\"QHBoxLayout\" name=\"_23\">\n                   <property name=\"spacing\">\n                    <number>6</number>\n                   </property>\n                   <property name=\"margin\">\n                    <number>0</number>\n                   </property>\n                   <item>\n                    <widget class=\"QLabel\" name=\"label_120\">\n                     <property name=\"sizePolicy\">\n                      <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                       <horstretch>0</horstretch>\n                       <verstretch>0</verstretch>\n                      </sizepolicy>\n                     </property>\n                     <property name=\"text\">\n                      <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Allow blank part names for arrangements and part recordings if arrangement/partial label is provided&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                     </property>\n                     <property name=\"buddy\">\n                      <cstring>cwp_retries</cstring>\n                     </property>\n                    </widget>\n                   </item>\n                   <item>\n                    <widget class=\"QCheckBox\" name=\"cwp_allow_empty_parts\">\n                     <property name=\"layoutDirection\">\n                      <enum>Qt::RightToLeft</enum>\n                     </property>\n                     <property name=\"text\">\n                      <string/>\n                     </property>\n                    </widget>\n                   </item>\n                  </layout>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"parent_child_text_box\">\n                   <property name=\"palette\">\n                    <palette>\n                     <active>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>241</red>\n                         <green>241</green>\n                         <blue>167</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>241</red>\n                         <green>241</green>\n                         <blue>167</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>241</red>\n                         <green>241</green>\n                         <blue>167</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </active>\n                     <inactive>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>241</red>\n                         <green>241</green>\n                         <blue>167</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>241</red>\n                         <green>241</green>\n                         <blue>167</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>241</red>\n                         <green>241</green>\n                         <blue>167</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </inactive>\n                     <disabled>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>241</red>\n                         <green>241</green>\n                         <blue>167</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>241</red>\n                         <green>241</green>\n                         <blue>167</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>241</red>\n                         <green>241</green>\n                         <blue>167</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </disabled>\n                    </palette>\n                   </property>\n                   <property name=\"autoFillBackground\">\n                    <bool>false</bool>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(241, 241, 167);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string/>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_35\">\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_114\">\n                      <property name=\"text\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:10pt; font-weight:600;&quot;&gt;Removal of common text between parent and child works&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <layout class=\"QHBoxLayout\" name=\"_22\">\n                      <property name=\"spacing\">\n                       <number>6</number>\n                      </property>\n                      <property name=\"margin\">\n                       <number>0</number>\n                      </property>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_90\">\n                        <property name=\"sizePolicy\">\n                         <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                          <horstretch>0</horstretch>\n                          <verstretch>0</verstretch>\n                         </sizepolicy>\n                        </property>\n                        <property name=\"text\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Minimum number of similar words required before eliminating. Use zero for no elimination.&lt;br/&gt;(Punctuation and accents etc. will be ignored in word comparison)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"buddy\">\n                         <cstring>cwp_retries</cstring>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QSpinBox\" name=\"cwp_common_chars\">\n                        <property name=\"styleSheet\">\n                         <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                        </property>\n                        <property name=\"suffix\">\n                         <string/>\n                        </property>\n                        <property name=\"minimum\">\n                         <number>0</number>\n                        </property>\n                        <property name=\"maximum\">\n                         <number>99</number>\n                        </property>\n                       </widget>\n                      </item>\n                     </layout>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_89\">\n                      <property name=\"text\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;NB Parent name text at the start of a work which is followed by punctuation in the work name will always be stripped regardless of this setting.&lt;br/&gt;Synonyms in the next section also apply.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"title_metadata_box\">\n                   <property name=\"palette\">\n                    <palette>\n                     <active>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>202</red>\n                         <green>202</green>\n                         <blue>140</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>202</red>\n                         <green>202</green>\n                         <blue>140</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>202</red>\n                         <green>202</green>\n                         <blue>140</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </active>\n                     <inactive>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>202</red>\n                         <green>202</green>\n                         <blue>140</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>202</red>\n                         <green>202</green>\n                         <blue>140</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>202</red>\n                         <green>202</green>\n                         <blue>140</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </inactive>\n                     <disabled>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>202</red>\n                         <green>202</green>\n                         <blue>140</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>202</red>\n                         <green>202</green>\n                         <blue>140</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>202</red>\n                         <green>202</green>\n                         <blue>140</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </disabled>\n                    </palette>\n                   </property>\n                   <property name=\"whatsThis\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This subsection contains various parameters affecting the processing of strings in titles. Because titles are free-form, not all circumstances can be anticipated. Detailed documentation of these is beyond the scope of this Readme as the effects can be quite complex and subtle and may require an understanding of the plugin code (which is of course open-source) to acsertain them. If pure canonical works are used (&amp;quot;Use only metadata from canonical works&amp;quot; and, if necessary, &amp;quot;Full MusicBrainz work hierarchy&amp;quot; on the Works and parts tab, section 2) then this processing should be irrelevant, but no text from titles will be included. Some explanations are given below:&lt;/p&gt;&lt;p&gt;    &amp;quot;Proximity of new words&amp;quot;. When using extended metadata - i.e. &amp;quot;metadata enhanced with title text&amp;quot;, the plugin will attempt to remove similar words between the canonical work name (in MusicBrainz) and the title before extending the canonical name. After removing such words, a rather &amp;quot;bitty&amp;quot; result may occur. To avoid this, any new words with the specified proximity will have the words between them (or up to the end) included even if they repeat words in the work name.&lt;/p&gt;&lt;p&gt;    &amp;quot;Prefixes&amp;quot;. When using &amp;quot;metadata from titles&amp;quot; or extended metadata, the structure of the works in MusicBrainz is used to infer the structure in the title text, so strings that are repeated between tracks which are part of the same MusicBrainz work will be treated as &amp;quot;higher level&amp;quot;. This can lead to anomolies if, for instance, the titles are &amp;quot;Work name: Part 1&amp;quot;, &amp;quot;Work name: Part 2&amp;quot;, &amp;quot;Part&amp;quot; will be treated as part of the parent work name. Specifying such words in &amp;quot;Prefixes&amp;quot; will prevent this.&lt;/p&gt;&lt;p&gt;    &amp;quot;Synonyms&amp;quot;. These words will be considered equivalent when comparing work name and title text. Thus if one word appears in the work name, that and its synonym will be removed from the title in extending the metadata (subject to the proximity setting above).&lt;/p&gt;&lt;p&gt;    &amp;quot;Replacements&amp;quot;. These words/phrases will be replaced in the title text in extended metadata, regardless of the text in the work name.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"autoFillBackground\">\n                    <bool>false</bool>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(202, 202, 140);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string/>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_115\">\n                      <property name=\"text\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:10pt; font-weight:600;&quot;&gt;How title metadata should be included in extended metadata&lt;/span&gt;&lt;span style=&quot; font-size:10pt;&quot;&gt; (use cautiously - read documentation)&lt;/span&gt;&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;(Mostly only applies if &amp;quot;Use canonical work metadata enhanced with title text&amp;quot; selected on &amp;quot;Works and parts&amp;quot; tab. &lt;/span&gt;&lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;However synonyms also apply to parent/child text removal&lt;/span&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;.)&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <layout class=\"QHBoxLayout\" name=\"_3\">\n                      <property name=\"spacing\">\n                       <number>6</number>\n                      </property>\n                      <property name=\"margin\">\n                       <number>0</number>\n                      </property>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_6\">\n                        <property name=\"sizePolicy\">\n                         <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                          <horstretch>0</horstretch>\n                          <verstretch>0</verstretch>\n                         </sizepolicy>\n                        </property>\n                        <property name=\"text\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Proximity of new words (to each other) to trigger in-fill with existing words (default = 2)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"buddy\">\n                         <cstring>cwp_retries</cstring>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QSpinBox\" name=\"cwp_proximity\">\n                        <property name=\"styleSheet\">\n                         <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                        </property>\n                        <property name=\"suffix\">\n                         <string/>\n                        </property>\n                        <property name=\"maximum\">\n                         <number>99</number>\n                        </property>\n                       </widget>\n                      </item>\n                     </layout>\n                    </item>\n                    <item>\n                     <layout class=\"QHBoxLayout\" name=\"_4\">\n                      <property name=\"spacing\">\n                       <number>6</number>\n                      </property>\n                      <property name=\"margin\">\n                       <number>0</number>\n                      </property>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_7\">\n                        <property name=\"sizePolicy\">\n                         <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                          <horstretch>0</horstretch>\n                          <verstretch>0</verstretch>\n                         </sizepolicy>\n                        </property>\n                        <property name=\"text\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Proximity of new words (to start or end) to trigger in-fill with existing words (default =1)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"buddy\">\n                         <cstring>cwp_retries</cstring>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QSpinBox\" name=\"cwp_end_proximity\">\n                        <property name=\"styleSheet\">\n                         <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                        </property>\n                        <property name=\"suffix\">\n                         <string/>\n                        </property>\n                        <property name=\"maximum\">\n                         <number>99</number>\n                        </property>\n                       </widget>\n                      </item>\n                     </layout>\n                    </item>\n                    <item>\n                     <layout class=\"QHBoxLayout\" name=\"_5\">\n                      <property name=\"spacing\">\n                       <number>6</number>\n                      </property>\n                      <property name=\"margin\">\n                       <number>0</number>\n                      </property>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_5\">\n                        <property name=\"sizePolicy\">\n                         <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                          <horstretch>0</horstretch>\n                          <verstretch>0</verstretch>\n                         </sizepolicy>\n                        </property>\n                        <property name=\"text\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Treat hyphenated words as two words for comparison purposes&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"buddy\">\n                         <cstring>cwp_retries</cstring>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QCheckBox\" name=\"cwp_split_hyphenated\">\n                        <property name=\"layoutDirection\">\n                         <enum>Qt::RightToLeft</enum>\n                        </property>\n                        <property name=\"text\">\n                         <string/>\n                        </property>\n                       </widget>\n                      </item>\n                     </layout>\n                    </item>\n                    <item>\n                     <layout class=\"QHBoxLayout\" name=\"_25\">\n                      <property name=\"spacing\">\n                       <number>6</number>\n                      </property>\n                      <property name=\"margin\">\n                       <number>0</number>\n                      </property>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_93\">\n                        <property name=\"sizePolicy\">\n                         <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                          <horstretch>0</horstretch>\n                          <verstretch>0</verstretch>\n                         </sizepolicy>\n                        </property>\n                        <property name=\"text\">\n                         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Proportion of a string to be matched to a (usually larger) string for it to be considered essentially similar (default = 66%)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"buddy\">\n                         <cstring>cwp_retries</cstring>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QSpinBox\" name=\"cwp_substring_match\">\n                        <property name=\"styleSheet\">\n                         <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                        </property>\n                        <property name=\"suffix\">\n                         <string>%</string>\n                        </property>\n                        <property name=\"maximum\">\n                         <number>100</number>\n                        </property>\n                       </widget>\n                      </item>\n                     </layout>\n                    </item>\n                    <item>\n                     <layout class=\"QHBoxLayout\" name=\"_7\">\n                      <property name=\"spacing\">\n                       <number>6</number>\n                      </property>\n                      <property name=\"margin\">\n                       <number>0</number>\n                      </property>\n                      <item>\n                       <widget class=\"QLabel\" name=\"label_87\">\n                        <property name=\"sizePolicy\">\n                         <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n                          <horstretch>0</horstretch>\n                          <verstretch>0</verstretch>\n                         </sizepolicy>\n                        </property>\n                        <property name=\"text\">\n                         <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;\n&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;\np, li { white-space: pre-wrap; }\n&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Fill part name with title text if it would otherwise have no text other than arrangement or partial annotations&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                        </property>\n                        <property name=\"buddy\">\n                         <cstring>cwp_retries</cstring>\n                        </property>\n                       </widget>\n                      </item>\n                      <item>\n                       <widget class=\"QCheckBox\" name=\"cwp_fill_part\">\n                        <property name=\"layoutDirection\">\n                         <enum>Qt::RightToLeft</enum>\n                        </property>\n                        <property name=\"text\">\n                         <string/>\n                        </property>\n                       </widget>\n                      </item>\n                     </layout>\n                    </item>\n                    <item>\n                     <widget class=\"Line\" name=\"line_8\">\n                      <property name=\"orientation\">\n                       <enum>Qt::Horizontal</enum>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_92\">\n                      <property name=\"text\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Prepositions/conjunctions and prefixes&lt;/span&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;&lt;br/&gt;&lt;/span&gt;DO NOT USE ANY COMMAS OR QUOTE MARKS (apostophes in words are acceptable)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_91\">\n                      <property name=\"text\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Prepositions &amp;amp; conjunctions: these are words that will not be regarded as providing additional information (not treated as 'new' words) unless they precede a new word.&lt;br/&gt;Use lower case only, comma separated&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"cwp_prepositions\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"cwp_removewords_2\">\n                      <property name=\"text\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Prefixes to be ignored in comparison (case insensitive, comma separated)&lt;br/&gt;To prevent a prefix from being ignored when extending metadata with title info, precede it with a space. &lt;br/&gt;To ensure only whole words are removed, follow with a space.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"cwp_removewords\">\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Separate multiple names by commas. Do not use any quotation marks.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"Line\" name=\"line\">\n                      <property name=\"orientation\">\n                       <enum>Qt::Horizontal</enum>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"cwp_synonyms_2\">\n                      <property name=\"text\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Synonyms and replacements&lt;/span&gt; - must be written as tuples separated by forward slashes - e.g (a,b) / (c,d,e) - a tuple may have two or more synonyms.&lt;/p&gt;&lt;p&gt;N.B. The matching is case-insensitive. Roman numerals will be treated as synonyms of arabic numerals in any event, so no need to enter these.&lt;/p&gt;&lt;p&gt;The last member of the tuple should be the canonical form (i.e. the one to which others are converted for comparison or replacement) and &lt;span style=&quot; text-decoration: underline;&quot;&gt;must be a normal strin&lt;/span&gt;g (not a regex). &lt;span style=&quot; font-weight:600;&quot;&gt;See readme for full details&lt;/span&gt;.&lt;br/&gt;&lt;span style=&quot; font-weight:600; font-style:italic;&quot;&gt;Unless entering a regular expression, use backslash \\ to escape any regex metacharacters, namely \\ ^ $ . | ? * + ( ) [ ] { &lt;br/&gt;Also escape commas , and forward slashes /. Do not enclose strings in quote marks.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Enter SYNONYM tuples below - each item in a tuple will be treated as similar when comparing works/parts and titles. The text in tags will be unaltered.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QTextEdit\" name=\"cwp_synonyms\">\n                      <property name=\"maximumSize\">\n                       <size>\n                        <width>16777215</width>\n                        <height>120</height>\n                       </size>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_16\">\n                      <property name=\"text\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter REMOVALS/REPLACEMENTS below - these will result in the &amp;quot;extended&amp;quot; text in tags being changed&lt;br/&gt;Put the word(s), phrase(s), or regular exprerssion(s) in the first part(s) of the tuple. The replacement text (or nothing - to remove) goes in the last member of the tuple.&lt;/p&gt;&lt;p&gt;N.B. Replacement text will operate BEFORE synonyms are considered.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"cwp_replacements\">\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Entries must be 2-tuples, e.g. (Replace this, with this). Separate multiple tuples by forward slash. Do not use any quotation marks. Spaces are acceptable. The first item of a tuple may be a regular expression - enclose it with double exclamation marks - e.g.(!!regex here!!, replacement text here).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"advanced_genres_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_58\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"advanced_genres_label\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(208, 208, 156);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Genres etc.&lt;/span&gt; (only required if Muso-specific options are used for genres/periods)&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"advanced_genres_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(229, 229, 197);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QGridLayout\" name=\"gridLayout_8\">\n                 <item row=\"1\" column=\"0\">\n                  <widget class=\"QLabel\" name=\"label_85\">\n                   <property name=\"text\">\n                    <string>Path to Muso reference database:</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"1\" column=\"3\">\n                  <widget class=\"QLineEdit\" name=\"cwp_muso_refdb\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"1\" column=\"2\">\n                  <widget class=\"QLabel\" name=\"label_84\">\n                   <property name=\"text\">\n                    <string>Name of Muso reference database</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"1\" column=\"1\">\n                  <widget class=\"QLineEdit\" name=\"cwp_muso_path\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item row=\"0\" column=\"0\" colspan=\"2\">\n                  <widget class=\"QLabel\" name=\"label_86\">\n                   <property name=\"text\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;RESTART PICARD AFTER CHANGING THESE&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"logging_frame\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_59\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"label_117\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(208, 208, 156);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Logging options&lt;/span&gt;*&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QGroupBox\" name=\"logging_box\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"whatsThis\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;These options are in addition to the options chosen in Picard's &amp;quot;Help-&amp;gt;View error/debug log&amp;quot; settings. They only affect messages written by this plugin. To enable debug messages to be shown, the flag needs to be set here and &amp;quot;Debug mode&amp;quot; needs to be turned on in the log. It is strongly advised to keep the &amp;quot;debug&amp;quot; and &amp;quot;info&amp;quot; flags unchecked unless debugging is required as they slow up processing significantly and may even cause Picard to crash on large releases. The &amp;quot;error&amp;quot; and &amp;quot;warning&amp;quot; flags should be left checked, unless it is required to suppress messages written out to tags (the default is to write messages to the tags 001_errors and 002_warnings).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n                <property name=\"layoutDirection\">\n                 <enum>Qt::LeftToRight</enum>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(229, 229, 197);</string>\n                </property>\n                <property name=\"title\">\n                 <string/>\n                </property>\n                <layout class=\"QHBoxLayout\" name=\"horizontalLayout_2\">\n                 <item>\n                  <widget class=\"QCheckBox\" name=\"log_error\">\n                   <property name=\"text\">\n                    <string>Error</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QCheckBox\" name=\"log_warning\">\n                   <property name=\"text\">\n                    <string>Warning</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QCheckBox\" name=\"log_debug\">\n                   <property name=\"text\">\n                    <string>Debug</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"custom_logging_box\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(229, 229, 159);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Custom logging</string>\n                   </property>\n                   <layout class=\"QHBoxLayout\" name=\"horizontalLayout_34\">\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"log_basic\">\n                      <property name=\"text\">\n                       <string>Basic</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QRadioButton\" name=\"log_info\">\n                      <property name=\"text\">\n                       <string>Full</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QFrame\" name=\"special_tags_frame_outer\">\n             <property name=\"frameShape\">\n              <enum>QFrame::StyledPanel</enum>\n             </property>\n             <property name=\"frameShadow\">\n              <enum>QFrame::Raised</enum>\n             </property>\n             <layout class=\"QVBoxLayout\" name=\"verticalLayout_60\">\n              <property name=\"spacing\">\n               <number>0</number>\n              </property>\n              <property name=\"margin\">\n               <number>0</number>\n              </property>\n              <item>\n               <widget class=\"QLabel\" name=\"label_118\">\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(208, 208, 156);</string>\n                </property>\n                <property name=\"text\">\n                 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Classical Extras Special Tags&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                </property>\n               </widget>\n              </item>\n              <item>\n               <widget class=\"QFrame\" name=\"special_tags_frame_inner\">\n                <property name=\"palette\">\n                 <palette>\n                  <active>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </active>\n                  <inactive>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </inactive>\n                  <disabled>\n                   <colorrole role=\"Button\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Base\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                   <colorrole role=\"Window\">\n                    <brush brushstyle=\"SolidPattern\">\n                     <color alpha=\"255\">\n                      <red>229</red>\n                      <green>229</green>\n                      <blue>197</blue>\n                     </color>\n                    </brush>\n                   </colorrole>\n                  </disabled>\n                 </palette>\n                </property>\n                <property name=\"autoFillBackground\">\n                 <bool>false</bool>\n                </property>\n                <property name=\"styleSheet\">\n                 <string notr=\"true\">background-color: rgb(229, 229, 197);</string>\n                </property>\n                <property name=\"frameShape\">\n                 <enum>QFrame::StyledPanel</enum>\n                </property>\n                <property name=\"frameShadow\">\n                 <enum>QFrame::Raised</enum>\n                </property>\n                <layout class=\"QVBoxLayout\" name=\"verticalLayout_16\">\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"save_options_box\">\n                   <property name=\"palette\">\n                    <palette>\n                     <active>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </active>\n                     <inactive>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </inactive>\n                     <disabled>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </disabled>\n                    </palette>\n                   </property>\n                   <property name=\"whatsThis\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This can be used so that the user has a record of the version of Classical Extras which generated the tags and which options were selected to achieve the resulting tags. Note that the tags will be blanked first so this will only show the last options used on a particular file. The same tag can be used for both sets of options, resulting in a multi-valued tag. &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"autoFillBackground\">\n                    <bool>false</bool>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(229, 229, 159);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Save plugin details and options in a tag?*</string>\n                   </property>\n                   <layout class=\"QHBoxLayout\" name=\"horizontalLayout_4\">\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_41\">\n                      <property name=\"text\">\n                       <string>Tag name for plugin version</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"ce_version_tag\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_36\">\n                      <property name=\"layoutDirection\">\n                       <enum>Qt::RightToLeft</enum>\n                      </property>\n                      <property name=\"text\">\n                       <string>Tag name for artist/mapping/misc. options</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"cea_options_tag\">\n                      <property name=\"layoutDirection\">\n                       <enum>Qt::RightToLeft</enum>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_38\">\n                      <property name=\"layoutDirection\">\n                       <enum>Qt::RightToLeft</enum>\n                      </property>\n                      <property name=\"text\">\n                       <string>Tag name for work/genre options</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLineEdit\" name=\"cwp_options_tag\">\n                      <property name=\"layoutDirection\">\n                       <enum>Qt::RightToLeft</enum>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"override_box\">\n                   <property name=\"palette\">\n                    <palette>\n                     <active>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </active>\n                     <inactive>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </inactive>\n                     <disabled>\n                      <colorrole role=\"Button\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Base\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                      <colorrole role=\"Window\">\n                       <brush brushstyle=\"SolidPattern\">\n                        <color alpha=\"255\">\n                         <red>229</red>\n                         <green>229</green>\n                         <blue>159</blue>\n                        </color>\n                       </brush>\n                      </colorrole>\n                     </disabled>\n                    </palette>\n                   </property>\n                   <property name=\"whatsThis\">\n                    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If options have previously been saved (see above), selecting these will cause the saved options to be used in preference to the displayed options. The displayed options will not be affected and will be used if no saved options are present. The default is for no over-ride. If artist options over-ride is chosen, then tag map detail options may be included or not in the override.&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;The last checkbox, &amp;quot;Overwrite options in Options Pages&amp;quot;, is for &lt;span style=&quot; font-weight:600;&quot;&gt;VERY CAREFUL USE ONLY&lt;/span&gt;. It will cause any options read from the saved tags (if the relevant box has been ticked) to over-write the options on the plugin Options Page UI. The intended use of this is if for some reason the user's preferred options have been erased/reverted to default - by using this option, the previously-used choices from a reliable filed album can be used to populate the Options Page. The box will automatically be unticked after loading/refreshing one album, to prevent inadvertant use. Far better is to make a &lt;span style=&quot; font-weight:600;&quot;&gt;backup copy&lt;/span&gt; of the picard.ini file.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                   </property>\n                   <property name=\"autoFillBackground\">\n                    <bool>false</bool>\n                   </property>\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(229, 229, 159);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Over-ride plugin options displayed in Options Pages with options from local file tags (previously saved using method in box above)?*</string>\n                   </property>\n                   <layout class=\"QHBoxLayout\" name=\"horizontalLayout_10\">\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"cea_override\">\n                      <property name=\"text\">\n                       <string>Artist options</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"cwp_override\">\n                      <property name=\"text\">\n                       <string>Work options</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"ce_genres_override\">\n                      <property name=\"enabled\">\n                       <bool>true</bool>\n                      </property>\n                      <property name=\"text\">\n                       <string>Genres etc. options</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"ce_tagmap_override\">\n                      <property name=\"enabled\">\n                       <bool>true</bool>\n                      </property>\n                      <property name=\"toolTip\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Will not over-ride displayed options unless artist options over-ride is also selected&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Tag mapping options</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"Line\" name=\"line_11\">\n                      <property name=\"orientation\">\n                       <enum>Qt::Vertical</enum>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QCheckBox\" name=\"ce_options_overwrite\">\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(255, 0, 0);</string>\n                      </property>\n                      <property name=\"text\">\n                       <string>Overwrite options in Options Pages (READ WARNINGS in Readme)</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QLabel\" name=\"label_121\">\n                   <property name=\"text\">\n                    <string>Note that the above saved options include the related &quot;advanced&quot; options on this tab as well as the options on each of the main tabs.</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"Line\" name=\"line_2\">\n                   <property name=\"orientation\">\n                    <enum>Qt::Horizontal</enum>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QCheckBox\" name=\"ce_show_ui_tags\">\n                   <property name=\"text\">\n                    <string>Show additional tags in Picard UI (rhs panel) - N.B. RESTART NEEDED FOR CHANGE TO TAKE EFFECT</string>\n                   </property>\n                  </widget>\n                 </item>\n                 <item>\n                  <widget class=\"QGroupBox\" name=\"groupBox\">\n                   <property name=\"styleSheet\">\n                    <string notr=\"true\">background-color: rgb(229, 229, 159);</string>\n                   </property>\n                   <property name=\"title\">\n                    <string>Additional columns for Picard UI to show specific tags*</string>\n                   </property>\n                   <layout class=\"QVBoxLayout\" name=\"verticalLayout_42\">\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_94\">\n                      <property name=\"text\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;RESTART PICARD AFTER CHANGING THESE&lt;/span&gt; - otherwise changes will not take effect&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QTextEdit\" name=\"ce_ui_tags\">\n                      <property name=\"sizePolicy\">\n                       <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Expanding\">\n                        <horstretch>0</horstretch>\n                        <verstretch>0</verstretch>\n                       </sizepolicy>\n                      </property>\n                      <property name=\"maximumSize\">\n                       <size>\n                        <width>16777215</width>\n                        <height>120</height>\n                       </size>\n                      </property>\n                      <property name=\"styleSheet\">\n                       <string notr=\"true\">background-color: rgb(250, 250, 250);</string>\n                      </property>\n                     </widget>\n                    </item>\n                    <item>\n                     <widget class=\"QLabel\" name=\"label_8\">\n                      <property name=\"sizePolicy\">\n                       <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n                        <horstretch>0</horstretch>\n                        <verstretch>0</verstretch>\n                       </sizepolicy>\n                      </property>\n                      <property name=\"text\">\n                       <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Notes:&lt;/p&gt;&lt;p&gt;1. Use the format &lt;span style=&quot; font-style:italic;&quot;&gt;column_name_A: (include_tag_1, include_tag_2) / column_name_B: include_tag_3 &lt;/span&gt; etc. (i.e. put multiple tags to be concatenated in brackets)&lt;br/&gt;2. To just flag tags that have changed, rather than show the contents, add _DIFF at the end of the tag name&lt;br/&gt;3. If more than one tag name is included for a column, then:&lt;br/&gt;(a) if the tags are _DIFF tags, then the column will be flagged if any of them have changed&lt;br/&gt;(b) otherwise the tag contents will be concatenated&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n                      </property>\n                     </widget>\n                    </item>\n                   </layout>\n                  </widget>\n                 </item>\n                </layout>\n               </widget>\n              </item>\n             </layout>\n            </widget>\n           </item>\n           <item>\n            <widget class=\"QLabel\" name=\"label_83\">\n             <property name=\"text\">\n              <string>* ASTERISKED OPTIONS ARE NOT SAVED IN FILE TAGS</string>\n             </property>\n            </widget>\n           </item>\n          </layout>\n         </widget>\n        </widget>\n       </item>\n      </layout>\n     </widget>\n     <widget class=\"QWidget\" name=\"Help\">\n      <attribute name=\"title\">\n       <string>Help</string>\n      </attribute>\n      <widget class=\"QScrollArea\" name=\"scrollArea_5\">\n       <property name=\"geometry\">\n        <rect>\n         <x>0</x>\n         <y>-10</y>\n         <width>671</width>\n         <height>561</height>\n        </rect>\n       </property>\n       <property name=\"sizePolicy\">\n        <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n         <horstretch>0</horstretch>\n         <verstretch>0</verstretch>\n        </sizepolicy>\n       </property>\n       <property name=\"widgetResizable\">\n        <bool>true</bool>\n       </property>\n       <widget class=\"QWidget\" name=\"scrollAreaWidgetContents_4\">\n        <property name=\"geometry\">\n         <rect>\n          <x>0</x>\n          <y>0</y>\n          <width>669</width>\n          <height>559</height>\n         </rect>\n        </property>\n        <widget class=\"QTextBrowser\" name=\"textBrowser_2\">\n         <property name=\"geometry\">\n          <rect>\n           <x>9</x>\n           <y>109</y>\n           <width>641</width>\n           <height>421</height>\n          </rect>\n         </property>\n         <property name=\"sizePolicy\">\n          <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Expanding\">\n           <horstretch>0</horstretch>\n           <verstretch>0</verstretch>\n          </sizepolicy>\n         </property>\n         <property name=\"horizontalScrollBarPolicy\">\n          <enum>Qt::ScrollBarAsNeeded</enum>\n         </property>\n         <property name=\"html\">\n          <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;\n&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;\np, li { white-space: pre-wrap; }\n&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:10pt; font-weight:600; text-decoration: underline;&quot;&gt;General description&lt;/span&gt;&lt;/p&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Classical Extras provides tagging enhancements for Picard and, in particular, utilises MusicBrainz’s hierarchy of works to provide work/movement tags. All options are set through a user interface in Picard options-&amp;gt;plugins. This interface provides separate sections to enhance artist/performer tags, works and parts, genres and also allows for a generalised &amp;quot;tag mapping&amp;quot; (simple scripting). While it is designed to cater for the complexities of classical music tagging, it may also be useful for other music which has more than just basic song/artist/album data. &lt;br /&gt;&lt;br /&gt;The options screen provides five tabs for users to control the tags produced: &lt;br /&gt;&lt;br /&gt;1. Artists: Options as to whether artist tags will contain standard MB names, aliases or as-credited names. Ability to include and annotate names for specialist roles (chorus master, arranger, lyricist etc.). Ability to read lyrics tags on the file which has been loaded and assign them to track and album levels if required. (Note: Picard will not normally process incoming file tags). &lt;br /&gt;&lt;br /&gt;2. Works and parts: The plugin will build a hierarchy of works and parts (e.g. Work -&amp;gt; Part -&amp;gt; Movement or Opera -&amp;gt; Act -&amp;gt; Number) based on the works in MusicBrainz's database. These can then be displayed in tags in a variety of ways according to user preferences. Furthermore partial recordings, medleys, arrangements and collections of works are all handled according to user choices. There is a processing overhead for this at present because MusicBrainz limits look-ups to one per second. &lt;br /&gt;&lt;br /&gt;3. Genres etc.: Options are available to customise the source and display of information relating to genres, instruments, keys, work dates and periods. Additional capabilities are provided for users of Muso (or others who provide the relevant XML files) to use pre-existing databases of classical genres, classical composers and classical periods. &lt;br /&gt;&lt;br /&gt;4. Tag mapping: in some ways, this is a simple substitute for some of Picard's scripting capability. The main advantage is that the plugin will remember what tag mapping you use for each release (or even track). &lt;br /&gt;&lt;br /&gt;5. Advanced: Various options to control the detailed processing of the above. &lt;br /&gt;&lt;br /&gt;All user options can be saved on a per-album (or even per-track) basis so that tweaks can be used to deal with inconsistencies in the MusicBrainz data (e.g. include English titles from the track listing where the MusicBrainz works are in the composer's language and/or script). Also existing file tags can be processed (not possible in native Picard). &lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n         </property>\n         <property name=\"openExternalLinks\">\n          <bool>true</bool>\n         </property>\n        </widget>\n        <widget class=\"QLabel\" name=\"label_58\">\n         <property name=\"geometry\">\n          <rect>\n           <x>9</x>\n           <y>9</y>\n           <width>471</width>\n           <height>16</height>\n          </rect>\n         </property>\n         <property name=\"sizePolicy\">\n          <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Maximum\">\n           <horstretch>0</horstretch>\n           <verstretch>0</verstretch>\n          </sizepolicy>\n         </property>\n         <property name=\"text\">\n          <string/>\n         </property>\n        </widget>\n        <widget class=\"QTextBrowser\" name=\"textBrowser_3\">\n         <property name=\"geometry\">\n          <rect>\n           <x>9</x>\n           <y>28</y>\n           <width>641</width>\n           <height>81</height>\n          </rect>\n         </property>\n         <property name=\"sizePolicy\">\n          <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Expanding\">\n           <horstretch>0</horstretch>\n           <verstretch>0</verstretch>\n          </sizepolicy>\n         </property>\n         <property name=\"html\">\n          <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;\n&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;\np, li { white-space: pre-wrap; }\n&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:12pt; font-weight:600;&quot;&gt;Please see&lt;/span&gt;&lt;span style=&quot; font-size:14pt; font-weight:600;&quot;&gt; &lt;/span&gt;&lt;a href=&quot;http://music.highmossergate.co.uk/symphony/tagging/classical-extras/&quot;&gt;&lt;span style=&quot; font-size:14pt; text-decoration: underline; color:#0000ff;&quot;&gt;my website&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot; font-size:14pt; font-weight:600;&quot;&gt; &lt;/span&gt;&lt;span style=&quot; font-size:12pt; font-weight:600;&quot;&gt;for  full details of this plugin and how to use it.&lt;/span&gt;&lt;/p&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:10pt;&quot;&gt;This help page now has only general information.&lt;/span&gt;&lt;/p&gt;\n&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:10pt;&quot;&gt;There are extensive tooltips and &amp;quot;What's This&amp;quot; popups (right click for them)&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n         </property>\n         <property name=\"openExternalLinks\">\n          <bool>true</bool>\n         </property>\n        </widget>\n       </widget>\n      </widget>\n     </widget>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections>\n  <connection>\n   <sender>cea_ra_use</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>ra_replace_merge_options_box</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>515</x>\n     <y>552</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>1085</x>\n     <y>511</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cea_ra_replace_ta</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_ra_noblank_ta</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>1075</x>\n     <y>536</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>704</x>\n     <y>559</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_cea</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>lyrics_frame</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>158</x>\n     <y>59</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>562</x>\n     <y>933</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_cwp</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cwp_collections</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>94</x>\n     <y>60</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>508</x>\n     <y>76</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cea_split_lyrics</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>lyrics_and_notes_tags_frame</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>160</x>\n     <y>1016</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>610</x>\n     <y>947</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_cwp</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>use_cache</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>124</x>\n     <y>60</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>850</x>\n     <y>76</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_cea</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>other_artist_options_frame</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>158</x>\n     <y>59</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>562</x>\n     <y>682</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cea_arrangers</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>annotations_lh_box</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>180</x>\n     <y>680</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>468</x>\n     <y>663</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cea_arrangers</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>annotations_rh_box</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>180</x>\n     <y>680</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>830</x>\n     <y>663</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_1</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_1</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>116</x>\n     <y>439</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>450</x>\n     <y>440</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_2</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_2</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>116</x>\n     <y>467</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>191</x>\n     <y>468</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_3</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_3</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>116</x>\n     <y>495</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>191</x>\n     <y>496</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_4</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_4</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>116</x>\n     <y>523</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>191</x>\n     <y>524</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_5</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_5</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>116</x>\n     <y>551</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>191</x>\n     <y>552</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_6</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_6</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>116</x>\n     <y>579</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>191</x>\n     <y>580</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_7</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_7</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>116</x>\n     <y>607</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>191</x>\n     <y>608</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_8</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_8</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>116</x>\n     <y>635</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>191</x>\n     <y>636</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_9</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_9</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>116</x>\n     <y>663</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>191</x>\n     <y>664</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_10</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_10</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>116</x>\n     <y>691</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>298</x>\n     <y>710</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_11</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_11</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>119</x>\n     <y>742</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>298</x>\n     <y>740</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_12</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_12</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>119</x>\n     <y>772</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>298</x>\n     <y>770</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_13</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_13</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>119</x>\n     <y>802</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>298</x>\n     <y>800</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_14</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_14</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>119</x>\n     <y>832</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>298</x>\n     <y>830</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_15</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_15</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>119</x>\n     <y>862</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>298</x>\n     <y>860</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>toolButton_16</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cea_source_16</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>119</x>\n     <y>892</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>298</x>\n     <y>890</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_cea</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>advanced_artists_frame</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>158</x>\n     <y>59</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>573</x>\n     <y>-721</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_cwp</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>advanced_workparts_frame</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>139</x>\n     <y>76</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>573</x>\n     <y>-228</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_cea</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>naming_options_frame</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>158</x>\n     <y>59</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>128</x>\n     <y>149</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_cea</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>recording_artists_options_frame</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>158</x>\n     <y>59</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>572</x>\n     <y>499</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_cwp</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>work_style_frame</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>47</x>\n     <y>63</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>562</x>\n     <y>158</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_cwp</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>work_aliases_frame</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>139</x>\n     <y>76</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>562</x>\n     <y>305</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_cwp</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>works_parts_tags_frame</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>139</x>\n     <y>76</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>562</x>\n     <y>566</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_cwp</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>songkong_frame</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>139</x>\n     <y>76</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>573</x>\n     <y>1047</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_use_muso_refdb</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cwp_muso_genres</receiver>\n   <slot>setVisible(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>89</x>\n     <y>-357</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>136</x>\n     <y>-94</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_use_muso_refdb</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cwp_muso_genres</receiver>\n   <slot>setChecked(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>120</x>\n     <y>-357</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>169</x>\n     <y>-94</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_use_muso_refdb</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cwp_muso_classical</receiver>\n   <slot>setVisible(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>161</x>\n     <y>-357</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>173</x>\n     <y>126</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_use_muso_refdb</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cwp_muso_classical</receiver>\n   <slot>setChecked(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>180</x>\n     <y>-357</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>219</x>\n     <y>126</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_use_muso_refdb</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cwp_muso_dates</receiver>\n   <slot>setVisible(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>584</x>\n     <y>-357</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>626</x>\n     <y>726</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_use_muso_refdb</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cwp_muso_periods</receiver>\n   <slot>setVisible(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>584</x>\n     <y>-357</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>213</x>\n     <y>826</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_use_muso_refdb</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cwp_muso_dates</receiver>\n   <slot>setChecked(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>584</x>\n     <y>-357</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>626</x>\n     <y>726</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_use_muso_refdb</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cwp_muso_periods</receiver>\n   <slot>setChecked(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>584</x>\n     <y>-357</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>213</x>\n     <y>826</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_muso_genres</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cwp_genres_classical_main</receiver>\n   <slot>setHidden(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>226</x>\n     <y>-94</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>618</x>\n     <y>-80</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_muso_classical</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cwp_genres_arranger_as_composer</receiver>\n   <slot>setVisible(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>130</x>\n     <y>126</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>143</x>\n     <y>149</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_muso_dates</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cwp_periods_arranger_as_composer</receiver>\n   <slot>setVisible(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>130</x>\n     <y>726</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>140</x>\n     <y>749</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_muso_periods</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cwp_period_map</receiver>\n   <slot>setHidden(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>207</x>\n     <y>826</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>777</x>\n     <y>826</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_muso_periods</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>period_map_annotation_label</receiver>\n   <slot>setHidden(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>150</x>\n     <y>826</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>780</x>\n     <y>845</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_genres_filter</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>genre_filters_frame</receiver>\n   <slot>setVisible(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>53</x>\n     <y>-201</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>50</x>\n     <y>-120</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_cwp</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>partial_arrangements_medleys_frame</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>139</x>\n     <y>76</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>234</x>\n     <y>816</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_titles</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>source_of_canonical_box</receiver>\n   <slot>setDisabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>135</x>\n     <y>146</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>582</x>\n     <y>137</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>cwp_titles</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>partial_arrangements_medleys_frame</receiver>\n   <slot>setHidden(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>263</x>\n     <y>150</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>562</x>\n     <y>864</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_cache</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>cwp_use_sk</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>918</x>\n     <y>68</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>320</x>\n     <y>1071</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>ce_show_ui_tags</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>groupBox</receiver>\n   <slot>setVisible(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>55</x>\n     <y>529</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>42</x>\n     <y>563</y>\n    </hint>\n   </hints>\n  </connection>\n </connections>\n</ui>\n"
  },
  {
    "path": "plugins/classical_extras/suffixtree.py",
    "content": "# -*- coding: utf-8\n\n\"\"\"\nSearch longest common substrings using generalized suffix trees built with Ukkonen's algorithm\n\nAuthor: Ilya Stepanov <code at ilyastepanov.com>\n\n(c) 2013\n\nModified by <a href=\"https://github.com/MetaTunes\">Mark Evens</a> as part of Picard Classical Extras project\nNot for stand-alone use - use the original code\nAccepts list or string inputs, but returns list outputs\nChanged to allow a range of different special characters in case $ is in a string\n(c) 2018\n\"\"\"\n\nimport sys\n\nEND_OF_STRING = sys.maxsize\n\nclass SuffixTreeNode:\n    \"\"\"\n    Suffix tree node class. Actually, it also respresents a tree edge that points to this node.\n    \"\"\"\n    new_identifier = 0\n\n    def __init__(self, start=0, end=END_OF_STRING):\n        self.identifier = SuffixTreeNode.new_identifier\n        SuffixTreeNode.new_identifier += 1\n\n        # suffix link is required by Ukkonen's algorithm\n        self.suffix_link = None\n\n        # child edges/nodes, each dict key represents the first letter of an edge\n        self.edges = {}\n\n        # stores reference to parent\n        self.parent = None\n\n        # bit vector shows to which strings this node belongs\n        self.bit_vector = 0\n\n        # edge info: start index and end index\n        self.start = start\n        self.end = end\n\n    def add_child(self, key, start, end):\n        \"\"\"\n        Create a new child node\n\n        Agrs:\n            key: a char that will be used during active edge searching\n            start, end: node's edge start and end indices\n\n        Returns:\n            created child node\n\n        \"\"\"\n        child = SuffixTreeNode(start=start, end=end)\n        child.parent = self\n        self.edges[key] = child\n        return child\n\n    def add_exisiting_node_as_child(self, key, node):\n        \"\"\"\n        Add an existing node as a child\n\n        Args:\n            key: a char that will be used during active edge searching\n            node: a node that will be added as a child\n        \"\"\"\n        node.parent = self\n        self.edges[key] = node\n\n    def get_edge_length(self, current_index):\n        \"\"\"\n        Get length of an edge that points to this node\n\n        Args:\n            current_index: index of current processing symbol (usefull for leaf nodes that have \"infinity\" end index)\n        \"\"\"\n        return min(self.end, current_index + 1) - self.start\n\n    def __str__(self):\n        return 'id=' + str(self.identifier)\n\n\nclass SuffixTree:\n    \"\"\"\n    Generalized suffix tree\n    \"\"\"\n\n    def __init__(self):\n        # the root node\n        self.root = SuffixTreeNode()\n\n        # all strings are concatenaited together. Tree's nodes stores only indices\n        self.input_string = []\n\n        # number of strings stored by this tree\n        self.strings_count = 0\n\n        # list of tree leaves\n        self.leaves = []\n\n    def append_string(self, input_string, special_char):\n        \"\"\"\n        Add new string to the suffix tree\n        \"\"\"\n        start_index = len(self.input_string)\n        current_string_index = self.strings_count\n\n        # each sting should have a unique ending\n        input_string += special_char + str(current_string_index)\n\n        # gathering 'em all together\n        self.input_string += input_string\n        self.strings_count += 1\n\n        # these 3 variables represents current \"active point\"\n        active_node = self.root\n        active_edge = 0\n        active_length = 0\n\n        # shows how many\n        remainder = 0\n\n        # new leaves appended to tree\n        new_leaves = []\n\n        # main circle\n        for index in range(start_index, len(self.input_string)):\n            previous_node = None\n            remainder += 1\n            while remainder > 0:\n                if active_length == 0:\n                    active_edge = index\n\n                if self.input_string[active_edge] not in active_node.edges:\n                    # no edge starting with current char, so creating a new leaf node\n                    leaf_node = active_node.add_child(self.input_string[active_edge], index, END_OF_STRING)\n\n                    # a leaf node will always be leaf node belonging to only one string\n                    # (because each string has different termination)\n                    leaf_node.bit_vector = 1 << current_string_index\n                    new_leaves.append(leaf_node)\n\n                    # doing suffix link magic\n                    if previous_node is not None:\n                        previous_node.suffix_link = active_node\n                    previous_node = active_node\n                else:\n                    # ok, we've got an active edge\n                    next_node = active_node.edges[self.input_string[active_edge]]\n\n                    # walking down through edges (if active_length is bigger than edge length)\n                    next_edge_length = next_node.get_edge_length(index)\n                    if active_length >= next_node.get_edge_length(index):\n                        active_edge += next_edge_length\n                        active_length -= next_edge_length\n                        active_node = next_node\n                        continue\n\n                    # current edge already contains the suffix we need to insert.\n                    # Increase the active_length and go forward\n                    if self.input_string[next_node.start + active_length] == self.input_string[index]:\n                        active_length += 1\n                        if previous_node is not None:\n                            previous_node.suffix_link = active_node\n                        previous_node = active_node\n                        break\n\n                    # splitting edge\n                    split_node = active_node.add_child(\n                        self.input_string[active_edge],\n                        next_node.start,\n                        next_node.start + active_length\n                    )\n                    next_node.start += active_length\n                    split_node.add_exisiting_node_as_child(self.input_string[next_node.start], next_node)\n                    leaf_node = split_node.add_child(self.input_string[index], index, END_OF_STRING)\n                    leaf_node.bit_vector = 1 << current_string_index\n                    new_leaves.append(leaf_node)\n\n                    # suffix link magic again\n                    if previous_node is not None:\n                        previous_node.suffix_link = split_node\n                    previous_node = split_node\n\n                remainder -= 1\n\n                # follow suffix link (if exists) or go to root\n                if active_node == self.root and active_length > 0:\n                    active_length -= 1\n                    active_edge = index - remainder + 1\n                else:\n                    active_node = active_node.suffix_link if active_node.suffix_link is not None else self.root\n\n        # update leaves ends from \"infinity\" to actual string end\n        for leaf in new_leaves:\n            leaf.end = len(self.input_string)\n        self.leaves.extend(new_leaves)\n\n    def find_longest_common_substrings(self, special_char):\n        \"\"\"\n        Search longest common substrings in the tree by locating lowest common ancestors that belong to all strings\n        \"\"\"\n\n        # all bits are set\n        success_bit_vector = 2 ** self.strings_count - 1\n\n        lowest_common_ancestors = []\n\n        # going up to the root\n        for leaf in self.leaves:\n            node = leaf\n            while node.parent is not None:\n                if node.bit_vector != success_bit_vector:\n                    # updating parent's bit vector\n                    node.parent.bit_vector |= node.bit_vector\n                    node = node.parent\n                else:\n                    # hey, we've found a lowest common ancestor!\n                    lowest_common_ancestors.append(node)\n                    break\n\n        longest_common_substrings = []\n        longest_length = 0\n\n        # need to filter the result array and get the longest common strings\n        for common_ancestor in lowest_common_ancestors:\n            common_substring = []\n            node = common_ancestor\n            while node.parent is not None:\n                label = self.input_string[node.start:node.end]\n                common_substring = label + common_substring\n                node = node.parent\n            # remove unique endings (<special_char><number>), we don't need them anymore ...\n            if special_char in common_substring:\n                common_substring = common_substring[:common_substring.index(special_char)]\n            # ... also in input strings to avoid mutation problems\n            if len(common_substring) > longest_length:\n                longest_length = len(common_substring)\n                longest_common_substrings = [common_substring]\n            elif len(common_substring) == longest_length and common_substring not in longest_common_substrings:\n                longest_common_substrings.append(common_substring)\n\n        return longest_common_substrings\n\n\ndef multi_lcs(strings_list):\n    \"\"\"\n    Returns longest common string (or list) for a list of strings (or lists)\n    :param strings_list: a list of lists or a list of strings\n    :return: a list of longest common strings (or lists)\n    (more than one is possible if they are distinct and of the same length)\n    \"\"\"\n\n    if not isinstance(strings_list, list):\n        return {'response': [], 'error': 'Argument is not a list'}\n    arg_type = type(strings_list[0])\n    for item in strings_list:\n        if not isinstance(item, arg_type):\n            return {'response': [], 'error': 'List members are not of the same type'}\n    if arg_type is not list and arg_type is not str:\n        return {'response': [], 'error': 'List members are not lists or strings'}\n\n    suffix_tree = SuffixTree()\n    special_char = None\n    for char in ['|', '$', '#', '%', '@', '_']:\n        test_strings = [x for x in strings_list if char in x]\n        if not test_strings:\n            special_char = char\n            break\n    if special_char:\n        # print special_char\n        for s in strings_list:\n            if arg_type is list:\n                s_copy = s[:]   # to avoid mutating input lists\n            else:\n                s_copy = s\n            suffix_tree.append_string(s_copy, special_char)\n        lcs = suffix_tree.find_longest_common_substrings(special_char)\n    else:\n        return {'response': [], 'error': 'Too many special characters'}\n    if arg_type is list:\n        if lcs:\n            return {'response': lcs[0]}\n        else:\n            return {'response': [], 'error': 'Internal suffix tree problems'}\n    else:\n        if lcs:\n            return {'response': [''.join(x) for x in lcs[0]]}\n        else:\n            return {'response': [], 'error': 'Internal suffix tree problems'}\n\n"
  },
  {
    "path": "plugins/classical_extras/ui_options_classical_extras.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'M:\\Documents\\Mark's documents\\Music\\Picard 230\\Classical Extras development\\classical_extras\\options_classical_extras.ui'\n#\n# Created by: PyQt5 UI code generator 5.11.2\n#\n# WARNING! All changes made in this file will be lost!\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\nclass Ui_ClassicalExtrasOptionsPage(object):\n    def setupUi(self, ClassicalExtrasOptionsPage):\n        ClassicalExtrasOptionsPage.setObjectName(\"ClassicalExtrasOptionsPage\")\n        ClassicalExtrasOptionsPage.resize(1145, 918)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(ClassicalExtrasOptionsPage.sizePolicy().hasHeightForWidth())\n        ClassicalExtrasOptionsPage.setSizePolicy(sizePolicy)\n        ClassicalExtrasOptionsPage.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)\n        ClassicalExtrasOptionsPage.setAcceptDrops(False)\n        self.vboxlayout = QtWidgets.QVBoxLayout(ClassicalExtrasOptionsPage)\n        self.vboxlayout.setContentsMargins(9, 9, 9, 9)\n        self.vboxlayout.setSpacing(6)\n        self.vboxlayout.setObjectName(\"vboxlayout\")\n        self.tabWidget = QtWidgets.QTabWidget(ClassicalExtrasOptionsPage)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.tabWidget.sizePolicy().hasHeightForWidth())\n        self.tabWidget.setSizePolicy(sizePolicy)\n        self.tabWidget.setMaximumSize(QtCore.QSize(1200, 1200))\n        palette = QtGui.QPalette()\n        self.tabWidget.setPalette(palette)\n        self.tabWidget.setAutoFillBackground(False)\n        self.tabWidget.setStyleSheet(\"\")\n        self.tabWidget.setObjectName(\"tabWidget\")\n        self.Artists = QtWidgets.QWidget()\n        self.Artists.setObjectName(\"Artists\")\n        self.verticalLayout_10 = QtWidgets.QVBoxLayout(self.Artists)\n        self.verticalLayout_10.setObjectName(\"verticalLayout_10\")\n        self.scrollArea = QtWidgets.QScrollArea(self.Artists)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth())\n        self.scrollArea.setSizePolicy(sizePolicy)\n        self.scrollArea.setFocusPolicy(QtCore.Qt.WheelFocus)\n        self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame)\n        self.scrollArea.setFrameShadow(QtWidgets.QFrame.Plain)\n        self.scrollArea.setLineWidth(1)\n        self.scrollArea.setWidgetResizable(True)\n        self.scrollArea.setObjectName(\"scrollArea\")\n        self.scrollAreaWidgetContents = QtWidgets.QWidget()\n        self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 1086, 1071))\n        self.scrollAreaWidgetContents.setAcceptDrops(False)\n        self.scrollAreaWidgetContents.setObjectName(\"scrollAreaWidgetContents\")\n        self.verticalLayout_13 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents)\n        self.verticalLayout_13.setObjectName(\"verticalLayout_13\")\n        self.artists_run_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.artists_run_frame.setPalette(palette)\n        self.artists_run_frame.setAutoFillBackground(False)\n        self.artists_run_frame.setStyleSheet(\"background-color: rgb(255, 255, 222);\")\n        self.artists_run_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.artists_run_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.artists_run_frame.setObjectName(\"artists_run_frame\")\n        self.horizontalLayout_9 = QtWidgets.QHBoxLayout(self.artists_run_frame)\n        self.horizontalLayout_9.setObjectName(\"horizontalLayout_9\")\n        self.use_cea = QtWidgets.QCheckBox(self.artists_run_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.use_cea.setPalette(palette)\n        self.use_cea.setStyleSheet(\"\")\n        self.use_cea.setObjectName(\"use_cea\")\n        self.horizontalLayout_9.addWidget(self.use_cea)\n        self.infer_worktypes_old_label = QtWidgets.QLabel(self.artists_run_frame)\n        self.infer_worktypes_old_label.setObjectName(\"infer_worktypes_old_label\")\n        self.horizontalLayout_9.addWidget(self.infer_worktypes_old_label)\n        self.verticalLayout_13.addWidget(self.artists_run_frame)\n        self.line_4 = QtWidgets.QFrame(self.scrollAreaWidgetContents)\n        self.line_4.setFrameShape(QtWidgets.QFrame.HLine)\n        self.line_4.setFrameShadow(QtWidgets.QFrame.Sunken)\n        self.line_4.setObjectName(\"line_4\")\n        self.verticalLayout_13.addWidget(self.line_4)\n        self.naming_style_note_label = QtWidgets.QLabel(self.scrollAreaWidgetContents)\n        self.naming_style_note_label.setObjectName(\"naming_style_note_label\")\n        self.verticalLayout_13.addWidget(self.naming_style_note_label)\n        self.naming_options_frame_2 = QtWidgets.QFrame(self.scrollAreaWidgetContents)\n        self.naming_options_frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.naming_options_frame_2.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.naming_options_frame_2.setObjectName(\"naming_options_frame_2\")\n        self.verticalLayout_36 = QtWidgets.QVBoxLayout(self.naming_options_frame_2)\n        self.verticalLayout_36.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_36.setSpacing(0)\n        self.verticalLayout_36.setObjectName(\"verticalLayout_36\")\n        self.naming_options_label = QtWidgets.QLabel(self.naming_options_frame_2)\n        self.naming_options_label.setStyleSheet(\"background-color: rgb(176, 220, 192);\")\n        self.naming_options_label.setObjectName(\"naming_options_label\")\n        self.verticalLayout_36.addWidget(self.naming_options_label)\n        self.naming_options_frame = QtWidgets.QFrame(self.naming_options_frame_2)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.naming_options_frame.setPalette(palette)\n        self.naming_options_frame.setCursor(QtGui.QCursor(QtCore.Qt.UpArrowCursor))\n        self.naming_options_frame.setAutoFillBackground(False)\n        self.naming_options_frame.setStyleSheet(\"background-color: rgb(211, 248, 224);\")\n        self.naming_options_frame.setFrameShape(QtWidgets.QFrame.NoFrame)\n        self.naming_options_frame.setObjectName(\"naming_options_frame\")\n        self.formLayout_3 = QtWidgets.QFormLayout(self.naming_options_frame)\n        self.formLayout_3.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow)\n        self.formLayout_3.setContentsMargins(9, 0, 9, -1)\n        self.formLayout_3.setVerticalSpacing(6)\n        self.formLayout_3.setObjectName(\"formLayout_3\")\n        self.label_22 = QtWidgets.QLabel(self.naming_options_frame)\n        self.label_22.setObjectName(\"label_22\")\n        self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.SpanningRole, self.label_22)\n        self.credited_as_options_box = QtWidgets.QGroupBox(self.naming_options_frame)\n        self.credited_as_options_box.setObjectName(\"credited_as_options_box\")\n        self.horizontalLayout_18 = QtWidgets.QHBoxLayout(self.credited_as_options_box)\n        self.horizontalLayout_18.setObjectName(\"horizontalLayout_18\")\n        self.names_to_use_box = QtWidgets.QGroupBox(self.credited_as_options_box)\n        self.names_to_use_box.setAutoFillBackground(False)\n        self.names_to_use_box.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.names_to_use_box.setObjectName(\"names_to_use_box\")\n        self.verticalLayout_26 = QtWidgets.QVBoxLayout(self.names_to_use_box)\n        self.verticalLayout_26.setObjectName(\"verticalLayout_26\")\n        self.cea_recording_credited = QtWidgets.QCheckBox(self.names_to_use_box)\n        self.cea_recording_credited.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_recording_credited.setObjectName(\"cea_recording_credited\")\n        self.verticalLayout_26.addWidget(self.cea_recording_credited)\n        self.cea_group_credited = QtWidgets.QCheckBox(self.names_to_use_box)\n        self.cea_group_credited.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_group_credited.setAutoFillBackground(False)\n        self.cea_group_credited.setStyleSheet(\"\")\n        self.cea_group_credited.setObjectName(\"cea_group_credited\")\n        self.verticalLayout_26.addWidget(self.cea_group_credited)\n        self.cea_credited = QtWidgets.QCheckBox(self.names_to_use_box)\n        self.cea_credited.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_credited.setObjectName(\"cea_credited\")\n        self.verticalLayout_26.addWidget(self.cea_credited)\n        self.cea_release_relationship_credited = QtWidgets.QCheckBox(self.names_to_use_box)\n        self.cea_release_relationship_credited.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_release_relationship_credited.setObjectName(\"cea_release_relationship_credited\")\n        self.verticalLayout_26.addWidget(self.cea_release_relationship_credited)\n        self.cea_recording_relationship_credited = QtWidgets.QCheckBox(self.names_to_use_box)\n        self.cea_recording_relationship_credited.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_recording_relationship_credited.setObjectName(\"cea_recording_relationship_credited\")\n        self.verticalLayout_26.addWidget(self.cea_recording_relationship_credited)\n        self.cea_track_credited = QtWidgets.QCheckBox(self.names_to_use_box)\n        self.cea_track_credited.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_track_credited.setObjectName(\"cea_track_credited\")\n        self.verticalLayout_26.addWidget(self.cea_track_credited)\n        self.label_24 = QtWidgets.QLabel(self.names_to_use_box)\n        self.label_24.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.label_24.setObjectName(\"label_24\")\n        self.verticalLayout_26.addWidget(self.label_24)\n        self.label_88 = QtWidgets.QLabel(self.names_to_use_box)\n        self.label_88.setObjectName(\"label_88\")\n        self.verticalLayout_26.addWidget(self.label_88)\n        self.horizontalLayout_18.addWidget(self.names_to_use_box)\n        self.places_to_use_them_box = QtWidgets.QGroupBox(self.credited_as_options_box)\n        self.places_to_use_them_box.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.places_to_use_them_box.setObjectName(\"places_to_use_them_box\")\n        self.verticalLayout_27 = QtWidgets.QVBoxLayout(self.places_to_use_them_box)\n        self.verticalLayout_27.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)\n        self.verticalLayout_27.setContentsMargins(9, -1, -1, -1)\n        self.verticalLayout_27.setObjectName(\"verticalLayout_27\")\n        self.cea_performer_credited = QtWidgets.QCheckBox(self.places_to_use_them_box)\n        self.cea_performer_credited.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_performer_credited.setObjectName(\"cea_performer_credited\")\n        self.verticalLayout_27.addWidget(self.cea_performer_credited)\n        self.cea_composer_credited = QtWidgets.QCheckBox(self.places_to_use_them_box)\n        self.cea_composer_credited.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_composer_credited.setObjectName(\"cea_composer_credited\")\n        self.verticalLayout_27.addWidget(self.cea_composer_credited)\n        self.horizontalLayout_18.addWidget(self.places_to_use_them_box)\n        self.formLayout_3.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.credited_as_options_box)\n        self.naming_sub_options_box = QtWidgets.QGroupBox(self.naming_options_frame)\n        self.naming_sub_options_box.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.naming_sub_options_box.setObjectName(\"naming_sub_options_box\")\n        self.horizontalLayout_11 = QtWidgets.QHBoxLayout(self.naming_sub_options_box)\n        self.horizontalLayout_11.setObjectName(\"horizontalLayout_11\")\n        self.cea_alias_overrides = QtWidgets.QRadioButton(self.naming_sub_options_box)\n        self.cea_alias_overrides.setObjectName(\"cea_alias_overrides\")\n        self.horizontalLayout_11.addWidget(self.cea_alias_overrides)\n        self.cea_credited_overrides = QtWidgets.QRadioButton(self.naming_sub_options_box)\n        self.cea_credited_overrides.setObjectName(\"cea_credited_overrides\")\n        self.horizontalLayout_11.addWidget(self.cea_credited_overrides)\n        self.cea_cyrillic = QtWidgets.QCheckBox(self.naming_sub_options_box)\n        self.cea_cyrillic.setObjectName(\"cea_cyrillic\")\n        self.horizontalLayout_11.addWidget(self.cea_cyrillic)\n        self.formLayout_3.setWidget(4, QtWidgets.QFormLayout.SpanningRole, self.naming_sub_options_box)\n        self.MB_std_names_aliases_box_outer = QtWidgets.QGroupBox(self.naming_options_frame)\n        self.MB_std_names_aliases_box_outer.setStyleSheet(\"background-color: rgb(211, 248, 224);\")\n        self.MB_std_names_aliases_box_outer.setObjectName(\"MB_std_names_aliases_box_outer\")\n        self.verticalLayout_24 = QtWidgets.QVBoxLayout(self.MB_std_names_aliases_box_outer)\n        self.verticalLayout_24.setObjectName(\"verticalLayout_24\")\n        self.MB_std_names_aliases_box_inner = QtWidgets.QGroupBox(self.MB_std_names_aliases_box_outer)\n        self.MB_std_names_aliases_box_inner.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.MB_std_names_aliases_box_inner.setTitle(\"\")\n        self.MB_std_names_aliases_box_inner.setObjectName(\"MB_std_names_aliases_box_inner\")\n        self.verticalLayout_40 = QtWidgets.QVBoxLayout(self.MB_std_names_aliases_box_inner)\n        self.verticalLayout_40.setObjectName(\"verticalLayout_40\")\n        self.cea_no_aliases = QtWidgets.QRadioButton(self.MB_std_names_aliases_box_inner)\n        self.cea_no_aliases.setObjectName(\"cea_no_aliases\")\n        self.verticalLayout_40.addWidget(self.cea_no_aliases)\n        self.cea_aliases = QtWidgets.QRadioButton(self.MB_std_names_aliases_box_inner)\n        self.cea_aliases.setObjectName(\"cea_aliases\")\n        self.verticalLayout_40.addWidget(self.cea_aliases)\n        self.cea_aliases_composer = QtWidgets.QRadioButton(self.MB_std_names_aliases_box_inner)\n        self.cea_aliases_composer.setObjectName(\"cea_aliases_composer\")\n        self.verticalLayout_40.addWidget(self.cea_aliases_composer)\n        self.verticalLayout_24.addWidget(self.MB_std_names_aliases_box_inner)\n        self.formLayout_3.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.MB_std_names_aliases_box_outer)\n        self.verticalLayout_36.addWidget(self.naming_options_frame)\n        self.verticalLayout_13.addWidget(self.naming_options_frame_2)\n        self.recording_artists_options_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents)\n        self.recording_artists_options_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.recording_artists_options_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.recording_artists_options_frame.setObjectName(\"recording_artists_options_frame\")\n        self.verticalLayout_37 = QtWidgets.QVBoxLayout(self.recording_artists_options_frame)\n        self.verticalLayout_37.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_37.setSpacing(0)\n        self.verticalLayout_37.setObjectName(\"verticalLayout_37\")\n        self.recording_artist_options_label = QtWidgets.QLabel(self.recording_artists_options_frame)\n        self.recording_artist_options_label.setStyleSheet(\"background-color: rgb(176, 220, 192);\")\n        self.recording_artist_options_label.setObjectName(\"recording_artist_options_label\")\n        self.verticalLayout_37.addWidget(self.recording_artist_options_label)\n        self.recording_artists_options_box = QtWidgets.QGroupBox(self.recording_artists_options_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.recording_artists_options_box.setPalette(palette)\n        self.recording_artists_options_box.setAutoFillBackground(False)\n        self.recording_artists_options_box.setStyleSheet(\"background-color: rgb(211, 248, 224);\")\n        self.recording_artists_options_box.setTitle(\"\")\n        self.recording_artists_options_box.setObjectName(\"recording_artists_options_box\")\n        self.horizontalLayout_24 = QtWidgets.QHBoxLayout(self.recording_artists_options_box)\n        self.horizontalLayout_24.setObjectName(\"horizontalLayout_24\")\n        self.naming_convention_box = QtWidgets.QGroupBox(self.recording_artists_options_box)\n        self.naming_convention_box.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.naming_convention_box.setObjectName(\"naming_convention_box\")\n        self.verticalLayout_31 = QtWidgets.QVBoxLayout(self.naming_convention_box)\n        self.verticalLayout_31.setObjectName(\"verticalLayout_31\")\n        self.cea_ra_trackartist = QtWidgets.QRadioButton(self.naming_convention_box)\n        self.cea_ra_trackartist.setObjectName(\"cea_ra_trackartist\")\n        self.verticalLayout_31.addWidget(self.cea_ra_trackartist)\n        self.cea_ra_performer = QtWidgets.QRadioButton(self.naming_convention_box)\n        self.cea_ra_performer.setObjectName(\"cea_ra_performer\")\n        self.verticalLayout_31.addWidget(self.cea_ra_performer)\n        self.horizontalLayout_24.addWidget(self.naming_convention_box)\n        self.line_7 = QtWidgets.QFrame(self.recording_artists_options_box)\n        self.line_7.setFrameShape(QtWidgets.QFrame.VLine)\n        self.line_7.setFrameShadow(QtWidgets.QFrame.Sunken)\n        self.line_7.setObjectName(\"line_7\")\n        self.horizontalLayout_24.addWidget(self.line_7)\n        self.cea_ra_use = QtWidgets.QCheckBox(self.recording_artists_options_box)\n        self.cea_ra_use.setObjectName(\"cea_ra_use\")\n        self.horizontalLayout_24.addWidget(self.cea_ra_use)\n        self.ra_replace_merge_options_box = QtWidgets.QGroupBox(self.recording_artists_options_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.ra_replace_merge_options_box.sizePolicy().hasHeightForWidth())\n        self.ra_replace_merge_options_box.setSizePolicy(sizePolicy)\n        self.ra_replace_merge_options_box.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.ra_replace_merge_options_box.setObjectName(\"ra_replace_merge_options_box\")\n        self.verticalLayout_29 = QtWidgets.QVBoxLayout(self.ra_replace_merge_options_box)\n        self.verticalLayout_29.setObjectName(\"verticalLayout_29\")\n        self.cea_ra_replace_ta = QtWidgets.QRadioButton(self.ra_replace_merge_options_box)\n        self.cea_ra_replace_ta.setObjectName(\"cea_ra_replace_ta\")\n        self.verticalLayout_29.addWidget(self.cea_ra_replace_ta)\n        self.cea_ra_noblank_ta = QtWidgets.QCheckBox(self.ra_replace_merge_options_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.cea_ra_noblank_ta.sizePolicy().hasHeightForWidth())\n        self.cea_ra_noblank_ta.setSizePolicy(sizePolicy)\n        self.cea_ra_noblank_ta.setLayoutDirection(QtCore.Qt.LeftToRight)\n        self.cea_ra_noblank_ta.setObjectName(\"cea_ra_noblank_ta\")\n        self.verticalLayout_29.addWidget(self.cea_ra_noblank_ta)\n        self.cea_ra_merge_ta = QtWidgets.QRadioButton(self.ra_replace_merge_options_box)\n        self.cea_ra_merge_ta.setObjectName(\"cea_ra_merge_ta\")\n        self.verticalLayout_29.addWidget(self.cea_ra_merge_ta)\n        self.horizontalLayout_24.addWidget(self.ra_replace_merge_options_box)\n        self.verticalLayout_37.addWidget(self.recording_artists_options_box)\n        self.verticalLayout_13.addWidget(self.recording_artists_options_frame)\n        self.other_artist_options_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents)\n        self.other_artist_options_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.other_artist_options_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.other_artist_options_frame.setObjectName(\"other_artist_options_frame\")\n        self.verticalLayout_38 = QtWidgets.QVBoxLayout(self.other_artist_options_frame)\n        self.verticalLayout_38.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_38.setSpacing(0)\n        self.verticalLayout_38.setObjectName(\"verticalLayout_38\")\n        self.other_artist_options_label = QtWidgets.QLabel(self.other_artist_options_frame)\n        self.other_artist_options_label.setStyleSheet(\"background-color: rgb(176, 220, 192);\")\n        self.other_artist_options_label.setObjectName(\"other_artist_options_label\")\n        self.verticalLayout_38.addWidget(self.other_artist_options_label)\n        self.other_artist_options_box = QtWidgets.QGroupBox(self.other_artist_options_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(211, 248, 224))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.other_artist_options_box.setPalette(palette)\n        self.other_artist_options_box.setAutoFillBackground(False)\n        self.other_artist_options_box.setStyleSheet(\"background-color: rgb(211, 248, 224);\")\n        self.other_artist_options_box.setTitle(\"\")\n        self.other_artist_options_box.setObjectName(\"other_artist_options_box\")\n        self.gridLayout = QtWidgets.QGridLayout(self.other_artist_options_box)\n        self.gridLayout.setVerticalSpacing(6)\n        self.gridLayout.setObjectName(\"gridLayout\")\n        self.annotations_lh_box = QtWidgets.QGroupBox(self.other_artist_options_box)\n        self.annotations_lh_box.setStyleSheet(\"background-color: rgba(250, 250, 250, 250);\")\n        self.annotations_lh_box.setObjectName(\"annotations_lh_box\")\n        self.verticalLayout_15 = QtWidgets.QVBoxLayout(self.annotations_lh_box)\n        self.verticalLayout_15.setObjectName(\"verticalLayout_15\")\n        self.chorusmaster_frame = QtWidgets.QFrame(self.annotations_lh_box)\n        self.chorusmaster_frame.setStyleSheet(\"background-color: rgb(211, 248, 224);\")\n        self.chorusmaster_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.chorusmaster_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.chorusmaster_frame.setObjectName(\"chorusmaster_frame\")\n        self.horizontalLayout_8 = QtWidgets.QHBoxLayout(self.chorusmaster_frame)\n        self.horizontalLayout_8.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)\n        self.horizontalLayout_8.setContentsMargins(-1, 1, -1, 9)\n        self.horizontalLayout_8.setSpacing(6)\n        self.horizontalLayout_8.setObjectName(\"horizontalLayout_8\")\n        self.label_44 = QtWidgets.QLabel(self.chorusmaster_frame)\n        self.label_44.setObjectName(\"label_44\")\n        self.horizontalLayout_8.addWidget(self.label_44)\n        self.cea_chorusmaster = QtWidgets.QLineEdit(self.chorusmaster_frame)\n        self.cea_chorusmaster.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_chorusmaster.setObjectName(\"cea_chorusmaster\")\n        self.horizontalLayout_8.addWidget(self.cea_chorusmaster)\n        self.verticalLayout_15.addWidget(self.chorusmaster_frame)\n        self.concertmaster_frame = QtWidgets.QFrame(self.annotations_lh_box)\n        self.concertmaster_frame.setStyleSheet(\"background-color: rgb(211, 248, 224);\")\n        self.concertmaster_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.concertmaster_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.concertmaster_frame.setObjectName(\"concertmaster_frame\")\n        self.horizontalLayout_6 = QtWidgets.QHBoxLayout(self.concertmaster_frame)\n        self.horizontalLayout_6.setContentsMargins(-1, 1, -1, -1)\n        self.horizontalLayout_6.setObjectName(\"horizontalLayout_6\")\n        self.label_46 = QtWidgets.QLabel(self.concertmaster_frame)\n        self.label_46.setObjectName(\"label_46\")\n        self.horizontalLayout_6.addWidget(self.label_46)\n        self.cea_concertmaster = QtWidgets.QLineEdit(self.concertmaster_frame)\n        self.cea_concertmaster.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_concertmaster.setObjectName(\"cea_concertmaster\")\n        self.horizontalLayout_6.addWidget(self.cea_concertmaster)\n        self.verticalLayout_15.addWidget(self.concertmaster_frame)\n        self.lyricist_frame = QtWidgets.QFrame(self.annotations_lh_box)\n        self.lyricist_frame.setStyleSheet(\"background-color: rgb(211, 248, 224);\")\n        self.lyricist_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.lyricist_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.lyricist_frame.setObjectName(\"lyricist_frame\")\n        self.horizontalLayout_23 = QtWidgets.QHBoxLayout(self.lyricist_frame)\n        self.horizontalLayout_23.setObjectName(\"horizontalLayout_23\")\n        self.label_34 = QtWidgets.QLabel(self.lyricist_frame)\n        self.label_34.setObjectName(\"label_34\")\n        self.horizontalLayout_23.addWidget(self.label_34)\n        self.cea_lyricist = QtWidgets.QLineEdit(self.lyricist_frame)\n        self.cea_lyricist.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_lyricist.setObjectName(\"cea_lyricist\")\n        self.horizontalLayout_23.addWidget(self.cea_lyricist)\n        self.verticalLayout_15.addWidget(self.lyricist_frame)\n        self.librettist_frame = QtWidgets.QFrame(self.annotations_lh_box)\n        self.librettist_frame.setStyleSheet(\"background-color: rgb(211, 248, 224);\")\n        self.librettist_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.librettist_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.librettist_frame.setObjectName(\"librettist_frame\")\n        self.horizontalLayout_19 = QtWidgets.QHBoxLayout(self.librettist_frame)\n        self.horizontalLayout_19.setObjectName(\"horizontalLayout_19\")\n        self.label_26 = QtWidgets.QLabel(self.librettist_frame)\n        self.label_26.setObjectName(\"label_26\")\n        self.horizontalLayout_19.addWidget(self.label_26)\n        self.cea_librettist = QtWidgets.QLineEdit(self.librettist_frame)\n        self.cea_librettist.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_librettist.setObjectName(\"cea_librettist\")\n        self.horizontalLayout_19.addWidget(self.cea_librettist)\n        self.verticalLayout_15.addWidget(self.librettist_frame)\n        self.translator_frame = QtWidgets.QFrame(self.annotations_lh_box)\n        self.translator_frame.setStyleSheet(\"background-color: rgb(211, 248, 224);\")\n        self.translator_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.translator_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.translator_frame.setObjectName(\"translator_frame\")\n        self.horizontalLayout_21 = QtWidgets.QHBoxLayout(self.translator_frame)\n        self.horizontalLayout_21.setObjectName(\"horizontalLayout_21\")\n        self.label_30 = QtWidgets.QLabel(self.translator_frame)\n        self.label_30.setObjectName(\"label_30\")\n        self.horizontalLayout_21.addWidget(self.label_30)\n        self.cea_translator = QtWidgets.QLineEdit(self.translator_frame)\n        self.cea_translator.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_translator.setObjectName(\"cea_translator\")\n        self.horizontalLayout_21.addWidget(self.cea_translator)\n        self.verticalLayout_15.addWidget(self.translator_frame)\n        self.gridLayout.addWidget(self.annotations_lh_box, 0, 1, 1, 1)\n        self.other_artist_checkboxes_box = QtWidgets.QGroupBox(self.other_artist_options_box)\n        self.other_artist_checkboxes_box.setStyleSheet(\"background-color: rgba(250, 250, 250, 250);\")\n        self.other_artist_checkboxes_box.setTitle(\"\")\n        self.other_artist_checkboxes_box.setObjectName(\"other_artist_checkboxes_box\")\n        self.verticalLayout_14 = QtWidgets.QVBoxLayout(self.other_artist_checkboxes_box)\n        self.verticalLayout_14.setSpacing(6)\n        self.verticalLayout_14.setObjectName(\"verticalLayout_14\")\n        self.cea_arrangers = QtWidgets.QCheckBox(self.other_artist_checkboxes_box)\n        self.cea_arrangers.setObjectName(\"cea_arrangers\")\n        self.verticalLayout_14.addWidget(self.cea_arrangers)\n        self.cea_composer_album = QtWidgets.QCheckBox(self.other_artist_checkboxes_box)\n        self.cea_composer_album.setObjectName(\"cea_composer_album\")\n        self.verticalLayout_14.addWidget(self.cea_composer_album)\n        self.cea_no_lyricists = QtWidgets.QCheckBox(self.other_artist_checkboxes_box)\n        self.cea_no_lyricists.setObjectName(\"cea_no_lyricists\")\n        self.verticalLayout_14.addWidget(self.cea_no_lyricists)\n        self.cea_inst_credit = QtWidgets.QCheckBox(self.other_artist_checkboxes_box)\n        self.cea_inst_credit.setObjectName(\"cea_inst_credit\")\n        self.verticalLayout_14.addWidget(self.cea_inst_credit)\n        self.cea_no_solo = QtWidgets.QCheckBox(self.other_artist_checkboxes_box)\n        self.cea_no_solo.setObjectName(\"cea_no_solo\")\n        self.verticalLayout_14.addWidget(self.cea_no_solo)\n        self.gridLayout.addWidget(self.other_artist_checkboxes_box, 0, 0, 1, 1)\n        self.annotations_rh_box = QtWidgets.QGroupBox(self.other_artist_options_box)\n        self.annotations_rh_box.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.annotations_rh_box.setObjectName(\"annotations_rh_box\")\n        self.verticalLayout_21 = QtWidgets.QVBoxLayout(self.annotations_rh_box)\n        self.verticalLayout_21.setObjectName(\"verticalLayout_21\")\n        self.writer_frame = QtWidgets.QFrame(self.annotations_rh_box)\n        self.writer_frame.setStyleSheet(\"background-color: rgb(211, 248, 224);\")\n        self.writer_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.writer_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.writer_frame.setObjectName(\"writer_frame\")\n        self.horizontalLayout_30 = QtWidgets.QHBoxLayout(self.writer_frame)\n        self.horizontalLayout_30.setObjectName(\"horizontalLayout_30\")\n        self.label_56 = QtWidgets.QLabel(self.writer_frame)\n        self.label_56.setObjectName(\"label_56\")\n        self.horizontalLayout_30.addWidget(self.label_56)\n        self.cea_writer = QtWidgets.QLineEdit(self.writer_frame)\n        self.cea_writer.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_writer.setObjectName(\"cea_writer\")\n        self.horizontalLayout_30.addWidget(self.cea_writer)\n        self.verticalLayout_21.addWidget(self.writer_frame)\n        self.arranger_frame = QtWidgets.QFrame(self.annotations_rh_box)\n        self.arranger_frame.setStyleSheet(\"background-color: rgb(211, 248, 224);\")\n        self.arranger_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.arranger_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.arranger_frame.setObjectName(\"arranger_frame\")\n        self.horizontalLayout_29 = QtWidgets.QHBoxLayout(self.arranger_frame)\n        self.horizontalLayout_29.setObjectName(\"horizontalLayout_29\")\n        self.label_54 = QtWidgets.QLabel(self.arranger_frame)\n        self.label_54.setObjectName(\"label_54\")\n        self.horizontalLayout_29.addWidget(self.label_54)\n        self.cea_arranger = QtWidgets.QLineEdit(self.arranger_frame)\n        self.cea_arranger.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_arranger.setObjectName(\"cea_arranger\")\n        self.horizontalLayout_29.addWidget(self.cea_arranger)\n        self.verticalLayout_21.addWidget(self.arranger_frame)\n        self.orchestrator_frame = QtWidgets.QFrame(self.annotations_rh_box)\n        self.orchestrator_frame.setStyleSheet(\"background-color: rgb(211, 248, 224);\")\n        self.orchestrator_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.orchestrator_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.orchestrator_frame.setObjectName(\"orchestrator_frame\")\n        self.horizontalLayout_7 = QtWidgets.QHBoxLayout(self.orchestrator_frame)\n        self.horizontalLayout_7.setContentsMargins(-1, 1, -1, -1)\n        self.horizontalLayout_7.setObjectName(\"horizontalLayout_7\")\n        self.label_45 = QtWidgets.QLabel(self.orchestrator_frame)\n        self.label_45.setObjectName(\"label_45\")\n        self.horizontalLayout_7.addWidget(self.label_45)\n        self.cea_orchestrator = QtWidgets.QLineEdit(self.orchestrator_frame)\n        self.cea_orchestrator.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_orchestrator.setObjectName(\"cea_orchestrator\")\n        self.horizontalLayout_7.addWidget(self.cea_orchestrator)\n        self.verticalLayout_21.addWidget(self.orchestrator_frame)\n        self.reconstructed_frame = QtWidgets.QFrame(self.annotations_rh_box)\n        self.reconstructed_frame.setStyleSheet(\"background-color: rgb(211, 248, 224);\")\n        self.reconstructed_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.reconstructed_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.reconstructed_frame.setObjectName(\"reconstructed_frame\")\n        self.horizontalLayout_22 = QtWidgets.QHBoxLayout(self.reconstructed_frame)\n        self.horizontalLayout_22.setObjectName(\"horizontalLayout_22\")\n        self.label_32 = QtWidgets.QLabel(self.reconstructed_frame)\n        self.label_32.setObjectName(\"label_32\")\n        self.horizontalLayout_22.addWidget(self.label_32)\n        self.cea_reconstructed = QtWidgets.QLineEdit(self.reconstructed_frame)\n        self.cea_reconstructed.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_reconstructed.setObjectName(\"cea_reconstructed\")\n        self.horizontalLayout_22.addWidget(self.cea_reconstructed)\n        self.verticalLayout_21.addWidget(self.reconstructed_frame)\n        self.revised_frame = QtWidgets.QFrame(self.annotations_rh_box)\n        self.revised_frame.setStyleSheet(\"background-color: rgb(211, 248, 224);\")\n        self.revised_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.revised_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.revised_frame.setObjectName(\"revised_frame\")\n        self.horizontalLayout_20 = QtWidgets.QHBoxLayout(self.revised_frame)\n        self.horizontalLayout_20.setObjectName(\"horizontalLayout_20\")\n        self.label_28 = QtWidgets.QLabel(self.revised_frame)\n        self.label_28.setObjectName(\"label_28\")\n        self.horizontalLayout_20.addWidget(self.label_28)\n        self.cea_revised = QtWidgets.QLineEdit(self.revised_frame)\n        self.cea_revised.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_revised.setObjectName(\"cea_revised\")\n        self.horizontalLayout_20.addWidget(self.cea_revised)\n        self.verticalLayout_21.addWidget(self.revised_frame)\n        self.gridLayout.addWidget(self.annotations_rh_box, 0, 2, 1, 1)\n        self.verticalLayout_38.addWidget(self.other_artist_options_box)\n        self.verticalLayout_13.addWidget(self.other_artist_options_frame)\n        self.line_3 = QtWidgets.QFrame(self.scrollAreaWidgetContents)\n        self.line_3.setLineWidth(1)\n        self.line_3.setFrameShape(QtWidgets.QFrame.HLine)\n        self.line_3.setFrameShadow(QtWidgets.QFrame.Sunken)\n        self.line_3.setObjectName(\"line_3\")\n        self.verticalLayout_13.addWidget(self.line_3)\n        self.lyrics_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents)\n        self.lyrics_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.lyrics_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.lyrics_frame.setObjectName(\"lyrics_frame\")\n        self.verticalLayout_39 = QtWidgets.QVBoxLayout(self.lyrics_frame)\n        self.verticalLayout_39.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_39.setSpacing(0)\n        self.verticalLayout_39.setObjectName(\"verticalLayout_39\")\n        self.lyrics_label = QtWidgets.QLabel(self.lyrics_frame)\n        self.lyrics_label.setStyleSheet(\"background-color: rgb(204, 168, 161);\")\n        self.lyrics_label.setObjectName(\"lyrics_label\")\n        self.verticalLayout_39.addWidget(self.lyrics_label)\n        self.lyrics_box = QtWidgets.QGroupBox(self.lyrics_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(230, 215, 211))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(230, 215, 211))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(230, 215, 211))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(230, 215, 211))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(230, 215, 211))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(230, 215, 211))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(230, 215, 211))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(230, 215, 211))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(230, 215, 211))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.lyrics_box.setPalette(palette)\n        self.lyrics_box.setAutoFillBackground(False)\n        self.lyrics_box.setStyleSheet(\"background-color: rgb(230, 215, 211);\")\n        self.lyrics_box.setTitle(\"\")\n        self.lyrics_box.setObjectName(\"lyrics_box\")\n        self.horizontalLayout_25 = QtWidgets.QHBoxLayout(self.lyrics_box)\n        self.horizontalLayout_25.setObjectName(\"horizontalLayout_25\")\n        self.cea_split_lyrics = QtWidgets.QCheckBox(self.lyrics_box)\n        self.cea_split_lyrics.setObjectName(\"cea_split_lyrics\")\n        self.horizontalLayout_25.addWidget(self.cea_split_lyrics)\n        self.lyrics_and_notes_tags_frame = QtWidgets.QGroupBox(self.lyrics_box)\n        self.lyrics_and_notes_tags_frame.setTitle(\"\")\n        self.lyrics_and_notes_tags_frame.setObjectName(\"lyrics_and_notes_tags_frame\")\n        self.verticalLayout_30 = QtWidgets.QVBoxLayout(self.lyrics_and_notes_tags_frame)\n        self.verticalLayout_30.setObjectName(\"verticalLayout_30\")\n        self.lyrics_tags_frame = QtWidgets.QFrame(self.lyrics_and_notes_tags_frame)\n        self.lyrics_tags_frame.setStyleSheet(\"background-color: rgb(204, 168, 161);\")\n        self.lyrics_tags_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.lyrics_tags_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.lyrics_tags_frame.setObjectName(\"lyrics_tags_frame\")\n        self.horizontalLayout_26 = QtWidgets.QHBoxLayout(self.lyrics_tags_frame)\n        self.horizontalLayout_26.setObjectName(\"horizontalLayout_26\")\n        self.label_50 = QtWidgets.QLabel(self.lyrics_tags_frame)\n        self.label_50.setObjectName(\"label_50\")\n        self.horizontalLayout_26.addWidget(self.label_50)\n        self.cea_lyrics_tag = QtWidgets.QLineEdit(self.lyrics_tags_frame)\n        self.cea_lyrics_tag.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_lyrics_tag.setObjectName(\"cea_lyrics_tag\")\n        self.horizontalLayout_26.addWidget(self.cea_lyrics_tag)\n        self.verticalLayout_30.addWidget(self.lyrics_tags_frame)\n        self.album_notes_tags_frame = QtWidgets.QFrame(self.lyrics_and_notes_tags_frame)\n        self.album_notes_tags_frame.setStyleSheet(\"background-color: rgb(204, 168, 161);\")\n        self.album_notes_tags_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.album_notes_tags_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.album_notes_tags_frame.setObjectName(\"album_notes_tags_frame\")\n        self.horizontalLayout_27 = QtWidgets.QHBoxLayout(self.album_notes_tags_frame)\n        self.horizontalLayout_27.setObjectName(\"horizontalLayout_27\")\n        self.label_51 = QtWidgets.QLabel(self.album_notes_tags_frame)\n        self.label_51.setObjectName(\"label_51\")\n        self.horizontalLayout_27.addWidget(self.label_51)\n        self.cea_album_lyrics = QtWidgets.QLineEdit(self.album_notes_tags_frame)\n        self.cea_album_lyrics.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_album_lyrics.setObjectName(\"cea_album_lyrics\")\n        self.horizontalLayout_27.addWidget(self.cea_album_lyrics)\n        self.verticalLayout_30.addWidget(self.album_notes_tags_frame)\n        self.track_notes_tags_frame = QtWidgets.QFrame(self.lyrics_and_notes_tags_frame)\n        self.track_notes_tags_frame.setStyleSheet(\"background-color: rgb(204, 168, 161);\")\n        self.track_notes_tags_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.track_notes_tags_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.track_notes_tags_frame.setObjectName(\"track_notes_tags_frame\")\n        self.horizontalLayout_28 = QtWidgets.QHBoxLayout(self.track_notes_tags_frame)\n        self.horizontalLayout_28.setObjectName(\"horizontalLayout_28\")\n        self.label_52 = QtWidgets.QLabel(self.track_notes_tags_frame)\n        self.label_52.setObjectName(\"label_52\")\n        self.horizontalLayout_28.addWidget(self.label_52)\n        self.cea_track_lyrics = QtWidgets.QLineEdit(self.track_notes_tags_frame)\n        self.cea_track_lyrics.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_track_lyrics.setObjectName(\"cea_track_lyrics\")\n        self.horizontalLayout_28.addWidget(self.cea_track_lyrics)\n        self.verticalLayout_30.addWidget(self.track_notes_tags_frame)\n        self.horizontalLayout_25.addWidget(self.lyrics_and_notes_tags_frame)\n        self.verticalLayout_39.addWidget(self.lyrics_box)\n        self.verticalLayout_13.addWidget(self.lyrics_frame)\n        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.verticalLayout_13.addItem(spacerItem)\n        self.scrollArea.setWidget(self.scrollAreaWidgetContents)\n        self.verticalLayout_10.addWidget(self.scrollArea)\n        self.tabWidget.addTab(self.Artists, \"\")\n        self.Works = QtWidgets.QWidget()\n        self.Works.setObjectName(\"Works\")\n        self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.Works)\n        self.verticalLayout_4.setObjectName(\"verticalLayout_4\")\n        self.scrollArea_3 = QtWidgets.QScrollArea(self.Works)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.scrollArea_3.sizePolicy().hasHeightForWidth())\n        self.scrollArea_3.setSizePolicy(sizePolicy)\n        self.scrollArea_3.setWidgetResizable(True)\n        self.scrollArea_3.setObjectName(\"scrollArea_3\")\n        self.scrollAreaWidgetContents_3 = QtWidgets.QWidget()\n        self.scrollAreaWidgetContents_3.setGeometry(QtCore.QRect(0, 0, 1084, 1086))\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.scrollAreaWidgetContents_3.sizePolicy().hasHeightForWidth())\n        self.scrollAreaWidgetContents_3.setSizePolicy(sizePolicy)\n        self.scrollAreaWidgetContents_3.setObjectName(\"scrollAreaWidgetContents_3\")\n        self.verticalLayout_25 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents_3)\n        self.verticalLayout_25.setObjectName(\"verticalLayout_25\")\n        self.works_run_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_3)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Maximum)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.works_run_frame.sizePolicy().hasHeightForWidth())\n        self.works_run_frame.setSizePolicy(sizePolicy)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 255, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.works_run_frame.setPalette(palette)\n        self.works_run_frame.setAutoFillBackground(False)\n        self.works_run_frame.setStyleSheet(\"background-color: rgb(255, 255, 222);\")\n        self.works_run_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.works_run_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.works_run_frame.setObjectName(\"works_run_frame\")\n        self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.works_run_frame)\n        self.horizontalLayout_3.setObjectName(\"horizontalLayout_3\")\n        self.use_cwp = QtWidgets.QCheckBox(self.works_run_frame)\n        self.use_cwp.setObjectName(\"use_cwp\")\n        self.horizontalLayout_3.addWidget(self.use_cwp)\n        self.cwp_collections = QtWidgets.QCheckBox(self.works_run_frame)\n        self.cwp_collections.setObjectName(\"cwp_collections\")\n        self.horizontalLayout_3.addWidget(self.cwp_collections)\n        self.use_cache = QtWidgets.QCheckBox(self.works_run_frame)\n        self.use_cache.setObjectName(\"use_cache\")\n        self.horizontalLayout_3.addWidget(self.use_cache)\n        self.verticalLayout_25.addWidget(self.works_run_frame)\n        self.work_style_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_3)\n        self.work_style_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.work_style_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.work_style_frame.setObjectName(\"work_style_frame\")\n        self.verticalLayout_44 = QtWidgets.QVBoxLayout(self.work_style_frame)\n        self.verticalLayout_44.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_44.setSpacing(0)\n        self.verticalLayout_44.setObjectName(\"verticalLayout_44\")\n        self.work_style_label = QtWidgets.QLabel(self.work_style_frame)\n        self.work_style_label.setStyleSheet(\"background-color: rgb(205, 230, 255);\")\n        self.work_style_label.setObjectName(\"work_style_label\")\n        self.verticalLayout_44.addWidget(self.work_style_label)\n        self.work_style_box = QtWidgets.QGroupBox(self.work_style_frame)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Maximum)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.work_style_box.sizePolicy().hasHeightForWidth())\n        self.work_style_box.setSizePolicy(sizePolicy)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(225, 240, 255))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(225, 240, 255))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(225, 240, 255))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(225, 240, 255))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(225, 240, 255))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(225, 240, 255))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(225, 240, 255))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(225, 240, 255))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(225, 240, 255))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.work_style_box.setPalette(palette)\n        self.work_style_box.setAutoFillBackground(False)\n        self.work_style_box.setStyleSheet(\"background-color: rgb(225, 240, 255);\")\n        self.work_style_box.setTitle(\"\")\n        self.work_style_box.setObjectName(\"work_style_box\")\n        self.formLayout_2 = QtWidgets.QFormLayout(self.work_style_box)\n        self.formLayout_2.setObjectName(\"formLayout_2\")\n        self.works_source_box = QtWidgets.QGroupBox(self.work_style_box)\n        self.works_source_box.setObjectName(\"works_source_box\")\n        self.verticalLayout_11 = QtWidgets.QVBoxLayout(self.works_source_box)\n        self.verticalLayout_11.setObjectName(\"verticalLayout_11\")\n        self.cwp_titles = QtWidgets.QRadioButton(self.works_source_box)\n        self.cwp_titles.setObjectName(\"cwp_titles\")\n        self.verticalLayout_11.addWidget(self.cwp_titles)\n        self.cwp_works = QtWidgets.QRadioButton(self.works_source_box)\n        self.cwp_works.setObjectName(\"cwp_works\")\n        self.verticalLayout_11.addWidget(self.cwp_works)\n        self.cwp_extended = QtWidgets.QRadioButton(self.works_source_box)\n        self.cwp_extended.setObjectName(\"cwp_extended\")\n        self.verticalLayout_11.addWidget(self.cwp_extended)\n        self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.works_source_box)\n        self.source_of_canonical_box = QtWidgets.QGroupBox(self.work_style_box)\n        self.source_of_canonical_box.setEnabled(True)\n        self.source_of_canonical_box.setObjectName(\"source_of_canonical_box\")\n        self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.source_of_canonical_box)\n        self.verticalLayout_6.setObjectName(\"verticalLayout_6\")\n        self.cwp_hierarchical_works = QtWidgets.QRadioButton(self.source_of_canonical_box)\n        self.cwp_hierarchical_works.setObjectName(\"cwp_hierarchical_works\")\n        self.verticalLayout_6.addWidget(self.cwp_hierarchical_works)\n        self.cwp_level0_works = QtWidgets.QRadioButton(self.source_of_canonical_box)\n        self.cwp_level0_works.setObjectName(\"cwp_level0_works\")\n        self.verticalLayout_6.addWidget(self.cwp_level0_works)\n        self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.source_of_canonical_box)\n        self.cwp_derive_works_from_title = QtWidgets.QCheckBox(self.work_style_box)\n        self.cwp_derive_works_from_title.setObjectName(\"cwp_derive_works_from_title\")\n        self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.SpanningRole, self.cwp_derive_works_from_title)\n        self.verticalLayout_44.addWidget(self.work_style_box)\n        self.verticalLayout_25.addWidget(self.work_style_frame)\n        self.work_aliases_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_3)\n        self.work_aliases_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.work_aliases_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.work_aliases_frame.setObjectName(\"work_aliases_frame\")\n        self.verticalLayout_45 = QtWidgets.QVBoxLayout(self.work_aliases_frame)\n        self.verticalLayout_45.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_45.setSpacing(0)\n        self.verticalLayout_45.setObjectName(\"verticalLayout_45\")\n        self.work_aliases_label = QtWidgets.QLabel(self.work_aliases_frame)\n        self.work_aliases_label.setStyleSheet(\"background-color: rgb(255, 186, 189);\")\n        self.work_aliases_label.setObjectName(\"work_aliases_label\")\n        self.verticalLayout_45.addWidget(self.work_aliases_label)\n        self.work_aliases_box = QtWidgets.QGroupBox(self.work_aliases_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(255, 220, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 220, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 220, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 220, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 220, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 220, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 220, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 220, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 220, 222))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.work_aliases_box.setPalette(palette)\n        self.work_aliases_box.setAutoFillBackground(False)\n        self.work_aliases_box.setStyleSheet(\"background-color: rgb(255, 220, 222);\")\n        self.work_aliases_box.setTitle(\"\")\n        self.work_aliases_box.setObjectName(\"work_aliases_box\")\n        self.horizontalLayout_16 = QtWidgets.QHBoxLayout(self.work_aliases_box)\n        self.horizontalLayout_16.setObjectName(\"horizontalLayout_16\")\n        self.replace_MBworknames_box = QtWidgets.QGroupBox(self.work_aliases_box)\n        self.replace_MBworknames_box.setObjectName(\"replace_MBworknames_box\")\n        self.verticalLayout_22 = QtWidgets.QVBoxLayout(self.replace_MBworknames_box)\n        self.verticalLayout_22.setObjectName(\"verticalLayout_22\")\n        self.cwp_aliases = QtWidgets.QRadioButton(self.replace_MBworknames_box)\n        self.cwp_aliases.setObjectName(\"cwp_aliases\")\n        self.verticalLayout_22.addWidget(self.cwp_aliases)\n        self.cwp_no_aliases = QtWidgets.QRadioButton(self.replace_MBworknames_box)\n        self.cwp_no_aliases.setObjectName(\"cwp_no_aliases\")\n        self.verticalLayout_22.addWidget(self.cwp_no_aliases)\n        self.horizontalLayout_16.addWidget(self.replace_MBworknames_box)\n        self.what_to_replace_outer_box = QtWidgets.QGroupBox(self.work_aliases_box)\n        self.what_to_replace_outer_box.setStyleSheet(\"\")\n        self.what_to_replace_outer_box.setObjectName(\"what_to_replace_outer_box\")\n        self.horizontalLayout_17 = QtWidgets.QHBoxLayout(self.what_to_replace_outer_box)\n        self.horizontalLayout_17.setObjectName(\"horizontalLayout_17\")\n        self.what_to_replace_radios_box = QtWidgets.QGroupBox(self.what_to_replace_outer_box)\n        self.what_to_replace_radios_box.setTitle(\"\")\n        self.what_to_replace_radios_box.setObjectName(\"what_to_replace_radios_box\")\n        self.verticalLayout_23 = QtWidgets.QVBoxLayout(self.what_to_replace_radios_box)\n        self.verticalLayout_23.setObjectName(\"verticalLayout_23\")\n        self.cwp_aliases_all = QtWidgets.QRadioButton(self.what_to_replace_radios_box)\n        self.cwp_aliases_all.setObjectName(\"cwp_aliases_all\")\n        self.verticalLayout_23.addWidget(self.cwp_aliases_all)\n        self.cwp_aliases_greek = QtWidgets.QRadioButton(self.what_to_replace_radios_box)\n        self.cwp_aliases_greek.setObjectName(\"cwp_aliases_greek\")\n        self.verticalLayout_23.addWidget(self.cwp_aliases_greek)\n        self.cwp_aliases_tagged = QtWidgets.QRadioButton(self.what_to_replace_radios_box)\n        self.cwp_aliases_tagged.setObjectName(\"cwp_aliases_tagged\")\n        self.verticalLayout_23.addWidget(self.cwp_aliases_tagged)\n        self.horizontalLayout_17.addWidget(self.what_to_replace_radios_box)\n        self.works_alias_tags_box = QtWidgets.QGroupBox(self.what_to_replace_outer_box)\n        self.works_alias_tags_box.setObjectName(\"works_alias_tags_box\")\n        self.formLayout = QtWidgets.QFormLayout(self.works_alias_tags_box)\n        self.formLayout.setObjectName(\"formLayout\")\n        self.cwp_aliases_tags_all = QtWidgets.QRadioButton(self.works_alias_tags_box)\n        self.cwp_aliases_tags_all.setObjectName(\"cwp_aliases_tags_all\")\n        self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.cwp_aliases_tags_all)\n        self.cwp_aliases_tags_user = QtWidgets.QRadioButton(self.works_alias_tags_box)\n        self.cwp_aliases_tags_user.setObjectName(\"cwp_aliases_tags_user\")\n        self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.cwp_aliases_tags_user)\n        self.cwp_aliases_tag_text = QtWidgets.QLineEdit(self.works_alias_tags_box)\n        self.cwp_aliases_tag_text.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_aliases_tag_text.setObjectName(\"cwp_aliases_tag_text\")\n        self.formLayout.setWidget(0, QtWidgets.QFormLayout.SpanningRole, self.cwp_aliases_tag_text)\n        self.horizontalLayout_17.addWidget(self.works_alias_tags_box)\n        self.horizontalLayout_16.addWidget(self.what_to_replace_outer_box)\n        self.verticalLayout_45.addWidget(self.work_aliases_box)\n        self.verticalLayout_25.addWidget(self.work_aliases_frame)\n        self.works_parts_tags_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_3)\n        self.works_parts_tags_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.works_parts_tags_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.works_parts_tags_frame.setObjectName(\"works_parts_tags_frame\")\n        self.verticalLayout_46 = QtWidgets.QVBoxLayout(self.works_parts_tags_frame)\n        self.verticalLayout_46.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_46.setSpacing(0)\n        self.verticalLayout_46.setObjectName(\"verticalLayout_46\")\n        self.work_parts_tags_label = QtWidgets.QLabel(self.works_parts_tags_frame)\n        self.work_parts_tags_label.setStyleSheet(\"background-color: rgb(255, 194, 158);\")\n        self.work_parts_tags_label.setObjectName(\"work_parts_tags_label\")\n        self.verticalLayout_46.addWidget(self.work_parts_tags_label)\n        self.works_parts_tags_box = QtWidgets.QGroupBox(self.works_parts_tags_frame)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.MinimumExpanding)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.works_parts_tags_box.sizePolicy().hasHeightForWidth())\n        self.works_parts_tags_box.setSizePolicy(sizePolicy)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(255, 221, 201))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 221, 201))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 221, 201))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 221, 201))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 221, 201))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 221, 201))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 221, 201))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 221, 201))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 221, 201))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.works_parts_tags_box.setPalette(palette)\n        self.works_parts_tags_box.setAutoFillBackground(False)\n        self.works_parts_tags_box.setStyleSheet(\"background-color: rgb(255, 221, 201);\")\n        self.works_parts_tags_box.setTitle(\"\")\n        self.works_parts_tags_box.setObjectName(\"works_parts_tags_box\")\n        self.verticalLayout_9 = QtWidgets.QVBoxLayout(self.works_parts_tags_box)\n        self.verticalLayout_9.setObjectName(\"verticalLayout_9\")\n        self.works_tags_box = QtWidgets.QGroupBox(self.works_parts_tags_box)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.works_tags_box.setPalette(palette)\n        self.works_tags_box.setAutoFillBackground(False)\n        self.works_tags_box.setStyleSheet(\"background-color: rgb(255, 209, 182);\")\n        self.works_tags_box.setObjectName(\"works_tags_box\")\n        self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.works_tags_box)\n        self.verticalLayout_8.setObjectName(\"verticalLayout_8\")\n        self.label_40 = QtWidgets.QLabel(self.works_tags_box)\n        self.label_40.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)\n        self.label_40.setObjectName(\"label_40\")\n        self.verticalLayout_8.addWidget(self.label_40)\n        self._8 = QtWidgets.QHBoxLayout()\n        self._8.setContentsMargins(0, 0, 0, 0)\n        self._8.setSpacing(6)\n        self._8.setObjectName(\"_8\")\n        self.label_11 = QtWidgets.QLabel(self.works_tags_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_11.sizePolicy().hasHeightForWidth())\n        self.label_11.setSizePolicy(sizePolicy)\n        self.label_11.setObjectName(\"label_11\")\n        self._8.addWidget(self.label_11)\n        self.cwp_work_tag_multi = QtWidgets.QLineEdit(self.works_tags_box)\n        self.cwp_work_tag_multi.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_work_tag_multi.setObjectName(\"cwp_work_tag_multi\")\n        self._8.addWidget(self.cwp_work_tag_multi)\n        self.cwp_multi_work_sep = QtWidgets.QComboBox(self.works_tags_box)\n        self.cwp_multi_work_sep.setEditable(True)\n        self.cwp_multi_work_sep.setObjectName(\"cwp_multi_work_sep\")\n        self.cwp_multi_work_sep.addItem(\"\")\n        self.cwp_multi_work_sep.setItemText(0, \"\")\n        self.cwp_multi_work_sep.addItem(\"\")\n        self.cwp_multi_work_sep.addItem(\"\")\n        self.cwp_multi_work_sep.addItem(\"\")\n        self.cwp_multi_work_sep.addItem(\"\")\n        self.cwp_multi_work_sep.addItem(\"\")\n        self._8.addWidget(self.cwp_multi_work_sep)\n        self.verticalLayout_8.addLayout(self._8)\n        self.label = QtWidgets.QLabel(self.works_tags_box)\n        self.label.setObjectName(\"label\")\n        self.verticalLayout_8.addWidget(self.label)\n        self._12 = QtWidgets.QHBoxLayout()\n        self._12.setContentsMargins(0, 0, 0, 0)\n        self._12.setSpacing(6)\n        self._12.setObjectName(\"_12\")\n        self.label_15 = QtWidgets.QLabel(self.works_tags_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_15.sizePolicy().hasHeightForWidth())\n        self.label_15.setSizePolicy(sizePolicy)\n        self.label_15.setObjectName(\"label_15\")\n        self._12.addWidget(self.label_15)\n        self.cwp_work_tag_single = QtWidgets.QLineEdit(self.works_tags_box)\n        self.cwp_work_tag_single.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_work_tag_single.setObjectName(\"cwp_work_tag_single\")\n        self._12.addWidget(self.cwp_work_tag_single)\n        self.cwp_single_work_sep = QtWidgets.QComboBox(self.works_tags_box)\n        self.cwp_single_work_sep.setEditable(True)\n        self.cwp_single_work_sep.setObjectName(\"cwp_single_work_sep\")\n        self.cwp_single_work_sep.addItem(\"\")\n        self.cwp_single_work_sep.setItemText(0, \"\")\n        self.cwp_single_work_sep.addItem(\"\")\n        self.cwp_single_work_sep.addItem(\"\")\n        self.cwp_single_work_sep.addItem(\"\")\n        self.cwp_single_work_sep.addItem(\"\")\n        self.cwp_single_work_sep.addItem(\"\")\n        self._12.addWidget(self.cwp_single_work_sep)\n        self.verticalLayout_8.addLayout(self._12)\n        self.label_2 = QtWidgets.QLabel(self.works_tags_box)\n        self.label_2.setObjectName(\"label_2\")\n        self.verticalLayout_8.addWidget(self.label_2)\n        self._9 = QtWidgets.QHBoxLayout()\n        self._9.setContentsMargins(0, 0, 0, 0)\n        self._9.setSpacing(6)\n        self._9.setObjectName(\"_9\")\n        self.label_12 = QtWidgets.QLabel(self.works_tags_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_12.sizePolicy().hasHeightForWidth())\n        self.label_12.setSizePolicy(sizePolicy)\n        self.label_12.setObjectName(\"label_12\")\n        self._9.addWidget(self.label_12)\n        self.cwp_top_tag = QtWidgets.QLineEdit(self.works_tags_box)\n        self.cwp_top_tag.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_top_tag.setObjectName(\"cwp_top_tag\")\n        self._9.addWidget(self.cwp_top_tag)\n        self.label_10 = QtWidgets.QLabel(self.works_tags_box)\n        self.label_10.setObjectName(\"label_10\")\n        self._9.addWidget(self.label_10)\n        self.verticalLayout_8.addLayout(self._9)\n        self.verticalLayout_9.addWidget(self.works_tags_box)\n        self.parts_tags_box = QtWidgets.QGroupBox(self.works_parts_tags_box)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 209, 182))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.parts_tags_box.setPalette(palette)\n        self.parts_tags_box.setAutoFillBackground(False)\n        self.parts_tags_box.setStyleSheet(\"background-color: rgb(255, 209, 182);\")\n        self.parts_tags_box.setObjectName(\"parts_tags_box\")\n        self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.parts_tags_box)\n        self.verticalLayout_7.setObjectName(\"verticalLayout_7\")\n        self._10 = QtWidgets.QHBoxLayout()\n        self._10.setContentsMargins(0, 0, 0, 0)\n        self._10.setSpacing(6)\n        self._10.setObjectName(\"_10\")\n        self.label_13 = QtWidgets.QLabel(self.parts_tags_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_13.sizePolicy().hasHeightForWidth())\n        self.label_13.setSizePolicy(sizePolicy)\n        self.label_13.setObjectName(\"label_13\")\n        self._10.addWidget(self.label_13)\n        self.cwp_movt_no_tag = QtWidgets.QLineEdit(self.parts_tags_box)\n        self.cwp_movt_no_tag.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_movt_no_tag.setObjectName(\"cwp_movt_no_tag\")\n        self._10.addWidget(self.cwp_movt_no_tag)\n        self.cwp_movt_no_sep = QtWidgets.QComboBox(self.parts_tags_box)\n        self.cwp_movt_no_sep.setEditable(True)\n        self.cwp_movt_no_sep.setObjectName(\"cwp_movt_no_sep\")\n        self.cwp_movt_no_sep.addItem(\"\")\n        self.cwp_movt_no_sep.setItemText(0, \"\")\n        self.cwp_movt_no_sep.addItem(\"\")\n        self.cwp_movt_no_sep.addItem(\"\")\n        self.cwp_movt_no_sep.addItem(\"\")\n        self.cwp_movt_no_sep.addItem(\"\")\n        self.cwp_movt_no_sep.addItem(\"\")\n        self._10.addWidget(self.cwp_movt_no_sep)\n        self.verticalLayout_7.addLayout(self._10)\n        self._24 = QtWidgets.QHBoxLayout()\n        self._24.setContentsMargins(0, 0, 0, 0)\n        self._24.setSpacing(6)\n        self._24.setObjectName(\"_24\")\n        self.label_43 = QtWidgets.QLabel(self.parts_tags_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_43.sizePolicy().hasHeightForWidth())\n        self.label_43.setSizePolicy(sizePolicy)\n        self.label_43.setObjectName(\"label_43\")\n        self._24.addWidget(self.label_43)\n        self.cwp_movt_tot_tag = QtWidgets.QLineEdit(self.parts_tags_box)\n        self.cwp_movt_tot_tag.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_movt_tot_tag.setObjectName(\"cwp_movt_tot_tag\")\n        self._24.addWidget(self.cwp_movt_tot_tag)\n        self.label_79 = QtWidgets.QLabel(self.parts_tags_box)\n        self.label_79.setObjectName(\"label_79\")\n        self._24.addWidget(self.label_79)\n        self.verticalLayout_7.addLayout(self._24)\n        self.frame = QtWidgets.QFrame(self.parts_tags_box)\n        self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.frame.setObjectName(\"frame\")\n        self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.frame)\n        self.horizontalLayout_5.setContentsMargins(0, -1, -1, -1)\n        self.horizontalLayout_5.setObjectName(\"horizontalLayout_5\")\n        self.label_49 = QtWidgets.QLabel(self.frame)\n        self.label_49.setObjectName(\"label_49\")\n        self.horizontalLayout_5.addWidget(self.label_49)\n        self.label_47 = QtWidgets.QLabel(self.frame)\n        self.label_47.setObjectName(\"label_47\")\n        self.horizontalLayout_5.addWidget(self.label_47)\n        self.label_48 = QtWidgets.QLabel(self.frame)\n        self.label_48.setObjectName(\"label_48\")\n        self.horizontalLayout_5.addWidget(self.label_48)\n        self.verticalLayout_7.addWidget(self.frame)\n        self._11 = QtWidgets.QHBoxLayout()\n        self._11.setContentsMargins(0, 0, 0, 0)\n        self._11.setSpacing(6)\n        self._11.setObjectName(\"_11\")\n        self.label_14 = QtWidgets.QLabel(self.parts_tags_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_14.sizePolicy().hasHeightForWidth())\n        self.label_14.setSizePolicy(sizePolicy)\n        self.label_14.setObjectName(\"label_14\")\n        self._11.addWidget(self.label_14)\n        self.cwp_movt_tag_exc = QtWidgets.QLineEdit(self.parts_tags_box)\n        self.cwp_movt_tag_exc.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_movt_tag_exc.setObjectName(\"cwp_movt_tag_exc\")\n        self._11.addWidget(self.cwp_movt_tag_exc)\n        self.cwp_movt_tag_exc1 = QtWidgets.QLineEdit(self.parts_tags_box)\n        self.cwp_movt_tag_exc1.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_movt_tag_exc1.setObjectName(\"cwp_movt_tag_exc1\")\n        self._11.addWidget(self.cwp_movt_tag_exc1)\n        self.label_39 = QtWidgets.QLabel(self.parts_tags_box)\n        self.label_39.setObjectName(\"label_39\")\n        self._11.addWidget(self.label_39)\n        self.verticalLayout_7.addLayout(self._11)\n        self._6 = QtWidgets.QHBoxLayout()\n        self._6.setContentsMargins(0, 0, 0, 0)\n        self._6.setSpacing(6)\n        self._6.setObjectName(\"_6\")\n        self.label_9 = QtWidgets.QLabel(self.parts_tags_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_9.sizePolicy().hasHeightForWidth())\n        self.label_9.setSizePolicy(sizePolicy)\n        self.label_9.setObjectName(\"label_9\")\n        self._6.addWidget(self.label_9)\n        self.cwp_movt_tag_inc = QtWidgets.QLineEdit(self.parts_tags_box)\n        self.cwp_movt_tag_inc.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_movt_tag_inc.setObjectName(\"cwp_movt_tag_inc\")\n        self._6.addWidget(self.cwp_movt_tag_inc)\n        self.cwp_movt_tag_inc1 = QtWidgets.QLineEdit(self.parts_tags_box)\n        self.cwp_movt_tag_inc1.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_movt_tag_inc1.setObjectName(\"cwp_movt_tag_inc1\")\n        self._6.addWidget(self.cwp_movt_tag_inc1)\n        self.label_37 = QtWidgets.QLabel(self.parts_tags_box)\n        self.label_37.setObjectName(\"label_37\")\n        self._6.addWidget(self.label_37)\n        self.verticalLayout_7.addLayout(self._6)\n        self.verticalLayout_9.addWidget(self.parts_tags_box)\n        self.verticalLayout_46.addWidget(self.works_parts_tags_box)\n        self.verticalLayout_25.addWidget(self.works_parts_tags_frame)\n        self.partial_arrangements_medleys_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_3)\n        self.partial_arrangements_medleys_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.partial_arrangements_medleys_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.partial_arrangements_medleys_frame.setObjectName(\"partial_arrangements_medleys_frame\")\n        self.verticalLayout_47 = QtWidgets.QVBoxLayout(self.partial_arrangements_medleys_frame)\n        self.verticalLayout_47.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_47.setSpacing(0)\n        self.verticalLayout_47.setObjectName(\"verticalLayout_47\")\n        self.partial_arrangements_medleys_label = QtWidgets.QLabel(self.partial_arrangements_medleys_frame)\n        self.partial_arrangements_medleys_label.setStyleSheet(\"background-color: rgb(195, 168, 179);\")\n        self.partial_arrangements_medleys_label.setObjectName(\"partial_arrangements_medleys_label\")\n        self.verticalLayout_47.addWidget(self.partial_arrangements_medleys_label)\n        self.partial_arrangements_medleys_box = QtWidgets.QGroupBox(self.partial_arrangements_medleys_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(221, 209, 221))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(221, 209, 221))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(221, 209, 221))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(221, 209, 221))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(221, 209, 221))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(221, 209, 221))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(221, 209, 221))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(221, 209, 221))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(221, 209, 221))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.partial_arrangements_medleys_box.setPalette(palette)\n        self.partial_arrangements_medleys_box.setAutoFillBackground(False)\n        self.partial_arrangements_medleys_box.setStyleSheet(\"background-color: rgb(221, 209, 221);\")\n        self.partial_arrangements_medleys_box.setTitle(\"\")\n        self.partial_arrangements_medleys_box.setObjectName(\"partial_arrangements_medleys_box\")\n        self.verticalLayout_19 = QtWidgets.QVBoxLayout(self.partial_arrangements_medleys_box)\n        self.verticalLayout_19.setObjectName(\"verticalLayout_19\")\n        self.label_20 = QtWidgets.QLabel(self.partial_arrangements_medleys_box)\n        self.label_20.setStyleSheet(\"font: 75 8pt \\\"MS Shell Dlg 2\\\";\\n\"\n\"text-decoration: underline;\")\n        self.label_20.setObjectName(\"label_20\")\n        self.verticalLayout_19.addWidget(self.label_20)\n        self.partial_box = QtWidgets.QGroupBox(self.partial_arrangements_medleys_box)\n        self.partial_box.setObjectName(\"partial_box\")\n        self.horizontalLayout_13 = QtWidgets.QHBoxLayout(self.partial_box)\n        self.horizontalLayout_13.setObjectName(\"horizontalLayout_13\")\n        self.cwp_partial = QtWidgets.QCheckBox(self.partial_box)\n        self.cwp_partial.setObjectName(\"cwp_partial\")\n        self.horizontalLayout_13.addWidget(self.cwp_partial)\n        self.cwp_partial_text = QtWidgets.QLineEdit(self.partial_box)\n        self.cwp_partial_text.setEnabled(True)\n        self.cwp_partial_text.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_partial_text.setObjectName(\"cwp_partial_text\")\n        self.horizontalLayout_13.addWidget(self.cwp_partial_text)\n        self.verticalLayout_19.addWidget(self.partial_box)\n        self.arrangements_box = QtWidgets.QGroupBox(self.partial_arrangements_medleys_box)\n        self.arrangements_box.setObjectName(\"arrangements_box\")\n        self.horizontalLayout_15 = QtWidgets.QHBoxLayout(self.arrangements_box)\n        self.horizontalLayout_15.setObjectName(\"horizontalLayout_15\")\n        self.cwp_arrangements = QtWidgets.QCheckBox(self.arrangements_box)\n        self.cwp_arrangements.setObjectName(\"cwp_arrangements\")\n        self.horizontalLayout_15.addWidget(self.cwp_arrangements)\n        self.cwp_arrangements_text = QtWidgets.QLineEdit(self.arrangements_box)\n        self.cwp_arrangements_text.setEnabled(True)\n        self.cwp_arrangements_text.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_arrangements_text.setDragEnabled(False)\n        self.cwp_arrangements_text.setObjectName(\"cwp_arrangements_text\")\n        self.horizontalLayout_15.addWidget(self.cwp_arrangements_text)\n        self.verticalLayout_19.addWidget(self.arrangements_box)\n        self.medleys_box = QtWidgets.QGroupBox(self.partial_arrangements_medleys_box)\n        self.medleys_box.setObjectName(\"medleys_box\")\n        self.horizontalLayout_12 = QtWidgets.QHBoxLayout(self.medleys_box)\n        self.horizontalLayout_12.setObjectName(\"horizontalLayout_12\")\n        self.cwp_medley = QtWidgets.QCheckBox(self.medleys_box)\n        self.cwp_medley.setObjectName(\"cwp_medley\")\n        self.horizontalLayout_12.addWidget(self.cwp_medley)\n        self.cwp_medley_text = QtWidgets.QLineEdit(self.medleys_box)\n        self.cwp_medley_text.setEnabled(True)\n        self.cwp_medley_text.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_medley_text.setObjectName(\"cwp_medley_text\")\n        self.horizontalLayout_12.addWidget(self.cwp_medley_text)\n        self.verticalLayout_19.addWidget(self.medleys_box)\n        self.verticalLayout_47.addWidget(self.partial_arrangements_medleys_box)\n        self.verticalLayout_25.addWidget(self.partial_arrangements_medleys_frame)\n        self.songkong_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_3)\n        self.songkong_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.songkong_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.songkong_frame.setObjectName(\"songkong_frame\")\n        self.verticalLayout_48 = QtWidgets.QVBoxLayout(self.songkong_frame)\n        self.verticalLayout_48.setContentsMargins(0, 0, -1, 0)\n        self.verticalLayout_48.setSpacing(0)\n        self.verticalLayout_48.setObjectName(\"verticalLayout_48\")\n        self.songkong_label = QtWidgets.QLabel(self.songkong_frame)\n        self.songkong_label.setStyleSheet(\"background-color: rgb(182, 182, 62);\")\n        self.songkong_label.setObjectName(\"songkong_label\")\n        self.verticalLayout_48.addWidget(self.songkong_label)\n        self.songkong_box = QtWidgets.QGroupBox(self.songkong_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(227, 227, 143))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(227, 227, 143))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(227, 227, 143))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(227, 227, 143))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(227, 227, 143))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(227, 227, 143))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(227, 227, 143))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(227, 227, 143))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(227, 227, 143))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.songkong_box.setPalette(palette)\n        self.songkong_box.setAutoFillBackground(False)\n        self.songkong_box.setStyleSheet(\"background-color: rgb(227, 227, 143);\")\n        self.songkong_box.setTitle(\"\")\n        self.songkong_box.setObjectName(\"songkong_box\")\n        self.horizontalLayout_14 = QtWidgets.QHBoxLayout(self.songkong_box)\n        self.horizontalLayout_14.setObjectName(\"horizontalLayout_14\")\n        self.cwp_use_sk = QtWidgets.QCheckBox(self.songkong_box)\n        self.cwp_use_sk.setObjectName(\"cwp_use_sk\")\n        self.horizontalLayout_14.addWidget(self.cwp_use_sk)\n        self.cwp_write_sk = QtWidgets.QCheckBox(self.songkong_box)\n        self.cwp_write_sk.setObjectName(\"cwp_write_sk\")\n        self.horizontalLayout_14.addWidget(self.cwp_write_sk)\n        self.verticalLayout_48.addWidget(self.songkong_box)\n        self.verticalLayout_25.addWidget(self.songkong_frame)\n        self.label_82 = QtWidgets.QLabel(self.scrollAreaWidgetContents_3)\n        self.label_82.setObjectName(\"label_82\")\n        self.verticalLayout_25.addWidget(self.label_82)\n        self.scrollArea_3.setWidget(self.scrollAreaWidgetContents_3)\n        self.verticalLayout_4.addWidget(self.scrollArea_3)\n        self.tabWidget.addTab(self.Works, \"\")\n        self.Genres = QtWidgets.QWidget()\n        self.Genres.setObjectName(\"Genres\")\n        self.gridLayout_2 = QtWidgets.QGridLayout(self.Genres)\n        self.gridLayout_2.setObjectName(\"gridLayout_2\")\n        self.scrollArea_6 = QtWidgets.QScrollArea(self.Genres)\n        self.scrollArea_6.setWidgetResizable(True)\n        self.scrollArea_6.setObjectName(\"scrollArea_6\")\n        self.scrollAreaWidgetContents_5 = QtWidgets.QWidget()\n        self.scrollAreaWidgetContents_5.setGeometry(QtCore.QRect(0, -26, 1084, 1310))\n        self.scrollAreaWidgetContents_5.setObjectName(\"scrollAreaWidgetContents_5\")\n        self.gridLayout_13 = QtWidgets.QGridLayout(self.scrollAreaWidgetContents_5)\n        self.gridLayout_13.setObjectName(\"gridLayout_13\")\n        self.genre_tag_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_5)\n        self.genre_tag_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.genre_tag_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.genre_tag_frame.setObjectName(\"genre_tag_frame\")\n        self.verticalLayout_52 = QtWidgets.QVBoxLayout(self.genre_tag_frame)\n        self.verticalLayout_52.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_52.setSpacing(0)\n        self.verticalLayout_52.setObjectName(\"verticalLayout_52\")\n        self.genre_tag_label = QtWidgets.QLabel(self.genre_tag_frame)\n        self.genre_tag_label.setStyleSheet(\"background-color: rgb(138, 222, 187);\")\n        self.genre_tag_label.setObjectName(\"genre_tag_label\")\n        self.verticalLayout_52.addWidget(self.genre_tag_label)\n        self.genre_tag_box = QtWidgets.QGroupBox(self.genre_tag_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.genre_tag_box.setPalette(palette)\n        self.genre_tag_box.setAutoFillBackground(False)\n        self.genre_tag_box.setStyleSheet(\"background-color: rgb(207, 236, 225);\")\n        self.genre_tag_box.setTitle(\"\")\n        self.genre_tag_box.setObjectName(\"genre_tag_box\")\n        self.gridLayout_5 = QtWidgets.QGridLayout(self.genre_tag_box)\n        self.gridLayout_5.setObjectName(\"gridLayout_5\")\n        self.label_73 = QtWidgets.QLabel(self.genre_tag_box)\n        self.label_73.setObjectName(\"label_73\")\n        self.gridLayout_5.addWidget(self.label_73, 0, 0, 1, 1)\n        self.label_74 = QtWidgets.QLabel(self.genre_tag_box)\n        self.label_74.setObjectName(\"label_74\")\n        self.gridLayout_5.addWidget(self.label_74, 0, 2, 1, 1)\n        self.cwp_subgenre_tag = QtWidgets.QLineEdit(self.genre_tag_box)\n        self.cwp_subgenre_tag.setStyleSheet(\"background-color: rgb(250,250,250);\")\n        self.cwp_subgenre_tag.setObjectName(\"cwp_subgenre_tag\")\n        self.gridLayout_5.addWidget(self.cwp_subgenre_tag, 0, 3, 1, 1)\n        self.cwp_genre_tag = QtWidgets.QLineEdit(self.genre_tag_box)\n        self.cwp_genre_tag.setStyleSheet(\"background-color: rgb(250,250,250);\")\n        self.cwp_genre_tag.setObjectName(\"cwp_genre_tag\")\n        self.gridLayout_5.addWidget(self.cwp_genre_tag, 0, 1, 1, 1)\n        self.verticalLayout_52.addWidget(self.genre_tag_box)\n        self.gridLayout_13.addWidget(self.genre_tag_frame, 2, 0, 1, 1)\n        self.classical_genre_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_5)\n        self.classical_genre_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.classical_genre_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.classical_genre_frame.setObjectName(\"classical_genre_frame\")\n        self.verticalLayout_51 = QtWidgets.QVBoxLayout(self.classical_genre_frame)\n        self.verticalLayout_51.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_51.setSpacing(0)\n        self.verticalLayout_51.setObjectName(\"verticalLayout_51\")\n        self.classical_genre_label = QtWidgets.QLabel(self.classical_genre_frame)\n        self.classical_genre_label.setStyleSheet(\"background-color: rgb(138, 222, 187);\")\n        self.classical_genre_label.setObjectName(\"classical_genre_label\")\n        self.verticalLayout_51.addWidget(self.classical_genre_label)\n        self.classical_genre_box = QtWidgets.QGroupBox(self.classical_genre_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.classical_genre_box.setPalette(palette)\n        self.classical_genre_box.setAutoFillBackground(False)\n        self.classical_genre_box.setStyleSheet(\"background-color: rgb(207, 236, 225);\")\n        self.classical_genre_box.setTitle(\"\")\n        self.classical_genre_box.setObjectName(\"classical_genre_box\")\n        self.gridLayout_3 = QtWidgets.QGridLayout(self.classical_genre_box)\n        self.gridLayout_3.setObjectName(\"gridLayout_3\")\n        self.cwp_genres_classical_exclude = QtWidgets.QCheckBox(self.classical_genre_box)\n        self.cwp_genres_classical_exclude.setObjectName(\"cwp_genres_classical_exclude\")\n        self.gridLayout_3.addWidget(self.cwp_genres_classical_exclude, 1, 3, 1, 2)\n        self.cwp_genres_classical_selective = QtWidgets.QRadioButton(self.classical_genre_box)\n        self.cwp_genres_classical_selective.setObjectName(\"cwp_genres_classical_selective\")\n        self.gridLayout_3.addWidget(self.cwp_genres_classical_selective, 0, 2, 1, 2)\n        self.cwp_muso_classical = QtWidgets.QCheckBox(self.classical_genre_box)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.cwp_muso_classical.setFont(font)\n        self.cwp_muso_classical.setObjectName(\"cwp_muso_classical\")\n        self.gridLayout_3.addWidget(self.cwp_muso_classical, 1, 0, 1, 3)\n        self.cwp_genres_classical_all = QtWidgets.QRadioButton(self.classical_genre_box)\n        self.cwp_genres_classical_all.setObjectName(\"cwp_genres_classical_all\")\n        self.gridLayout_3.addWidget(self.cwp_genres_classical_all, 0, 0, 1, 2)\n        self.label_64 = QtWidgets.QLabel(self.classical_genre_box)\n        self.label_64.setObjectName(\"label_64\")\n        self.gridLayout_3.addWidget(self.label_64, 3, 0, 1, 1)\n        self.cwp_genres_flag_text = QtWidgets.QLineEdit(self.classical_genre_box)\n        self.cwp_genres_flag_text.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_genres_flag_text.setObjectName(\"cwp_genres_flag_text\")\n        self.gridLayout_3.addWidget(self.cwp_genres_flag_text, 3, 1, 1, 3)\n        self.label_71 = QtWidgets.QLabel(self.classical_genre_box)\n        self.label_71.setObjectName(\"label_71\")\n        self.gridLayout_3.addWidget(self.label_71, 3, 4, 1, 1)\n        self.cwp_genres_flag_tag = QtWidgets.QLineEdit(self.classical_genre_box)\n        self.cwp_genres_flag_tag.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_genres_flag_tag.setObjectName(\"cwp_genres_flag_tag\")\n        self.gridLayout_3.addWidget(self.cwp_genres_flag_tag, 3, 5, 1, 1)\n        self.cwp_genres_arranger_as_composer = QtWidgets.QCheckBox(self.classical_genre_box)\n        self.cwp_genres_arranger_as_composer.setObjectName(\"cwp_genres_arranger_as_composer\")\n        self.gridLayout_3.addWidget(self.cwp_genres_arranger_as_composer, 2, 0, 1, 1)\n        self.verticalLayout_51.addWidget(self.classical_genre_box)\n        self.gridLayout_13.addWidget(self.classical_genre_frame, 5, 0, 1, 1)\n        self.label_81 = QtWidgets.QLabel(self.scrollAreaWidgetContents_5)\n        self.label_81.setObjectName(\"label_81\")\n        self.gridLayout_13.addWidget(self.label_81, 8, 0, 1, 1)\n        self.cwp_use_muso_refdb = QtWidgets.QCheckBox(self.scrollAreaWidgetContents_5)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.cwp_use_muso_refdb.setFont(font)\n        self.cwp_use_muso_refdb.setObjectName(\"cwp_use_muso_refdb\")\n        self.gridLayout_13.addWidget(self.cwp_use_muso_refdb, 1, 0, 1, 1)\n        self.instruments_keys_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_5)\n        self.instruments_keys_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.instruments_keys_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.instruments_keys_frame.setObjectName(\"instruments_keys_frame\")\n        self.verticalLayout_53 = QtWidgets.QVBoxLayout(self.instruments_keys_frame)\n        self.verticalLayout_53.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_53.setSpacing(0)\n        self.verticalLayout_53.setObjectName(\"verticalLayout_53\")\n        self.instruments_keys_label = QtWidgets.QLabel(self.instruments_keys_frame)\n        self.instruments_keys_label.setStyleSheet(\"background-color: rgb(225, 168, 171);\")\n        self.instruments_keys_label.setObjectName(\"instruments_keys_label\")\n        self.verticalLayout_53.addWidget(self.instruments_keys_label)\n        self.instruments_keys_box = QtWidgets.QGroupBox(self.instruments_keys_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(245, 224, 226))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 224, 226))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 224, 226))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 224, 226))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 224, 226))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 224, 226))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 224, 226))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 224, 226))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 224, 226))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.instruments_keys_box.setPalette(palette)\n        self.instruments_keys_box.setAutoFillBackground(False)\n        self.instruments_keys_box.setStyleSheet(\"background-color: rgb(245, 224, 226);\")\n        self.instruments_keys_box.setTitle(\"\")\n        self.instruments_keys_box.setObjectName(\"instruments_keys_box\")\n        self.verticalLayout_34 = QtWidgets.QVBoxLayout(self.instruments_keys_box)\n        self.verticalLayout_34.setObjectName(\"verticalLayout_34\")\n        self.instruments_box = QtWidgets.QGroupBox(self.instruments_keys_box)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.instruments_box.setPalette(palette)\n        self.instruments_box.setAutoFillBackground(False)\n        self.instruments_box.setStyleSheet(\"background-color: rgb(245, 210, 213);\")\n        self.instruments_box.setObjectName(\"instruments_box\")\n        self.gridLayout_11 = QtWidgets.QGridLayout(self.instruments_box)\n        self.gridLayout_11.setObjectName(\"gridLayout_11\")\n        self.label_66 = QtWidgets.QLabel(self.instruments_box)\n        self.label_66.setObjectName(\"label_66\")\n        self.gridLayout_11.addWidget(self.label_66, 0, 0, 1, 1)\n        self.cwp_instruments_tag = QtWidgets.QLineEdit(self.instruments_box)\n        self.cwp_instruments_tag.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_instruments_tag.setObjectName(\"cwp_instruments_tag\")\n        self.gridLayout_11.addWidget(self.cwp_instruments_tag, 0, 1, 1, 1)\n        self.instruments_source_box = QtWidgets.QGroupBox(self.instruments_box)\n        self.instruments_source_box.setObjectName(\"instruments_source_box\")\n        self.horizontalLayout_33 = QtWidgets.QHBoxLayout(self.instruments_source_box)\n        self.horizontalLayout_33.setObjectName(\"horizontalLayout_33\")\n        self.cwp_instruments_MB_names = QtWidgets.QCheckBox(self.instruments_source_box)\n        self.cwp_instruments_MB_names.setObjectName(\"cwp_instruments_MB_names\")\n        self.horizontalLayout_33.addWidget(self.cwp_instruments_MB_names)\n        self.cwp_instruments_credited_names = QtWidgets.QCheckBox(self.instruments_source_box)\n        self.cwp_instruments_credited_names.setObjectName(\"cwp_instruments_credited_names\")\n        self.horizontalLayout_33.addWidget(self.cwp_instruments_credited_names)\n        self.gridLayout_11.addWidget(self.instruments_source_box, 1, 0, 1, 2)\n        self.verticalLayout_34.addWidget(self.instruments_box)\n        self.keys_box = QtWidgets.QGroupBox(self.instruments_keys_box)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(245, 210, 213))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.keys_box.setPalette(palette)\n        self.keys_box.setAutoFillBackground(False)\n        self.keys_box.setStyleSheet(\"background-color: rgb(245, 210, 213);\")\n        self.keys_box.setObjectName(\"keys_box\")\n        self.gridLayout_4 = QtWidgets.QGridLayout(self.keys_box)\n        self.gridLayout_4.setObjectName(\"gridLayout_4\")\n        self.label_72 = QtWidgets.QLabel(self.keys_box)\n        self.label_72.setObjectName(\"label_72\")\n        self.gridLayout_4.addWidget(self.label_72, 0, 0, 1, 1)\n        self.cwp_key_tag = QtWidgets.QLineEdit(self.keys_box)\n        self.cwp_key_tag.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_key_tag.setObjectName(\"cwp_key_tag\")\n        self.gridLayout_4.addWidget(self.cwp_key_tag, 0, 2, 1, 1)\n        self.keys_include_box = QtWidgets.QGroupBox(self.keys_box)\n        self.keys_include_box.setObjectName(\"keys_include_box\")\n        self.horizontalLayout_35 = QtWidgets.QHBoxLayout(self.keys_include_box)\n        self.horizontalLayout_35.setObjectName(\"horizontalLayout_35\")\n        self.cwp_key_never_include = QtWidgets.QRadioButton(self.keys_include_box)\n        self.cwp_key_never_include.setObjectName(\"cwp_key_never_include\")\n        self.horizontalLayout_35.addWidget(self.cwp_key_never_include)\n        self.cwp_key_contingent_include = QtWidgets.QRadioButton(self.keys_include_box)\n        self.cwp_key_contingent_include.setObjectName(\"cwp_key_contingent_include\")\n        self.horizontalLayout_35.addWidget(self.cwp_key_contingent_include)\n        self.cwp_key_include = QtWidgets.QRadioButton(self.keys_include_box)\n        self.cwp_key_include.setObjectName(\"cwp_key_include\")\n        self.horizontalLayout_35.addWidget(self.cwp_key_include)\n        self.gridLayout_4.addWidget(self.keys_include_box, 1, 0, 1, 2)\n        self.verticalLayout_34.addWidget(self.keys_box)\n        self.verticalLayout_53.addWidget(self.instruments_keys_box)\n        self.gridLayout_13.addWidget(self.instruments_keys_frame, 6, 0, 1, 1)\n        self.allowed_genres_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_5)\n        self.allowed_genres_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.allowed_genres_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.allowed_genres_frame.setLineWidth(1)\n        self.allowed_genres_frame.setMidLineWidth(0)\n        self.allowed_genres_frame.setObjectName(\"allowed_genres_frame\")\n        self.verticalLayout_50 = QtWidgets.QVBoxLayout(self.allowed_genres_frame)\n        self.verticalLayout_50.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_50.setSpacing(0)\n        self.verticalLayout_50.setObjectName(\"verticalLayout_50\")\n        self.allowed_filters_label = QtWidgets.QLabel(self.allowed_genres_frame)\n        self.allowed_filters_label.setStyleSheet(\"background-color: rgb(138, 222, 187);\")\n        self.allowed_filters_label.setObjectName(\"allowed_filters_label\")\n        self.verticalLayout_50.addWidget(self.allowed_filters_label)\n        self.cwp_genres_filter = QtWidgets.QCheckBox(self.allowed_genres_frame)\n        self.cwp_genres_filter.setLayoutDirection(QtCore.Qt.LeftToRight)\n        self.cwp_genres_filter.setStyleSheet(\"background-color: rgb(138, 222, 187);\")\n        self.cwp_genres_filter.setObjectName(\"cwp_genres_filter\")\n        self.verticalLayout_50.addWidget(self.cwp_genres_filter)\n        self.genre_filters_frame = QtWidgets.QGroupBox(self.allowed_genres_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.genre_filters_frame.setPalette(palette)\n        self.genre_filters_frame.setAutoFillBackground(False)\n        self.genre_filters_frame.setStyleSheet(\"background-color: rgb(207, 236, 225);\")\n        self.genre_filters_frame.setTitle(\"\")\n        self.genre_filters_frame.setObjectName(\"genre_filters_frame\")\n        self.gridLayout_6 = QtWidgets.QGridLayout(self.genre_filters_frame)\n        self.gridLayout_6.setObjectName(\"gridLayout_6\")\n        self.classical_genres_box = QtWidgets.QGroupBox(self.genre_filters_frame)\n        self.classical_genres_box.setStyleSheet(\"background-color: rgb(190, 236, 219);\")\n        self.classical_genres_box.setObjectName(\"classical_genres_box\")\n        self.gridLayout_9 = QtWidgets.QGridLayout(self.classical_genres_box)\n        self.gridLayout_9.setObjectName(\"gridLayout_9\")\n        self.label_60 = QtWidgets.QLabel(self.classical_genres_box)\n        self.label_60.setObjectName(\"label_60\")\n        self.gridLayout_9.addWidget(self.label_60, 0, 0, 1, 1)\n        self.cwp_genres_classical_main = QtWidgets.QPlainTextEdit(self.classical_genres_box)\n        self.cwp_genres_classical_main.setMaximumSize(QtCore.QSize(16777215, 50))\n        self.cwp_genres_classical_main.setStyleSheet(\"background-color: rgb(250,250,250);\")\n        self.cwp_genres_classical_main.setObjectName(\"cwp_genres_classical_main\")\n        self.gridLayout_9.addWidget(self.cwp_genres_classical_main, 0, 1, 3, 1)\n        self.label_75 = QtWidgets.QLabel(self.classical_genres_box)\n        self.label_75.setObjectName(\"label_75\")\n        self.gridLayout_9.addWidget(self.label_75, 0, 2, 1, 1)\n        self.cwp_genres_classical_sub = QtWidgets.QPlainTextEdit(self.classical_genres_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.cwp_genres_classical_sub.sizePolicy().hasHeightForWidth())\n        self.cwp_genres_classical_sub.setSizePolicy(sizePolicy)\n        self.cwp_genres_classical_sub.setMaximumSize(QtCore.QSize(16777215, 50))\n        self.cwp_genres_classical_sub.setStyleSheet(\"background-color: rgb(250,250,250);\")\n        self.cwp_genres_classical_sub.setObjectName(\"cwp_genres_classical_sub\")\n        self.gridLayout_9.addWidget(self.cwp_genres_classical_sub, 0, 3, 3, 1)\n        self.cwp_muso_genres = QtWidgets.QCheckBox(self.classical_genres_box)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.cwp_muso_genres.setFont(font)\n        self.cwp_muso_genres.setObjectName(\"cwp_muso_genres\")\n        self.gridLayout_9.addWidget(self.cwp_muso_genres, 1, 0, 1, 1)\n        self.gridLayout_6.addWidget(self.classical_genres_box, 2, 0, 1, 2)\n        self.general_genres_box = QtWidgets.QGroupBox(self.genre_filters_frame)\n        self.general_genres_box.setStyleSheet(\"background-color: rgb(190, 236, 219);\")\n        self.general_genres_box.setObjectName(\"general_genres_box\")\n        self.horizontalLayout_36 = QtWidgets.QHBoxLayout(self.general_genres_box)\n        self.horizontalLayout_36.setObjectName(\"horizontalLayout_36\")\n        self.label_62 = QtWidgets.QLabel(self.general_genres_box)\n        self.label_62.setObjectName(\"label_62\")\n        self.horizontalLayout_36.addWidget(self.label_62)\n        self.cwp_genres_other_main = QtWidgets.QPlainTextEdit(self.general_genres_box)\n        self.cwp_genres_other_main.setMaximumSize(QtCore.QSize(16777215, 50))\n        self.cwp_genres_other_main.setStyleSheet(\"background-color: rgb(250,250,250);\")\n        self.cwp_genres_other_main.setObjectName(\"cwp_genres_other_main\")\n        self.horizontalLayout_36.addWidget(self.cwp_genres_other_main)\n        self.label_76 = QtWidgets.QLabel(self.general_genres_box)\n        self.label_76.setObjectName(\"label_76\")\n        self.horizontalLayout_36.addWidget(self.label_76)\n        self.cwp_genres_other_sub = QtWidgets.QPlainTextEdit(self.general_genres_box)\n        self.cwp_genres_other_sub.setMaximumSize(QtCore.QSize(16777215, 50))\n        self.cwp_genres_other_sub.setStyleSheet(\"background-color: rgb(250,250,250);\")\n        self.cwp_genres_other_sub.setObjectName(\"cwp_genres_other_sub\")\n        self.horizontalLayout_36.addWidget(self.cwp_genres_other_sub)\n        self.cwp_genres_other_main.raise_()\n        self.cwp_genres_other_sub.raise_()\n        self.label_62.raise_()\n        self.label_76.raise_()\n        self.gridLayout_6.addWidget(self.general_genres_box, 3, 0, 1, 2)\n        self.label_80 = QtWidgets.QLabel(self.genre_filters_frame)\n        self.label_80.setObjectName(\"label_80\")\n        self.gridLayout_6.addWidget(self.label_80, 4, 0, 1, 1)\n        self.cwp_genres_default = QtWidgets.QLineEdit(self.genre_filters_frame)\n        self.cwp_genres_default.setStyleSheet(\"background-color: rgb(250,250,250);\")\n        self.cwp_genres_default.setObjectName(\"cwp_genres_default\")\n        self.gridLayout_6.addWidget(self.cwp_genres_default, 4, 1, 1, 1)\n        self.label_77 = QtWidgets.QLabel(self.genre_filters_frame)\n        self.label_77.setObjectName(\"label_77\")\n        self.gridLayout_6.addWidget(self.label_77, 0, 0, 1, 2)\n        self.label_78 = QtWidgets.QLabel(self.genre_filters_frame)\n        self.label_78.setObjectName(\"label_78\")\n        self.gridLayout_6.addWidget(self.label_78, 1, 0, 1, 1)\n        self.verticalLayout_50.addWidget(self.genre_filters_frame)\n        self.gridLayout_13.addWidget(self.allowed_genres_frame, 4, 0, 1, 1)\n        self.source_of_genres_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_5)\n        self.source_of_genres_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.source_of_genres_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.source_of_genres_frame.setObjectName(\"source_of_genres_frame\")\n        self.verticalLayout_49 = QtWidgets.QVBoxLayout(self.source_of_genres_frame)\n        self.verticalLayout_49.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_49.setSpacing(0)\n        self.verticalLayout_49.setObjectName(\"verticalLayout_49\")\n        self.source_of_genres_label = QtWidgets.QLabel(self.source_of_genres_frame)\n        self.source_of_genres_label.setStyleSheet(\"background-color: rgb(138, 222, 187);\")\n        self.source_of_genres_label.setObjectName(\"source_of_genres_label\")\n        self.verticalLayout_49.addWidget(self.source_of_genres_label)\n        self.source_of_genres_box = QtWidgets.QGroupBox(self.source_of_genres_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(207, 236, 225))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.source_of_genres_box.setPalette(palette)\n        self.source_of_genres_box.setAutoFillBackground(False)\n        self.source_of_genres_box.setStyleSheet(\"background-color: rgb(207, 236, 225);\")\n        self.source_of_genres_box.setTitle(\"\")\n        self.source_of_genres_box.setObjectName(\"source_of_genres_box\")\n        self.horizontalLayout_32 = QtWidgets.QHBoxLayout(self.source_of_genres_box)\n        self.horizontalLayout_32.setObjectName(\"horizontalLayout_32\")\n        self.cwp_genres_use_file = QtWidgets.QCheckBox(self.source_of_genres_box)\n        self.cwp_genres_use_file.setObjectName(\"cwp_genres_use_file\")\n        self.horizontalLayout_32.addWidget(self.cwp_genres_use_file)\n        self.cwp_genres_use_folks = QtWidgets.QCheckBox(self.source_of_genres_box)\n        self.cwp_genres_use_folks.setObjectName(\"cwp_genres_use_folks\")\n        self.horizontalLayout_32.addWidget(self.cwp_genres_use_folks)\n        self.cwp_genres_use_worktype = QtWidgets.QCheckBox(self.source_of_genres_box)\n        self.cwp_genres_use_worktype.setObjectName(\"cwp_genres_use_worktype\")\n        self.horizontalLayout_32.addWidget(self.cwp_genres_use_worktype)\n        self.cwp_genres_infer = QtWidgets.QCheckBox(self.source_of_genres_box)\n        self.cwp_genres_infer.setObjectName(\"cwp_genres_infer\")\n        self.horizontalLayout_32.addWidget(self.cwp_genres_infer)\n        self.verticalLayout_49.addWidget(self.source_of_genres_box)\n        self.gridLayout_13.addWidget(self.source_of_genres_frame, 3, 0, 1, 1)\n        self.periods_dates_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_5)\n        self.periods_dates_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.periods_dates_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.periods_dates_frame.setObjectName(\"periods_dates_frame\")\n        self.verticalLayout_54 = QtWidgets.QVBoxLayout(self.periods_dates_frame)\n        self.verticalLayout_54.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_54.setSpacing(0)\n        self.verticalLayout_54.setObjectName(\"verticalLayout_54\")\n        self.label_109 = QtWidgets.QLabel(self.periods_dates_frame)\n        self.label_109.setStyleSheet(\"background-color: rgb(195, 183, 213);\")\n        self.label_109.setObjectName(\"label_109\")\n        self.verticalLayout_54.addWidget(self.label_109)\n        self.periods_dates_box = QtWidgets.QGroupBox(self.periods_dates_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(229, 224, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 224, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 224, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 224, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 224, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 224, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 224, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 224, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 224, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.periods_dates_box.setPalette(palette)\n        self.periods_dates_box.setAutoFillBackground(False)\n        self.periods_dates_box.setStyleSheet(\"background-color: rgb(229, 224, 236);\")\n        self.periods_dates_box.setTitle(\"\")\n        self.periods_dates_box.setObjectName(\"periods_dates_box\")\n        self.verticalLayout_32 = QtWidgets.QVBoxLayout(self.periods_dates_box)\n        self.verticalLayout_32.setObjectName(\"verticalLayout_32\")\n        self.dates_box = QtWidgets.QGroupBox(self.periods_dates_box)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.dates_box.setPalette(palette)\n        self.dates_box.setAutoFillBackground(False)\n        self.dates_box.setStyleSheet(\"background-color: rgb(222, 212, 236);\")\n        self.dates_box.setObjectName(\"dates_box\")\n        self.gridLayout_10 = QtWidgets.QGridLayout(self.dates_box)\n        self.gridLayout_10.setObjectName(\"gridLayout_10\")\n        self.dates_box_inner = QtWidgets.QGroupBox(self.dates_box)\n        self.dates_box_inner.setObjectName(\"dates_box_inner\")\n        self.gridLayout_7 = QtWidgets.QGridLayout(self.dates_box_inner)\n        self.gridLayout_7.setObjectName(\"gridLayout_7\")\n        self.cwp_workdate_source_composed = QtWidgets.QCheckBox(self.dates_box_inner)\n        self.cwp_workdate_source_composed.setObjectName(\"cwp_workdate_source_composed\")\n        self.gridLayout_7.addWidget(self.cwp_workdate_source_composed, 0, 0, 1, 1)\n        self.cwp_workdate_source_published = QtWidgets.QCheckBox(self.dates_box_inner)\n        self.cwp_workdate_source_published.setObjectName(\"cwp_workdate_source_published\")\n        self.gridLayout_7.addWidget(self.cwp_workdate_source_published, 0, 1, 1, 1)\n        self.cwp_workdate_source_premiered = QtWidgets.QCheckBox(self.dates_box_inner)\n        self.cwp_workdate_source_premiered.setObjectName(\"cwp_workdate_source_premiered\")\n        self.gridLayout_7.addWidget(self.cwp_workdate_source_premiered, 0, 2, 1, 1)\n        self.line_9 = QtWidgets.QFrame(self.dates_box_inner)\n        self.line_9.setFrameShape(QtWidgets.QFrame.HLine)\n        self.line_9.setFrameShadow(QtWidgets.QFrame.Sunken)\n        self.line_9.setObjectName(\"line_9\")\n        self.gridLayout_7.addWidget(self.line_9, 1, 0, 1, 3)\n        self.cwp_workdate_use_first = QtWidgets.QRadioButton(self.dates_box_inner)\n        self.cwp_workdate_use_first.setObjectName(\"cwp_workdate_use_first\")\n        self.gridLayout_7.addWidget(self.cwp_workdate_use_first, 2, 0, 1, 1)\n        self.cwp_workdate_use_all = QtWidgets.QRadioButton(self.dates_box_inner)\n        self.cwp_workdate_use_all.setObjectName(\"cwp_workdate_use_all\")\n        self.gridLayout_7.addWidget(self.cwp_workdate_use_all, 2, 1, 1, 1)\n        self.cwp_workdate_annotate = QtWidgets.QCheckBox(self.dates_box_inner)\n        self.cwp_workdate_annotate.setObjectName(\"cwp_workdate_annotate\")\n        self.gridLayout_7.addWidget(self.cwp_workdate_annotate, 2, 2, 1, 1)\n        self.gridLayout_10.addWidget(self.dates_box_inner, 1, 0, 1, 3)\n        self.label_68 = QtWidgets.QLabel(self.dates_box)\n        self.label_68.setObjectName(\"label_68\")\n        self.gridLayout_10.addWidget(self.label_68, 0, 0, 1, 1)\n        self.cwp_workdate_include = QtWidgets.QCheckBox(self.dates_box)\n        self.cwp_workdate_include.setObjectName(\"cwp_workdate_include\")\n        self.gridLayout_10.addWidget(self.cwp_workdate_include, 2, 0, 1, 2)\n        self.cwp_workdate_tag = QtWidgets.QLineEdit(self.dates_box)\n        self.cwp_workdate_tag.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_workdate_tag.setObjectName(\"cwp_workdate_tag\")\n        self.gridLayout_10.addWidget(self.cwp_workdate_tag, 0, 1, 1, 2)\n        self.verticalLayout_32.addWidget(self.dates_box)\n        self.line_5 = QtWidgets.QFrame(self.periods_dates_box)\n        self.line_5.setMidLineWidth(0)\n        self.line_5.setFrameShape(QtWidgets.QFrame.HLine)\n        self.line_5.setFrameShadow(QtWidgets.QFrame.Sunken)\n        self.line_5.setObjectName(\"line_5\")\n        self.verticalLayout_32.addWidget(self.line_5)\n        self.periods_box = QtWidgets.QGroupBox(self.periods_dates_box)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(222, 212, 236))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.periods_box.setPalette(palette)\n        self.periods_box.setAutoFillBackground(False)\n        self.periods_box.setStyleSheet(\"background-color: rgb(222, 212, 236);\")\n        self.periods_box.setObjectName(\"periods_box\")\n        self.gridLayout_12 = QtWidgets.QGridLayout(self.periods_box)\n        self.gridLayout_12.setObjectName(\"gridLayout_12\")\n        self.label_69 = QtWidgets.QLabel(self.periods_box)\n        self.label_69.setObjectName(\"label_69\")\n        self.gridLayout_12.addWidget(self.label_69, 0, 0, 1, 1)\n        self.cwp_muso_periods = QtWidgets.QCheckBox(self.periods_box)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.cwp_muso_periods.setFont(font)\n        self.cwp_muso_periods.setObjectName(\"cwp_muso_periods\")\n        self.gridLayout_12.addWidget(self.cwp_muso_periods, 4, 0, 1, 2)\n        self.cwp_muso_dates = QtWidgets.QCheckBox(self.periods_box)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.cwp_muso_dates.setFont(font)\n        self.cwp_muso_dates.setObjectName(\"cwp_muso_dates\")\n        self.gridLayout_12.addWidget(self.cwp_muso_dates, 1, 0, 1, 3)\n        self.cwp_period_tag = QtWidgets.QLineEdit(self.periods_box)\n        self.cwp_period_tag.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_period_tag.setObjectName(\"cwp_period_tag\")\n        self.gridLayout_12.addWidget(self.cwp_period_tag, 0, 1, 1, 2)\n        self.label_70 = QtWidgets.QLabel(self.periods_box)\n        self.label_70.setObjectName(\"label_70\")\n        self.gridLayout_12.addWidget(self.label_70, 3, 0, 1, 1)\n        self.cwp_period_map = QtWidgets.QPlainTextEdit(self.periods_box)\n        self.cwp_period_map.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_period_map.setObjectName(\"cwp_period_map\")\n        self.gridLayout_12.addWidget(self.cwp_period_map, 3, 2, 2, 1)\n        self.period_map_annotation_label = QtWidgets.QLabel(self.periods_box)\n        self.period_map_annotation_label.setObjectName(\"period_map_annotation_label\")\n        self.gridLayout_12.addWidget(self.period_map_annotation_label, 5, 2, 1, 1)\n        self.cwp_periods_arranger_as_composer = QtWidgets.QCheckBox(self.periods_box)\n        self.cwp_periods_arranger_as_composer.setObjectName(\"cwp_periods_arranger_as_composer\")\n        self.gridLayout_12.addWidget(self.cwp_periods_arranger_as_composer, 2, 0, 1, 1)\n        self.verticalLayout_32.addWidget(self.periods_box)\n        self.verticalLayout_54.addWidget(self.periods_dates_box)\n        self.gridLayout_13.addWidget(self.periods_dates_frame, 7, 0, 1, 1)\n        self.label_119 = QtWidgets.QLabel(self.scrollAreaWidgetContents_5)\n        self.label_119.setObjectName(\"label_119\")\n        self.gridLayout_13.addWidget(self.label_119, 0, 0, 1, 1)\n        self.scrollArea_6.setWidget(self.scrollAreaWidgetContents_5)\n        self.gridLayout_2.addWidget(self.scrollArea_6, 1, 0, 1, 1)\n        self.tabWidget.addTab(self.Genres, \"\")\n        self.Tag_mapping = QtWidgets.QWidget()\n        self.Tag_mapping.setObjectName(\"Tag_mapping\")\n        self.verticalLayout_28 = QtWidgets.QVBoxLayout(self.Tag_mapping)\n        self.verticalLayout_28.setObjectName(\"verticalLayout_28\")\n        self.scrollArea_4 = QtWidgets.QScrollArea(self.Tag_mapping)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.scrollArea_4.sizePolicy().hasHeightForWidth())\n        self.scrollArea_4.setSizePolicy(sizePolicy)\n        self.scrollArea_4.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)\n        self.scrollArea_4.setWidgetResizable(True)\n        self.scrollArea_4.setObjectName(\"scrollArea_4\")\n        self.scrollAreaWidgetContents_8 = QtWidgets.QWidget()\n        self.scrollAreaWidgetContents_8.setGeometry(QtCore.QRect(0, 0, 1084, 917))\n        self.scrollAreaWidgetContents_8.setObjectName(\"scrollAreaWidgetContents_8\")\n        self.verticalLayout_33 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents_8)\n        self.verticalLayout_33.setObjectName(\"verticalLayout_33\")\n        self.label_18 = QtWidgets.QLabel(self.scrollAreaWidgetContents_8)\n        self.label_18.setStyleSheet(\"\")\n        self.label_18.setObjectName(\"label_18\")\n        self.verticalLayout_33.addWidget(self.label_18)\n        self.initial_tag_processing_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_8)\n        self.initial_tag_processing_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.initial_tag_processing_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.initial_tag_processing_frame.setObjectName(\"initial_tag_processing_frame\")\n        self.verticalLayout_41 = QtWidgets.QVBoxLayout(self.initial_tag_processing_frame)\n        self.verticalLayout_41.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_41.setSpacing(0)\n        self.verticalLayout_41.setObjectName(\"verticalLayout_41\")\n        self.label_97 = QtWidgets.QLabel(self.initial_tag_processing_frame)\n        self.label_97.setStyleSheet(\"background-color: rgb(209, 171, 222);\\n\"\n\"\")\n        self.label_97.setObjectName(\"label_97\")\n        self.verticalLayout_41.addWidget(self.label_97)\n        self.initial_tag_processing_box = QtWidgets.QGroupBox(self.initial_tag_processing_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.initial_tag_processing_box.setPalette(palette)\n        self.initial_tag_processing_box.setAutoFillBackground(False)\n        self.initial_tag_processing_box.setStyleSheet(\"background-color: rgb(242, 221, 245);\")\n        self.initial_tag_processing_box.setTitle(\"\")\n        self.initial_tag_processing_box.setObjectName(\"initial_tag_processing_box\")\n        self.verticalLayout_17 = QtWidgets.QVBoxLayout(self.initial_tag_processing_box)\n        self.verticalLayout_17.setObjectName(\"verticalLayout_17\")\n        self.tags_to_blank = QtWidgets.QGroupBox(self.initial_tag_processing_box)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(242, 221, 245))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.tags_to_blank.setPalette(palette)\n        self.tags_to_blank.setAutoFillBackground(False)\n        self.tags_to_blank.setStyleSheet(\"\")\n        self.tags_to_blank.setObjectName(\"tags_to_blank\")\n        self.verticalLayout_12 = QtWidgets.QVBoxLayout(self.tags_to_blank)\n        self.verticalLayout_12.setObjectName(\"verticalLayout_12\")\n        self.label_3 = QtWidgets.QLabel(self.tags_to_blank)\n        self.label_3.setObjectName(\"label_3\")\n        self.verticalLayout_12.addWidget(self.label_3)\n        self.cea_blank_tag = QtWidgets.QLineEdit(self.tags_to_blank)\n        self.cea_blank_tag.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_blank_tag.setObjectName(\"cea_blank_tag\")\n        self.verticalLayout_12.addWidget(self.cea_blank_tag)\n        self.cea_blank_tag_2 = QtWidgets.QLineEdit(self.tags_to_blank)\n        self.cea_blank_tag_2.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_blank_tag_2.setObjectName(\"cea_blank_tag_2\")\n        self.verticalLayout_12.addWidget(self.cea_blank_tag_2)\n        self.verticalLayout_17.addWidget(self.tags_to_blank)\n        self.label_19 = QtWidgets.QLabel(self.initial_tag_processing_box)\n        self.label_19.setObjectName(\"label_19\")\n        self.verticalLayout_17.addWidget(self.label_19)\n        self.cea_keep = QtWidgets.QLineEdit(self.initial_tag_processing_box)\n        self.cea_keep.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_keep.setObjectName(\"cea_keep\")\n        self.verticalLayout_17.addWidget(self.cea_keep)\n        self.cea_clear_tags = QtWidgets.QCheckBox(self.initial_tag_processing_box)\n        self.cea_clear_tags.setObjectName(\"cea_clear_tags\")\n        self.verticalLayout_17.addWidget(self.cea_clear_tags)\n        self.verticalLayout_41.addWidget(self.initial_tag_processing_box)\n        self.verticalLayout_33.addWidget(self.initial_tag_processing_frame)\n        self.tagmap_details_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_8)\n        self.tagmap_details_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.tagmap_details_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.tagmap_details_frame.setObjectName(\"tagmap_details_frame\")\n        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.tagmap_details_frame)\n        self.verticalLayout_3.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_3.setSpacing(0)\n        self.verticalLayout_3.setObjectName(\"verticalLayout_3\")\n        self.label_98 = QtWidgets.QLabel(self.tagmap_details_frame)\n        self.label_98.setStyleSheet(\"background-color: rgb(229, 174, 142);\")\n        self.label_98.setObjectName(\"label_98\")\n        self.verticalLayout_3.addWidget(self.label_98)\n        self.tagmap_details_box = QtWidgets.QGroupBox(self.tagmap_details_frame)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.tagmap_details_box.sizePolicy().hasHeightForWidth())\n        self.tagmap_details_box.setSizePolicy(sizePolicy)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(255, 229, 214))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 229, 214))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 229, 214))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 229, 214))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 229, 214))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 229, 214))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 229, 214))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 229, 214))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(255, 229, 214))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.tagmap_details_box.setPalette(palette)\n        self.tagmap_details_box.setFocusPolicy(QtCore.Qt.NoFocus)\n        self.tagmap_details_box.setAutoFillBackground(False)\n        self.tagmap_details_box.setStyleSheet(\"font: 75 8pt \\\"MS Shell Dlg 2\\\";\\n\"\n\"background-color: rgb(255, 229, 214);\")\n        self.tagmap_details_box.setTitle(\"\")\n        self.tagmap_details_box.setObjectName(\"tagmap_details_box\")\n        self.gridLayout_14 = QtWidgets.QGridLayout(self.tagmap_details_box)\n        self.gridLayout_14.setObjectName(\"gridLayout_14\")\n        self.textBrowser = QtWidgets.QTextBrowser(self.tagmap_details_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.textBrowser.sizePolicy().hasHeightForWidth())\n        self.textBrowser.setSizePolicy(sizePolicy)\n        self.textBrowser.setMaximumSize(QtCore.QSize(16777215, 100))\n        self.textBrowser.setStyleSheet(\"background-color: rgb(249, 215, 187);\")\n        self.textBrowser.setObjectName(\"textBrowser\")\n        self.gridLayout_14.addWidget(self.textBrowser, 0, 0, 1, 1)\n        self.frame_25 = QtWidgets.QFrame(self.tagmap_details_box)\n        self.frame_25.setObjectName(\"frame_25\")\n        self._14 = QtWidgets.QHBoxLayout(self.frame_25)\n        self._14.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)\n        self._14.setContentsMargins(1, 1, 1, 1)\n        self._14.setSpacing(6)\n        self._14.setObjectName(\"_14\")\n        self.toolButton_1 = QtWidgets.QToolButton(self.frame_25)\n        self.toolButton_1.setAutoFillBackground(False)\n        self.toolButton_1.setStyleSheet(\"\")\n        self.toolButton_1.setCheckable(True)\n        self.toolButton_1.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)\n        self.toolButton_1.setAutoRaise(False)\n        self.toolButton_1.setObjectName(\"toolButton_1\")\n        self._14.addWidget(self.toolButton_1)\n        self.cea_source_1 = QtWidgets.QComboBox(self.frame_25)\n        self.cea_source_1.setEnabled(False)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.cea_source_1.sizePolicy().hasHeightForWidth())\n        self.cea_source_1.setSizePolicy(sizePolicy)\n        self.cea_source_1.setMouseTracking(False)\n        self.cea_source_1.setFocusPolicy(QtCore.Qt.StrongFocus)\n        self.cea_source_1.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_1.setEditable(True)\n        self.cea_source_1.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)\n        self.cea_source_1.setObjectName(\"cea_source_1\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.setItemText(0, \"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self.cea_source_1.addItem(\"\")\n        self._14.addWidget(self.cea_source_1)\n        self.label_21 = QtWidgets.QLabel(self.frame_25)\n        self.label_21.setObjectName(\"label_21\")\n        self._14.addWidget(self.label_21)\n        self.cea_tag_1 = QtWidgets.QLineEdit(self.frame_25)\n        self.cea_tag_1.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_1.setObjectName(\"cea_tag_1\")\n        self._14.addWidget(self.cea_tag_1)\n        self.cea_cond_1 = QtWidgets.QCheckBox(self.frame_25)\n        self.cea_cond_1.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_1.setObjectName(\"cea_cond_1\")\n        self._14.addWidget(self.cea_cond_1)\n        self.gridLayout_14.addWidget(self.frame_25, 1, 0, 1, 1)\n        self._15 = QtWidgets.QHBoxLayout()\n        self._15.setContentsMargins(0, 0, 0, 0)\n        self._15.setSpacing(6)\n        self._15.setObjectName(\"_15\")\n        self.toolButton_2 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_2.setStyleSheet(\"\")\n        self.toolButton_2.setCheckable(True)\n        self.toolButton_2.setObjectName(\"toolButton_2\")\n        self._15.addWidget(self.toolButton_2)\n        self.cea_source_2 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_2.setEnabled(False)\n        self.cea_source_2.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_2.setEditable(True)\n        self.cea_source_2.setObjectName(\"cea_source_2\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.setItemText(0, \"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self.cea_source_2.addItem(\"\")\n        self._15.addWidget(self.cea_source_2)\n        self.label_23 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_23.setObjectName(\"label_23\")\n        self._15.addWidget(self.label_23)\n        self.cea_tag_2 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_2.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_2.setObjectName(\"cea_tag_2\")\n        self._15.addWidget(self.cea_tag_2)\n        self.cea_cond_2 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_2.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_2.setObjectName(\"cea_cond_2\")\n        self._15.addWidget(self.cea_cond_2)\n        self.gridLayout_14.addLayout(self._15, 2, 0, 1, 1)\n        self._16 = QtWidgets.QHBoxLayout()\n        self._16.setContentsMargins(0, 0, 0, 0)\n        self._16.setSpacing(6)\n        self._16.setObjectName(\"_16\")\n        self.toolButton_3 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_3.setStyleSheet(\"\")\n        self.toolButton_3.setCheckable(True)\n        self.toolButton_3.setObjectName(\"toolButton_3\")\n        self._16.addWidget(self.toolButton_3)\n        self.cea_source_3 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_3.setEnabled(False)\n        self.cea_source_3.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_3.setEditable(True)\n        self.cea_source_3.setObjectName(\"cea_source_3\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.setItemText(0, \"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self.cea_source_3.addItem(\"\")\n        self._16.addWidget(self.cea_source_3)\n        self.label_25 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_25.setObjectName(\"label_25\")\n        self._16.addWidget(self.label_25)\n        self.cea_tag_3 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_3.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_3.setObjectName(\"cea_tag_3\")\n        self._16.addWidget(self.cea_tag_3)\n        self.cea_cond_3 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_3.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_3.setObjectName(\"cea_cond_3\")\n        self._16.addWidget(self.cea_cond_3)\n        self.gridLayout_14.addLayout(self._16, 3, 0, 1, 1)\n        self._17 = QtWidgets.QHBoxLayout()\n        self._17.setContentsMargins(0, 0, 0, 0)\n        self._17.setSpacing(6)\n        self._17.setObjectName(\"_17\")\n        self.toolButton_4 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_4.setStyleSheet(\"\")\n        self.toolButton_4.setCheckable(True)\n        self.toolButton_4.setObjectName(\"toolButton_4\")\n        self._17.addWidget(self.toolButton_4)\n        self.cea_source_4 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_4.setEnabled(False)\n        self.cea_source_4.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_4.setEditable(True)\n        self.cea_source_4.setObjectName(\"cea_source_4\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.setItemText(0, \"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self.cea_source_4.addItem(\"\")\n        self._17.addWidget(self.cea_source_4)\n        self.label_27 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_27.setObjectName(\"label_27\")\n        self._17.addWidget(self.label_27)\n        self.cea_tag_4 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_4.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_4.setObjectName(\"cea_tag_4\")\n        self._17.addWidget(self.cea_tag_4)\n        self.cea_cond_4 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_4.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_4.setObjectName(\"cea_cond_4\")\n        self._17.addWidget(self.cea_cond_4)\n        self.gridLayout_14.addLayout(self._17, 4, 0, 1, 1)\n        self._18 = QtWidgets.QHBoxLayout()\n        self._18.setContentsMargins(0, 0, 0, 0)\n        self._18.setSpacing(6)\n        self._18.setObjectName(\"_18\")\n        self.toolButton_5 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_5.setStyleSheet(\"\")\n        self.toolButton_5.setCheckable(True)\n        self.toolButton_5.setObjectName(\"toolButton_5\")\n        self._18.addWidget(self.toolButton_5)\n        self.cea_source_5 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_5.setEnabled(False)\n        self.cea_source_5.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_5.setEditable(True)\n        self.cea_source_5.setObjectName(\"cea_source_5\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.setItemText(0, \"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self.cea_source_5.addItem(\"\")\n        self._18.addWidget(self.cea_source_5)\n        self.label_29 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_29.setObjectName(\"label_29\")\n        self._18.addWidget(self.label_29)\n        self.cea_tag_5 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_5.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_5.setObjectName(\"cea_tag_5\")\n        self._18.addWidget(self.cea_tag_5)\n        self.cea_cond_5 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_5.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_5.setObjectName(\"cea_cond_5\")\n        self._18.addWidget(self.cea_cond_5)\n        self.gridLayout_14.addLayout(self._18, 5, 0, 1, 1)\n        self._19 = QtWidgets.QHBoxLayout()\n        self._19.setContentsMargins(0, 0, 0, 0)\n        self._19.setSpacing(6)\n        self._19.setObjectName(\"_19\")\n        self.toolButton_6 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_6.setStyleSheet(\"\")\n        self.toolButton_6.setCheckable(True)\n        self.toolButton_6.setObjectName(\"toolButton_6\")\n        self._19.addWidget(self.toolButton_6)\n        self.cea_source_6 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_6.setEnabled(False)\n        self.cea_source_6.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_6.setEditable(True)\n        self.cea_source_6.setObjectName(\"cea_source_6\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.setItemText(0, \"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self.cea_source_6.addItem(\"\")\n        self._19.addWidget(self.cea_source_6)\n        self.label_31 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_31.setObjectName(\"label_31\")\n        self._19.addWidget(self.label_31)\n        self.cea_tag_6 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_6.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_6.setObjectName(\"cea_tag_6\")\n        self._19.addWidget(self.cea_tag_6)\n        self.cea_cond_6 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_6.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_6.setObjectName(\"cea_cond_6\")\n        self._19.addWidget(self.cea_cond_6)\n        self.gridLayout_14.addLayout(self._19, 6, 0, 1, 1)\n        self._20 = QtWidgets.QHBoxLayout()\n        self._20.setContentsMargins(0, 0, 0, 0)\n        self._20.setSpacing(6)\n        self._20.setObjectName(\"_20\")\n        self.toolButton_7 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_7.setStyleSheet(\"\")\n        self.toolButton_7.setCheckable(True)\n        self.toolButton_7.setObjectName(\"toolButton_7\")\n        self._20.addWidget(self.toolButton_7)\n        self.cea_source_7 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_7.setEnabled(False)\n        self.cea_source_7.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_7.setEditable(True)\n        self.cea_source_7.setObjectName(\"cea_source_7\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.setItemText(0, \"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self.cea_source_7.addItem(\"\")\n        self._20.addWidget(self.cea_source_7)\n        self.label_33 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_33.setObjectName(\"label_33\")\n        self._20.addWidget(self.label_33)\n        self.cea_tag_7 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_7.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_7.setObjectName(\"cea_tag_7\")\n        self._20.addWidget(self.cea_tag_7)\n        self.cea_cond_7 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_7.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_7.setObjectName(\"cea_cond_7\")\n        self._20.addWidget(self.cea_cond_7)\n        self.gridLayout_14.addLayout(self._20, 7, 0, 1, 1)\n        self._21 = QtWidgets.QHBoxLayout()\n        self._21.setContentsMargins(0, 0, 0, 0)\n        self._21.setSpacing(6)\n        self._21.setObjectName(\"_21\")\n        self.toolButton_8 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_8.setStyleSheet(\"\")\n        self.toolButton_8.setCheckable(True)\n        self.toolButton_8.setObjectName(\"toolButton_8\")\n        self._21.addWidget(self.toolButton_8)\n        self.cea_source_8 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_8.setEnabled(False)\n        self.cea_source_8.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_8.setEditable(True)\n        self.cea_source_8.setObjectName(\"cea_source_8\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.setItemText(0, \"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self.cea_source_8.addItem(\"\")\n        self._21.addWidget(self.cea_source_8)\n        self.label_35 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_35.setObjectName(\"label_35\")\n        self._21.addWidget(self.label_35)\n        self.cea_tag_8 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_8.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_8.setObjectName(\"cea_tag_8\")\n        self._21.addWidget(self.cea_tag_8)\n        self.cea_cond_8 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_8.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_8.setObjectName(\"cea_cond_8\")\n        self._21.addWidget(self.cea_cond_8)\n        self.gridLayout_14.addLayout(self._21, 8, 0, 1, 1)\n        self._30 = QtWidgets.QHBoxLayout()\n        self._30.setContentsMargins(0, 0, 0, 0)\n        self._30.setSpacing(6)\n        self._30.setObjectName(\"_30\")\n        self.toolButton_9 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_9.setStyleSheet(\"\")\n        self.toolButton_9.setCheckable(True)\n        self.toolButton_9.setObjectName(\"toolButton_9\")\n        self._30.addWidget(self.toolButton_9)\n        self.cea_source_9 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_9.setEnabled(False)\n        self.cea_source_9.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_9.setEditable(True)\n        self.cea_source_9.setObjectName(\"cea_source_9\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.setItemText(0, \"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self.cea_source_9.addItem(\"\")\n        self._30.addWidget(self.cea_source_9)\n        self.label_53 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_53.setObjectName(\"label_53\")\n        self._30.addWidget(self.label_53)\n        self.cea_tag_9 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_9.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_9.setObjectName(\"cea_tag_9\")\n        self._30.addWidget(self.cea_tag_9)\n        self.cea_cond_9 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_9.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_9.setObjectName(\"cea_cond_9\")\n        self._30.addWidget(self.cea_cond_9)\n        self.gridLayout_14.addLayout(self._30, 9, 0, 1, 1)\n        self._31 = QtWidgets.QHBoxLayout()\n        self._31.setContentsMargins(0, 0, 0, 0)\n        self._31.setSpacing(6)\n        self._31.setObjectName(\"_31\")\n        self.toolButton_10 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_10.setStyleSheet(\"\")\n        self.toolButton_10.setCheckable(True)\n        self.toolButton_10.setObjectName(\"toolButton_10\")\n        self._31.addWidget(self.toolButton_10)\n        self.cea_source_10 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_10.setEnabled(False)\n        self.cea_source_10.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_10.setEditable(True)\n        self.cea_source_10.setObjectName(\"cea_source_10\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.setItemText(0, \"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self.cea_source_10.addItem(\"\")\n        self._31.addWidget(self.cea_source_10)\n        self.label_55 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_55.setObjectName(\"label_55\")\n        self._31.addWidget(self.label_55)\n        self.cea_tag_10 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_10.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_10.setObjectName(\"cea_tag_10\")\n        self._31.addWidget(self.cea_tag_10)\n        self.cea_cond_10 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_10.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_10.setObjectName(\"cea_cond_10\")\n        self._31.addWidget(self.cea_cond_10)\n        self.gridLayout_14.addLayout(self._31, 10, 0, 1, 1)\n        self._32 = QtWidgets.QHBoxLayout()\n        self._32.setContentsMargins(0, 0, 0, 0)\n        self._32.setSpacing(6)\n        self._32.setObjectName(\"_32\")\n        self.toolButton_11 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_11.setStyleSheet(\"\")\n        self.toolButton_11.setCheckable(True)\n        self.toolButton_11.setObjectName(\"toolButton_11\")\n        self._32.addWidget(self.toolButton_11)\n        self.cea_source_11 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_11.setEnabled(False)\n        self.cea_source_11.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_11.setEditable(True)\n        self.cea_source_11.setObjectName(\"cea_source_11\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.setItemText(0, \"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self.cea_source_11.addItem(\"\")\n        self._32.addWidget(self.cea_source_11)\n        self.label_57 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_57.setObjectName(\"label_57\")\n        self._32.addWidget(self.label_57)\n        self.cea_tag_11 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_11.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_11.setObjectName(\"cea_tag_11\")\n        self._32.addWidget(self.cea_tag_11)\n        self.cea_cond_11 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_11.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_11.setObjectName(\"cea_cond_11\")\n        self._32.addWidget(self.cea_cond_11)\n        self.gridLayout_14.addLayout(self._32, 11, 0, 1, 1)\n        self._33 = QtWidgets.QHBoxLayout()\n        self._33.setContentsMargins(0, 0, 0, 0)\n        self._33.setSpacing(6)\n        self._33.setObjectName(\"_33\")\n        self.toolButton_12 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_12.setStyleSheet(\"\")\n        self.toolButton_12.setCheckable(True)\n        self.toolButton_12.setObjectName(\"toolButton_12\")\n        self._33.addWidget(self.toolButton_12)\n        self.cea_source_12 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_12.setEnabled(False)\n        self.cea_source_12.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_12.setEditable(True)\n        self.cea_source_12.setObjectName(\"cea_source_12\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.setItemText(0, \"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self.cea_source_12.addItem(\"\")\n        self._33.addWidget(self.cea_source_12)\n        self.label_59 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_59.setObjectName(\"label_59\")\n        self._33.addWidget(self.label_59)\n        self.cea_tag_12 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_12.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_12.setObjectName(\"cea_tag_12\")\n        self._33.addWidget(self.cea_tag_12)\n        self.cea_cond_12 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_12.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_12.setObjectName(\"cea_cond_12\")\n        self._33.addWidget(self.cea_cond_12)\n        self.gridLayout_14.addLayout(self._33, 12, 0, 1, 1)\n        self._34 = QtWidgets.QHBoxLayout()\n        self._34.setContentsMargins(0, 0, 0, 0)\n        self._34.setSpacing(6)\n        self._34.setObjectName(\"_34\")\n        self.toolButton_13 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_13.setStyleSheet(\"\")\n        self.toolButton_13.setCheckable(True)\n        self.toolButton_13.setObjectName(\"toolButton_13\")\n        self._34.addWidget(self.toolButton_13)\n        self.cea_source_13 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_13.setEnabled(False)\n        self.cea_source_13.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_13.setEditable(True)\n        self.cea_source_13.setObjectName(\"cea_source_13\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.setItemText(0, \"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self.cea_source_13.addItem(\"\")\n        self._34.addWidget(self.cea_source_13)\n        self.label_61 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_61.setObjectName(\"label_61\")\n        self._34.addWidget(self.label_61)\n        self.cea_tag_13 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_13.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_13.setObjectName(\"cea_tag_13\")\n        self._34.addWidget(self.cea_tag_13)\n        self.cea_cond_13 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_13.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_13.setObjectName(\"cea_cond_13\")\n        self._34.addWidget(self.cea_cond_13)\n        self.gridLayout_14.addLayout(self._34, 13, 0, 1, 1)\n        self._35 = QtWidgets.QHBoxLayout()\n        self._35.setContentsMargins(0, 0, 0, 0)\n        self._35.setSpacing(6)\n        self._35.setObjectName(\"_35\")\n        self.toolButton_14 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_14.setStyleSheet(\"\")\n        self.toolButton_14.setCheckable(True)\n        self.toolButton_14.setObjectName(\"toolButton_14\")\n        self._35.addWidget(self.toolButton_14)\n        self.cea_source_14 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_14.setEnabled(False)\n        self.cea_source_14.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_14.setEditable(True)\n        self.cea_source_14.setObjectName(\"cea_source_14\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.setItemText(0, \"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self.cea_source_14.addItem(\"\")\n        self._35.addWidget(self.cea_source_14)\n        self.label_63 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_63.setObjectName(\"label_63\")\n        self._35.addWidget(self.label_63)\n        self.cea_tag_14 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_14.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_14.setObjectName(\"cea_tag_14\")\n        self._35.addWidget(self.cea_tag_14)\n        self.cea_cond_14 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_14.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_14.setObjectName(\"cea_cond_14\")\n        self._35.addWidget(self.cea_cond_14)\n        self.gridLayout_14.addLayout(self._35, 14, 0, 1, 1)\n        self._36 = QtWidgets.QHBoxLayout()\n        self._36.setContentsMargins(0, 0, 0, 0)\n        self._36.setSpacing(6)\n        self._36.setObjectName(\"_36\")\n        self.toolButton_15 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_15.setStyleSheet(\"\")\n        self.toolButton_15.setCheckable(True)\n        self.toolButton_15.setObjectName(\"toolButton_15\")\n        self._36.addWidget(self.toolButton_15)\n        self.cea_source_15 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_15.setEnabled(False)\n        self.cea_source_15.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_15.setEditable(True)\n        self.cea_source_15.setObjectName(\"cea_source_15\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.setItemText(0, \"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self.cea_source_15.addItem(\"\")\n        self._36.addWidget(self.cea_source_15)\n        self.label_65 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_65.setObjectName(\"label_65\")\n        self._36.addWidget(self.label_65)\n        self.cea_tag_15 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_15.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_15.setObjectName(\"cea_tag_15\")\n        self._36.addWidget(self.cea_tag_15)\n        self.cea_cond_15 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_15.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_15.setObjectName(\"cea_cond_15\")\n        self._36.addWidget(self.cea_cond_15)\n        self.gridLayout_14.addLayout(self._36, 15, 0, 1, 1)\n        self._37 = QtWidgets.QHBoxLayout()\n        self._37.setContentsMargins(0, 0, 0, 0)\n        self._37.setSpacing(6)\n        self._37.setObjectName(\"_37\")\n        self.toolButton_16 = QtWidgets.QToolButton(self.tagmap_details_box)\n        self.toolButton_16.setStyleSheet(\"\")\n        self.toolButton_16.setCheckable(True)\n        self.toolButton_16.setObjectName(\"toolButton_16\")\n        self._37.addWidget(self.toolButton_16)\n        self.cea_source_16 = QtWidgets.QComboBox(self.tagmap_details_box)\n        self.cea_source_16.setEnabled(False)\n        self.cea_source_16.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_source_16.setEditable(True)\n        self.cea_source_16.setObjectName(\"cea_source_16\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.setItemText(0, \"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self.cea_source_16.addItem(\"\")\n        self._37.addWidget(self.cea_source_16)\n        self.label_67 = QtWidgets.QLabel(self.tagmap_details_box)\n        self.label_67.setObjectName(\"label_67\")\n        self._37.addWidget(self.label_67)\n        self.cea_tag_16 = QtWidgets.QLineEdit(self.tagmap_details_box)\n        self.cea_tag_16.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_tag_16.setObjectName(\"cea_tag_16\")\n        self._37.addWidget(self.cea_tag_16)\n        self.cea_cond_16 = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_cond_16.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_cond_16.setObjectName(\"cea_cond_16\")\n        self._37.addWidget(self.cea_cond_16)\n        self.gridLayout_14.addLayout(self._37, 16, 0, 1, 1)\n        self._38 = QtWidgets.QHBoxLayout()\n        self._38.setContentsMargins(0, 0, 0, 0)\n        self._38.setSpacing(6)\n        self._38.setObjectName(\"_38\")\n        self.label_42 = QtWidgets.QLabel(self.tagmap_details_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_42.sizePolicy().hasHeightForWidth())\n        self.label_42.setSizePolicy(sizePolicy)\n        self.label_42.setLayoutDirection(QtCore.Qt.LeftToRight)\n        self.label_42.setLineWidth(1)\n        self.label_42.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)\n        self.label_42.setObjectName(\"label_42\")\n        self._38.addWidget(self.label_42)\n        self.label_17 = QtWidgets.QLabel(self.tagmap_details_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_17.sizePolicy().hasHeightForWidth())\n        self.label_17.setSizePolicy(sizePolicy)\n        self.label_17.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.label_17.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)\n        self.label_17.setObjectName(\"label_17\")\n        self._38.addWidget(self.label_17)\n        self.gridLayout_14.addLayout(self._38, 17, 0, 1, 1)\n        self.cea_tag_sort = QtWidgets.QCheckBox(self.tagmap_details_box)\n        self.cea_tag_sort.setLayoutDirection(QtCore.Qt.LeftToRight)\n        self.cea_tag_sort.setObjectName(\"cea_tag_sort\")\n        self.gridLayout_14.addWidget(self.cea_tag_sort, 18, 0, 1, 1)\n        self.verticalLayout_3.addWidget(self.tagmap_details_box)\n        self.verticalLayout_33.addWidget(self.tagmap_details_frame)\n        self.scrollArea_4.setWidget(self.scrollAreaWidgetContents_8)\n        self.verticalLayout_28.addWidget(self.scrollArea_4)\n        self.tabWidget.addTab(self.Tag_mapping, \"\")\n        self.Advanced = QtWidgets.QWidget()\n        self.Advanced.setObjectName(\"Advanced\")\n        self.horizontalLayout_31 = QtWidgets.QHBoxLayout(self.Advanced)\n        self.horizontalLayout_31.setObjectName(\"horizontalLayout_31\")\n        self.scrollArea_2 = QtWidgets.QScrollArea(self.Advanced)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.scrollArea_2.sizePolicy().hasHeightForWidth())\n        self.scrollArea_2.setSizePolicy(sizePolicy)\n        self.scrollArea_2.setWidgetResizable(True)\n        self.scrollArea_2.setObjectName(\"scrollArea_2\")\n        self.scrollAreaWidgetContents_2 = QtWidgets.QWidget()\n        self.scrollAreaWidgetContents_2.setGeometry(QtCore.QRect(0, -853, 1084, 1707))\n        self.scrollAreaWidgetContents_2.setObjectName(\"scrollAreaWidgetContents_2\")\n        self.verticalLayout_18 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents_2)\n        self.verticalLayout_18.setObjectName(\"verticalLayout_18\")\n        self.advanced_general_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_2)\n        self.advanced_general_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.advanced_general_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.advanced_general_frame.setObjectName(\"advanced_general_frame\")\n        self.verticalLayout_55 = QtWidgets.QVBoxLayout(self.advanced_general_frame)\n        self.verticalLayout_55.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_55.setSpacing(0)\n        self.verticalLayout_55.setObjectName(\"verticalLayout_55\")\n        self.label_110 = QtWidgets.QLabel(self.advanced_general_frame)\n        self.label_110.setStyleSheet(\"background-color: rgb(208, 208, 156);\")\n        self.label_110.setObjectName(\"label_110\")\n        self.verticalLayout_55.addWidget(self.label_110)\n        self.advanced_general_box = QtWidgets.QGroupBox(self.advanced_general_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.advanced_general_box.setPalette(palette)\n        self.advanced_general_box.setAutoFillBackground(False)\n        self.advanced_general_box.setStyleSheet(\"background-color: rgb(229, 229, 197);\")\n        self.advanced_general_box.setTitle(\"\")\n        self.advanced_general_box.setObjectName(\"advanced_general_box\")\n        self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.advanced_general_box)\n        self.verticalLayout_5.setObjectName(\"verticalLayout_5\")\n        self.ce_no_run = QtWidgets.QCheckBox(self.advanced_general_box)\n        self.ce_no_run.setObjectName(\"ce_no_run\")\n        self.verticalLayout_5.addWidget(self.ce_no_run)\n        self.verticalLayout_55.addWidget(self.advanced_general_box)\n        self.verticalLayout_18.addWidget(self.advanced_general_frame)\n        self.advanced_artists_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_2)\n        self.advanced_artists_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.advanced_artists_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.advanced_artists_frame.setObjectName(\"advanced_artists_frame\")\n        self.verticalLayout_56 = QtWidgets.QVBoxLayout(self.advanced_artists_frame)\n        self.verticalLayout_56.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_56.setSpacing(0)\n        self.verticalLayout_56.setObjectName(\"verticalLayout_56\")\n        self.advanced_artists_label = QtWidgets.QLabel(self.advanced_artists_frame)\n        self.advanced_artists_label.setStyleSheet(\"background-color: rgb(208, 208, 156);\")\n        self.advanced_artists_label.setObjectName(\"advanced_artists_label\")\n        self.verticalLayout_56.addWidget(self.advanced_artists_label)\n        self.advanced_artists_box = QtWidgets.QGroupBox(self.advanced_artists_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.advanced_artists_box.setPalette(palette)\n        self.advanced_artists_box.setAutoFillBackground(False)\n        self.advanced_artists_box.setStyleSheet(\"background-color: rgb(229, 229, 197);\")\n        self.advanced_artists_box.setTitle(\"\")\n        self.advanced_artists_box.setObjectName(\"advanced_artists_box\")\n        self.verticalLayout_20 = QtWidgets.QVBoxLayout(self.advanced_artists_box)\n        self.verticalLayout_20.setObjectName(\"verticalLayout_20\")\n        self.ensemble_strings_label = QtWidgets.QLabel(self.advanced_artists_box)\n        self.ensemble_strings_label.setObjectName(\"ensemble_strings_label\")\n        self.verticalLayout_20.addWidget(self.ensemble_strings_label)\n        self.ensemble_strings_box = QtWidgets.QGroupBox(self.advanced_artists_box)\n        self.ensemble_strings_box.setTitle(\"\")\n        self.ensemble_strings_box.setObjectName(\"ensemble_strings_box\")\n        self._13 = QtWidgets.QVBoxLayout(self.ensemble_strings_box)\n        self._13.setContentsMargins(9, 9, 9, 9)\n        self._13.setSpacing(2)\n        self._13.setObjectName(\"_13\")\n        self.cea_orchestras_2 = QtWidgets.QLabel(self.ensemble_strings_box)\n        self.cea_orchestras_2.setObjectName(\"cea_orchestras_2\")\n        self._13.addWidget(self.cea_orchestras_2)\n        self.cea_orchestras = QtWidgets.QLineEdit(self.ensemble_strings_box)\n        self.cea_orchestras.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_orchestras.setObjectName(\"cea_orchestras\")\n        self._13.addWidget(self.cea_orchestras)\n        self.cea_choirs_2 = QtWidgets.QLabel(self.ensemble_strings_box)\n        self.cea_choirs_2.setObjectName(\"cea_choirs_2\")\n        self._13.addWidget(self.cea_choirs_2)\n        self.cea_choirs = QtWidgets.QLineEdit(self.ensemble_strings_box)\n        self.cea_choirs.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_choirs.setObjectName(\"cea_choirs\")\n        self._13.addWidget(self.cea_choirs)\n        self.cea_groups_2 = QtWidgets.QLabel(self.ensemble_strings_box)\n        self.cea_groups_2.setObjectName(\"cea_groups_2\")\n        self._13.addWidget(self.cea_groups_2)\n        self.cea_groups = QtWidgets.QLineEdit(self.ensemble_strings_box)\n        self.cea_groups.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_groups.setObjectName(\"cea_groups\")\n        self._13.addWidget(self.cea_groups)\n        self.verticalLayout_20.addWidget(self.ensemble_strings_box)\n        self.verticalLayout_56.addWidget(self.advanced_artists_box)\n        self.verticalLayout_18.addWidget(self.advanced_artists_frame)\n        self.advanced_workparts_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_2)\n        self.advanced_workparts_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.advanced_workparts_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.advanced_workparts_frame.setObjectName(\"advanced_workparts_frame\")\n        self.verticalLayout_57 = QtWidgets.QVBoxLayout(self.advanced_workparts_frame)\n        self.verticalLayout_57.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_57.setSpacing(0)\n        self.verticalLayout_57.setObjectName(\"verticalLayout_57\")\n        self.advanced_workparts_label = QtWidgets.QLabel(self.advanced_workparts_frame)\n        self.advanced_workparts_label.setStyleSheet(\"background-color: rgb(208, 208, 156);\")\n        self.advanced_workparts_label.setObjectName(\"advanced_workparts_label\")\n        self.verticalLayout_57.addWidget(self.advanced_workparts_label)\n        self.advanced_workparts_box = QtWidgets.QGroupBox(self.advanced_workparts_frame)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.advanced_workparts_box.sizePolicy().hasHeightForWidth())\n        self.advanced_workparts_box.setSizePolicy(sizePolicy)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.advanced_workparts_box.setPalette(palette)\n        self.advanced_workparts_box.setAutoFillBackground(False)\n        self.advanced_workparts_box.setStyleSheet(\"background-color: rgb(229, 229, 197);\")\n        self.advanced_workparts_box.setTitle(\"\")\n        self.advanced_workparts_box.setObjectName(\"advanced_workparts_box\")\n        self.verticalLayout = QtWidgets.QVBoxLayout(self.advanced_workparts_box)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self._2 = QtWidgets.QHBoxLayout()\n        self._2.setContentsMargins(0, 0, 0, 0)\n        self._2.setSpacing(6)\n        self._2.setObjectName(\"_2\")\n        self.label_4 = QtWidgets.QLabel(self.advanced_workparts_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth())\n        self.label_4.setSizePolicy(sizePolicy)\n        self.label_4.setObjectName(\"label_4\")\n        self._2.addWidget(self.label_4)\n        self.cwp_retries = QtWidgets.QSpinBox(self.advanced_workparts_box)\n        self.cwp_retries.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_retries.setSuffix(\"\")\n        self.cwp_retries.setMinimum(0)\n        self.cwp_retries.setMaximum(20)\n        self.cwp_retries.setObjectName(\"cwp_retries\")\n        self._2.addWidget(self.cwp_retries)\n        self.verticalLayout.addLayout(self._2)\n        self._23 = QtWidgets.QHBoxLayout()\n        self._23.setContentsMargins(0, 0, 0, 0)\n        self._23.setSpacing(6)\n        self._23.setObjectName(\"_23\")\n        self.label_120 = QtWidgets.QLabel(self.advanced_workparts_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_120.sizePolicy().hasHeightForWidth())\n        self.label_120.setSizePolicy(sizePolicy)\n        self.label_120.setObjectName(\"label_120\")\n        self._23.addWidget(self.label_120)\n        self.cwp_allow_empty_parts = QtWidgets.QCheckBox(self.advanced_workparts_box)\n        self.cwp_allow_empty_parts.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cwp_allow_empty_parts.setText(\"\")\n        self.cwp_allow_empty_parts.setObjectName(\"cwp_allow_empty_parts\")\n        self._23.addWidget(self.cwp_allow_empty_parts)\n        self.verticalLayout.addLayout(self._23)\n        self.parent_child_text_box = QtWidgets.QGroupBox(self.advanced_workparts_box)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(241, 241, 167))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(241, 241, 167))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(241, 241, 167))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(241, 241, 167))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(241, 241, 167))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(241, 241, 167))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(241, 241, 167))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(241, 241, 167))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(241, 241, 167))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.parent_child_text_box.setPalette(palette)\n        self.parent_child_text_box.setAutoFillBackground(False)\n        self.parent_child_text_box.setStyleSheet(\"background-color: rgb(241, 241, 167);\")\n        self.parent_child_text_box.setTitle(\"\")\n        self.parent_child_text_box.setObjectName(\"parent_child_text_box\")\n        self.verticalLayout_35 = QtWidgets.QVBoxLayout(self.parent_child_text_box)\n        self.verticalLayout_35.setObjectName(\"verticalLayout_35\")\n        self.label_114 = QtWidgets.QLabel(self.parent_child_text_box)\n        self.label_114.setObjectName(\"label_114\")\n        self.verticalLayout_35.addWidget(self.label_114)\n        self._22 = QtWidgets.QHBoxLayout()\n        self._22.setContentsMargins(0, 0, 0, 0)\n        self._22.setSpacing(6)\n        self._22.setObjectName(\"_22\")\n        self.label_90 = QtWidgets.QLabel(self.parent_child_text_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_90.sizePolicy().hasHeightForWidth())\n        self.label_90.setSizePolicy(sizePolicy)\n        self.label_90.setObjectName(\"label_90\")\n        self._22.addWidget(self.label_90)\n        self.cwp_common_chars = QtWidgets.QSpinBox(self.parent_child_text_box)\n        self.cwp_common_chars.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_common_chars.setSuffix(\"\")\n        self.cwp_common_chars.setMinimum(0)\n        self.cwp_common_chars.setMaximum(99)\n        self.cwp_common_chars.setObjectName(\"cwp_common_chars\")\n        self._22.addWidget(self.cwp_common_chars)\n        self.verticalLayout_35.addLayout(self._22)\n        self.label_89 = QtWidgets.QLabel(self.parent_child_text_box)\n        self.label_89.setObjectName(\"label_89\")\n        self.verticalLayout_35.addWidget(self.label_89)\n        self.verticalLayout.addWidget(self.parent_child_text_box)\n        self.title_metadata_box = QtWidgets.QGroupBox(self.advanced_workparts_box)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(202, 202, 140))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(202, 202, 140))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(202, 202, 140))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(202, 202, 140))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(202, 202, 140))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(202, 202, 140))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(202, 202, 140))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(202, 202, 140))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(202, 202, 140))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.title_metadata_box.setPalette(palette)\n        self.title_metadata_box.setAutoFillBackground(False)\n        self.title_metadata_box.setStyleSheet(\"background-color: rgb(202, 202, 140);\")\n        self.title_metadata_box.setTitle(\"\")\n        self.title_metadata_box.setObjectName(\"title_metadata_box\")\n        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.title_metadata_box)\n        self.verticalLayout_2.setObjectName(\"verticalLayout_2\")\n        self.label_115 = QtWidgets.QLabel(self.title_metadata_box)\n        self.label_115.setObjectName(\"label_115\")\n        self.verticalLayout_2.addWidget(self.label_115)\n        self._3 = QtWidgets.QHBoxLayout()\n        self._3.setContentsMargins(0, 0, 0, 0)\n        self._3.setSpacing(6)\n        self._3.setObjectName(\"_3\")\n        self.label_6 = QtWidgets.QLabel(self.title_metadata_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_6.sizePolicy().hasHeightForWidth())\n        self.label_6.setSizePolicy(sizePolicy)\n        self.label_6.setObjectName(\"label_6\")\n        self._3.addWidget(self.label_6)\n        self.cwp_proximity = QtWidgets.QSpinBox(self.title_metadata_box)\n        self.cwp_proximity.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_proximity.setSuffix(\"\")\n        self.cwp_proximity.setMaximum(99)\n        self.cwp_proximity.setObjectName(\"cwp_proximity\")\n        self._3.addWidget(self.cwp_proximity)\n        self.verticalLayout_2.addLayout(self._3)\n        self._4 = QtWidgets.QHBoxLayout()\n        self._4.setContentsMargins(0, 0, 0, 0)\n        self._4.setSpacing(6)\n        self._4.setObjectName(\"_4\")\n        self.label_7 = QtWidgets.QLabel(self.title_metadata_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_7.sizePolicy().hasHeightForWidth())\n        self.label_7.setSizePolicy(sizePolicy)\n        self.label_7.setObjectName(\"label_7\")\n        self._4.addWidget(self.label_7)\n        self.cwp_end_proximity = QtWidgets.QSpinBox(self.title_metadata_box)\n        self.cwp_end_proximity.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_end_proximity.setSuffix(\"\")\n        self.cwp_end_proximity.setMaximum(99)\n        self.cwp_end_proximity.setObjectName(\"cwp_end_proximity\")\n        self._4.addWidget(self.cwp_end_proximity)\n        self.verticalLayout_2.addLayout(self._4)\n        self._5 = QtWidgets.QHBoxLayout()\n        self._5.setContentsMargins(0, 0, 0, 0)\n        self._5.setSpacing(6)\n        self._5.setObjectName(\"_5\")\n        self.label_5 = QtWidgets.QLabel(self.title_metadata_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_5.sizePolicy().hasHeightForWidth())\n        self.label_5.setSizePolicy(sizePolicy)\n        self.label_5.setObjectName(\"label_5\")\n        self._5.addWidget(self.label_5)\n        self.cwp_split_hyphenated = QtWidgets.QCheckBox(self.title_metadata_box)\n        self.cwp_split_hyphenated.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cwp_split_hyphenated.setText(\"\")\n        self.cwp_split_hyphenated.setObjectName(\"cwp_split_hyphenated\")\n        self._5.addWidget(self.cwp_split_hyphenated)\n        self.verticalLayout_2.addLayout(self._5)\n        self._25 = QtWidgets.QHBoxLayout()\n        self._25.setContentsMargins(0, 0, 0, 0)\n        self._25.setSpacing(6)\n        self._25.setObjectName(\"_25\")\n        self.label_93 = QtWidgets.QLabel(self.title_metadata_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_93.sizePolicy().hasHeightForWidth())\n        self.label_93.setSizePolicy(sizePolicy)\n        self.label_93.setObjectName(\"label_93\")\n        self._25.addWidget(self.label_93)\n        self.cwp_substring_match = QtWidgets.QSpinBox(self.title_metadata_box)\n        self.cwp_substring_match.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_substring_match.setMaximum(100)\n        self.cwp_substring_match.setObjectName(\"cwp_substring_match\")\n        self._25.addWidget(self.cwp_substring_match)\n        self.verticalLayout_2.addLayout(self._25)\n        self._7 = QtWidgets.QHBoxLayout()\n        self._7.setContentsMargins(0, 0, 0, 0)\n        self._7.setSpacing(6)\n        self._7.setObjectName(\"_7\")\n        self.label_87 = QtWidgets.QLabel(self.title_metadata_box)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_87.sizePolicy().hasHeightForWidth())\n        self.label_87.setSizePolicy(sizePolicy)\n        self.label_87.setObjectName(\"label_87\")\n        self._7.addWidget(self.label_87)\n        self.cwp_fill_part = QtWidgets.QCheckBox(self.title_metadata_box)\n        self.cwp_fill_part.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cwp_fill_part.setText(\"\")\n        self.cwp_fill_part.setObjectName(\"cwp_fill_part\")\n        self._7.addWidget(self.cwp_fill_part)\n        self.verticalLayout_2.addLayout(self._7)\n        self.line_8 = QtWidgets.QFrame(self.title_metadata_box)\n        self.line_8.setFrameShape(QtWidgets.QFrame.HLine)\n        self.line_8.setFrameShadow(QtWidgets.QFrame.Sunken)\n        self.line_8.setObjectName(\"line_8\")\n        self.verticalLayout_2.addWidget(self.line_8)\n        self.label_92 = QtWidgets.QLabel(self.title_metadata_box)\n        self.label_92.setObjectName(\"label_92\")\n        self.verticalLayout_2.addWidget(self.label_92)\n        self.label_91 = QtWidgets.QLabel(self.title_metadata_box)\n        self.label_91.setObjectName(\"label_91\")\n        self.verticalLayout_2.addWidget(self.label_91)\n        self.cwp_prepositions = QtWidgets.QLineEdit(self.title_metadata_box)\n        self.cwp_prepositions.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_prepositions.setObjectName(\"cwp_prepositions\")\n        self.verticalLayout_2.addWidget(self.cwp_prepositions)\n        self.cwp_removewords_2 = QtWidgets.QLabel(self.title_metadata_box)\n        self.cwp_removewords_2.setObjectName(\"cwp_removewords_2\")\n        self.verticalLayout_2.addWidget(self.cwp_removewords_2)\n        self.cwp_removewords = QtWidgets.QLineEdit(self.title_metadata_box)\n        self.cwp_removewords.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_removewords.setObjectName(\"cwp_removewords\")\n        self.verticalLayout_2.addWidget(self.cwp_removewords)\n        self.line = QtWidgets.QFrame(self.title_metadata_box)\n        self.line.setFrameShape(QtWidgets.QFrame.HLine)\n        self.line.setFrameShadow(QtWidgets.QFrame.Sunken)\n        self.line.setObjectName(\"line\")\n        self.verticalLayout_2.addWidget(self.line)\n        self.cwp_synonyms_2 = QtWidgets.QLabel(self.title_metadata_box)\n        self.cwp_synonyms_2.setObjectName(\"cwp_synonyms_2\")\n        self.verticalLayout_2.addWidget(self.cwp_synonyms_2)\n        self.cwp_synonyms = QtWidgets.QTextEdit(self.title_metadata_box)\n        self.cwp_synonyms.setMaximumSize(QtCore.QSize(16777215, 120))\n        self.cwp_synonyms.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_synonyms.setObjectName(\"cwp_synonyms\")\n        self.verticalLayout_2.addWidget(self.cwp_synonyms)\n        self.label_16 = QtWidgets.QLabel(self.title_metadata_box)\n        self.label_16.setObjectName(\"label_16\")\n        self.verticalLayout_2.addWidget(self.label_16)\n        self.cwp_replacements = QtWidgets.QLineEdit(self.title_metadata_box)\n        self.cwp_replacements.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_replacements.setObjectName(\"cwp_replacements\")\n        self.verticalLayout_2.addWidget(self.cwp_replacements)\n        self.verticalLayout.addWidget(self.title_metadata_box)\n        self.verticalLayout_57.addWidget(self.advanced_workparts_box)\n        self.verticalLayout_18.addWidget(self.advanced_workparts_frame)\n        self.advanced_genres_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_2)\n        self.advanced_genres_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.advanced_genres_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.advanced_genres_frame.setObjectName(\"advanced_genres_frame\")\n        self.verticalLayout_58 = QtWidgets.QVBoxLayout(self.advanced_genres_frame)\n        self.verticalLayout_58.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_58.setSpacing(0)\n        self.verticalLayout_58.setObjectName(\"verticalLayout_58\")\n        self.advanced_genres_label = QtWidgets.QLabel(self.advanced_genres_frame)\n        self.advanced_genres_label.setStyleSheet(\"background-color: rgb(208, 208, 156);\")\n        self.advanced_genres_label.setObjectName(\"advanced_genres_label\")\n        self.verticalLayout_58.addWidget(self.advanced_genres_label)\n        self.advanced_genres_box = QtWidgets.QGroupBox(self.advanced_genres_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.advanced_genres_box.setPalette(palette)\n        self.advanced_genres_box.setAutoFillBackground(False)\n        self.advanced_genres_box.setStyleSheet(\"background-color: rgb(229, 229, 197);\")\n        self.advanced_genres_box.setTitle(\"\")\n        self.advanced_genres_box.setObjectName(\"advanced_genres_box\")\n        self.gridLayout_8 = QtWidgets.QGridLayout(self.advanced_genres_box)\n        self.gridLayout_8.setObjectName(\"gridLayout_8\")\n        self.label_85 = QtWidgets.QLabel(self.advanced_genres_box)\n        self.label_85.setObjectName(\"label_85\")\n        self.gridLayout_8.addWidget(self.label_85, 1, 0, 1, 1)\n        self.cwp_muso_refdb = QtWidgets.QLineEdit(self.advanced_genres_box)\n        self.cwp_muso_refdb.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_muso_refdb.setObjectName(\"cwp_muso_refdb\")\n        self.gridLayout_8.addWidget(self.cwp_muso_refdb, 1, 3, 1, 1)\n        self.label_84 = QtWidgets.QLabel(self.advanced_genres_box)\n        self.label_84.setObjectName(\"label_84\")\n        self.gridLayout_8.addWidget(self.label_84, 1, 2, 1, 1)\n        self.cwp_muso_path = QtWidgets.QLineEdit(self.advanced_genres_box)\n        self.cwp_muso_path.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_muso_path.setObjectName(\"cwp_muso_path\")\n        self.gridLayout_8.addWidget(self.cwp_muso_path, 1, 1, 1, 1)\n        self.label_86 = QtWidgets.QLabel(self.advanced_genres_box)\n        self.label_86.setObjectName(\"label_86\")\n        self.gridLayout_8.addWidget(self.label_86, 0, 0, 1, 2)\n        self.verticalLayout_58.addWidget(self.advanced_genres_box)\n        self.verticalLayout_18.addWidget(self.advanced_genres_frame)\n        self.logging_frame = QtWidgets.QFrame(self.scrollAreaWidgetContents_2)\n        self.logging_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.logging_frame.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.logging_frame.setObjectName(\"logging_frame\")\n        self.verticalLayout_59 = QtWidgets.QVBoxLayout(self.logging_frame)\n        self.verticalLayout_59.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_59.setSpacing(0)\n        self.verticalLayout_59.setObjectName(\"verticalLayout_59\")\n        self.label_117 = QtWidgets.QLabel(self.logging_frame)\n        self.label_117.setStyleSheet(\"background-color: rgb(208, 208, 156);\")\n        self.label_117.setObjectName(\"label_117\")\n        self.verticalLayout_59.addWidget(self.label_117)\n        self.logging_box = QtWidgets.QGroupBox(self.logging_frame)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.logging_box.setPalette(palette)\n        self.logging_box.setLayoutDirection(QtCore.Qt.LeftToRight)\n        self.logging_box.setAutoFillBackground(False)\n        self.logging_box.setStyleSheet(\"background-color: rgb(229, 229, 197);\")\n        self.logging_box.setTitle(\"\")\n        self.logging_box.setObjectName(\"logging_box\")\n        self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.logging_box)\n        self.horizontalLayout_2.setObjectName(\"horizontalLayout_2\")\n        self.log_error = QtWidgets.QCheckBox(self.logging_box)\n        self.log_error.setObjectName(\"log_error\")\n        self.horizontalLayout_2.addWidget(self.log_error)\n        self.log_warning = QtWidgets.QCheckBox(self.logging_box)\n        self.log_warning.setObjectName(\"log_warning\")\n        self.horizontalLayout_2.addWidget(self.log_warning)\n        self.log_debug = QtWidgets.QCheckBox(self.logging_box)\n        self.log_debug.setObjectName(\"log_debug\")\n        self.horizontalLayout_2.addWidget(self.log_debug)\n        self.custom_logging_box = QtWidgets.QGroupBox(self.logging_box)\n        self.custom_logging_box.setStyleSheet(\"background-color: rgb(229, 229, 159);\")\n        self.custom_logging_box.setObjectName(\"custom_logging_box\")\n        self.horizontalLayout_34 = QtWidgets.QHBoxLayout(self.custom_logging_box)\n        self.horizontalLayout_34.setObjectName(\"horizontalLayout_34\")\n        self.log_basic = QtWidgets.QRadioButton(self.custom_logging_box)\n        self.log_basic.setObjectName(\"log_basic\")\n        self.horizontalLayout_34.addWidget(self.log_basic)\n        self.log_info = QtWidgets.QRadioButton(self.custom_logging_box)\n        self.log_info.setObjectName(\"log_info\")\n        self.horizontalLayout_34.addWidget(self.log_info)\n        self.horizontalLayout_2.addWidget(self.custom_logging_box)\n        self.verticalLayout_59.addWidget(self.logging_box)\n        self.verticalLayout_18.addWidget(self.logging_frame)\n        self.special_tags_frame_outer = QtWidgets.QFrame(self.scrollAreaWidgetContents_2)\n        self.special_tags_frame_outer.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.special_tags_frame_outer.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.special_tags_frame_outer.setObjectName(\"special_tags_frame_outer\")\n        self.verticalLayout_60 = QtWidgets.QVBoxLayout(self.special_tags_frame_outer)\n        self.verticalLayout_60.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_60.setSpacing(0)\n        self.verticalLayout_60.setObjectName(\"verticalLayout_60\")\n        self.label_118 = QtWidgets.QLabel(self.special_tags_frame_outer)\n        self.label_118.setStyleSheet(\"background-color: rgb(208, 208, 156);\")\n        self.label_118.setObjectName(\"label_118\")\n        self.verticalLayout_60.addWidget(self.label_118)\n        self.special_tags_frame_inner = QtWidgets.QFrame(self.special_tags_frame_outer)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 197))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.special_tags_frame_inner.setPalette(palette)\n        self.special_tags_frame_inner.setAutoFillBackground(False)\n        self.special_tags_frame_inner.setStyleSheet(\"background-color: rgb(229, 229, 197);\")\n        self.special_tags_frame_inner.setFrameShape(QtWidgets.QFrame.StyledPanel)\n        self.special_tags_frame_inner.setFrameShadow(QtWidgets.QFrame.Raised)\n        self.special_tags_frame_inner.setObjectName(\"special_tags_frame_inner\")\n        self.verticalLayout_16 = QtWidgets.QVBoxLayout(self.special_tags_frame_inner)\n        self.verticalLayout_16.setObjectName(\"verticalLayout_16\")\n        self.save_options_box = QtWidgets.QGroupBox(self.special_tags_frame_inner)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.save_options_box.setPalette(palette)\n        self.save_options_box.setAutoFillBackground(False)\n        self.save_options_box.setStyleSheet(\"background-color: rgb(229, 229, 159);\")\n        self.save_options_box.setObjectName(\"save_options_box\")\n        self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.save_options_box)\n        self.horizontalLayout_4.setObjectName(\"horizontalLayout_4\")\n        self.label_41 = QtWidgets.QLabel(self.save_options_box)\n        self.label_41.setObjectName(\"label_41\")\n        self.horizontalLayout_4.addWidget(self.label_41)\n        self.ce_version_tag = QtWidgets.QLineEdit(self.save_options_box)\n        self.ce_version_tag.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.ce_version_tag.setObjectName(\"ce_version_tag\")\n        self.horizontalLayout_4.addWidget(self.ce_version_tag)\n        self.label_36 = QtWidgets.QLabel(self.save_options_box)\n        self.label_36.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.label_36.setObjectName(\"label_36\")\n        self.horizontalLayout_4.addWidget(self.label_36)\n        self.cea_options_tag = QtWidgets.QLineEdit(self.save_options_box)\n        self.cea_options_tag.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cea_options_tag.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cea_options_tag.setObjectName(\"cea_options_tag\")\n        self.horizontalLayout_4.addWidget(self.cea_options_tag)\n        self.label_38 = QtWidgets.QLabel(self.save_options_box)\n        self.label_38.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.label_38.setObjectName(\"label_38\")\n        self.horizontalLayout_4.addWidget(self.label_38)\n        self.cwp_options_tag = QtWidgets.QLineEdit(self.save_options_box)\n        self.cwp_options_tag.setLayoutDirection(QtCore.Qt.RightToLeft)\n        self.cwp_options_tag.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.cwp_options_tag.setObjectName(\"cwp_options_tag\")\n        self.horizontalLayout_4.addWidget(self.cwp_options_tag)\n        self.verticalLayout_16.addWidget(self.save_options_box)\n        self.override_box = QtWidgets.QGroupBox(self.special_tags_frame_inner)\n        palette = QtGui.QPalette()\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)\n        brush = QtGui.QBrush(QtGui.QColor(229, 229, 159))\n        brush.setStyle(QtCore.Qt.SolidPattern)\n        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)\n        self.override_box.setPalette(palette)\n        self.override_box.setAutoFillBackground(False)\n        self.override_box.setStyleSheet(\"background-color: rgb(229, 229, 159);\")\n        self.override_box.setObjectName(\"override_box\")\n        self.horizontalLayout_10 = QtWidgets.QHBoxLayout(self.override_box)\n        self.horizontalLayout_10.setObjectName(\"horizontalLayout_10\")\n        self.cea_override = QtWidgets.QCheckBox(self.override_box)\n        self.cea_override.setObjectName(\"cea_override\")\n        self.horizontalLayout_10.addWidget(self.cea_override)\n        self.cwp_override = QtWidgets.QCheckBox(self.override_box)\n        self.cwp_override.setObjectName(\"cwp_override\")\n        self.horizontalLayout_10.addWidget(self.cwp_override)\n        self.ce_genres_override = QtWidgets.QCheckBox(self.override_box)\n        self.ce_genres_override.setEnabled(True)\n        self.ce_genres_override.setObjectName(\"ce_genres_override\")\n        self.horizontalLayout_10.addWidget(self.ce_genres_override)\n        self.ce_tagmap_override = QtWidgets.QCheckBox(self.override_box)\n        self.ce_tagmap_override.setEnabled(True)\n        self.ce_tagmap_override.setObjectName(\"ce_tagmap_override\")\n        self.horizontalLayout_10.addWidget(self.ce_tagmap_override)\n        self.line_11 = QtWidgets.QFrame(self.override_box)\n        self.line_11.setFrameShape(QtWidgets.QFrame.VLine)\n        self.line_11.setFrameShadow(QtWidgets.QFrame.Sunken)\n        self.line_11.setObjectName(\"line_11\")\n        self.horizontalLayout_10.addWidget(self.line_11)\n        self.ce_options_overwrite = QtWidgets.QCheckBox(self.override_box)\n        self.ce_options_overwrite.setStyleSheet(\"background-color: rgb(255, 0, 0);\")\n        self.ce_options_overwrite.setObjectName(\"ce_options_overwrite\")\n        self.horizontalLayout_10.addWidget(self.ce_options_overwrite)\n        self.verticalLayout_16.addWidget(self.override_box)\n        self.label_121 = QtWidgets.QLabel(self.special_tags_frame_inner)\n        self.label_121.setObjectName(\"label_121\")\n        self.verticalLayout_16.addWidget(self.label_121)\n        self.line_2 = QtWidgets.QFrame(self.special_tags_frame_inner)\n        self.line_2.setFrameShape(QtWidgets.QFrame.HLine)\n        self.line_2.setFrameShadow(QtWidgets.QFrame.Sunken)\n        self.line_2.setObjectName(\"line_2\")\n        self.verticalLayout_16.addWidget(self.line_2)\n        self.ce_show_ui_tags = QtWidgets.QCheckBox(self.special_tags_frame_inner)\n        self.ce_show_ui_tags.setObjectName(\"ce_show_ui_tags\")\n        self.verticalLayout_16.addWidget(self.ce_show_ui_tags)\n        self.groupBox = QtWidgets.QGroupBox(self.special_tags_frame_inner)\n        self.groupBox.setStyleSheet(\"background-color: rgb(229, 229, 159);\")\n        self.groupBox.setObjectName(\"groupBox\")\n        self.verticalLayout_42 = QtWidgets.QVBoxLayout(self.groupBox)\n        self.verticalLayout_42.setObjectName(\"verticalLayout_42\")\n        self.label_94 = QtWidgets.QLabel(self.groupBox)\n        self.label_94.setObjectName(\"label_94\")\n        self.verticalLayout_42.addWidget(self.label_94)\n        self.ce_ui_tags = QtWidgets.QTextEdit(self.groupBox)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.ce_ui_tags.sizePolicy().hasHeightForWidth())\n        self.ce_ui_tags.setSizePolicy(sizePolicy)\n        self.ce_ui_tags.setMaximumSize(QtCore.QSize(16777215, 120))\n        self.ce_ui_tags.setStyleSheet(\"background-color: rgb(250, 250, 250);\")\n        self.ce_ui_tags.setObjectName(\"ce_ui_tags\")\n        self.verticalLayout_42.addWidget(self.ce_ui_tags)\n        self.label_8 = QtWidgets.QLabel(self.groupBox)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_8.sizePolicy().hasHeightForWidth())\n        self.label_8.setSizePolicy(sizePolicy)\n        self.label_8.setObjectName(\"label_8\")\n        self.verticalLayout_42.addWidget(self.label_8)\n        self.verticalLayout_16.addWidget(self.groupBox)\n        self.verticalLayout_60.addWidget(self.special_tags_frame_inner)\n        self.verticalLayout_18.addWidget(self.special_tags_frame_outer)\n        self.label_83 = QtWidgets.QLabel(self.scrollAreaWidgetContents_2)\n        self.label_83.setObjectName(\"label_83\")\n        self.verticalLayout_18.addWidget(self.label_83)\n        self.scrollArea_2.setWidget(self.scrollAreaWidgetContents_2)\n        self.horizontalLayout_31.addWidget(self.scrollArea_2)\n        self.tabWidget.addTab(self.Advanced, \"\")\n        self.Help = QtWidgets.QWidget()\n        self.Help.setObjectName(\"Help\")\n        self.scrollArea_5 = QtWidgets.QScrollArea(self.Help)\n        self.scrollArea_5.setGeometry(QtCore.QRect(0, -10, 671, 561))\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.scrollArea_5.sizePolicy().hasHeightForWidth())\n        self.scrollArea_5.setSizePolicy(sizePolicy)\n        self.scrollArea_5.setWidgetResizable(True)\n        self.scrollArea_5.setObjectName(\"scrollArea_5\")\n        self.scrollAreaWidgetContents_4 = QtWidgets.QWidget()\n        self.scrollAreaWidgetContents_4.setGeometry(QtCore.QRect(0, 0, 669, 559))\n        self.scrollAreaWidgetContents_4.setObjectName(\"scrollAreaWidgetContents_4\")\n        self.textBrowser_2 = QtWidgets.QTextBrowser(self.scrollAreaWidgetContents_4)\n        self.textBrowser_2.setGeometry(QtCore.QRect(9, 109, 641, 421))\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.textBrowser_2.sizePolicy().hasHeightForWidth())\n        self.textBrowser_2.setSizePolicy(sizePolicy)\n        self.textBrowser_2.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)\n        self.textBrowser_2.setOpenExternalLinks(True)\n        self.textBrowser_2.setObjectName(\"textBrowser_2\")\n        self.label_58 = QtWidgets.QLabel(self.scrollAreaWidgetContents_4)\n        self.label_58.setGeometry(QtCore.QRect(9, 9, 471, 16))\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Maximum)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_58.sizePolicy().hasHeightForWidth())\n        self.label_58.setSizePolicy(sizePolicy)\n        self.label_58.setText(\"\")\n        self.label_58.setObjectName(\"label_58\")\n        self.textBrowser_3 = QtWidgets.QTextBrowser(self.scrollAreaWidgetContents_4)\n        self.textBrowser_3.setGeometry(QtCore.QRect(9, 28, 641, 81))\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.textBrowser_3.sizePolicy().hasHeightForWidth())\n        self.textBrowser_3.setSizePolicy(sizePolicy)\n        self.textBrowser_3.setOpenExternalLinks(True)\n        self.textBrowser_3.setObjectName(\"textBrowser_3\")\n        self.scrollArea_5.setWidget(self.scrollAreaWidgetContents_4)\n        self.tabWidget.addTab(self.Help, \"\")\n        self.vboxlayout.addWidget(self.tabWidget)\n        self.label_4.setBuddy(self.cwp_retries)\n        self.label_120.setBuddy(self.cwp_retries)\n        self.label_90.setBuddy(self.cwp_retries)\n        self.label_6.setBuddy(self.cwp_retries)\n        self.label_7.setBuddy(self.cwp_retries)\n        self.label_5.setBuddy(self.cwp_retries)\n        self.label_93.setBuddy(self.cwp_retries)\n        self.label_87.setBuddy(self.cwp_retries)\n\n        self.retranslateUi(ClassicalExtrasOptionsPage)\n        self.tabWidget.setCurrentIndex(4)\n        self.cea_ra_use.toggled['bool'].connect(self.ra_replace_merge_options_box.setEnabled)\n        self.cea_ra_replace_ta.toggled['bool'].connect(self.cea_ra_noblank_ta.setEnabled)\n        self.use_cea.toggled['bool'].connect(self.lyrics_frame.setEnabled)\n        self.use_cwp.toggled['bool'].connect(self.cwp_collections.setEnabled)\n        self.cea_split_lyrics.toggled['bool'].connect(self.lyrics_and_notes_tags_frame.setEnabled)\n        self.use_cwp.toggled['bool'].connect(self.use_cache.setEnabled)\n        self.use_cea.toggled['bool'].connect(self.other_artist_options_frame.setEnabled)\n        self.cea_arrangers.toggled['bool'].connect(self.annotations_lh_box.setEnabled)\n        self.cea_arrangers.toggled['bool'].connect(self.annotations_rh_box.setEnabled)\n        self.toolButton_1.toggled['bool'].connect(self.cea_source_1.setEnabled)\n        self.toolButton_2.toggled['bool'].connect(self.cea_source_2.setEnabled)\n        self.toolButton_3.toggled['bool'].connect(self.cea_source_3.setEnabled)\n        self.toolButton_4.toggled['bool'].connect(self.cea_source_4.setEnabled)\n        self.toolButton_5.toggled['bool'].connect(self.cea_source_5.setEnabled)\n        self.toolButton_6.toggled['bool'].connect(self.cea_source_6.setEnabled)\n        self.toolButton_7.toggled['bool'].connect(self.cea_source_7.setEnabled)\n        self.toolButton_8.toggled['bool'].connect(self.cea_source_8.setEnabled)\n        self.toolButton_9.toggled['bool'].connect(self.cea_source_9.setEnabled)\n        self.toolButton_10.toggled['bool'].connect(self.cea_source_10.setEnabled)\n        self.toolButton_11.toggled['bool'].connect(self.cea_source_11.setEnabled)\n        self.toolButton_12.toggled['bool'].connect(self.cea_source_12.setEnabled)\n        self.toolButton_13.toggled['bool'].connect(self.cea_source_13.setEnabled)\n        self.toolButton_14.toggled['bool'].connect(self.cea_source_14.setEnabled)\n        self.toolButton_15.toggled['bool'].connect(self.cea_source_15.setEnabled)\n        self.toolButton_16.toggled['bool'].connect(self.cea_source_16.setEnabled)\n        self.use_cea.toggled['bool'].connect(self.advanced_artists_frame.setEnabled)\n        self.use_cwp.toggled['bool'].connect(self.advanced_workparts_frame.setEnabled)\n        self.use_cea.toggled['bool'].connect(self.naming_options_frame.setEnabled)\n        self.use_cea.toggled['bool'].connect(self.recording_artists_options_frame.setEnabled)\n        self.use_cwp.toggled['bool'].connect(self.work_style_frame.setEnabled)\n        self.use_cwp.toggled['bool'].connect(self.work_aliases_frame.setEnabled)\n        self.use_cwp.toggled['bool'].connect(self.works_parts_tags_frame.setEnabled)\n        self.use_cwp.toggled['bool'].connect(self.songkong_frame.setEnabled)\n        self.cwp_use_muso_refdb.toggled['bool'].connect(self.cwp_muso_genres.setVisible)\n        self.cwp_use_muso_refdb.toggled['bool'].connect(self.cwp_muso_genres.setChecked)\n        self.cwp_use_muso_refdb.toggled['bool'].connect(self.cwp_muso_classical.setVisible)\n        self.cwp_use_muso_refdb.toggled['bool'].connect(self.cwp_muso_classical.setChecked)\n        self.cwp_use_muso_refdb.toggled['bool'].connect(self.cwp_muso_dates.setVisible)\n        self.cwp_use_muso_refdb.toggled['bool'].connect(self.cwp_muso_periods.setVisible)\n        self.cwp_use_muso_refdb.toggled['bool'].connect(self.cwp_muso_dates.setChecked)\n        self.cwp_use_muso_refdb.toggled['bool'].connect(self.cwp_muso_periods.setChecked)\n        self.cwp_muso_genres.toggled['bool'].connect(self.cwp_genres_classical_main.setHidden)\n        self.cwp_muso_classical.toggled['bool'].connect(self.cwp_genres_arranger_as_composer.setVisible)\n        self.cwp_muso_dates.toggled['bool'].connect(self.cwp_periods_arranger_as_composer.setVisible)\n        self.cwp_muso_periods.toggled['bool'].connect(self.cwp_period_map.setHidden)\n        self.cwp_muso_periods.toggled['bool'].connect(self.period_map_annotation_label.setHidden)\n        self.cwp_genres_filter.toggled['bool'].connect(self.genre_filters_frame.setVisible)\n        self.use_cwp.toggled['bool'].connect(self.partial_arrangements_medleys_frame.setEnabled)\n        self.cwp_titles.toggled['bool'].connect(self.source_of_canonical_box.setDisabled)\n        self.cwp_titles.toggled['bool'].connect(self.partial_arrangements_medleys_frame.setHidden)\n        self.use_cache.toggled['bool'].connect(self.cwp_use_sk.setEnabled)\n        self.ce_show_ui_tags.toggled['bool'].connect(self.groupBox.setVisible)\n        QtCore.QMetaObject.connectSlotsByName(ClassicalExtrasOptionsPage)\n\n    def retranslateUi(self, ClassicalExtrasOptionsPage):\n        _translate = QtCore.QCoreApplication.translate\n        self.Artists.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><br/></p></body></html>\"))\n        self.use_cea.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>should be selected otherwise this section will not run</p></body></html>\"))\n        self.use_cea.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Create extra artist metadata (MUST BE TICKED FOR THIS SECTION TO RUN)\"))\n        self.infer_worktypes_old_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"(Note that the \\\"infer work-types\\\" option has moved to the \\\"genres\\\" tab)\"))\n        self.naming_style_note_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600; font-style:italic;\\\">The naming style for \\'artist\\' tags is set in the main Picard Options-&gt;Metadata section</span></p></body></html>\"))\n        self.naming_options_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Work-artist / performer naming options</span></p></body></html>\"))\n        self.naming_options_frame.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>&quot;Work-artists&quot; are types such as composer, writer, arranger and lyricist who belong to the MusicBrainz Work-Artist relationship</p><p>&quot;Performers&quot; are types such as performer and conductor who belong to the MusicBrainz Recording-Artist relationship</p></body></html>\"))\n        self.label_22.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>This section does not change the contents of &quot;artist&quot; or &quot;album artist&quot; tags - it only affects writer (composer etc.) and peformer tags, by using as-credited/alias names from the artist data for the release.</p></body></html>\"))\n        self.credited_as_options_box.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><br/></p></body></html>\"))\n        self.credited_as_options_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Credited-as options:-\"))\n        self.names_to_use_box.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Select the source for \\'as-credited\\' names - whether these are applied depends on the sub-options choices.</p></body></html>\"))\n        self.names_to_use_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Names to use...\"))\n        self.cea_recording_credited.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use \\\"credited as\\\" name for work-artists/performers who are recording artists\"))\n        self.cea_group_credited.setText(_translate(\"ClassicalExtrasOptionsPage\", \"and/or release group artists\"))\n        self.cea_credited.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><br/></p></body></html>\"))\n        self.cea_credited.setText(_translate(\"ClassicalExtrasOptionsPage\", \"and/or release artists\"))\n        self.cea_release_relationship_credited.setText(_translate(\"ClassicalExtrasOptionsPage\", \"and/or release relationship artists\"))\n        self.cea_recording_relationship_credited.setText(_translate(\"ClassicalExtrasOptionsPage\", \"and/or recording relationship artists\"))\n        self.cea_track_credited.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><br/></p></body></html>\"))\n        self.cea_track_credited.setText(_translate(\"ClassicalExtrasOptionsPage\", \"and/or track artists\"))\n        self.label_24.setText(_translate(\"ClassicalExtrasOptionsPage\", \"The above are applied in sequence - e.g. track artist credit will over-ride release artist credit.\"))\n        self.label_88.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Names are cached. A restart is necessary if any of the above name sources are removed.\"))\n        self.places_to_use_them_box.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Select the tag types where any \\'as-credited\\' names will be applied - whether these are applied depends on the sub-options choices.</p></body></html>\"))\n        self.places_to_use_them_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Places to use them ...\"))\n        self.cea_performer_credited.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use for performing artists\"))\n        self.cea_composer_credited.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use for work-artists\"))\n        self.naming_sub_options_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Sub-options\"))\n        self.cea_alias_overrides.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Alias (if it exists) will over-ride as-credited</p></body></html>\"))\n        self.cea_alias_overrides.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Alias over-rides credited-as\"))\n        self.cea_credited_overrides.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>As-credited (if it exists) will over-ride alias</p></body></html>\"))\n        self.cea_credited_overrides.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Credited-as over-rides MB/Alias\"))\n        self.cea_cyrillic.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Will be based on sort names. For cyrillic script names, patronyms will be removed.</p></body></html>\"))\n        self.cea_cyrillic.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Fix non-Latin text in names (where possible and if not fixed by other naming options)\"))\n        self.MB_std_names_aliases_box_outer.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"MusicBrainz standard names and Aliases\"))\n        self.cea_no_aliases.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Do not use aliases (but may be replaced by as-credited name)</p></body></html>\"))\n        self.cea_no_aliases.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use MB standard names\"))\n        self.cea_aliases.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Alias will only be available for use if the work-artist/performer is also a release artist, recording artist or track artist.</p></body></html>\"))\n        self.cea_aliases.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use alias for all work-artists/performers\"))\n        self.cea_aliases_composer.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Only use alias (if available) for work-artists (writers, composers, arrangers, lyricists etc.)</p></body></html>\"))\n        self.cea_aliases_composer.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use alias for work-artists only\"))\n        self.recording_artist_options_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Recording artist options</span></p></body></html>\"))\n        self.recording_artists_options_box.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Select recording artist options (see &quot;What\\'s this&quot;)</p></body></html>\"))\n        self.recording_artists_options_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>In MusicBrainz, the recording artist may be different from the track artist. For classical music, the MusicBrainz guidelines state that the track artist should be the composer; however the recording artist(s) is/are usually the principal performer(s).</p><p>Classical Extras puts the recording artists into \\'hidden variables\\' (as a minimum) using the chosen naming convention.</p><p>There is also option to allow you to replace the track artist by the recording artist (or to merge them). The chosen action will be applied to the \\'artist\\', \\'artists\\', \\'artistsort\\' and \\'artists_sort\\' tags. Note that \\'artist\\' is a single-valued string whereas \\'artists\\' is a list and may be multi-valued. Lists are properly merged, but because the \\'artist\\' string may have different join-phrases etc, a merged tag may have the recording artist(s) in brackets after the track artist(s).</p></body></html>\"))\n        self.naming_convention_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>In classical music (in MusicBrainz), recording artists will usually be performers whereas track artists are composers. By default, the naming convention for performers (set in the previous section) will be used (although only the as-credited name set for the recording artist will be applied). Alternatively, the naming convention for track artists can be used - which is determined by the main Picard metadat options.</p></body></html>\"))\n        self.naming_convention_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Naming convention as for ...\"))\n        self.cea_ra_trackartist.setText(_translate(\"ClassicalExtrasOptionsPage\", \"...track artist (set in Picard options)\"))\n        self.cea_ra_performer.setText(_translate(\"ClassicalExtrasOptionsPage\", \"... perfomers (set above)\"))\n        self.cea_ra_use.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use recording artists to update track artists ->\"))\n        self.ra_replace_merge_options_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Replace/merge options\"))\n        self.cea_ra_replace_ta.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Replace track artist by recording artist\"))\n        self.cea_ra_noblank_ta.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Only replace if rec. artist exists\"))\n        self.cea_ra_merge_ta.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Merge track artist and recording artist\"))\n        self.other_artist_options_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Other artist options</span></p></body></html>\"))\n        self.annotations_lh_box.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Enter text to appear in annotations. Do not use any quotation marks.</p></body></html>\"))\n        self.annotations_lh_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Annotations - performers and lyricists\"))\n        self.label_44.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Annotation to include with &quot;chorus master&quot; in conductor tag.</p></body></html>\"))\n        self.label_44.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Chorus Master\"))\n        self.label_46.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Annotation to include for &quot;concert master&quot; in performer tag.</p></body></html>\"))\n        self.label_46.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Concert Master\"))\n        self.label_34.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Annotation for lyricist, to include in lyricist tag</p></body></html>\"))\n        self.label_34.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Lyricist\"))\n        self.label_26.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Annotation for librettist, to include in lyricist tag</p></body></html>\"))\n        self.label_26.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Librettist\"))\n        self.label_30.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Annotation for translator, to include in lyricist tag</p></body></html>\"))\n        self.label_30.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Translator\"))\n        self.other_artist_checkboxes_box.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Select as required. See &quot;What\\'s this&quot; for details.</p></body></html>\"))\n        self.cea_arrangers.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><br/></p></body></html>\"))\n        self.cea_arrangers.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>This will gather together, for example, any arranger-type information from the recording, work or parent works and place it in the &quot;arranger&quot; tag (\\'host\\' tag), with the annotation (see details to right of this box) in brackets. All arranger types will also be put in a hidden variable, e.g. _cwp_orchestrators. The table below shows the artist types, host tag and hidden variable for each artist type.</p><p>| Artist type | Host tag | Hidden variable |</p><p>| ----------------- | ------------------| -------------------------------------- |</p><p>| writer | composer | writers |</p><p>| lyricist | lyricist | lyricists |</p><p>| librettist | lyricist | librettists |</p><p>| revised by | arranger | revisors |</p><p>| translator | lyricist | translators |</p><p>| arranger | arranger | arrangers |</p><p>| reconstructed by | arranger | reconstructors |</p><p>| orchestrator | arranger | orchestrators |</p><p>| instrument arranger | arranger | arrangers (with instrument type in brackets) |</p><p>| vocal arranger | arranger | arrangers (with voice type in brackets) |</p><p>| chorus master | conductor | chorusmasters |</p><p>| concertmaster | performer (with annotation as a sub-key) | leaders |</p></body></html>\"))\n        self.cea_arrangers.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Modify host tags and include annotations (see =>)\"))\n        self.cea_composer_album.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><br/></p></body></html>\"))\n        self.cea_composer_album.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>This will add the composer(s) last name(s) before the album name, if they are listed as album artists. If there is more than one composer, they will be listed in the descending order of the length of their music on the release.</p></body></html>\"))\n        self.cea_composer_album.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Name album as \\\"Composer Last Name(s): Album Name\\\"\"))\n        self.cea_no_lyricists.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>This applies to both the Picard \\'lyricist\\' tag and the related internal plugin hidden variables \\'_cwp_lyricists\\' etc.</p></body></html>\"))\n        self.cea_no_lyricists.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Do not write \\'lyricist\\' tag if no vocal performers\"))\n        self.cea_inst_credit.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use \\\"credited-as\\\" name for instrument\"))\n        self.cea_no_solo.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Select to eliminate &quot;additional&quot;, &quot;solo&quot; or &quot;guest&quot; from instrument description</p></body></html>\"))\n        self.cea_no_solo.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>MusicBrainz permits the use of &quot;solo&quot;, &quot;guest&quot; and &quot;additional&quot; as instrument attributes although, for classical music, its use should be fairly rare - usually only if explicitly stated as a &quot;solo&quot; on the the sleevenotes. Classical Extras provides the option to exclude these attributes (the default), but you may wish to enable them for certain releases or non-Classical / cross-over releases.</p></body></html>\"))\n        self.cea_no_solo.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Do not include attributes (e.g. \\'solo\\') in an instrument type\"))\n        self.annotations_rh_box.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Enter text to appear in annotations. Do not use any quotation marks.</p></body></html>\"))\n        self.annotations_rh_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Annotations - writers and arrangers\"))\n        self.label_56.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Writer\"))\n        self.label_54.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Arranger\"))\n        self.label_45.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Text with which to annotate orchestrator in the arranger tag.</p></body></html>\"))\n        self.label_45.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Orchestrator\"))\n        self.label_32.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Annotation for &quot;reconstructed by&quot;, to include in arranger tag</p></body></html>\"))\n        self.label_32.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Reconstructed by\"))\n        self.label_28.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Annotation for &quot;revised by&quot;, to include in arranger tag tag</p></body></html>\"))\n        self.label_28.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Revised by\"))\n        self.lyrics_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Lyrics</span></p></body></html>\"))\n        self.lyrics_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Please note that this section operates on the underlying input file tags, not the Picard-generated tags (MusicBrainz does not have lyrics)</span></p><p>  Sometimes &quot;lyrics&quot; tags can contain album notes (repeated for every track in an album) as well as track notes and lyrics. This section will filter out the common text and place it in a different tag from the text which is unique to each track.</p></body></html>\"))\n        self.cea_split_lyrics.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>enables this section</p></body></html>\"))\n        self.cea_split_lyrics.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Split lyrics tag into track and album levels\"))\n        self.lyrics_and_notes_tags_frame.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Enter a valid tag name (no spaces, punctuation or special charcaters other than underline; use lower case)</p></body></html>\"))\n        self.label_50.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>The name of the lyrics file tag in the input file (normally just \\'lyrics\\')</p></body></html>\"))\n        self.label_50.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Incoming lyrics tag (i.e. file tag)\"))\n        self.label_51.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>The name of the tag where common text should be placed</p></body></html>\"))\n        self.label_51.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Tag for album notes / lyrics\"))\n        self.label_52.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>The name of the tag where notes/lyrics unique to a track should be placed</p></body></html>\"))\n        self.label_52.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Tag for track notes / lyrics\"))\n        self.tabWidget.setTabText(self.tabWidget.indexOf(self.Artists), _translate(\"ClassicalExtrasOptionsPage\", \"Artists\"))\n        self.use_cwp.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>&quot;Include all work levels&quot; should be selected otherwise this section will not run.</p></body></html>\"))\n        self.use_cwp.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Include all work levels (MUST BE TICKED FOR THIS SECTION TO RUN)*\"))\n        self.cwp_collections.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>This will include parent works where the relationship has the attribute \\'part of collection\\'.<br/>PLEASE BE CONSISTENT and do not use different options on albums with the same works, or the results may be unexpected.</p></body></html>\"))\n        self.cwp_collections.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Include collection relationships (but not \\\"series\\\")\"))\n        self.use_cache.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Select to use cached works. Deselect to refesh from MusicBrainz.</p></body></html>\"))\n        self.use_cache.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>&quot;Use cache&quot; prevents excessive look-ups of the MB database. Every look-up of a parent work needs to be performed separately (hopefully the MB database might make this easier some day). Network usage constraints by MB means that each look-up takes a minimum of 1 second. Once a release has been looked-up, the works are retained in cache, significantly reducing the time required if, say, the options are changed and the data refreshed. However, if the user edits the works in the MB database then the cache will need to be turned off temporarily for the refresh to find the new/changed works. Also some types of work (e.g. arrangements) will require a full look-up if options have been changed.</p></body></html>\"))\n        self.use_cache.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use cache (if available)*\"))\n        self.work_style_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Tagging style</span></p></body></html>\"))\n        self.work_style_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.0//EN\\\" \\\"http://www.w3.org/TR/REC-html40/strict.dtd\\\">\\n\"\n\"<html><head><meta name=\\\"qrichtext\\\" content=\\\"1\\\" /><style type=\\\"text/css\\\">\\n\"\n\"p, li { white-space: pre-wrap; }\\n\"\n\"</style></head><body style=\\\" font-family:\\'MS Shell Dlg 2\\'; font-size:8pt; font-weight:400; font-style:normal;\\\">\\n\"\n\"<p style=\\\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">&quot;Tagging style&quot;. This section determines how the hierarchy of works will be sourced. </p>\\n\"\n\"<p style=\\\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\"><span style=\\\" font-weight:600;\\\">Works source</span>: There are 3 options for determing the principal source of the works metadata </p>\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">&quot;Use only metadata from title text&quot;. The plugin will attempt to extract the hierarchy of works from the track title by looking for repetitions and patterns. If the title does not contain all the work names in the hierarchy then obviously this will limit what can be provided. </p>\\n\"\n\"<p style=\\\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\"><br /></p>\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">&quot;Use only metadata from canonical works&quot;. The hierarchy in the MB database will be used. Assuming the work is correctly entered in MB, this should provide all the data. However the text may differ from the track titles and will be the same for all recordings. It may also be in the language of the composer whereas the titles will probably be in the language of the release. (This language issue can also be addressed by using aliases - see below). </p>\\n\"\n\"<p style=\\\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\"><br /></p>\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">&quot;Use canonical work metadata enhanced with title text&quot;. This supplements the canonical data with text from the titles <span style=\\\" font-weight:600;\\\">where it is significantly different</span>. The supplementary title data will be in curly brackets. This is clearly the most complete metadata style of the three but may lead to long descriptions. It is particularly useful for providing translations.</p>\\n\"\n\"<p style=\\\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\"><span style=\\\" font-weight:600;\\\">Source of canonical work text</span>. Where either of the second two options above are chosen, there is a further choice to be made: </p>\\n\"\n\"<p style=\\\" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">&quot;Full MusicBrainz work hierarchy&quot;. The names of each level of work are used to populate the relevant tags. E.g. if &quot;Má vlast: I. Vyšehrad, JB 1:112/1&quot; (level 0) is part of &quot;Má vlast, JB 1:112&quot; (level 1) then the parent work will be tagged as &quot;Má vlast, JB 1:112&quot;, not &quot;Má vlast&quot;. So, while accurate, this option might be more verbose. </p>\\n\"\n\"<p style=\\\" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">&quot;Consistent with lowest level work description&quot;. The names of the level 0 work are used to populate the relevant tags. I.e. if &quot;Má vlast: I. Vyšehrad, JB 1:112/1&quot; (level 0) is part of &quot;Má vlast, JB 1:112&quot; (level 1) then the parent work will be tagged as &quot;Má vlast&quot;, not &quot;Má vlast, JB 1:112&quot;. This frequently looks better, but not always, <span style=\\\" font-weight:600;\\\">particularly if the level 0 work name does not contain all the parent work detail</span>. If the full structure is not implicit in the level 0 name then a warning will be logged and written to the &quot;warning&quot; tag. </p>\\n\"\n\"<p style=\\\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\"><span style=\\\" font-weight:600;\\\">Strategy for setting style:</span> <span style=\\\" font-style:italic;\\\">It is suggested that you start with &quot;extended/enhanced&quot; style and the &quot;Consistent with lowest level work description&quot; as the source (this is the default). If this does not give acceptable results, try switching to &quot;Full MusicBrainz work hierarchy&quot;. If the &quot;enhanced&quot; details in curly brackets (from the track title) give odd results then switch the style to &quot;canonical works&quot; only. Any remaining oddities are then probably in the MusicBrainz data, which may require editing.</span> </p></body></html>\"))\n        self.works_source_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>There are 3 options for determing the principal source of the works metadata</p><p>      - &quot;Use only metadata from title text&quot;. The plugin will attempt to extract the hierarchy of works from the track title by looking for repetitions and patterns. If the title does not contain all the work names in the hierarchy then obviously this will limit what can be provided.</p><p>      - &quot;Use only metadata from canonical works&quot;. The hierarchy in the MB database will be used. Assuming the work is correctly entered in MB, this should provide all the data. However the text may differ from the track titles and will be the same for all recordings. It may also be in the language of the composer whereas the titles will be in the language of the release.</p><p>      - &quot;Use canonical work metadata enhanced with title text&quot;. This supplements the canonical data with text from the titles <span style=\\\" font-weight:600;\\\">where it is significantly different</span>. The supplementary data will be in curly brackets. This is clearly the most complete metadata style of the three but may lead to long descriptions. It is particularly useful for providing translations - see image below for an example (using the Muso library manager).</p></body></html>\"))\n        self.works_source_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Works source\"))\n        self.cwp_titles.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>&quot;Use only metadata from title text&quot;. The plugin will attempt to extract the hierarchy of works from the track title by looking for repetitions and patterns. If the title does not contain all the work names in the hierarchy then obviously this will limit what can be provided.</p></body></html>\"))\n        self.cwp_titles.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use only metadata from title text\"))\n        self.cwp_works.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>&quot;Use only metadata from canonical works&quot;. The hierarchy in the MB database will be used. Assuming the work is correctly entered in MB, this should provide all the data. However the text may differ from the track titles and will be the same for all recordings. It may also be in the language of the composer whereas the titles will probably be in the language of the release. (This language issue can also be addressed by using aliases - see below).</p></body></html>\"))\n        self.cwp_works.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use only metadata from canonical works\"))\n        self.cwp_extended.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>&quot;Use canonical work metadata enhanced with title text&quot;. This supplements the canonical data with text from the titles **where it is significantly different**. The supplementary title data will be in curly brackets. This is clearly the most complete metadata style of the three but may lead to long descriptions. It is particularly useful for providing translations</p></body></html>\"))\n        self.cwp_extended.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use canonical work metadata enhanced with title text\"))\n        self.source_of_canonical_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Where either of the second two options above are chosen, there is a further choice to be made:</p><p>      - &quot;Full MusicBrainz work hierarchy&quot;. The names of each level of work are used to populate the relevant tags. I.e. if &quot;Má vlast: I. Vyšehrad, JB 1:112/1&quot; (level 0) is part of &quot;Má vlast, JB 1:112&quot; (level 1) then the parent work will be tagged as &quot;Má vlast, JB 1:112&quot;, not &quot;Má vlast&quot;. So, while accurate, this option might be more verbose.</p><p>      - &quot;Consistent with lowest level work description&quot;. The names of the level 0 work are used to populate the relevant tags. I.e. if &quot;Má vlast: I. Vyšehrad, JB 1:112/1&quot; (level 0) is part of &quot;Má vlast, JB 1:112&quot; (level 1) then the parent work will be tagged as &quot;Má vlast&quot;, not &quot;Má vlast, JB 1:112&quot;. This frequently looks better, but not always, <span style=\\\" font-weight:600;\\\">particularly if the level 0 work name does not contain all the parent work detail</span>. If the full structure is not implicit in the level 0 name then a warning will be logged and written to the &quot;warning&quot; tag.</p></body></html>\"))\n        self.source_of_canonical_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Source of canonical work text (if applicable)\"))\n        self.cwp_hierarchical_works.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>&quot;Full MusicBrainz work hierarchy&quot;. The names of each level of work are used to populate the relevant tags. E.g. if &quot;Má vlast: I. Vyšehrad, JB 1:112/1&quot; (level 0) is part of &quot;Má vlast, JB 1:112&quot; (level 1) then the parent work will be tagged as &quot;Má vlast, JB 1:112&quot;, not &quot;Má vlast&quot;. So, while accurate, this option might be more verbose.</p></body></html>\"))\n        self.cwp_hierarchical_works.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Full MusicBrainz work hierarchy (may be more verbose)\"))\n        self.cwp_level0_works.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>&quot;Consistent with lowest level work description&quot;. The names of the level 0 work are used to populate the relevant tags. I.e. if &quot;Má vlast: I. Vyšehrad, JB 1:112/1&quot; (level 0) is part of &quot;Má vlast, JB 1:112&quot; (level 1) then the parent work will be tagged as &quot;Má vlast&quot;, not &quot;Má vlast, JB 1:112&quot;. This frequently looks better, but not always, <span style=\\\" font-weight:600;\\\">particularly if the level 0 work name does not contain all the parent work detail</span>. If the full structure is not implicit in the level 0 name then a warning will be logged and written to the &quot;warning&quot; tag.</p></body></html>\"))\n        self.cwp_level0_works.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Consistent with lowest level work description (may be less verbose, but not always complete)\"))\n        self.cwp_derive_works_from_title.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Attempt to get works and movement info from title if there are no work relationships? (Requires title in form \\\"work: movement\\\")\"))\n        self.work_aliases_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Aliases</span> (NB Use a consistent approach throughout the library otherwise duplicate works may occur - see the Readme)*</p></body></html>\"))\n        self.work_aliases_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>&quot;Replace work names by aliases&quot; will use <span style=\\\" font-weight:600;\\\">primary</span> aliases for the chosen locale instead of standard MusicBrainz work names. To choose the locale, use the drop-down under &quot;translate artist names&quot; in the main Picard Options--&gt;Metadata page. Note that this option is not saved as a file tag since, if different choices are made for different releases, different work names may be stored and therefore cannot be grouped together in your player/library manager. </p><p>The sub-options allow either the replacement of all work names, where a primary alias exists, just the replacement of work names which are in non-Latin script, or only replace those which are flagged with user &quot;Folksonomy&quot; tags. The tag text needs to be included in the text box, in which case flagged works will be \\'aliased\\' as well as non-Latin script works, if the second sub-option is chosen. Note that the tags may either be anyone\\'s tags (&quot;Look in all tags&quot;) or the user\\'s own tags. If selecting &quot;Look in user\\'s own tags only&quot; you <span style=\\\" font-weight:600;\\\">must</span> be logged in to your MusicBrainz user account (in the Picard Options-&gt;General page), otherwise repeated dialogue boxes may be generated and you may need to force restart Picard.</p></body></html>\"))\n        self.replace_MBworknames_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Replace MB work names?\"))\n        self.cwp_aliases.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Use primary aliases for the chosen locale instead of standard MusicBrainz work names.</p></body></html>\"))\n        self.cwp_aliases.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Replace work names by aliases. Select method -->\"))\n        self.cwp_no_aliases.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Do not replace work names\"))\n        self.what_to_replace_outer_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"What to replace?\"))\n        self.cwp_aliases_all.setText(_translate(\"ClassicalExtrasOptionsPage\", \"All work names\"))\n        self.cwp_aliases_greek.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Non-latin work names\"))\n        self.cwp_aliases_tagged.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Only tagged works\"))\n        self.works_alias_tags_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Tags (\\\"Folksonomy\\\") identifying works to be replaced by aliases\"))\n        self.cwp_aliases_tags_all.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Look in all tags\"))\n        self.cwp_aliases_tags_user.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Look in user\\'s own tags only (MUST BE LOGGED IN!)\"))\n        self.cwp_aliases_tag_text.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Separate multiple tags by commas</p></body></html>\"))\n        self.work_parts_tags_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Tags to create</span> - Use commas to separate multiple tags or leave blank to omit</p></body></html>\"))\n        self.works_parts_tags_box.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Separate multiple tags by commas.</p></body></html>\"))\n        self.works_parts_tags_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>&quot;Tags to create&quot; sets the names of the tags that will be created from the sources described above. All these tags will be blanked before filling as specified. Tags specified against more than one source will have later sources appended in the sequence specified, separated by separators as specified.</p></body></html>\"))\n        self.works_tags_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Work tags\"))\n        self.label_40.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Separator\"))\n        self.label_11.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Some software (notably Muso) can display a 2-level work hierarchy as well as the work-movement hierarchy. This tag can be use to store the 2-level work name (a double colon :: is used to separate the levels within the tag).</p></body></html>\"))\n        self.label_11.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Tags for Work - for software with 2-level capability (e.g. Muso)</p></body></html>\"))\n        self.cwp_multi_work_sep.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"; \"))\n        self.cwp_multi_work_sep.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \": \"))\n        self.cwp_multi_work_sep.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \". \"))\n        self.cwp_multi_work_sep.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \", \"))\n        self.cwp_multi_work_sep.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"- \"))\n        self.label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"(In this format, intermediate works will be displayed after a double colon  :: )\"))\n        self.label_15.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Software which can display a movement and work (but no higher levels) could use any tags specified here. Note that if there are multiple work levels, the intermediate levels will not be tagged. Users wanting all the information should use the tags from the previous option (but it may cause some breaks in the display if levels change) - alternatively the missing work levels can be included in a movement tag (see below).</p></body></html>\"))\n        self.label_15.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Tags for Work - for software with 1-level capability (e.g. iTunes)</p></body></html>\"))\n        self.cwp_single_work_sep.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"; \"))\n        self.cwp_single_work_sep.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \": \"))\n        self.cwp_single_work_sep.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \". \"))\n        self.cwp_single_work_sep.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \", \"))\n        self.cwp_single_work_sep.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"- \"))\n        self.label_2.setText(_translate(\"ClassicalExtrasOptionsPage\", \"(Intermediate works will not be displayed:- Either 1. use the 2-level format if you wish to display them, but note that this will ceate new work, or 2. include them in the movement [see below])\"))\n        self.label_12.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>This is the top-level work held in MB. This can be useful for cataloguing and searching (if the library software is capable). Note that this will always be the &quot;canonical&quot; MB name, not one derived from titles or the lowest level work name and that no annotations (e.g. key or work year) will be added. However, if &quot;replace work names by aliases&quot; has been selected and is applicable, the relevant alias will be used.</p></body></html>\"))\n        self.label_12.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Tags for top-level (canonical) work (for capable library managers)</p></body></html>\"))\n        self.label_10.setText(_translate(\"ClassicalExtrasOptionsPage\", \"  N/A    \"))\n        self.parts_tags_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Movement/Part tags\"))\n        self.label_13.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>The Picard standard tag is  \\'movementnumber\\' - include that or other(s) of your choice</p></body></html>\"))\n        self.label_13.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>This is not necessarily the embedded movt/part number, but is the sequence number of the movement within its parent work <span style=\\\" font-weight:600;\\\">on the current release</span>.</p></body></html>\"))\n        self.label_13.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Tags for (computed) movement number (Picard std tag is movementnumber)</p></body></html>\"))\n        self.cwp_movt_no_sep.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"; \"))\n        self.cwp_movt_no_sep.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \": \"))\n        self.cwp_movt_no_sep.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \". \"))\n        self.cwp_movt_no_sep.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \", \"))\n        self.cwp_movt_no_sep.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"- \"))\n        self.label_43.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>The Picard tag \\'movementtotal\\' will be populated in any case - no need to specify it</p></body></html>\"))\n        self.label_43.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>This is not necessarily the total number of movements in the parent work, but is the total number of movement tracks within the parent work <span style=\\\" font-weight:600;\\\">on the current release</span>.</p></body></html>\"))\n        self.label_43.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Tags for (computed) total number of movements (Picard std tag is movementtotal)</p></body></html>\"))\n        self.label_79.setText(_translate(\"ClassicalExtrasOptionsPage\", \"  N/A    \"))\n        self.label_49.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Movement name tags (Picard std tag is movement)<br/>Use different movement tags if required for different level systems ==&gt;</p></body></html>\"))\n        self.label_47.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><br/>for use with multi-level work tags</p></body></html>\"))\n        self.label_48.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><br/>for use with1-level work tags (intermediate works will prefix movement)</p></body></html>\"))\n        self.label_14.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>The Picard standard tag is  \\'movement\\' - include that or other(s) of your choice</p></body></html>\"))\n        self.label_14.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>As below, but without the movement part/number prefix (if applicable)</p></body></html>\"))\n        self.label_14.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Tags for Movement - excluding embedded movt/part numbers</p></body></html>\"))\n        self.label_39.setText(_translate(\"ClassicalExtrasOptionsPage\", \"  N/A    \"))\n        self.label_9.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>The Picard standard tag is  \\'movement\\' - include that or other(s) of your choice</p></body></html>\"))\n        self.label_9.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>This tag(s) will contain the full lowest-level part name extracted from the lowest-level work name, according to the chosen tagging style.</p></body></html>\"))\n        self.label_9.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Tags for Movement - including embedded movt/part numbers</p></body></html>\"))\n        self.label_37.setText(_translate(\"ClassicalExtrasOptionsPage\", \"  N/A    \"))\n        self.partial_arrangements_medleys_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Partial recordings, arrangements and medleys</span></p></body></html>\"))\n        self.partial_arrangements_medleys_box.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Enter text - do not use any quotation marks</p></body></html>\"))\n        self.label_20.setText(_translate(\"ClassicalExtrasOptionsPage\", \"N.B. If these options are selected or deselected, quit and restart Picard before proceeding\"))\n        self.partial_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>If this option is selected, partial recordings will be treated as a sub-part of the whole recording and will have the related text included in its name. Note that this text is at the end of the canonical name, but the latter will probably be stripped from the sub-part as it duplicates the recording work name; any title text will be appended to the whole. Note that, if &quot;Consistent with lowest level work description&quot; is chosen in section 2, the text may be treated as a &quot;prefix&quot; similar to those in the &quot;Advanced&quot; tab. If this eliminates other similar prefixes and has unwanted effects, then either change the desired text slightly (e.g. surround with brackets) or use the &quot;Full MusicBrainz work hierarchy&quot; option in section 2.</p></body></html>\"))\n        self.partial_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Partial recordings\"))\n        self.cwp_partial.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Show partial recordings as separate sub-part, labelled with ->\"))\n        self.arrangements_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>If this option is selected, works which are arrangements of other works will have the latter treated in the same manner as &quot;parent&quot; works, except that the arrangement work name will be prefixed by the text provided.</p></body></html>\"))\n        self.arrangements_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Arrangements\"))\n        self.cwp_arrangements.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Show arrangements as parts of original works, labelled with ->\"))\n        self.medleys_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Medleys\"))\n        self.cwp_medley.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Medleys</span></p><p>      These can occur in two ways in MusicBrainz: (a) the recording is described as a &quot;medley of&quot; a number of works and (b) the track is described as (more than one) &quot;medley including a recording of&quot; a work. In the first case, the specified text will be included in brackets after the work name, whereas in the second case, the track will be treated as a recording of multiple works and the specified text will appear in the parent work name.</p><p><br/></p></body></html>\"))\n        self.cwp_medley.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Include medley list, labelled with ->\"))\n        self.songkong_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">SongKong-compatible tag usage</span></p></body></html>\"))\n        self.songkong_box.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>See &quot;What\\'s this&quot;</p></body></html>\"))\n        self.songkong_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p> &quot;Use work tags on file (no look up on MB) if Use Cache selected&quot;: This will enable the existing work tags on the file to be used in preference to looking up on MusicBrainz, if those tags are SongKong-compatible (which should be the case if SongKong has been used or if the SongKong tags have been previously written by this plugin). If present, this can speed up processing considerably, but obviously any new data on MusicBrainz will be missed. For the option to operate, &quot;Use cache&quot; also needs to be selected. Although faster, some of the subtleties of a full look-up will be missed - for example, parent works which are arrangements will not be highlighted as such, some arrangers or composers of original works may be omitted and some medley information may be missed. **In general, therefore, the use of this option will result in poorer metadata than allowing the full database look-up to run. It is not recommended unless speed is more important than quality.**</p><p><br/></p><p>    &quot;Write SongKong-compatible work tags&quot; does what it says. These can then be used by the previous option, if the release is subsequently reloaded into Picard, to speed things up (assuming the reload was not to pick up new work data).</p><p><br/></p><p>Note that Picard and SongKong use the tag musicbrainz_workid to mean different things. If Picard has overwritten the SongKong tag (not a problem if this plugin is used) then a warning will be given and the works will be looked up on MusicBrainz. Also note that once a release is loaded, subsequent refreshes will use the cache (if option is ticked) in preference to the file tags.</p></body></html>\"))\n        self.cwp_use_sk.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use work tags on file (no look up on MB) if Use Cache selected* (NOT RECOMMENDED - SEE README)\"))\n        self.cwp_write_sk.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Write SongKong-compatible work tags*\"))\n        self.label_82.setText(_translate(\"ClassicalExtrasOptionsPage\", \"* ASTERISKED OPTIONS ARE NOT SAVED IN FILE TAGS\"))\n        self.tabWidget.setTabText(self.tabWidget.indexOf(self.Works), _translate(\"ClassicalExtrasOptionsPage\", \"Works and parts\"))\n        self.genre_tag_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Genre tags</span></p></body></html>\"))\n        self.label_73.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Name of genre tag\"))\n        self.label_74.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Name of sub-genre tag\"))\n        self.classical_genre_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">&quot;Classical&quot; genre </span></p></body></html>\"))\n        self.cwp_genres_classical_exclude.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Exclude the text \\\"classical\\\" from main genre tag even if listed above\"))\n        self.cwp_genres_classical_selective.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Make track \\\"classical\\\" only if there is a classical-specific genre (or do nothing if there is no filter)\"))\n        self.cwp_muso_classical.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use Muso composer list to determine if classical*\"))\n        self.cwp_genres_classical_all.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Make all tracks \\\"classical\\\"\"))\n        self.label_64.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Write a flag with text =\"))\n        self.label_71.setText(_translate(\"ClassicalExtrasOptionsPage\", \" in the following tag if the track is classical\"))\n        self.cwp_genres_arranger_as_composer.setText(_translate(\"ClassicalExtrasOptionsPage\", \"(Treat arrangers as for composers)\"))\n        self.label_81.setText(_translate(\"ClassicalExtrasOptionsPage\", \"* ASTERISKED OPTIONS ARE NOT SAVED IN FILE TAGS\"))\n        self.cwp_use_muso_refdb.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use Muso reference database (default path is set on \\\"advanced\\\" tab)*\"))\n        self.instruments_keys_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Instruments and keys</span></p></body></html>\"))\n        self.instruments_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Instruments\"))\n        self.label_66.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Tag name for instruments (will hold all instruments for a track)\"))\n        self.instruments_source_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Name sources to use for instruments (select at least one, otherwise no instruments will be included in tag)\"))\n        self.cwp_instruments_MB_names.setText(_translate(\"ClassicalExtrasOptionsPage\", \"MusicBrainz standard names\"))\n        self.cwp_instruments_credited_names.setText(_translate(\"ClassicalExtrasOptionsPage\", \"\\\"Credited-as\\\" names\"))\n        self.keys_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Keys\"))\n        self.label_72.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Tag name for key(s)\"))\n        self.keys_include_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Include key(s) in work name?\"))\n        self.cwp_key_never_include.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Never\"))\n        self.cwp_key_contingent_include.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Only if key not already mentioned in work name\"))\n        self.cwp_key_include.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>&quot;Include key(s) in work names&quot; gives the option to include the key signature for a work in brackets after the name of the work in the metadata. Keys will be added in the appropriate levels: e.g. Dvořák\\'s New World Symphony will get (E minor) at the work level, but only movements with different keys will be annotated viz. &quot;II. Largo (D-flat major, C-Sharp minor)&quot;</p></body></html>\"))\n        self.cwp_key_include.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Always\"))\n        self.allowed_filters_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Allowed genres (filter)</span></p></body></html>\"))\n        self.cwp_genres_filter.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Only apply genres to tags if they match pre-defined names:\"))\n        self.genre_filters_frame.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Explanation of genre-matching:</span></p><p><br/></p><p>Only genres matching those in the boxes will be placed in the genre or sub-genre tags.</p><p><br/></p><p>If  there is a matching genre found in the &quot;classical main genres&quot; or &quot;classical sub-genres&quot; box, then the track will be treated as being classical.</p></body></html>\"))\n        self.classical_genres_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Classical genres (i.e. specific to classical music) - List separated by commas\"))\n        self.label_60.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Main genres:\"))\n        self.label_75.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Sub-genres:\"))\n        self.cwp_muso_genres.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Select this to use the &quot;classical genres&quot; in Muso options as the &quot;Main classical genres&quot; here.</p></body></html>\"))\n        self.cwp_muso_genres.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use Muso classical genres*\"))\n        self.general_genres_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"General genres (may be associated with classical music, but not necessarily, e.g. \\\"instrumental\\\") - List separated by commas\"))\n        self.label_62.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Main genres\"))\n        self.label_76.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Sub-genres\"))\n        self.label_80.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Genre name to use if none of the above main genres apply (leave blank if not required)\"))\n        self.label_77.setText(_translate(\"ClassicalExtrasOptionsPage\", \"List genres, separated by commas. Only those genres listed will be included in tags.\"))\n        self.label_78.setText(_translate(\"ClassicalExtrasOptionsPage\", \"See \\\"what\\'s this\\\" for more details.\"))\n        self.source_of_genres_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Source of genres</span> - Note: if &quot;existing file tag&quot; is selected, information from the tag &quot;genre&quot; and the genre tag name specified above (if different) will be used</p></body></html>\"))\n        self.cwp_genres_use_file.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>NB: This will use the contents of the file tag with the name given above (usually \\'genre\\').</p></body></html>\"))\n        self.cwp_genres_use_file.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Existing file tag (see note above)\"))\n        self.cwp_genres_use_folks.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>This will use the folksonomy tags for <span style=\\\" font-weight:600;\\\">works</span> as a possible source of genres (if they match one of the lists below).</p><p><br/></p><p>To use the folksonomy tags for <span style=\\\" font-weight:600;\\\">releases/tracks</span>, select the main Picard option in Options-&gt;Metadata-&gt;&quot;Use folksonomy tags as genre&quot;. Again (unlike vanilla Picard) they will only be used by this plugin if they match one of the lists below.</p></body></html>\"))\n        self.cwp_genres_use_folks.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Folksonomy work tags\"))\n        self.cwp_genres_use_worktype.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Work-type\"))\n        self.cwp_genres_infer.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Infer from artist metadata\"))\n        self.label_109.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Periods and dates</span></p></body></html>\"))\n        self.dates_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Work dates\"))\n        self.dates_box_inner.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Source of work dates for above tag\"))\n        self.cwp_workdate_source_composed.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Composed date (or parent composed date)\"))\n        self.cwp_workdate_source_published.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Published date\"))\n        self.cwp_workdate_source_premiered.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Premiered date\"))\n        self.cwp_workdate_use_first.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use first available of above (in listed order)\"))\n        self.cwp_workdate_use_all.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Include all sources\"))\n        self.cwp_workdate_annotate.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Annotate dates using source name\"))\n        self.label_68.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Tag name for work date\"))\n        self.cwp_workdate_include.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>&quot;Includeworkdate in work names&quot; gives the option to include the \\'work year\\' for a work in brackets after the name of the work in the metadata. Dates (years) will be added in the appropriate levels: e.g. Smetana\\'s \\'Má vlast\\' will get (1874-1879) at the work level, but the movements with different dates will be annotated viz. &quot;Vyšehrad, JB 1:112/1 (1874)&quot;. If the dates are the same, there should be no repitetion at the movement level. (Work dates will be used in preference order, i.e. composed - published - premiered, with only the first available date being shown).</p></body></html>\"))\n        self.cwp_workdate_include.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Include workdate in work name (in preference order listed above, with no annotation)\"))\n        self.periods_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Periods\"))\n        self.label_69.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Tag name for period\"))\n        self.cwp_muso_periods.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use Muso map*\"))\n        self.cwp_muso_dates.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Use Muso composer dates (if no work date) to determine period*\"))\n        self.label_70.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Period map:\"))\n        self.period_map_annotation_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \" (Period name, Start year, End year; Period name2, ... etc.) - periods may overlap [Do not use commas or semi-colons within period name]\"))\n        self.cwp_periods_arranger_as_composer.setText(_translate(\"ClassicalExtrasOptionsPage\", \"(Treat arrangers as for composers)\"))\n        self.label_119.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-size:9pt; font-weight:600;\\\">N.B. At least one of the first two tabs (Artists: &quot;Create extra artist metadata&quot;, or Works and parts: &quot;Include all work levels&quot;) </span><span style=\\\" font-size:9pt; font-weight:600; text-decoration: underline;\\\">must</span><span style=\\\" font-size:9pt; font-weight:600;\\\"> be enabled for this section to run.<br/></span><span style=\\\" font-weight:600; font-style:italic;\\\">(Functionality will be reduced unless both the first two tabs are enabled.) </span></p></body></html>\"))\n        self.tabWidget.setTabText(self.tabWidget.indexOf(self.Genres), _translate(\"ClassicalExtrasOptionsPage\", \"Genres etc.\"))\n        self.label_18.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-size:9pt; font-weight:600;\\\">N.B. At least one of the first two tabs (Artists: &quot;Create extra artist metadata&quot;, or Works and parts: &quot;Include all work levels&quot;) </span><span style=\\\" font-size:9pt; font-weight:600; text-decoration: underline;\\\">must</span><span style=\\\" font-size:9pt; font-weight:600;\\\"> be enabled for this section to run. </span></p></body></html>\"))\n        self.label_97.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Initial tag processing</span></p></body></html>\"))\n        self.initial_tag_processing_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><br/></p></body></html>\"))\n        self.tags_to_blank.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Any tags specified in the next two rows will be blanked before applying the tag sources described in the following section. NB this applies only to Picard-generated tags, not to other tags which might pre-exist on the file: to blank those, use the main Options-&gt;Tags page. Comma-separate the tag names within the rows and note that these names are case-sensitive.</p></body></html>\"))\n        self.tags_to_blank.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Remove Picard-generated tags before applying subsequent actions? (NB existing LOCAL FILE tags will remain unless cleared using standard Picard options - to remove these, overwrite them in the next section)\"))\n        self.label_3.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" text-decoration: underline;\\\">Picard-generated</span> tags to blank (comma-separated, case-sensitive):</p></body></html>\"))\n        self.cea_blank_tag.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Enter file tag names, separated by commas</p></body></html>\"))\n        self.cea_blank_tag_2.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Enter file tag names, separated by commas</p></body></html>\"))\n        self.label_19.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><br/></p></body></html>\"))\n        self.label_19.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>List <span style=\\\" text-decoration: underline;\\\">existing file tags</span> which will be appended to rather than over-written by tag mapping (this will keep tags even if &quot;Clear existing tags&quot; is selected on main options)<br/>NB To allow appending to happen, <span style=\\\" text-decoration: underline;\\\">do not also include these tags in &quot;Preserve tags&quot;</span> on the main options.</p></body></html>\"))\n        self.cea_keep.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Enter file tag names, separated by commas</p></body></html>\"))\n        self.cea_keep.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>This refers to the tags which already exist on files which have been matched to MusicBrainz in the right-hand panel, not the tags generated by Picard from the MusicBrainz database. Normally, Picard cannot process these tags - either it will overwrite them (if it creates a similarly named tag), clear them (if \\'Clear existing tags\\' is specified in the main Options-&gt;Tags screen) or keep them (if \\'Preserve these tags...\\' is specified after the \\'Clear existing tags\\' option). Classical Extras allows a further option - for the tags to be appended to in the tag mapping section (see below). List file tags which will be appended to rather than over-written by tag mapping (NB this will keep tags even if &quot;Clear existing tags&quot; is selected on main options). In addition, certain existing tags may be used by Classical Extras - in particular genre-related tags (see the Genres etc. options tab for more).</p><p>Note that if &quot;Split lyrics tag&quot; is specified (see the Artists tab), then the tag named there will be included in the \\'Keep file tags\\' list and does not need to be added in this section.</p></body></html>\"))\n        self.cea_clear_tags.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Note that the main Picard option &quot;Clear existing tags&quot; should be <span style=\\\" text-decoration: underline;\\\">unchecked</span> for this option to operate in preference to that Picard option. The difference is that this option <span style=\\\" text-decoration: underline;\\\">will not intefere with cover art</span>, whereas the main Picard option will remove previous cover art.</p></body></html>\"))\n        self.cea_clear_tags.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>If selected: the bottom pane of Picard will only show tags which have been generated from the MusicBrainz lookups plus any existing file tags which are listed above or in the main options &quot;Preserve tags...&quot;.</p><p>This does not mean that the file tags will be removed when saving the file. For that to happen, &quot;Clear existing tags&quot; needs to be selected in the main options.</p></body></html>\"))\n        self.cea_clear_tags.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Do not show any file tags that are NOT listed above AND NOT listed in the main Picard \\\"Preserve tags...\\\" option (Options->Tags), even if \\\"Clear existing tags\\\" is not selected.\"))\n        self.label_98.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Tag map details</span></p></body></html>\"))\n        self.tagmap_details_box.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Enter tags, separated by commas.</p></body></html>\"))\n        self.textBrowser.setHtml(_translate(\"ClassicalExtrasOptionsPage\", \"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.0//EN\\\" \\\"http://www.w3.org/TR/REC-html40/strict.dtd\\\">\\n\"\n\"<html><head><meta name=\\\"qrichtext\\\" content=\\\"1\\\" /><style type=\\\"text/css\\\">\\n\"\n\"p, li { white-space: pre-wrap; }\\n\"\n\"</style></head><body style=\\\" font-family:\\'MS Shell Dlg 2\\'; font-size:8pt; font-weight:72; font-style:normal;\\\">\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\"><span style=\\\" font-weight:600; text-decoration: underline;\\\">Notes: </span></p>\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">Click &quot;Source from:&quot; button to edit source tags.</p>\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">Any valid Picard-generated tag can be entered in the &quot;source&quot; box, as well as Classical Extras sources, and mapped into other tags - not just restricted to artists.</p>\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">To put a constant in a tag, type it into the source box preceded by a backslash \\\\.</p>\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">In all cases, the source will be APPENDED to the Picard tag. To replace the standard tag, first blank it in the section above - add it back later in the list below if required (e.g. artist -&gt; artist).</p>\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">BUT note that any existing LOCAL FILE tag will be replaced by (not appended with) any Picard/Classical Extras tag UNLESS specified in the list box above.</p>\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">These tag-mapping options may be omitted from the over-riding of artist options - see advanced tab</p>\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">For more help seethe readme.</p></body></html>\"))\n        self.toolButton_1.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_1.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_1.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_1.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_1.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_1.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_1.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_1.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_1.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_1.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_1.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_1.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_1.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_1.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_1.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_1.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_1.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_1.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_1.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_1.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_1.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_1.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_1.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_21.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_1.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_1.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_2.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_2.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_2.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_2.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_2.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_2.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_2.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_2.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_2.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_2.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_2.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_2.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_2.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_2.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_2.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_2.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_2.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_2.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_2.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_2.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_2.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_2.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_2.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_23.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_2.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_2.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_3.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_3.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_3.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_3.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_3.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_3.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_3.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_3.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_3.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_3.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_3.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_3.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_3.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_3.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_3.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_3.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_3.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_3.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_3.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_3.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_3.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_3.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_3.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_25.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_3.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_3.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_4.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_4.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_4.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_4.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_4.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_4.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_4.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_4.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_4.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_4.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_4.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_4.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_4.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_4.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_4.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_4.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_4.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_4.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_4.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_4.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_4.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_4.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_4.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_27.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_4.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_4.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_5.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_5.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_5.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_5.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_5.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_5.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_5.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_5.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_5.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_5.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_5.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_5.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_5.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_5.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_5.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_5.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_5.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_5.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_5.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_5.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_5.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_5.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_5.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_29.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_5.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_5.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_6.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_6.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_6.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_6.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_6.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_6.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_6.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_6.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_6.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_6.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_6.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_6.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_6.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_6.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_6.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_6.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_6.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_6.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_6.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_6.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_6.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_6.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_6.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_31.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_6.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_6.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_7.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_7.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_7.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_7.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_7.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_7.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_7.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_7.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_7.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_7.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_7.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_7.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_7.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_7.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_7.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_7.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_7.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_7.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_7.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_7.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_7.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_7.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_7.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_33.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_7.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_7.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_8.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_8.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_8.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_8.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_8.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_8.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_8.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_8.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_8.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_8.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_8.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_8.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_8.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_8.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_8.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_8.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_8.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_8.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_8.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_8.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_8.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_8.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_8.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_35.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_8.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_8.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_9.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_9.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_9.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_9.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_9.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_9.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_9.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_9.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_9.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_9.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_9.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_9.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_9.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_9.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_9.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_9.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_9.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_9.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_9.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_9.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_9.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_9.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_9.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_53.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_9.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_9.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_10.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_10.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_10.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_10.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_10.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_10.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_10.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_10.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_10.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_10.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_10.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_10.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_10.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_10.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_10.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_10.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_10.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_10.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_10.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_10.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_10.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_10.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_10.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_55.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_10.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_10.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_11.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_11.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_11.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_11.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_11.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_11.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_11.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_11.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_11.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_11.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_11.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_11.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_11.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_11.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_11.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_11.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_11.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_11.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_11.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_11.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_11.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_11.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_11.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_57.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_11.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_11.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_12.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_12.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_12.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_12.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_12.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_12.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_12.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_12.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_12.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_12.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_12.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_12.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_12.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_12.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_12.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_12.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_12.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_12.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_12.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_12.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_12.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_12.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_12.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_59.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_12.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_12.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_13.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_13.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_13.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_13.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_13.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_13.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_13.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_13.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_13.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_13.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_13.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_13.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_13.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_13.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_13.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_13.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_13.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_13.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_13.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_13.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_13.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_13.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_13.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_61.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_13.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_13.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_14.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_14.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_14.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_14.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_14.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_14.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_14.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_14.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_14.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_14.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_14.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_14.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_14.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_14.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_14.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_14.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_14.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_14.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_14.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_14.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_14.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_14.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_14.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_63.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_14.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_14.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_15.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_15.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_15.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_15.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_15.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_15.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_15.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_15.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_15.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_15.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_15.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_15.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_15.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_15.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_15.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_15.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_15.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_15.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_15.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_15.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_15.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_15.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_15.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_65.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_15.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_15.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.toolButton_16.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click to edit sources\"))\n        self.toolButton_16.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Source from:\"))\n        self.cea_source_16.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Click button to edit. See notes above.\"))\n        self.cea_source_16.setItemText(1, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists, album_conductors, album_ensembles\"))\n        self.cea_source_16.setItemText(2, _translate(\"ClassicalExtrasOptionsPage\", \"soloists, conductors, ensembles, album_composers, composers\"))\n        self.cea_source_16.setItemText(3, _translate(\"ClassicalExtrasOptionsPage\", \"album_soloists\"))\n        self.cea_source_16.setItemText(4, _translate(\"ClassicalExtrasOptionsPage\", \"album_conductors\"))\n        self.cea_source_16.setItemText(5, _translate(\"ClassicalExtrasOptionsPage\", \"album_ensembles\"))\n        self.cea_source_16.setItemText(6, _translate(\"ClassicalExtrasOptionsPage\", \"album_composers\"))\n        self.cea_source_16.setItemText(7, _translate(\"ClassicalExtrasOptionsPage\", \"album_composer_lastnames\"))\n        self.cea_source_16.setItemText(8, _translate(\"ClassicalExtrasOptionsPage\", \"soloists\"))\n        self.cea_source_16.setItemText(9, _translate(\"ClassicalExtrasOptionsPage\", \"soloist_names\"))\n        self.cea_source_16.setItemText(10, _translate(\"ClassicalExtrasOptionsPage\", \"ensembles\"))\n        self.cea_source_16.setItemText(11, _translate(\"ClassicalExtrasOptionsPage\", \"ensemble_names\"))\n        self.cea_source_16.setItemText(12, _translate(\"ClassicalExtrasOptionsPage\", \"composers\"))\n        self.cea_source_16.setItemText(13, _translate(\"ClassicalExtrasOptionsPage\", \"arrangers\"))\n        self.cea_source_16.setItemText(14, _translate(\"ClassicalExtrasOptionsPage\", \"orchestrators\"))\n        self.cea_source_16.setItemText(15, _translate(\"ClassicalExtrasOptionsPage\", \"conductors\"))\n        self.cea_source_16.setItemText(16, _translate(\"ClassicalExtrasOptionsPage\", \"chorusmasters\"))\n        self.cea_source_16.setItemText(17, _translate(\"ClassicalExtrasOptionsPage\", \"leaders\"))\n        self.cea_source_16.setItemText(18, _translate(\"ClassicalExtrasOptionsPage\", \"support_performers\"))\n        self.cea_source_16.setItemText(19, _translate(\"ClassicalExtrasOptionsPage\", \"work_type\"))\n        self.cea_source_16.setItemText(20, _translate(\"ClassicalExtrasOptionsPage\", \"release\"))\n        self.label_67.setText(_translate(\"ClassicalExtrasOptionsPage\", \"into tags:\"))\n        self.cea_tag_16.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"Enter comma-separated list of tags\"))\n        self.cea_cond_16.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Conditional?\"))\n        self.label_42.setText(_translate(\"ClassicalExtrasOptionsPage\", \"(If source is empty, tag will be left unchanged)                                                                                                                                        \"))\n        self.label_17.setText(_translate(\"ClassicalExtrasOptionsPage\", \"(Conditional tags will only be filled if previously empty)\"))\n        self.cea_tag_sort.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Select to include sort-tags, where available. See<span style=\\\" font-style:italic;\\\"> &quot;What\\'s this?&quot;</span>  for more details.</p></body></html>\"))\n        self.cea_tag_sort.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>If a sort tag is associated with the source tag then the sort names will be placed in a sort tag corresponding to the destination tag. Note that the only explicit sort tags written by Picard are for artist, albumartist and composer. Piacrd also writes hidden variables \\'_artists_sort\\' and \\'albumartists_sort\\' (note the plurals - these are the sort tags for multi-valued alternatives \\'artists\\' and \\'_albumartists\\'). To be consistent with this approach, the plugin writes hidden variables for other tags - e.g. \\'_arranger_sort\\'. The plugin also writes hidden sort variables for the various hidden artist variables - e.g. \\'_cwp_librettists\\' has a matching sort variable \\'_cwp_librettists_sort\\'. Therefore most artist-type sources <span style=\\\" font-weight:600;\\\">will</span> have a sort tag/variable associated with them and these will be placed in a destination sort tag if this option is selected -<span style=\\\" font-weight:600;\\\"> in other words, selecting this option will cause most destination tags to have associated sort tags</span>.<span style=\\\" font-weight:600;\\\"> Furthermore, any hidden sort variables associated with tags which are not listed explicitly in the tag mapping section will also be written out as tags</span> (i.e. even if the related tags are not included as destination tags). Note, however, that composite sources (e.g. &quot; ensemble_names + \\\\;  + conductors&quot;) do not have sort tags associated with them.</p><p><br/></p><p>If this option is not selected, no additional sort tags will be written, but the hidden variables will still be available, so if a sort tag is required explicitly, just map the sort tag directly - e.g. map \\'conductors_sort\\' to \\'conductor_sort\\'.</p></body></html>\"))\n        self.cea_tag_sort.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Also populate sort tags\"))\n        self.tabWidget.setTabText(self.tabWidget.indexOf(self.Tag_mapping), _translate(\"ClassicalExtrasOptionsPage\", \"Tag mapping\"))\n        self.label_110.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">General</span></p></body></html>\"))\n        self.ce_no_run.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Do not run Classical Extras for tracks where no pre-existing file is detected (warning tag will be written)\"))\n        self.advanced_artists_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Artists (only effective if \\\"Artists\\\" section enabled)</span></p></body></html>\"))\n        self.advanced_artists_box.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Separate multiple names by commas. Do not use any quotation marks.</p></body></html>\"))\n        self.advanced_artists_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Permits the listing of strings by which ensembles of different types may be identified. This is used by the plugin to place performer details in the relevant hidden variables and thus make them available for use in the &quot;Tag mapping&quot; tab as sources for any required tags. </p><p>If it is important that only whole words are to be matched, be sure to include a space after the string.</p></body></html>\"))\n        self.ensemble_strings_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Ensemble strings</span> (separate names by commas)</p></body></html>\"))\n        self.cea_orchestras_2.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Orchestras\"))\n        self.cea_choirs_2.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Choirs\"))\n        self.cea_groups_2.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Groups (i.e. other ensembles such as quartets etc.)\"))\n        self.advanced_workparts_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Works and parts (only effective if \\\"Works and parts\\\" section enabled)</span></p></body></html>\"))\n        self.label_4.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Sometimes MB lookups fail. Unfortunately Picard (currently) has no automatic &quot;retry&quot; function. The plugin will attempt to retry for the specified number of attempts. If it still fails, the hidden variable _cwp_error will be set with a message; if error logging is checked in section 4, an error message will be written to the log and the contents of _cwp_error will be written out to a special tag called &quot;An_error_has_occurred&quot; which should appear prominently in the bottom pane of Picard. The problem may be resolved by refreshing, otherwise there may be a problem with the MB database availability. It is unlikely to be a software problem with the plugin.</p></body></html>\"))\n        self.label_4.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Max number of re-tries to access works (in case of server errors)*</p></body></html>\"))\n        self.label_120.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Allow blank part names for arrangements and part recordings if arrangement/partial label is provided</p></body></html>\"))\n        self.label_114.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-size:10pt; font-weight:600;\\\">Removal of common text between parent and child works</span></p></body></html>\"))\n        self.label_90.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Minimum number of similar words required before eliminating. Use zero for no elimination.<br/>(Punctuation and accents etc. will be ignored in word comparison)</p></body></html>\"))\n        self.label_89.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>NB Parent name text at the start of a work which is followed by punctuation in the work name will always be stripped regardless of this setting.<br/>Synonyms in the next section also apply.</p></body></html>\"))\n        self.title_metadata_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>This subsection contains various parameters affecting the processing of strings in titles. Because titles are free-form, not all circumstances can be anticipated. Detailed documentation of these is beyond the scope of this Readme as the effects can be quite complex and subtle and may require an understanding of the plugin code (which is of course open-source) to acsertain them. If pure canonical works are used (&quot;Use only metadata from canonical works&quot; and, if necessary, &quot;Full MusicBrainz work hierarchy&quot; on the Works and parts tab, section 2) then this processing should be irrelevant, but no text from titles will be included. Some explanations are given below:</p><p>    &quot;Proximity of new words&quot;. When using extended metadata - i.e. &quot;metadata enhanced with title text&quot;, the plugin will attempt to remove similar words between the canonical work name (in MusicBrainz) and the title before extending the canonical name. After removing such words, a rather &quot;bitty&quot; result may occur. To avoid this, any new words with the specified proximity will have the words between them (or up to the end) included even if they repeat words in the work name.</p><p>    &quot;Prefixes&quot;. When using &quot;metadata from titles&quot; or extended metadata, the structure of the works in MusicBrainz is used to infer the structure in the title text, so strings that are repeated between tracks which are part of the same MusicBrainz work will be treated as &quot;higher level&quot;. This can lead to anomolies if, for instance, the titles are &quot;Work name: Part 1&quot;, &quot;Work name: Part 2&quot;, &quot;Part&quot; will be treated as part of the parent work name. Specifying such words in &quot;Prefixes&quot; will prevent this.</p><p>    &quot;Synonyms&quot;. These words will be considered equivalent when comparing work name and title text. Thus if one word appears in the work name, that and its synonym will be removed from the title in extending the metadata (subject to the proximity setting above).</p><p>    &quot;Replacements&quot;. These words/phrases will be replaced in the title text in extended metadata, regardless of the text in the work name.</p></body></html>\"))\n        self.label_115.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-size:10pt; font-weight:600;\\\">How title metadata should be included in extended metadata</span><span style=\\\" font-size:10pt;\\\"> (use cautiously - read documentation)</span><br/><span style=\\\" font-style:italic;\\\">(Mostly only applies if &quot;Use canonical work metadata enhanced with title text&quot; selected on &quot;Works and parts&quot; tab. </span><span style=\\\" font-weight:600; font-style:italic;\\\">However synonyms also apply to parent/child text removal</span><span style=\\\" font-style:italic;\\\">.)</span></p></body></html>\"))\n        self.label_6.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Proximity of new words (to each other) to trigger in-fill with existing words (default = 2)</p></body></html>\"))\n        self.label_7.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Proximity of new words (to start or end) to trigger in-fill with existing words (default =1)</p></body></html>\"))\n        self.label_5.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Treat hyphenated words as two words for comparison purposes</p></body></html>\"))\n        self.label_93.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Proportion of a string to be matched to a (usually larger) string for it to be considered essentially similar (default = 66%)</p></body></html>\"))\n        self.cwp_substring_match.setSuffix(_translate(\"ClassicalExtrasOptionsPage\", \"%\"))\n        self.label_87.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.0//EN\\\" \\\"http://www.w3.org/TR/REC-html40/strict.dtd\\\">\\n\"\n\"<html><head><meta name=\\\"qrichtext\\\" content=\\\"1\\\" /><style type=\\\"text/css\\\">\\n\"\n\"p, li { white-space: pre-wrap; }\\n\"\n\"</style></head><body style=\\\" font-family:\\'MS Shell Dlg 2\\'; font-size:8pt; font-weight:400; font-style:normal;\\\">\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\">Fill part name with title text if it would otherwise have no text other than arrangement or partial annotations</p></body></html>\"))\n        self.label_92.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600; font-style:italic;\\\">Prepositions/conjunctions and prefixes</span><span style=\\\" font-weight:600;\\\"><br/></span>DO NOT USE ANY COMMAS OR QUOTE MARKS (apostophes in words are acceptable)</p></body></html>\"))\n        self.label_91.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Prepositions &amp; conjunctions: these are words that will not be regarded as providing additional information (not treated as \\'new\\' words) unless they precede a new word.<br/>Use lower case only, comma separated</p></body></html>\"))\n        self.cwp_removewords_2.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Prefixes to be ignored in comparison (case insensitive, comma separated)<br/>To prevent a prefix from being ignored when extending metadata with title info, precede it with a space. <br/>To ensure only whole words are removed, follow with a space.</p></body></html>\"))\n        self.cwp_removewords.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Separate multiple names by commas. Do not use any quotation marks.</p></body></html>\"))\n        self.cwp_synonyms_2.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600; font-style:italic;\\\">Synonyms and replacements</span> - must be written as tuples separated by forward slashes - e.g (a,b) / (c,d,e) - a tuple may have two or more synonyms.</p><p>N.B. The matching is case-insensitive. Roman numerals will be treated as synonyms of arabic numerals in any event, so no need to enter these.</p><p>The last member of the tuple should be the canonical form (i.e. the one to which others are converted for comparison or replacement) and <span style=\\\" text-decoration: underline;\\\">must be a normal strin</span>g (not a regex). <span style=\\\" font-weight:600;\\\">See readme for full details</span>.<br/><span style=\\\" font-weight:600; font-style:italic;\\\">Unless entering a regular expression, use backslash \\\\ to escape any regex metacharacters, namely \\\\ ^ $ . | ? * + ( ) [ ] { <br/>Also escape commas , and forward slashes /. Do not enclose strings in quote marks.</span></p><p>Enter SYNONYM tuples below - each item in a tuple will be treated as similar when comparing works/parts and titles. The text in tags will be unaltered.</p></body></html>\"))\n        self.label_16.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Enter REMOVALS/REPLACEMENTS below - these will result in the &quot;extended&quot; text in tags being changed<br/>Put the word(s), phrase(s), or regular exprerssion(s) in the first part(s) of the tuple. The replacement text (or nothing - to remove) goes in the last member of the tuple.</p><p>N.B. Replacement text will operate BEFORE synonyms are considered.</p></body></html>\"))\n        self.cwp_replacements.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Entries must be 2-tuples, e.g. (Replace this, with this). Separate multiple tuples by forward slash. Do not use any quotation marks. Spaces are acceptable. The first item of a tuple may be a regular expression - enclose it with double exclamation marks - e.g.(!!regex here!!, replacement text here).</p></body></html>\"))\n        self.advanced_genres_label.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Genres etc.</span> (only required if Muso-specific options are used for genres/periods)</p></body></html>\"))\n        self.label_85.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Path to Muso reference database:\"))\n        self.label_84.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Name of Muso reference database\"))\n        self.label_86.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">RESTART PICARD AFTER CHANGING THESE</span></p></body></html>\"))\n        self.label_117.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Logging options</span>*</p></body></html>\"))\n        self.logging_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>These options are in addition to the options chosen in Picard\\'s &quot;Help-&gt;View error/debug log&quot; settings. They only affect messages written by this plugin. To enable debug messages to be shown, the flag needs to be set here and &quot;Debug mode&quot; needs to be turned on in the log. It is strongly advised to keep the &quot;debug&quot; and &quot;info&quot; flags unchecked unless debugging is required as they slow up processing significantly and may even cause Picard to crash on large releases. The &quot;error&quot; and &quot;warning&quot; flags should be left checked, unless it is required to suppress messages written out to tags (the default is to write messages to the tags 001_errors and 002_warnings).</p></body></html>\"))\n        self.log_error.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Error\"))\n        self.log_warning.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Warning\"))\n        self.log_debug.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Debug\"))\n        self.custom_logging_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Custom logging\"))\n        self.log_basic.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Basic\"))\n        self.log_info.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Full\"))\n        self.label_118.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">Classical Extras Special Tags</span></p></body></html>\"))\n        self.save_options_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>This can be used so that the user has a record of the version of Classical Extras which generated the tags and which options were selected to achieve the resulting tags. Note that the tags will be blanked first so this will only show the last options used on a particular file. The same tag can be used for both sets of options, resulting in a multi-valued tag. </p></body></html>\"))\n        self.save_options_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Save plugin details and options in a tag?*\"))\n        self.label_41.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Tag name for plugin version\"))\n        self.label_36.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Tag name for artist/mapping/misc. options\"))\n        self.label_38.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Tag name for work/genre options\"))\n        self.override_box.setWhatsThis(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>If options have previously been saved (see above), selecting these will cause the saved options to be used in preference to the displayed options. The displayed options will not be affected and will be used if no saved options are present. The default is for no over-ride. If artist options over-ride is chosen, then tag map detail options may be included or not in the override.</p><p><br/></p><p>The last checkbox, &quot;Overwrite options in Options Pages&quot;, is for <span style=\\\" font-weight:600;\\\">VERY CAREFUL USE ONLY</span>. It will cause any options read from the saved tags (if the relevant box has been ticked) to over-write the options on the plugin Options Page UI. The intended use of this is if for some reason the user\\'s preferred options have been erased/reverted to default - by using this option, the previously-used choices from a reliable filed album can be used to populate the Options Page. The box will automatically be unticked after loading/refreshing one album, to prevent inadvertant use. Far better is to make a <span style=\\\" font-weight:600;\\\">backup copy</span> of the picard.ini file.</p></body></html>\"))\n        self.override_box.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Over-ride plugin options displayed in Options Pages with options from local file tags (previously saved using method in box above)?*\"))\n        self.cea_override.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Artist options\"))\n        self.cwp_override.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Work options\"))\n        self.ce_genres_override.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Genres etc. options\"))\n        self.ce_tagmap_override.setToolTip(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Will not over-ride displayed options unless artist options over-ride is also selected</p></body></html>\"))\n        self.ce_tagmap_override.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Tag mapping options\"))\n        self.ce_options_overwrite.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Overwrite options in Options Pages (READ WARNINGS in Readme)\"))\n        self.label_121.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Note that the above saved options include the related \\\"advanced\\\" options on this tab as well as the options on each of the main tabs.\"))\n        self.ce_show_ui_tags.setText(_translate(\"ClassicalExtrasOptionsPage\", \"Show additional tags in Picard UI (rhs panel) - N.B. RESTART NEEDED FOR CHANGE TO TAKE EFFECT\"))\n        self.groupBox.setTitle(_translate(\"ClassicalExtrasOptionsPage\", \"Additional columns for Picard UI to show specific tags*\"))\n        self.label_94.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p><span style=\\\" font-weight:600;\\\">RESTART PICARD AFTER CHANGING THESE</span> - otherwise changes will not take effect</p></body></html>\"))\n        self.label_8.setText(_translate(\"ClassicalExtrasOptionsPage\", \"<html><head/><body><p>Notes:</p><p>1. Use the format <span style=\\\" font-style:italic;\\\">column_name_A: (include_tag_1, include_tag_2) / column_name_B: include_tag_3 </span> etc. (i.e. put multiple tags to be concatenated in brackets)<br/>2. To just flag tags that have changed, rather than show the contents, add _DIFF at the end of the tag name<br/>3. If more than one tag name is included for a column, then:<br/>(a) if the tags are _DIFF tags, then the column will be flagged if any of them have changed<br/>(b) otherwise the tag contents will be concatenated</p></body></html>\"))\n        self.label_83.setText(_translate(\"ClassicalExtrasOptionsPage\", \"* ASTERISKED OPTIONS ARE NOT SAVED IN FILE TAGS\"))\n        self.tabWidget.setTabText(self.tabWidget.indexOf(self.Advanced), _translate(\"ClassicalExtrasOptionsPage\", \"Advanced\"))\n        self.textBrowser_2.setHtml(_translate(\"ClassicalExtrasOptionsPage\", \"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.0//EN\\\" \\\"http://www.w3.org/TR/REC-html40/strict.dtd\\\">\\n\"\n\"<html><head><meta name=\\\"qrichtext\\\" content=\\\"1\\\" /><style type=\\\"text/css\\\">\\n\"\n\"p, li { white-space: pre-wrap; }\\n\"\n\"</style></head><body style=\\\" font-family:\\'MS Shell Dlg 2\\'; font-size:8.25pt; font-weight:400; font-style:normal;\\\">\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\"><span style=\\\" font-size:10pt; font-weight:600; text-decoration: underline;\\\">General description</span></p>\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\"><span style=\\\" font-size:8pt;\\\">Classical Extras provides tagging enhancements for Picard and, in particular, utilises MusicBrainz’s hierarchy of works to provide work/movement tags. All options are set through a user interface in Picard options-&gt;plugins. This interface provides separate sections to enhance artist/performer tags, works and parts, genres and also allows for a generalised &quot;tag mapping&quot; (simple scripting). While it is designed to cater for the complexities of classical music tagging, it may also be useful for other music which has more than just basic song/artist/album data. <br /><br />The options screen provides five tabs for users to control the tags produced: <br /><br />1. Artists: Options as to whether artist tags will contain standard MB names, aliases or as-credited names. Ability to include and annotate names for specialist roles (chorus master, arranger, lyricist etc.). Ability to read lyrics tags on the file which has been loaded and assign them to track and album levels if required. (Note: Picard will not normally process incoming file tags). <br /><br />2. Works and parts: The plugin will build a hierarchy of works and parts (e.g. Work -&gt; Part -&gt; Movement or Opera -&gt; Act -&gt; Number) based on the works in MusicBrainz\\'s database. These can then be displayed in tags in a variety of ways according to user preferences. Furthermore partial recordings, medleys, arrangements and collections of works are all handled according to user choices. There is a processing overhead for this at present because MusicBrainz limits look-ups to one per second. <br /><br />3. Genres etc.: Options are available to customise the source and display of information relating to genres, instruments, keys, work dates and periods. Additional capabilities are provided for users of Muso (or others who provide the relevant XML files) to use pre-existing databases of classical genres, classical composers and classical periods. <br /><br />4. Tag mapping: in some ways, this is a simple substitute for some of Picard\\'s scripting capability. The main advantage is that the plugin will remember what tag mapping you use for each release (or even track). <br /><br />5. Advanced: Various options to control the detailed processing of the above. <br /><br />All user options can be saved on a per-album (or even per-track) basis so that tweaks can be used to deal with inconsistencies in the MusicBrainz data (e.g. include English titles from the track listing where the MusicBrainz works are in the composer\\'s language and/or script). Also existing file tags can be processed (not possible in native Picard). </span></p></body></html>\"))\n        self.textBrowser_3.setHtml(_translate(\"ClassicalExtrasOptionsPage\", \"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.0//EN\\\" \\\"http://www.w3.org/TR/REC-html40/strict.dtd\\\">\\n\"\n\"<html><head><meta name=\\\"qrichtext\\\" content=\\\"1\\\" /><style type=\\\"text/css\\\">\\n\"\n\"p, li { white-space: pre-wrap; }\\n\"\n\"</style></head><body style=\\\" font-family:\\'MS Shell Dlg 2\\'; font-size:8.25pt; font-weight:400; font-style:normal;\\\">\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\"><span style=\\\" font-size:12pt; font-weight:600;\\\">Please see</span><span style=\\\" font-size:14pt; font-weight:600;\\\"> </span><a href=\\\"http://music.highmossergate.co.uk/symphony/tagging/classical-extras/\\\"><span style=\\\" font-size:14pt; text-decoration: underline; color:#0000ff;\\\">my website</span></a><span style=\\\" font-size:14pt; font-weight:600;\\\"> </span><span style=\\\" font-size:12pt; font-weight:600;\\\">for  full details of this plugin and how to use it.</span></p>\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\"><span style=\\\" font-size:10pt;\\\">This help page now has only general information.</span></p>\\n\"\n\"<p style=\\\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\"><span style=\\\" font-size:10pt;\\\">There are extensive tooltips and &quot;What\\'s This&quot; popups (right click for them)</span></p></body></html>\"))\n        self.tabWidget.setTabText(self.tabWidget.indexOf(self.Help), _translate(\"ClassicalExtrasOptionsPage\", \"Help\"))\n\n"
  },
  {
    "path": "plugins/classicdiscnumber/classicdiscnumber.py",
    "content": "PLUGIN_NAME = 'Classic Disc Numbers'\nPLUGIN_AUTHOR = 'Lukas Lalinsky'\nPLUGIN_DESCRIPTION = '''Moves disc numbers and subtitles from the separate tags to album titles.'''\nPLUGIN_VERSION = \"0.2\"\nPLUGIN_API_VERSIONS = [\"0.15\", \"2.0\"]\n\nfrom picard.metadata import register_track_metadata_processor\nimport re\n\n\ndef add_discnumbers(tagger, metadata, track, release):\n    if int(metadata[\"totaldiscs\"] or \"0\") > 1:\n        if \"discsubtitle\" in metadata:\n            metadata[\"album\"] = \"%s (disc %s: %s)\" % (\n                metadata[\"album\"], metadata[\"discnumber\"],\n                metadata[\"discsubtitle\"])\n        else:\n            metadata[\"album\"] = \"%s (disc %s)\" % (\n                metadata[\"album\"], metadata[\"discnumber\"])\n\nregister_track_metadata_processor(add_discnumbers)\n"
  },
  {
    "path": "plugins/collect_artists/collect_artists.py",
    "content": "PLUGIN_NAME = \"Collect Album Artists\"\nPLUGIN_AUTHOR = \"johbi\"\nPLUGIN_DESCRIPTION = \"Adds a context menu shortcut to collect all track artists from a release and format them as the releases album artist.\"\nPLUGIN_VERSION = '0.1'\nPLUGIN_API_VERSIONS = ['2.1', '2.2']\nPLUGIN_LICENSE = \"GPL-3.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl.txt\"\n\nfrom picard.album import Album\nfrom picard.ui.itemviews import BaseAction, register_album_action\n\nclass CollectArtists(BaseAction):\n    NAME = 'Replace release artist with &track artists...'\n\n    def callback(self, objs):\n        for album in objs:\n            if isinstance(album, Album):\n                trackartists = []\n                for track in album.tracks:\n                    if \"artists\" in track.metadata:\n                        artists = track.metadata.getall(\"artists\")\n                    elif \"artist\" in track.metadata:\n                        artists = track.metadata.getall(\"artist\")\n                    else:\n                        continue\n\n                    for artist in artists:\n                        if artist not in trackartists:\n                            trackartists.append(artist)\n\n                if len(trackartists) >= 2:\n                    albumartist = (\", \").join(trackartists[:-1]) + ' & ' + trackartists[-1]\n                elif len(trackartists) == 1:\n                    albumartist = trackartists[0]\n                else:\n                    self.tagger.window.set_statusbar_message(\"Could not find any artists for the album: \\\"\" + album.metadata['album'] + \"\\\"\")\n                    continue\n                album.metadata.set(\"albumartist\", albumartist)\n\n                for track in album.tracks:\n                    track.metadata.set(\"albumartist\", albumartist)\n\n                    for files in track.linked_files:\n                        track.update_file_metadata(files)\n\n                album.update()\n\nregister_album_action(CollectArtists())\n"
  },
  {
    "path": "plugins/compatible_TXXX/compatible_TXXX.py",
    "content": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = u\"Compatible TXXX frames\"\nPLUGIN_AUTHOR = u'Tungol'\nPLUGIN_DESCRIPTION = \"\"\"This plugin improves the compatibility of ID3 tags \\\nby using only a single value for TXXX frames. Multiple value TXXX frames \\\ntechnically don't comply with the ID3 specification.\"\"\"\nPLUGIN_VERSION = \"0.1\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom picard import config\nfrom picard.formats import register_format\nfrom picard.formats.id3 import MP3File, TrueAudioFile, DSFFile, AiffFile\nfrom mutagen import id3\n\n\nid3v24_join_with = '; '\n\n\ndef build_compliant_TXXX(self, encoding, desc, values):\n    \"\"\"Return a TXXX frame with only a single value.\n\n    Use id3v23_join_with as the sperator if using id3v2.3, otherwise the value\n    set in this plugin (default \"; \").\n    \"\"\"\n    if config.setting['write_id3v23']:\n        sep = config.setting['id3v23_join_with']\n    else:\n        sep = id3v24_join_with\n    joined_values = [sep.join(values)]\n    return id3.TXXX(encoding=encoding, desc=desc, text=joined_values)\n\n\n# I can't actually remove the original MP3File et al formats once they're\n# registered. This depends on the name of the replacements sorting after the\n# name of the originals, because picard.formats.guess_format picks the last\n# item from a sorted list.\n\n\nclass MP3FileCompliant(MP3File):\n    \"\"\"Alternate MP3 format class which uses single-value TXXX frames.\"\"\"\n\n    build_TXXX = build_compliant_TXXX\n\n\nclass TrueAudioFileCompliant(TrueAudioFile):\n    \"\"\"Alternate TTA format class which uses single-value TXXX frames.\"\"\"\n\n    build_TXXX = build_compliant_TXXX\n\n\nclass DSFFileCompliant(DSFFile):\n    \"\"\"Alternate DSF format class which uses single-value TXXX frames.\"\"\"\n\n    build_TXXX = build_compliant_TXXX\n\n\nclass AiffFileCompliant(AiffFile):\n    \"\"\"Alternate AIFF format class which uses single-value TXXX frames.\"\"\"\n\n    build_TXXX = build_compliant_TXXX\n\n\nregister_format(MP3FileCompliant)\nregister_format(TrueAudioFileCompliant)\nregister_format(DSFFileCompliant)\nregister_format(AiffFileCompliant)\n"
  },
  {
    "path": "plugins/critiquebrainz/critiquebrainz.py",
    "content": "# -*- coding: utf-8 -*-\n# Critiquebrainz plugin for Picard\n# Copyright (C) 2022  Tobias Sarner\n#\n# This program is free software; you can redistribute it and/or modify\n# it under the terms of the GNU General Public License version 2 as\n# published by the Free Software Foundation.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License along\n# with this program; if not, write to the Free Software Foundation, Inc.,\n# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n#\n\nPLUGIN_NAME = 'Critiquebrainz Review Comment'\nPLUGIN_AUTHOR = 'Tobias Sarner'\nPLUGIN_DESCRIPTION = '''Uses Critiquebrainz for comment as review or rating.\n\nWARNING: Experimental plugin. All guarantees voided by use.\n\nExample: Taylor Swift\nRelease: Midnights\n https://musicbrainz.org/release/e348fdd6-f73b-47fe-94c4-670bfee26a39 ,\n https://critiquebrainz.org/release-group/0dcc84fb-c592-46e9-ba92-a52bb44dd553\n\nRecording:\n https://musicbrainz.org/recording/93113326-93e9-409c-a3d6-5ec91864ba30 ,\n https://critiquebrainz.org/recording/93113326-93e9-409c-a3d6-5ec91864ba30'''\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.txt\"\nPLUGIN_VERSION = \"1.0.2\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\n\nfrom functools import partial\nfrom picard import log\nfrom picard.metadata import register_album_metadata_processor\nfrom picard.metadata import register_track_metadata_processor\nfrom picard.webservice import ratecontrol\nfrom picard.util import load_json\n\nCRITIQUEBRAINZ_HOST = \"critiquebrainz.org\"\nCRITIQUEBRAINZ_PORT = 80\n\nratecontrol.set_minimum_delay((CRITIQUEBRAINZ_HOST, CRITIQUEBRAINZ_PORT), 50)\n\n\ndef result_review(album, metadata, data, reply, error):\n    if error:\n        album._requests -= 1\n        album._finalize_loading(None)\n        return\n\n    try:\n        reviews = load_json(data).get(\"reviews\")\n        if reviews:\n            for review in reviews:\n                if \"last_revision\" in review:\n                    ident = (review[\"entity_type\"].replace(\"_\", \"-\") + \"_review_\" + review[\"published_on\"] + \"_\" + review[\"user\"][\"display_name\"] + \"_\" + review[\"language\"]).lower()\n                    if review[\"last_revision\"][\"text\"] is not None:\n                        review_text = review[\"last_revision\"][\"text\"] + \"\\n\\nEine Bewertung mit \" + str(review[\"last_revision\"][\"rating\"]) + \" von 5 Sternen veröffentlich durch \" + review[\"user\"][\"display_name\"] + \" am \" + review[\"published_on\"] + \" lizensiert unter \" + review[\"full_name\"] + \".\";\n                        if \"comment:\" + ident in metadata:\n                            metadata[\"comment:\" + ident] = review_text\n                        else:\n                            metadata.add(\"comment:\" + ident, review_text)\n                    review_url = \"https://critiquebrainz.org/review/\" + review[\"id\"]\n                    if \"website:\" + ident in metadata:\n                        metadata[\"website:\" + ident] = review_url\n                    else:\n                        metadata.add(\"website:\" + ident, review_url)\n            log.debug(\"%s: success parsing %s reviews (%s) from response\", PLUGIN_NAME, reviews[\"count\"], metadata[\"albumtitle\"])\n    except Exception as e:\n        log.error(\"%s: error parsing review (%s) from response: %s\", PLUGIN_NAME, metadata[\"albumtitle\"], str(e))\n    finally:\n        album._requests -= 1\n        album._finalize_loading(None)\n\ndef process_releasegroup(album, metadata, release):\n    queryargs = {\n        \"entity_id\": metadata[\"musicbrainz_releasegroupid\"],\n        \"entity_type\": \"release_group\"\n    }\n    album.tagger.webservice.get(\n        CRITIQUEBRAINZ_HOST,\n        CRITIQUEBRAINZ_PORT,\n        \"/ws/1/review\",\n        partial(result_review, album, metadata),\n        priority=True,\n        parse_response_type=None,\n        queryargs=queryargs\n    )\n    album._requests += 1\n\ndef process_recording(album, metadata, track, release):\n    queryargs = {\n        \"entity_id\": metadata[\"musicbrainz_recordingid\"],\n        \"entity_type\": \"recording\"\n    }\n    album.tagger.webservice.get(\n        CRITIQUEBRAINZ_HOST,\n        CRITIQUEBRAINZ_PORT,\n        \"/ws/1/review\",\n        partial(result_review, album, metadata),\n        priority=True,\n        parse_response_type=None,\n        queryargs=queryargs\n    )\n    album._requests += 1\n\n\nregister_album_metadata_processor(process_releasegroup)\nregister_track_metadata_processor(process_recording)\n\n"
  },
  {
    "path": "plugins/cuesheet/cuesheet.py",
    "content": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = \"Generate Cuesheet\"\nPLUGIN_AUTHOR = \"Lukáš Lalinský, Sambhav Kothari\"\nPLUGIN_DESCRIPTION = \"Generate cuesheet (.cue file) from an album.\"\nPLUGIN_VERSION = \"1.2.2\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\n\n\nimport os.path\nimport re\nfrom PyQt5 import QtCore, QtWidgets\nfrom picard.util import find_existing_path, encode_filename\nfrom picard.ui.itemviews import BaseAction, register_album_action\n\n\n_whitespace_re = re.compile(r'\\s', re.UNICODE)\n_split_re = re.compile(r'\\s*(\"[^\"]*\"|[^ ]+)\\s*', re.UNICODE)\n\n\ndef msfToMs(msf):\n    msf = msf.split(\":\")\n    return ((int(msf[0]) * 60 + int(msf[1])) * 75 + int(msf[2])) * 1000 / 75\n\n\nclass CuesheetTrack(list):\n\n    def __init__(self, cuesheet, index):\n        list.__init__(self)\n        self.cuesheet = cuesheet\n        self.index = index\n\n    def set(self, *args):\n        self.append(args)\n\n    def find(self, prefix):\n        return [i for i in self if tuple(i[:len(prefix)]) == tuple(prefix)]\n\n    def getTrackNumber(self):\n        return self.index\n\n    def getLength(self):\n        try:\n            nextTrack = self.cuesheet.tracks[self.index + 1]\n            index0 = self.find((\"INDEX\", \"01\"))\n            index1 = nextTrack.find((\"INDEX\", \"01\"))\n            return msfToMs(index1[0][2]) - msfToMs(index0[0][2])\n        except IndexError:\n            return 0\n\n    def getField(self, prefix):\n        try:\n            return self.find(prefix)[0][len(prefix)]\n        except IndexError:\n            return \"\"\n\n    def getArtist(self):\n        return self.getField((\"PERFORMER\",))\n\n    def getTitle(self):\n        return self.getField((\"TITLE\",))\n\n    def setArtist(self, artist):\n        found = False\n        for item in self:\n            if item[0] == \"PERFORMER\":\n                if not found:\n                    item[1] = artist\n                    found = True\n                else:\n                    del item\n        if not found:\n            self.append((\"PERFORMER\", artist))\n\n    artist = property(getArtist, setArtist)\n\n\nclass Cuesheet(object):\n\n    def __init__(self, filename):\n        self.filename = filename\n        self.tracks = []\n\n    def read(self):\n        with open(encode_filename(self.filename)) as f:\n            self.parse(f.readlines())\n\n    def unquote(self, string):\n        if string.startswith('\"'):\n            if string.endswith('\"'):\n                return string[1:-1]\n            else:\n                return string[1:]\n        return string\n\n    def quote(self, string):\n        if _whitespace_re.search(string):\n            return '\"' + string.replace('\"', '\\'') + '\"'\n        return string\n\n    def parse(self, lines):\n        track = CuesheetTrack(self, 0)\n        self.tracks = [track]\n        isUnicode = False\n        for line in lines:\n            # remove BOM\n            if line.startswith('\\xfe\\xff'):\n                isUnicode = True\n                line = line[1:]\n            # decode to unicode string\n            line = line.strip()\n            if isUnicode:\n                line = line.decode('UTF-8', 'replace')\n            else:\n                line = line.decode('ISO-8859-1', 'replace')\n            # parse the line\n            split = [self.unquote(s) for s in _split_re.findall(line)]\n            keyword = split[0].upper()\n            if keyword == 'TRACK':\n                trackNum = int(split[1])\n                track = CuesheetTrack(self, trackNum)\n                self.tracks.append(track)\n            track.append(split)\n\n    def write(self):\n        lines = []\n        for track in self.tracks:\n            num = track.index\n            for line in track:\n                indent = 0\n                if num > 0:\n                    if line[0] == \"TRACK\":\n                        indent = 2\n                    elif line[0] != \"FILE\":\n                        indent = 4\n                line2 = \" \".join([self.quote(s) for s in line])\n                line2 = \" \" * indent + line2 + \"\\n\"\n                lines.append(line2.encode(\"UTF-8\"))\n        with open(encode_filename(self.filename), \"wb\") as f:\n            f.writelines(lines)\n\n\nclass GenerateCuesheet(BaseAction):\n    NAME = \"Generate &Cuesheet...\"\n\n    def callback(self, objs):\n        album = objs[0]\n        current_directory = self.config.persist[\"current_directory\"] or QtCore.QDir.homePath()\n        current_directory = find_existing_path(str(current_directory))\n        filename, selected_format = QtWidgets.QFileDialog.getSaveFileName(\n            None, \"\", current_directory, \"Cuesheet (*.cue)\")\n        if filename:\n            cuesheet = Cuesheet(filename)\n            #try: cuesheet.read()\n            #except IOError: pass\n            while len(cuesheet.tracks) <= len(album.tracks):\n                track = CuesheetTrack(cuesheet, len(cuesheet.tracks))\n                cuesheet.tracks.append(track)\n            #if len(cuesheet.tracks) > len(album.tracks) - 1:\n            #    cuesheet.tracks = cuesheet.tracks[0:len(album.tracks)+1]\n\n            t = cuesheet.tracks[0]\n            t.set(\"PERFORMER\", album.metadata[\"albumartist\"])\n            t.set(\"TITLE\", album.metadata[\"album\"])\n            t.set(\"REM\", \"MUSICBRAINZ_ALBUM_ID\", album.metadata[\"musicbrainz_albumid\"])\n            t.set(\"REM\", \"MUSICBRAINZ_ALBUM_ARTIST_ID\", album.metadata[\"musicbrainz_albumartistid\"])\n            if \"date\" in album.metadata:\n                t.set(\"REM\", \"DATE\", album.metadata[\"date\"])\n            index = 0.0\n            for i, track in enumerate(album.tracks):\n                mm = index / 60.0\n                ss = (mm - int(mm)) * 60.0\n                ff = (ss - int(ss)) * 75.0\n                index += track.metadata.length / 1000.0\n                t = cuesheet.tracks[i + 1]\n                t.set(\"TRACK\", \"%02d\" % (i + 1), \"AUDIO\")\n                t.set(\"PERFORMER\", track.metadata[\"artist\"])\n                t.set(\"TITLE\", track.metadata[\"title\"])\n                t.set(\"REM\", \"MUSICBRAINZ_TRACK_ID\", track.metadata[\"musicbrainz_trackid\"])\n                t.set(\"REM\", \"MUSICBRAINZ_ARTIST_ID\", track.metadata[\"musicbrainz_artistid\"])\n                t.set(\"INDEX\", \"01\", \"%02d:%02d:%02d\" % (mm, ss, ff))\n                for file in track.linked_files:\n                    audio_filename = file.filename\n                    extension = audio_filename.split(\".\")[-1].lower()\n                    if os.path.dirname(filename) == os.path.dirname(audio_filename):\n                        audio_filename = os.path.basename(audio_filename)\n                    if extension in [\"mp3\", \"mp2\", \"m2a\"]:\n                        file_type = \"MP3\"\n                    elif extension in [\"aiff\", \"aif\", \"aifc\"]:\n                        file_type = \"AIFF\"\n                    else:\n                        file_type = \"WAVE\"\n                    cuesheet.tracks[i].set(\"FILE\", audio_filename, file_type)\n\n            cuesheet.write()\n\n\naction = GenerateCuesheet()\nregister_album_action(action)\n"
  },
  {
    "path": "plugins/decade/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2019 Philipp Wolfer\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = 'Decade function'\nPLUGIN_AUTHOR = 'Philipp Wolfer'\nPLUGIN_DESCRIPTION = ('Add a $decade(date) function to get the decade '\n                      'from a year. E.g. $decade(1994-04-05) will give \"90s\". '\n                      'By default decades between 1920 and 2000 will be '\n                      'shortened to two digits. You can disable this with '\n                      'setting the second parameter to 0, e.g. '\n                      '$decade(1994,0) will give \"1990s\".')\nPLUGIN_VERSION = \"1.0\"\nPLUGIN_API_VERSIONS = [\"2.0\", \"2.1\", \"2.2\", \"2.3\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\n\nfrom picard.script import register_script_function\n\n\ndef decade(date, shorten=True):\n    \"\"\"\n    >>> decade(\"1970-09-18\")\n    '70s'\n    >>> decade(\"1994\")\n    '90s'\n    >>> decade(\"1994-04\")\n    '90s'\n    >>> decade(\"1994-04-05\")\n    '90s'\n    >>> decade(\"1994-04-05\", shorten=False)\n    '1990s'\n    >>> decade(\"1901\")\n    '1900s'\n    >>> decade(\"1917-08-22\")\n    '1910s'\n    >>> decade(\"1921-10-10\")\n    '20s'\n    >>> decade(\"1770-12-16\")\n    '1770s'\n    >>> decade(\"2017-07-20\")\n    '2010s'\n    >>> decade(\"2020\")\n    '2020s'\n    >>> decade(\"992\")\n    '990s'\n    >>> decade(\"992-01\")\n    '990s'\n    >>> decade(\"\")\n    ''\n    >>> decade(None)\n    ''\n    >>> decade(\"foo\")\n    ''\n    \"\"\"\n    try:\n        year = int(date.split('-')[0])\n    except (AttributeError, ValueError):\n        return \"\"\n    decade = year // 10 * 10\n    if shorten and 1920 <= decade < 2000:\n        decade -= 1900\n    return \"%ds\" % decade\n\n\ndef script_decade(parser, value, shorten=True):\n    return decade(value, shorten and shorten != '0')\n\n\nregister_script_function(script_decade, name=\"decade\")\n"
  },
  {
    "path": "plugins/decode_cyrillic/decode_cyrillic.py",
    "content": "# -*- coding: utf-8 -*-\n\n# This is the Decode Cyrillic plugin for MusicBrainz Picard.\n# Copyright (C) 2015 aeontech\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n\nfrom __future__ import print_function\nPLUGIN_NAME = \"Decode Cyrillic\"\nPLUGIN_AUTHOR = \"aeontech\"\nPLUGIN_DESCRIPTION = '''\nThis plugin helps you quickly convert mis-encoded cyrillic Windows-1251 tags\nto proper UTF-8 encoded strings. If your track/album names look something like\n\"Àëèñà â ñò›àíå ÷óäåñ\", run this plugin from the context menu\nbefore running the \"Lookup\" or \"Scan\" tools\n'''\nPLUGIN_VERSION = \"1.1\"\nPLUGIN_API_VERSIONS = [\"1.0\", \"2.0\"]\nPLUGIN_LICENSE = \"MIT\"\nPLUGIN_LICENSE_URL = \"https://opensource.org/licenses/MIT\"\n\nfrom picard import log\nfrom picard.cluster import Cluster\nfrom picard.ui.itemviews import BaseAction, register_cluster_action\n\n_decode_tags = [\n    'title',\n    'albumartist',\n    'artist',\n    'album',\n    'artistsort'\n]\n# _from_encoding = \"latin1\"\n# _to_encoding   = \"cp1251\"\n\n\n# TODO:\n# - extend to support multiple codepage decoding, not just cp1251->latin1\n#   instead, try the common variations, and show a dialog to the user,\n#   allowing him to select the correct transcoding. See 2cyr.com for example.\n# - also see http://stackoverflow.com/questions/23326531/how-to-decode-cp1252-string\n\nclass DecodeCyrillic(BaseAction):\n    NAME = \"Unmangle cyrillic metadata\"\n\n    def unmangle(self, tag, value):\n        try:\n            print(value, value.encode('latin1'))\n            unmangled_value = value.encode('latin1').decode('cp1251')\n        except UnicodeError:\n            unmangled_value = value\n            log.debug(\"%s: could not unmangle tag %s; original value: %s\" % (PLUGIN_NAME, tag, value))\n        return unmangled_value\n\n    def callback(self, objs):\n        for cluster in objs:\n            if not isinstance(cluster, Cluster):\n                continue\n\n            for tag in _decode_tags:\n                if not (tag in cluster.metadata):\n                    continue\n\n                cluster.metadata[tag] = self.unmangle(tag, cluster.metadata[tag])\n\n            log.debug(\"cluster name is %s by %s\" % (cluster.metadata['album'], cluster.metadata['albumartist']))\n\n            for i, file in enumerate(cluster.files):\n\n                log.debug(\"%s: Trying to unmangle file - original metadata %s\" % (PLUGIN_NAME, file.orig_metadata))\n\n                for tag in _decode_tags:\n\n                    if not (tag in file.metadata):\n                        continue\n\n                    unmangled_tag = self.unmangle(tag, file.metadata[tag])\n\n                    file.orig_metadata[tag] = unmangled_tag\n                    file.metadata[tag] = unmangled_tag\n\n                    file.orig_metadata.changed = True\n                    file.metadata.changed = True\n                    file.update(signal=True)\n\n            cluster.update()\n\nregister_cluster_action(DecodeCyrillic())\n"
  },
  {
    "path": "plugins/decode_greek_cyrillic/decode_greek1253.py",
    "content": "# -*- coding: utf-8 -*-\n#This is not my work. I just changed the language to Greek.\n#All the credits goes to the original coder.\n\n# This is the Decode Greek plugin for MusicBrainz Picard.\n# Copyright (C) 2015 aeontech\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n# THE SOFTWARE.\n\nfrom __future__ import print_function\nPLUGIN_NAME = \"Decode Cyrillic Greek\"\nPLUGIN_AUTHOR = \"aeontech, Lefteris NeNpO\"\nPLUGIN_VERSION = \"1.3\"\nPLUGIN_API_VERSIONS = [\"1.0\", \"2.0\"]\nPLUGIN_LICENSE = \"MIT\"\nPLUGIN_LICENSE_URL = \"https://opensource.org/licenses/MIT\"\nPLUGIN_DESCRIPTION = '''\nThis plugin helps you quickly convert mis-encoded Greek Windows-1253 tags\nto proper UTF-8 encoded strings. If your track/album names look something like\n\"Àëèñà â ñò›àíå ÷óäåñ\", run this plugin from the context menu\nbefore running the \"Lookup\" or \"Scan\" tools\n'''\nfrom picard import log\nfrom picard.cluster import Cluster\nfrom picard.ui.itemviews import BaseAction, register_cluster_action\n\n_decode_tags = [\n    'title',\n    'albumartist',\n    'albumartistsort',\n    'artist',\n    'artistsort',\n    'album',\n    'comment:',\n    'comment:ID3v1 Comment'\n]\nclass DecodeGreek(BaseAction):\n    NAME = \"Unmangle Greek metadata\"\n    def unmangle(self, tag, value):\n        try:\n            log.debug(\"%s: %s => %r\" % (PLUGIN_NAME, value, value.encode('latin1')))\n            unmangled_value = value.encode('latin1').decode('cp1253')\n        except UnicodeError:\n            unmangled_value = value\n            log.debug(\"%s: could not unmangle tag %s; original value: %s\" % (PLUGIN_NAME, tag, value))\n        return unmangled_value\n    def callback(self, objs):\n        for cluster in objs:\n            if not isinstance(cluster, Cluster):\n                continue\n            for tag in _decode_tags:\n                if not (tag in cluster.metadata):\n                    continue\n                cluster.metadata[tag] = self.unmangle(tag, cluster.metadata[tag])\n            log.debug(\"cluster name is %s by %s\" % (cluster.metadata['album'], cluster.metadata['albumartist']))\n            for file in cluster.files:\n                log.debug(\"%s: Trying to unmangle file - original metadata %s\" % (PLUGIN_NAME, file.orig_metadata))\n                for tag in _decode_tags:\n                    if not (tag in file.metadata):\n                        continue\n                    unmangled_tag = self.unmangle(tag, file.metadata[tag])\n                    file.orig_metadata[tag] = unmangled_tag\n                    file.metadata[tag] = unmangled_tag\n                    file.orig_metadata.changed = True\n                    file.metadata.changed = True\n                    file.update(signal=True)\n            cluster.update()\nregister_cluster_action(DecodeGreek())\n"
  },
  {
    "path": "plugins/deezerart/__init__.py",
    "content": "PLUGIN_NAME = \"Deezer cover art\"\nPLUGIN_AUTHOR = \"Fabio Forni <livingsilver94>\"\nPLUGIN_DESCRIPTION = \"Fetch cover arts from Deezer\"\nPLUGIN_VERSION = '1.2.2'\nPLUGIN_API_VERSIONS = ['2.5']\nPLUGIN_LICENSE = \"GPL-3.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-3.0.html\"\n\nfrom typing import Any, List, Optional\nfrom urllib.parse import urlsplit\n\nimport picard\nfrom picard import config\nfrom picard.coverart import providers\nfrom picard.coverart.image import CoverArtImage\nfrom picard.util.astrcmp import astrcmp\nfrom PyQt5 import QtNetwork as QtNet\n\nfrom .deezer import Client, SearchOptions, obj\nfrom .options import Ui_Form\n\n__version__ = PLUGIN_VERSION\n\nDEFAULT_SIMILARITY_THRESHOLD = 0.6\n\n\ndef is_similar(str1: str, str2: str, min_similarity: float = DEFAULT_SIMILARITY_THRESHOLD) -> bool:\n    if str1 in str2:\n        return True\n    return astrcmp(str1, str2) >= min_similarity\n\n\ndef is_deezer_url(url: str) -> bool:\n    return 'deezer.com' in urlsplit(url).netloc\n\n\nclass OptionsPage(providers.ProviderOptions):\n    NAME = 'Deezer'\n    TITLE = 'Deezer'\n    options = [\n        config.TextOption('setting', 'deezerart_size', obj.CoverSize.BIG.value),\n        config.FloatOption('setting', 'deezerart_min_similarity', DEFAULT_SIMILARITY_THRESHOLD),\n    ]\n    _options_ui = Ui_Form\n\n    def load(self):\n        for s in obj.CoverSize:\n            self.ui.size.addItem(str(s.name).title(), userData=s.value)\n        self.ui.size.setCurrentIndex(self.ui.size.findData(config.setting['deezerart_size']))\n        self.ui.min_similarity.setValue(int(config.setting[\"deezerart_min_similarity\"] * 100))\n\n    def save(self):\n        config.setting['deezerart_size'] = self.ui.size.currentData()\n        config.setting['deezerart_min_similarity'] = float(self.ui.min_similarity.value()) / 100.0\n\n\nclass Provider(providers.CoverArtProvider):\n    NAME = 'Deezer'\n    OPTIONS = OptionsPage\n    _log_prefix = 'Deezerart: '\n\n    def __init__(self, coverart):\n        super().__init__(coverart)\n        self.client = Client(self.album.tagger.webservice)\n        self._has_url_relation = False\n        self._retry_search = False\n\n    # Override.\n    def queue_images(self):\n        self.match_url_relations(['free streaming'], self._url_callback)\n        if not self._has_url_relation:\n            if not self._retry_search:\n                search_opts = SearchOptions(artist=self._artist(), album=self.metadata['album'])\n            else:\n                try:\n                    track = self.release['media'][0]['tracks'][1]['title']\n                except (IndexError, KeyError):\n                    self.error('cannot find a track name to retry a search. No cover found')\n                    return self.FINISHED\n                else:\n                    search_opts = SearchOptions(artist=self._artist(), track=track)\n            self.client.advanced_search(search_opts, self._queue_from_search)\n        self.album._requests += 1\n        return self.WAIT\n\n    # Override.\n    def error(self, msg):\n        super().error(self._log_prefix + msg)\n\n    def log_debug(self, msg: Any, *args):\n        picard.log.debug(self._log_prefix + msg, *args)\n\n    def _url_callback(self, url: str):\n        if is_deezer_url(url):\n            self._has_url_relation = True\n            self.client.obj_from_url(url, self._queue_from_url)\n\n    def _queue_from_url(self, album: obj.APIObject, error: QtNet.QNetworkReply.NetworkError):\n        self.album._requests -= 1\n        try:\n            if error:\n                self.error('could not get Deezer API object: {}'.format(error))\n                return\n            if not isinstance(album, obj.Album):\n                self.error('API object is not an album')\n                return\n            cover_url = album.cover_url(obj.CoverSize(config.setting['deezerart_size']))\n            self.queue_put(CoverArtImage(cover_url))\n            self.log_debug('queued cover using an URL relation')\n        finally:\n            self.next_in_queue()\n\n    def _queue_from_search(self, results: List[obj.APIObject], error: Optional[QtNet.QNetworkReply.NetworkError]):\n        self.album._requests -= 1\n        try:\n            if error:\n                self.error('could not fetch search results: {}'.format(error))\n                return\n            if len(results) == 0:\n                if self._retry_search:\n                    self.error('no results found')\n                    return\n                self._retry_search = True\n                self.queue_images()\n                return\n            artist = self._artist()\n            album = self.metadata['album']\n            min_similarity = config.setting['deezerart_min_similarity']\n            for result in results:\n                if not isinstance(result, obj.Track):\n                    continue\n                if not is_similar(artist, result.artist.name, min_similarity):\n                    self.log_debug('artist similarity below threshold: %r ~ %r', artist, result.artist.name)\n                    continue\n                if not is_similar(album, result.album.title, min_similarity):\n                    self.log_debug('album similarity below threshold: %r ~ %r', album, result.album.title)\n                    continue\n                cover_url = result.album.cover_url(obj.CoverSize(config.setting['deezerart_size']))\n                self.queue_put(CoverArtImage(cover_url))\n                self.log_debug('queued cover using a Deezer search')\n                return\n            self.error('no result matched the criteria')\n        finally:\n            self.next_in_queue()\n\n    def _artist(self) -> str:\n        # If there are many artists, we want to search\n        # the album in Deezer with just one as keyword.\n        # Deezerart may not specify all the artists\n        # MusicBrainz does, or it may use different separators.\n        return self.metadata.getraw('~albumartists')[0]\n\n\nproviders.register_cover_art_provider(Provider)\n"
  },
  {
    "path": "plugins/deezerart/deezer/__init__.py",
    "content": "from .client import Client, SearchOptions\n"
  },
  {
    "path": "plugins/deezerart/deezer/client.py",
    "content": "import json\nfrom functools import partial\nfrom typing import Callable, List, NamedTuple, Optional, TypeVar\nfrom urllib.parse import urlsplit, urlunsplit\n\nfrom picard.webservice import WebService\nfrom PyQt5.QtCore import QByteArray\nfrom PyQt5.QtNetwork import QNetworkReply\n\nfrom . import obj\n\nDEEZER_HOST = 'api.deezer.com'\nDEEZER_PORT = 443\n\n\nT = TypeVar('T', bound=obj.APIObject)\nSearchCallback = Callable[[List[T], Optional[QNetworkReply.NetworkError]], None]\nAPIURLCallback = Callable[[Optional[T], Optional[QNetworkReply.NetworkError]], None]\n\n\nclass SearchOptions(NamedTuple('SearchOptions', [('artist', str), ('album', str), ('track', str), ('label', str)])):\n    \"\"\"\n    Options for the advanced search.\n    \"\"\"\n\n    def __str__(self):\n        options = ['{}:\"{}\"'.format(k, v) for k, v in self._asdict().items() if v]\n        return ' '.join(options)\n\n\n# Python 3.5 cannot set defaults values in an othodox way.\nSearchOptions.__new__.__defaults__ = ('',) * len(SearchOptions._fields)\n\n\nclass Client:\n    def __init__(self, webservice: WebService):\n        self.webservice = webservice\n        self._get = partial(self.webservice.get, DEEZER_HOST, DEEZER_PORT)\n\n    def advanced_search(self, options: SearchOptions, callback: SearchCallback[obj.APIObject]):\n        path = '/search'\n\n        def handler(document: QByteArray, _: QNetworkReply, error: Optional[QNetworkReply.NetworkError]):\n            try:\n                parsed_doc = json.loads(str(document, 'utf-8'))\n            except json.JSONDecodeError:\n                callback([], error)\n            else:\n                result = []\n                error = None\n                if 'data' in parsed_doc:\n                    result = [obj.parse_json(dct) for dct in parsed_doc['data']]\n                elif 'error' in parsed_doc:\n                    error = parsed_doc['error'].get('message', 'Deezer responded with an unknown error')\n                else:\n                    error = 'Deezer returned an unexpected response'\n                callback(result, error)\n\n        self._get(path,\n                  queryargs={'q': str(options)},\n                  parse_response_type=None,\n                  handler=handler)\n\n    def obj_from_url(self, url: str, callback: APIURLCallback[obj.APIObject]):\n        def handler(document: QByteArray, _: QNetworkReply, error: Optional[QNetworkReply.NetworkError]):\n            try:\n                deezer_obj = obj.parse_json(str(document, 'utf-8'))\n            except json.JSONDecodeError:\n                deezer_obj = None\n            finally:\n                callback(deezer_obj, error)\n\n        self._get(self._remove_language_path(urlsplit(url).path),\n                  parse_response_type=None,\n                  handler=handler)\n\n    @staticmethod\n    def api_url(url: str) -> str:\n        parsed = urlsplit(url)\n        path = Client._remove_language_path(parsed.path)\n        return urlunsplit((parsed.scheme, DEEZER_HOST, path, parsed.query, parsed.fragment))\n\n    @staticmethod\n    def _remove_language_path(path: str) -> str:\n        # Deezer has a 2-letter language path, e.g. /us/track/123.\n        lang_len = 2\n        paths = path[1:].split('/', maxsplit=1)\n        return path[lang_len + 1:] if len(paths[0]) == lang_len else path\n"
  },
  {
    "path": "plugins/deezerart/deezer/obj.py",
    "content": "import enum\nimport json\nfrom typing import Any, List, Mapping, Optional, Union\n\n# This module is a perfect target for dataclasses.\n# Picard though supports Python 3.5+, so we cannot\n# use dataclasses for compatibility reasons.\n\n\nclass APIObject:\n    \"\"\"\n    Base class from Deezer API objects.\n    \"\"\"\n    fields = []  # type: List[str]\n\n    def __init__(self, **kwargs):\n        if len(self.fields) == 0:\n            raise NotImplementedError(type(self).__name__ + ' cannot have an empty field list')\n        for field in self.fields:\n            setattr(self, field, kwargs.get(field))\n\n    def __eq__(self, other):\n        for field in self.fields:\n            if getattr(self, field) != getattr(other, field):\n                return False\n        return True\n\n\nclass Artist(APIObject):\n    \"\"\"\n    The Artist API object.\n    \"\"\"\n    fields = ['name']\n\n\nclass CoverSize(enum.Enum):\n    \"\"\"\n    Cover size selector.\n    \"\"\"\n    THUMBNAIL = 'small'\n    SMALL = ''\n    MEDIUM = 'medium'\n    BIG = 'big'\n    LARGE = 'xl'\n\n\nclass Album(APIObject):\n    \"\"\"\n    The Album API object.\n    \"\"\"\n    fields = ['title', 'cover']\n\n    def cover_url(self, cover_size: CoverSize) -> str:\n        \"\"\"\n        Get the album cover URL based on the size wanted.\n        \"\"\"\n        return '{}?size={}'.format(self.cover, cover_size.value)\n\n\nclass Track(APIObject):\n    \"\"\"\n    The Track API object.\n    \"\"\"\n    fields = ['album', 'artist']\n\n\navailable_objects = {c.__name__.lower(): c for c in APIObject.__subclasses__()}\n\n\ndef parse_json(data: Union[str, Mapping[str, Any]]) -> APIObject:\n    if isinstance(data, str):\n        return json.loads(data, object_hook=_dict_to_object)\n\n    def convert_inner(data: Mapping[str, Any]):\n        \"\"\"\n        Traverse the dictionaries with post-order\n        algorithm to convert dict objects in Object instances.\n        \"\"\"\n        for k, v in data.items():\n            if isinstance(v, dict):\n                data[k] = convert_inner(v)\n        return _dict_to_object(data)\n\n    convert_inner(data)\n    return _dict_to_object(data)\n\n\ndef _dict_to_object(data: Mapping[str, Any]) -> Optional[APIObject]:\n    try:\n        obj_type = data['type']\n        obj_class = available_objects[obj_type]\n    except KeyError:\n        return None\n    else:\n        return obj_class(**data)\n"
  },
  {
    "path": "plugins/deezerart/options.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/deezerart/options.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.9\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_Form(object):\n    def setupUi(self, Form):\n        Form.setObjectName(\"Form\")\n        Form.resize(420, 320)\n        self.gridLayout = QtWidgets.QGridLayout(Form)\n        self.gridLayout.setObjectName(\"gridLayout\")\n        self.sizeLabel = QtWidgets.QLabel(Form)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.sizeLabel.sizePolicy().hasHeightForWidth())\n        self.sizeLabel.setSizePolicy(sizePolicy)\n        self.sizeLabel.setObjectName(\"sizeLabel\")\n        self.gridLayout.addWidget(self.sizeLabel, 0, 0, 1, 1)\n        self.size = QtWidgets.QComboBox(Form)\n        self.size.setObjectName(\"size\")\n        self.gridLayout.addWidget(self.size, 0, 2, 1, 1)\n        self.min_similarity_label = QtWidgets.QLabel(Form)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.min_similarity_label.sizePolicy().hasHeightForWidth())\n        self.min_similarity_label.setSizePolicy(sizePolicy)\n        self.min_similarity_label.setObjectName(\"min_similarity_label\")\n        self.gridLayout.addWidget(self.min_similarity_label, 1, 0, 1, 2)\n        self.min_similarity = QtWidgets.QSpinBox(Form)\n        self.min_similarity.setMaximum(100)\n        self.min_similarity.setObjectName(\"min_similarity\")\n        self.gridLayout.addWidget(self.min_similarity, 1, 2, 1, 1)\n        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.gridLayout.addItem(spacerItem, 2, 1, 1, 1)\n\n        self.retranslateUi(Form)\n        QtCore.QMetaObject.connectSlotsByName(Form)\n\n    def retranslateUi(self, Form):\n        _translate = QtCore.QCoreApplication.translate\n        Form.setWindowTitle(_translate(\"Form\", \"Form\"))\n        self.sizeLabel.setText(_translate(\"Form\", \"Cover size:\"))\n        self.min_similarity_label.setText(_translate(\"Form\", \"Minimal similarity for matches:\"))\n        self.min_similarity.setSuffix(_translate(\"Form\", \" %\"))\n        self.min_similarity.setPrefix(_translate(\"Form\", \" \"))\n"
  },
  {
    "path": "plugins/deezerart/options.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>Form</class>\n <widget class=\"QWidget\" name=\"Form\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>420</width>\n    <height>320</height>\n   </rect>\n  </property>\n  <property name=\"windowTitle\">\n   <string>Form</string>\n  </property>\n  <layout class=\"QGridLayout\" name=\"gridLayout\">\n   <item row=\"0\" column=\"0\">\n    <widget class=\"QLabel\" name=\"sizeLabel\">\n     <property name=\"sizePolicy\">\n      <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n       <horstretch>0</horstretch>\n       <verstretch>0</verstretch>\n      </sizepolicy>\n     </property>\n     <property name=\"text\">\n      <string>Cover size:</string>\n     </property>\n    </widget>\n   </item>\n   <item row=\"0\" column=\"2\">\n    <widget class=\"QComboBox\" name=\"size\"/>\n   </item>\n   <item row=\"1\" column=\"0\" colspan=\"2\">\n    <widget class=\"QLabel\" name=\"min_similarity_label\">\n     <property name=\"sizePolicy\">\n      <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n       <horstretch>0</horstretch>\n       <verstretch>0</verstretch>\n      </sizepolicy>\n     </property>\n     <property name=\"text\">\n      <string>Minimal similarity for matches:</string>\n     </property>\n    </widget>\n   </item>\n   <item row=\"1\" column=\"2\">\n    <widget class=\"QSpinBox\" name=\"min_similarity\">\n     <property name=\"suffix\">\n      <string> %</string>\n     </property>\n     <property name=\"prefix\">\n      <string> </string>\n     </property>\n     <property name=\"maximum\">\n      <number>100</number>\n     </property>\n    </widget>\n   </item>\n   <item row=\"2\" column=\"1\">\n    <spacer name=\"verticalSpacer\">\n     <property name=\"orientation\">\n      <enum>Qt::Vertical</enum>\n     </property>\n     <property name=\"sizeHint\" stdset=\"0\">\n      <size>\n       <width>20</width>\n       <height>40</height>\n      </size>\n     </property>\n    </spacer>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/discnumber/discnumber.py",
    "content": "PLUGIN_NAME = 'Disc Numbers'\nPLUGIN_AUTHOR = 'Lukas Lalinsky'\nPLUGIN_DESCRIPTION = '''Moves disc numbers and subtitles from album titles to separate tags. For example:<br/>\n<em>\"Aerial (disc 1: A Sea of Honey)\"</em>\n<ul>\n    <li>album = <em>\"Aerial\"</em></li>\n    <li>discnumber = <em>\"1\"</em></li>\n    <li>discsubtitle = <em>\"A Sea of Honey\"</em></li>\n</ul>'''\nPLUGIN_VERSION = \"0.1\"\nPLUGIN_API_VERSIONS = [\"0.9.0\", \"0.10\", \"0.15\", \"2.0\"]\n\nfrom picard.metadata import register_album_metadata_processor\nimport re\n\n_discnumber_re = re.compile(r\"\\s+\\(disc (\\d+)(?::\\s+([^)]+))?\\)\")\n\n\ndef remove_discnumbers(tagger, metadata, release):\n    matches = _discnumber_re.search(metadata[\"album\"])\n    if matches:\n        metadata[\"discnumber\"] = matches.group(1)\n        if matches.group(2):\n            metadata[\"discsubtitle\"] = matches.group(2)\n        metadata[\"album\"] = _discnumber_re.sub('', metadata[\"album\"])\n\nregister_album_metadata_processor(remove_discnumbers)\n"
  },
  {
    "path": "plugins/enhanced_titles/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2024 Giorgio Fontanive (twodoorcoupe)\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n\nPLUGIN_NAME = \"Enhanced Titles\"\nPLUGIN_AUTHOR = \"Giorgio Fontanive\"\nPLUGIN_DESCRIPTION = \"\"\"\nThis plugin sets the albumsort and titlesort tags. It also provides the script\nfunctions $swapprefix_lang, $delprefix_lang and $title_lang. The languages\nincluded at the moment are English, Spanish, Italian, German, French and Portuguese.\n\nThe functions do the same thing as their original counterparts, but take\nmultiple languages as parameters. If no languages are provided, all the available ones are\nincluded. Languages are provided with ISO 639-3 codes: eng, spa, ita, fra, deu, por.\n\nTagging and checking aliases can be disabled in the plugin's options page, found\nunder \"plugins\". Checking aliases will slow down processing.\n\nIf you wish to add your own language or to change the words that are not capitalized,\nplease feel free to code the changes and submit a pull request along with links to web\npages that give definitive language-specific title-case rules.\n\"\"\"\nPLUGIN_VERSION = \"0.1\"\nPLUGIN_API_VERSIONS = [\"2.10\"]\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom picard import log, config\nfrom picard.plugin import PluginPriority\nfrom picard.metadata import register_album_metadata_processor, register_track_metadata_processor\nfrom picard.script import register_script_function\nfrom picard.script.functions import func_swapprefix, func_delprefix\nfrom picard.webservice.api_helpers import MBAPIHelper\n\nfrom picard.ui.options import OptionsPage, register_options_page\nfrom .ui_options_enhanced_titles import Ui_EnhancedTitlesOptions\n\nfrom functools import partial\nimport re\n\n# Options.\nKEEP_ALLCAPS = \"et_keep_allcaps\"\nENABLE_TAGGING = \"et_enable_tagging\"\nCHECK_ALBUM = \"et_check_album_aliases\"\nCHECK_TRACK = \"et_check_track_aliases\"\n\n_articles = {\n    \"eng\": {\"the\", \"a\", \"an\"},\n    \"spa\": {\"el\", \"los\", \"la\", \"las\", \"lo\", \"un\", \"unos\", \"una\", \"unas\"},\n    \"ita\": {\"il\", \"l'\", \"la\", \"i\", \"gli\", \"le\", \"un\", \"uno\", \"una\", \"un'\"},\n    \"fra\": {\"le\", \"la\", \"les\", \"un\", \"une\", \"des\", \"l'\"},\n    \"deu\": {\"der\", \"den\", \"die\", \"das\", \"dem\", \"des\", \"den\"},\n    \"por\": {\"o\", \"os\", \"a\", \"as\", \"um\", \"uns\", \"uma\", \"umas\"}\n}\n\n# Prepositions and conjunctions with 3 letters or fewer.\n# These will stay in lower case when doing title case.\n_other_minor_words = {\n    \"eng\": {\"so\", \"yet\", \"as\", \"at\", \"by\", \"for\", \"in\", \"of\", \"off\", \"on\", \"per\",\n            \"to\", \"up\", \"via\", \"and\", \"as\", \"but\", \"for\", \"if\", \"nor\", \"or\", \"en\", \"via\", \"vs\"},\n    \"spa\": {\"mas\", \"que\", \"en\", \"con\", \"por\", \"de\", \"y\", \"e\", \"o\", \"u\", \"si\", \"ni\"},\n    \"ita\": {\"di\", \"a\", \"da\", \"in\", \"con\", \"su\", \"per\", \"tra\", \"fra\", \"e\", \"o\", \"ma\", \"se\"},\n    \"fra\": {\"à\", \"de\", \"en\", \"par\", \"sur\", \"et\", \"ou\", \"que\", \"si\"},\n    \"deu\": {\"bis\", \"für\", \"um\", \"an\", \"auf\", \"in\", \"vor\", \"aus\", \"bei\", \"mit\",\n            \"von\", \"zu\", \"la\", \"so\", \"daß\", \"als\", \"ob\", \"ehe\"},\n    \"por\": {\"dem\", \"em\", \"por\", \"ao\", \"à\", \"aos\", \"às\", \"do\", \"da\", \"dos\", \"das\",\n            \"no\", \"na\", \"nos\", \"nas\", \"num\", \"dum\", \"e\", \"mas\", \"até\", \"em\", \"ou\",\n            \"que\", \"se\", \"por\"}\n}\n\n\ndef flatten_values(dictionary):\n    l = list()\n    for v in dictionary.values():\n        l += v\n    return set(l)\n\n\n_articles_langs = set(_articles)\n_all_articles = flatten_values(_articles)\n_all_other_minor_words = flatten_values(_other_minor_words)\n\n\nclass ReleaseGroupHelper(MBAPIHelper):\n    \"\"\"API Helper to retrieve release group information.\n    \"\"\"\n\n    def get_release_group_by_id(self, release_id, handler, inc = None):\n        \"\"\"Gets the information for a release group.\n        \"\"\"\n        return self._get_by_id(\"release-group\", release_id, handler, inc)\n\n\nclass SortTagger:\n    \"\"\"Sets the titlesort and albumsort tags.\n\n    First, it checks if there is already a sort name available in one of the\n    aliases. If it does not find any, it swaps the prefix if there is one in\n    the title.\n    \"\"\"\n\n    def _select_alias(self, aliases, name):\n        \"\"\"Selects the first alias where the names match and the sort name is\n        different.\n\n        Args:\n            aliases (list): One dictionary for each alias available.\n            name (str): Title of the album/track.\n\n        Returns:\n            (str): The sort name of the first useful alias it finds. None if\n                   none are found.\n\n        For example, the album \"The Beatles\" has alias \"Le Double Blanc\" with\n        sort name \"Double Blanc, Le\", so it's not considered. But it also has alias\n        \"The Beatles\" with sort name \"Beatles, The\", so this one is chosen.\n        Another example, \"The Continuing Story of Bungalow Bill\" has an alias\n        with the sort name equal to the title, this is not considered because\n        it makes more sense to swap the prefix.\n        \"\"\"\n        name_casefold = name.casefold()\n        for alias in aliases:\n            sortname = alias[\"sort-name\"]\n            if (alias[\"name\"].casefold() == name_casefold and\n                    not sortname.casefold() == name_casefold):\n                log.info('Enhanced Titles: sort name found for \"%s\", \"%s\".', name, sortname)\n                return sortname\n        log.info('Enhanced Titles: no proper sort name found for \"%s\".', name)\n        return None\n\n    def _response_handler(self, document, reply, error, metadata = None, field = None):\n        \"\"\"Handles the response from MusicBrainz.\n\n        Args:\n            metadata (MetaData): The object that needs to be updated.\n            field (str): Either \"title\" or \"album\", depending on what is being\n                         updated.\n        \"\"\"\n        sortname = None\n        try:\n            if document:\n                if error:\n                    log.error(\"Enhanced Titles: information retrieval error.\")\n                if document[\"aliases\"]:\n                    sortname = self._select_alias(document[\"aliases\"], metadata[field])\n                else:\n                    log.info('Enhanced Titles: no aliases found for \"%s\".', metadata[field])\n        finally:\n            sortfield = field + \"sort\"\n            if sortname:\n                metadata[sortfield] = sortname\n            else:\n                metadata[sortfield] = self._swapprefix(metadata, field)\n\n    def _swapprefix(self, metadata, field):\n        \"\"\"Swaps the prefix of the title based on the album/track language.\n\n        If none of the languages are available, it just copies the title.\n        Otherwise, if no language information is found, then it uses all languages available.\n        Otherwise, it uses exclusively the languages that are also available.\n        \"\"\"\n        languages = lang_functions.find_languages(metadata)\n        if not languages:  # None of the languages found are available.\n            return metadata[field]\n        prefixes = lang_functions.find_prefixes(languages)\n        return func_swapprefix(None, metadata[field], *prefixes)\n\n    def set_track_titlesort(self, album, metadata, track, release):\n        \"\"\"Sets the track's titlesort field.\n        \"\"\"\n        if config.setting[ENABLE_TAGGING]:\n            handler = partial(self._response_handler, metadata = metadata, field = \"title\")\n            if config.setting[CHECK_TRACK]:\n                MBAPIHelper(album.tagger.webservice).get_track_by_id(\n                    metadata[\"musicbrainz_recordingid\"],\n                    handler,\n                    inc = [\"aliases\"]\n                )\n            else:\n                metadata[\"titlesort\"] = self._swapprefix(metadata, \"title\")\n\n    def set_album_titlesort(self, album, metadata, release):\n        \"\"\"Sets the album's albumsort field.\n        \"\"\"\n        if config.setting[ENABLE_TAGGING]:\n            handler = partial(self._response_handler, metadata = metadata, field = \"album\")\n            if config.setting[CHECK_ALBUM]:\n                ReleaseGroupHelper(album.tagger.webservice).get_release_group_by_id(\n                    metadata[\"musicbrainz_releasegroupid\"],\n                    handler,\n                    inc = [\"aliases\"]\n                )\n            else:\n                metadata[\"albumsort\"] = self._swapprefix(metadata, \"album\")\n\n\nclass LangFunctions:\n    \"\"\"Provides scripting functions swapprefix_lang, delprefix_lang, and title_lang.\n\n    Combinations of languages and their prefixes/minor words are stored after\n    they're used at least once.\n    \"\"\"\n\n    prefixes_cache = {}\n    minor_words_cache = {}\n\n    def __init__(self):\n        all_articles = set(_all_articles | set(article.capitalize() for article in _all_articles))\n        all_minor_words = set(_all_articles | _all_other_minor_words)\n        self.prefixes_cache[\"\"] = all_articles\n        self.minor_words_cache[\"\"] = all_minor_words\n\n    def _format_languages(self, languages):\n        \"\"\"Filters out the languages that are not available.\n\n        It returns a tuple to be used as key for the two cache dictionaries.\n        \"\"\"\n        languages = set(lang[:3].lower() for lang in languages)\n        return tuple(languages.intersection(_articles_langs))\n\n    def _create_prefixes_list(self, languages = None, is_title = False):\n        \"\"\"Creates a list of all the prefixes or minor words for all the given languages.\n\n        Args:\n            languages (set): All the languages to be considered.\n            is_title (bool): If true, only articles are included, with a lower case\n                             and a capitalized copy for each. This is because the\n                             swapprefix and delprefix functions are case sensitive.\n                             If false, also prepositions and conjunctions are included,\n                             all in lowercase.\n\n        Returns:\n            (set): The set of prefixes or minor words.\n\n        The available languages are saved with ISO 639-3 codes.\n        \"\"\"\n        prefixes = []\n        for language in languages:\n            prefixes.extend(_articles[language])\n            if is_title:\n                prefixes.extend(_other_minor_words[language])\n            else:\n                prefixes.extend([article.capitalize() for article in _articles[language]])\n        return set(prefixes)\n\n    def _title_case(self, text, lower_case_words):\n        \"\"\"Returns the text in titlecase.\n\n        If a word has an apostrophe and the segment to its left is an article,\n        it capitalizes only the word on the right. Otherwise it capitalizes\n        only the word on the left.\n        For example, \"let's groove\" becomes \"Let's Groove\", but \"voglio l'anima\"\n        becomes \"Voglio l'Anima\".\n        \"\"\"\n        new_text = []\n        words = re.split(r\"([\\w']+)\", text.strip().lower().replace(\"’\", \"'\"))\n        words = [word for word in words if word]\n        for word in words:\n            if \"'\" in word:  # Apply the rule described above\n                split = word.split(\"'\")\n                if split[0] + \"'\" in lower_case_words:\n                    split[1] = split[1].capitalize()\n                else:\n                    split[0] = split[0].capitalize()\n                word = \"'\".join(split)\n            elif word not in lower_case_words:\n                word = word.capitalize()\n            new_text.append(word)\n        if new_text:\n            new_text[0] = new_text[0].capitalize()\n            return \"\".join(new_text)\n        else:\n            return \"\"\n\n    def find_languages(self, metadata):\n        \"\"\"Finds the languages from the metadata.\n\n        Returns a set with only an empty string if no language information is found.\n        Returns None if the languages found are not available.\n        \"\"\"\n        languages = (metadata[\"language\"], metadata[\"_releaselanguage\"])\n        languages = set(language for language in languages if language)\n        if not languages:\n            return {\"\"}  # No language information is found.\n        languages = languages.intersection(_articles_langs)\n        if not languages:\n            return None  # None of the languages found are available.\n        return languages\n\n    def find_prefixes(self, languages):\n        \"\"\"Returns the list of prefixes for the given languages.\n\n        If the same combination was previously used, it finds the values stored\n        to avoid recreating the same list twice.\n        \"\"\"\n        if not languages or \"\" in languages:\n            return self.prefixes_cache[\"\"]\n        combination = self._format_languages(languages)\n        if combination in self.prefixes_cache:\n            return self.prefixes_cache[combination]\n        prefixes = self._create_prefixes_list(combination)\n        self.prefixes_cache[combination] = prefixes\n        return prefixes\n\n    def find_minor_words(self, languages):\n        \"\"\"Returns the list of minor words for the given languages.\n\n        If the same combination was previously used, it finds the values stored\n        to avoid recreating the same list twice.\n        \"\"\"\n        if not languages or \"\" in languages:\n            return self.minor_words_cache[\"\"]\n        combination = self._format_languages(languages)\n        if combination in self.minor_words_cache:\n            return self.minor_words_cache[combination]\n        minor_words = self._create_prefixes_list(combination, True)\n        self.minor_words_cache[combination] = minor_words\n        return minor_words\n\n    swapprefix_lang_documentation = N_(\n        \"\"\"`$swapprefix_lang(text,language1,language2,...)`\n\nMoves the prefix to the end of the text. It uses a list prefixes\ntaken from the specified languages.\nMultiple languages can be added as seperate parameters.\nIf none are provided, it uses all the available ones.\n        \"\"\")\n\n    def swapprefix_lang(self, parser, text, *languages):\n        \"\"\"\n        >>> lf = LangFunctions()\n        >>> lf.swapprefix_lang(None, \"The prefix is swapped\")\n        'prefix is swapped, The'\n        >>> lf.swapprefix_lang(None, \"the is swapped also in lower case\")\n        'is swapped also in lower case, the'\n        >>> lf.swapprefix_lang(None, \"the prefix is not in Spanish\", \"spa\")\n        'the prefix is not in Spanish'\n        >>> lf.swapprefix_lang(None, \"Il prefisso è scambiato\", \"ita\")\n        'prefisso è scambiato, Il'\n        >>> lf.swapprefix_lang(None, \"the-prefix-is-not-swapped\")\n        'the-prefix-is-not-swapped'\n        >>> lf.swapprefix_lang(None, \"the\")\n        'the'\n        \"\"\"\n        if parser and not languages:\n            languages = self.find_languages(parser.context)\n        prefixes = self.find_prefixes(languages)\n        return func_swapprefix(parser, text, *prefixes)\n\n    delprefix_lang_documentation = N_(\n        \"\"\"`$delprefix_lang(text,language1,language2,...)`\n\nDeletes the prefix from the text. It uses a list prefixes\ntaken from the specified languages.\nMultiple languages can be added as seperate parameters.\nIf none are provided, it uses all the available ones.\n        \"\"\")\n\n    def delprefix_lang(self, parser, text, *languages):\n        \"\"\"\n        >>> lf = LangFunctions()\n        >>> lf.delprefix_lang(None, \"The prefix is deleted\")\n        'prefix is deleted'\n        >>> lf.delprefix_lang(None, \"the is deleted also in lower case\")\n        'is deleted also in lower case'\n        >>> lf.delprefix_lang(None, \"the prefix is not in Spanish\", \"spa\")\n        'the prefix is not in Spanish'\n        >>> lf.delprefix_lang(None, \"Il prefisso è eliminato\", \"ita\")\n        'prefisso è eliminato'\n        >>> lf.delprefix_lang(None, \"the-prefix-is-not-deleted\")\n        'the-prefix-is-not-deleted'\n        >>> lf.delprefix_lang(None, \"the\")\n        'the'\n        \"\"\"\n        if parser and not languages:\n            languages = self.find_languages(parser.context)\n        prefixes = self.find_prefixes(languages)\n        return func_delprefix(parser, text, *prefixes)\n\n    title_lang_documentation = N_(\n        \"\"\"`$title_lang(text,language1,language2,...)`\n\nMakes the text title case based on the minor words of the specified languages.\nMultiple languages can be added as separate parameters.\nIf none are provided, it uses all the available ones.\n        \"\"\")\n\n    def title_lang(self, parser, text, *languages):\n        \"\"\"\n        >>> lf = LangFunctions()\n        >>> lf.title_lang(None, \"title case\")\n        'Title Case'\n        >>> lf.title_lang(None, \"prepositions and/or conjunctions\")\n        'Prepositions and/or Conjunctions'\n        >>> lf.title_lang(None, \"(random-punctuation/and.symbols@\")\n        '(Random-Punctuation/and.Symbols@'\n        >>> lf.title_lang(None, \"only in english: and, or, but...\", \"eng\")\n        'Only in English: and, or, but...'\n        >>> lf.title_lang(None, \"only in spanish: and, or, but...\", \"spa\")\n        'Only In Spanish: And, Or, But...'\n        >>> lf.title_lang(None, \"Also in other languages, con altre lingue\", \"eng\", \"ita\")\n        'Also in Other Languages, con Altre Lingue'\n        >>> lf.title_lang(None, \"with no valid language\", \"aaa\")\n        'With No Valid Language'\n        >>> lf.title_lang(None, \"let's groove\")\n        \"Let's Groove\"\n        >>> lf.title_lang(None, \"voglio l'anima\")\n        \"Voglio l'Anima\"\n        >>> lf.title_lang(None, \"casiopea, カシオペア\")\n        'Casiopea, カシオペア'\n        \"\"\"\n        if text.upper() == text and config.setting[KEEP_ALLCAPS]:\n            return text\n        if parser and not languages:\n            languages = self.find_languages(parser.context)\n        minor_words = self.find_minor_words(languages)\n        return self._title_case(text, minor_words)\n\n\nclass EnhancedTitlesOptions(OptionsPage):\n    \"\"\"Options page found under the \"plugins\" page.\n    \"\"\"\n\n    NAME = \"enhanced_titles\"\n    TITLE = \"Enhanced Titles\"\n    PARENT = \"plugins\"\n\n    options = [\n        config.BoolOption(\"setting\", KEEP_ALLCAPS, False),\n        config.BoolOption(\"setting\", ENABLE_TAGGING, True),\n        config.BoolOption(\"setting\", CHECK_ALBUM, True),\n        config.BoolOption(\"setting\", CHECK_TRACK, False),\n    ]\n\n    def __init__(self, parent = None):\n        super(EnhancedTitlesOptions, self).__init__(parent)\n        self.ui = Ui_EnhancedTitlesOptions()\n        self.ui.setupUi(self)\n\n    def load(self):\n        self.ui.check_allcaps.setChecked(config.setting[KEEP_ALLCAPS])\n        self.ui.check_tagging.setChecked(config.setting[ENABLE_TAGGING])\n        self.ui.check_album_aliases.setChecked(config.setting[CHECK_ALBUM])\n        self.ui.check_track_aliases.setChecked(config.setting[CHECK_TRACK])\n\n    def save(self):\n        config.setting[KEEP_ALLCAPS] = self.ui.check_allcaps.isChecked()\n        config.setting[ENABLE_TAGGING] = self.ui.check_tagging.isChecked()\n        config.setting[CHECK_ALBUM] = self.ui.check_album_aliases.isChecked()\n        config.setting[CHECK_TRACK] = self.ui.check_track_aliases.isChecked()\n\n\nsort_tagger = SortTagger()\nlang_functions = LangFunctions()\nregister_track_metadata_processor(sort_tagger.set_track_titlesort, priority = PluginPriority.LOW)\nregister_album_metadata_processor(sort_tagger.set_album_titlesort, priority = PluginPriority.LOW)\nregister_script_function(lang_functions.swapprefix_lang, check_argcount = False, documentation = lang_functions.swapprefix_lang_documentation)\nregister_script_function(lang_functions.delprefix_lang, check_argcount = False, documentation = lang_functions.delprefix_lang_documentation)\nregister_script_function(lang_functions.title_lang, check_argcount = False, documentation = lang_functions.title_lang_documentation)\nregister_options_page(EnhancedTitlesOptions)\n"
  },
  {
    "path": "plugins/enhanced_titles/options_enhanced_titles.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<ui version=\"4.0\">\r\n <class>EnhancedTitlesOptions</class>\r\n <widget class=\"QWidget\" name=\"EnhancedTitlesOptions\">\r\n  <property name=\"minimumSize\">\r\n   <size>\r\n    <width>100</width>\r\n    <height>0</height>\r\n   </size>\r\n  </property>\r\n  <property name=\"windowTitle\">\r\n   <string>Form</string>\r\n  </property>\r\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\r\n   <item>\r\n    <widget class=\"QScrollArea\" name=\"scrollArea\">\r\n     <property name=\"frameShape\">\r\n      <enum>QFrame::NoFrame</enum>\r\n     </property>\r\n     <property name=\"widgetResizable\">\r\n      <bool>true</bool>\r\n     </property>\r\n     <widget class=\"QWidget\" name=\"scrollAreaWidgetContents\">\r\n      <property name=\"geometry\">\r\n       <rect>\r\n        <x>0</x>\r\n        <y>0</y>\r\n        <width>379</width>\r\n        <height>502</height>\r\n       </rect>\r\n      </property>\r\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\r\n       <item>\r\n        <widget class=\"QGroupBox\" name=\"titles_all_caps\">\r\n         <property name=\"title\">\r\n          <string>Titles in All Caps</string>\r\n         </property>\r\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_6\">\r\n          <item>\r\n           <widget class=\"QLabel\" name=\"label_4\">\r\n            <property name=\"mouseTracking\">\r\n             <bool>true</bool>\r\n            </property>\r\n            <property name=\"text\">\r\n             <string>If this option is enabled, text in all caps will not be affected by the $title_lang script function.</string>\r\n            </property>\r\n            <property name=\"wordWrap\">\r\n             <bool>true</bool>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n          <item>\r\n           <widget class=\"QCheckBox\" name=\"check_allcaps\">\r\n            <property name=\"text\">\r\n             <string>Ignore titles in all caps.</string>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n         </layout>\r\n        </widget>\r\n       </item>\r\n       <item>\r\n        <widget class=\"QGroupBox\" name=\"check_for_sort_names\">\r\n         <property name=\"title\">\r\n          <string>Tagging</string>\r\n         </property>\r\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_3\">\r\n          <item>\r\n           <widget class=\"QLabel\" name=\"label\">\r\n            <property name=\"text\">\r\n             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If this option is enabled, the fields albumsort and titlesort will be filled. If you are only interested in the scripting functions, then disable this to reduce the time it takes to load releases.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\r\n            </property>\r\n            <property name=\"wordWrap\">\r\n             <bool>true</bool>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n          <item>\r\n           <widget class=\"QCheckBox\" name=\"check_tagging\">\r\n            <property name=\"text\">\r\n             <string>Tag albumsort and titlesort fields</string>\r\n            </property>\r\n            <property name=\"checked\">\r\n             <bool>true</bool>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n          <item>\r\n           <widget class=\"QLabel\" name=\"label_2\">\r\n            <property name=\"text\">\r\n             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If this option is enabled, the plugin will first check if there are any sort names already available in MusicBrainz. Otherwise, it will swap the title's prefix directly. Enabling this option will increase the time it takes to load releases, especially for tracks.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\r\n            </property>\r\n            <property name=\"wordWrap\">\r\n             <bool>true</bool>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n          <item>\r\n           <widget class=\"QCheckBox\" name=\"check_album_aliases\">\r\n            <property name=\"text\">\r\n             <string>Check for album aliases</string>\r\n            </property>\r\n            <property name=\"checkable\">\r\n             <bool>true</bool>\r\n            </property>\r\n            <property name=\"checked\">\r\n             <bool>true</bool>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n          <item>\r\n           <widget class=\"QCheckBox\" name=\"check_track_aliases\">\r\n            <property name=\"text\">\r\n             <string>Check for track aliases</string>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n         </layout>\r\n        </widget>\r\n       </item>\r\n       <item>\r\n        <spacer name=\"verticalSpacer\">\r\n         <property name=\"orientation\">\r\n          <enum>Qt::Vertical</enum>\r\n         </property>\r\n         <property name=\"sizeHint\" stdset=\"0\">\r\n          <size>\r\n           <width>20</width>\r\n           <height>40</height>\r\n          </size>\r\n         </property>\r\n        </spacer>\r\n       </item>\r\n      </layout>\r\n     </widget>\r\n    </widget>\r\n   </item>\r\n  </layout>\r\n </widget>\r\n <resources/>\r\n <connections/>\r\n</ui>\r\n"
  },
  {
    "path": "plugins/enhanced_titles/ui_options_enhanced_titles.py",
    "content": "# -*- coding: utf-8 -*-\n\n################################################################################\n## Form generated from reading UI file 'options_enhanced_titles.ui'\n##\n## Created by: Qt User Interface Compiler version 6.6.1\n##\n## WARNING! All changes made in this file will be lost when recompiling UI file!\n################################################################################\n\nfrom PyQt5.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,\n    QMetaObject, QObject, QPoint, QRect,\n    QSize, QTime, QUrl, Qt)\nfrom PyQt5.QtGui import (QBrush, QColor, QConicalGradient, QCursor,\n    QFont, QFontDatabase, QGradient, QIcon,\n    QImage, QKeySequence, QLinearGradient, QPainter,\n    QPalette, QPixmap, QRadialGradient, QTransform)\nfrom PyQt5.QtWidgets import (QApplication, QCheckBox, QFrame, QGroupBox,\n    QLabel, QScrollArea, QSizePolicy, QSpacerItem,\n    QVBoxLayout, QWidget)\n\nclass Ui_EnhancedTitlesOptions(object):\n    def setupUi(self, EnhancedTitlesOptions):\n        if not EnhancedTitlesOptions.objectName():\n            EnhancedTitlesOptions.setObjectName(u\"EnhancedTitlesOptions\")\n        EnhancedTitlesOptions.setMinimumSize(QSize(100, 0))\n        self.verticalLayout = QVBoxLayout(EnhancedTitlesOptions)\n        self.verticalLayout.setObjectName(u\"verticalLayout\")\n        self.scrollArea = QScrollArea(EnhancedTitlesOptions)\n        self.scrollArea.setObjectName(u\"scrollArea\")\n        self.scrollArea.setFrameShape(QFrame.NoFrame)\n        self.scrollArea.setWidgetResizable(True)\n        self.scrollAreaWidgetContents = QWidget()\n        self.scrollAreaWidgetContents.setObjectName(u\"scrollAreaWidgetContents\")\n        self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 379, 502))\n        self.verticalLayout_2 = QVBoxLayout(self.scrollAreaWidgetContents)\n        self.verticalLayout_2.setObjectName(u\"verticalLayout_2\")\n        self.titles_all_caps = QGroupBox(self.scrollAreaWidgetContents)\n        self.titles_all_caps.setObjectName(u\"titles_all_caps\")\n        self.verticalLayout_6 = QVBoxLayout(self.titles_all_caps)\n        self.verticalLayout_6.setObjectName(u\"verticalLayout_6\")\n        self.label_4 = QLabel(self.titles_all_caps)\n        self.label_4.setObjectName(u\"label_4\")\n        self.label_4.setMouseTracking(True)\n        self.label_4.setWordWrap(True)\n\n        self.verticalLayout_6.addWidget(self.label_4)\n\n        self.check_allcaps = QCheckBox(self.titles_all_caps)\n        self.check_allcaps.setObjectName(u\"check_allcaps\")\n\n        self.verticalLayout_6.addWidget(self.check_allcaps)\n\n\n        self.verticalLayout_2.addWidget(self.titles_all_caps)\n\n        self.check_for_sort_names = QGroupBox(self.scrollAreaWidgetContents)\n        self.check_for_sort_names.setObjectName(u\"check_for_sort_names\")\n        self.verticalLayout_3 = QVBoxLayout(self.check_for_sort_names)\n        self.verticalLayout_3.setObjectName(u\"verticalLayout_3\")\n        self.label = QLabel(self.check_for_sort_names)\n        self.label.setObjectName(u\"label\")\n        self.label.setWordWrap(True)\n\n        self.verticalLayout_3.addWidget(self.label)\n\n        self.check_tagging = QCheckBox(self.check_for_sort_names)\n        self.check_tagging.setObjectName(u\"check_tagging\")\n        self.check_tagging.setChecked(True)\n\n        self.verticalLayout_3.addWidget(self.check_tagging)\n\n        self.label_2 = QLabel(self.check_for_sort_names)\n        self.label_2.setObjectName(u\"label_2\")\n        self.label_2.setWordWrap(True)\n\n        self.verticalLayout_3.addWidget(self.label_2)\n\n        self.check_album_aliases = QCheckBox(self.check_for_sort_names)\n        self.check_album_aliases.setObjectName(u\"check_album_aliases\")\n        self.check_album_aliases.setCheckable(True)\n        self.check_album_aliases.setChecked(True)\n\n        self.verticalLayout_3.addWidget(self.check_album_aliases)\n\n        self.check_track_aliases = QCheckBox(self.check_for_sort_names)\n        self.check_track_aliases.setObjectName(u\"check_track_aliases\")\n\n        self.verticalLayout_3.addWidget(self.check_track_aliases)\n\n\n        self.verticalLayout_2.addWidget(self.check_for_sort_names)\n\n        self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)\n\n        self.verticalLayout_2.addItem(self.verticalSpacer)\n\n        self.scrollArea.setWidget(self.scrollAreaWidgetContents)\n\n        self.verticalLayout.addWidget(self.scrollArea)\n\n\n        self.retranslateUi(EnhancedTitlesOptions)\n\n        QMetaObject.connectSlotsByName(EnhancedTitlesOptions)\n    # setupUi\n\n    def retranslateUi(self, EnhancedTitlesOptions):\n        EnhancedTitlesOptions.setWindowTitle(QCoreApplication.translate(\"EnhancedTitlesOptions\", u\"Form\", None))\n        self.titles_all_caps.setTitle(QCoreApplication.translate(\"EnhancedTitlesOptions\", u\"Titles in All Caps\", None))\n        self.label_4.setText(QCoreApplication.translate(\"EnhancedTitlesOptions\", u\"If this option is enabled, text in all caps will not be affected by the $title_lang script function.\", None))\n        self.check_allcaps.setText(QCoreApplication.translate(\"EnhancedTitlesOptions\", u\"Ignore titles in all caps.\", None))\n        self.check_for_sort_names.setTitle(QCoreApplication.translate(\"EnhancedTitlesOptions\", u\"Tagging\", None))\n        self.label.setText(QCoreApplication.translate(\"EnhancedTitlesOptions\", u\"<html><head/><body><p>If this option is enabled, the fields albumsort and titlesort will be filled. If you are only interested in the scripting functions, then disable this to reduce the time it takes to load releases.</p></body></html>\", None))\n        self.check_tagging.setText(QCoreApplication.translate(\"EnhancedTitlesOptions\", u\"Tag albumsort and titlesort fields\", None))\n        self.label_2.setText(QCoreApplication.translate(\"EnhancedTitlesOptions\", u\"<html><head/><body><p>If this option is enabled, the plugin will first check if there are any sort names already available in MusicBrainz. Otherwise, it will swap the title's prefix directly. Enabling this option will increase the time it takes to load releases, especially for tracks.</p></body></html>\", None))\n        self.check_album_aliases.setText(QCoreApplication.translate(\"EnhancedTitlesOptions\", u\"Check for album aliases\", None))\n        self.check_track_aliases.setText(QCoreApplication.translate(\"EnhancedTitlesOptions\", u\"Check for track aliases\", None))\n    # retranslateUi\n\n"
  },
  {
    "path": "plugins/fanarttv/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2015-2021 Philipp Wolfer\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = 'fanart.tv cover art'\nPLUGIN_AUTHOR = 'Philipp Wolfer, Sambhav Kothari'\nPLUGIN_DESCRIPTION = ('Use cover art from fanart.tv.<br /><br />'\n                      'To use this plugin you have to register a personal API key on '\n                      '<a href=\"https://fanart.tv/get-an-api-key/\">fanart.tv</a>.')\nPLUGIN_VERSION = \"1.6.3\"\nPLUGIN_API_VERSIONS = [\"2.0\", \"2.1\", \"2.2\", \"2.3\", \"2.4\", \"2.5\", \"2.6\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom functools import partial\nfrom PyQt5.QtCore import QUrl\nfrom PyQt5.QtNetwork import QNetworkReply\nfrom picard import config, log\nfrom picard.coverart.providers import (\n    CoverArtProvider,\n    ProviderOptions,\n    register_cover_art_provider,\n)\nfrom picard.coverart.image import CoverArtImage\nfrom picard.config import TextOption\nfrom .ui_options_fanarttv import Ui_FanartTvOptionsPage\n\nFANART_HOST = \"webservice.fanart.tv\"\nFANART_PORT = 443\nFANART_APIKEY = \"21305dd1589766f4d544535ad4df12f4\"\n\nOPTION_CDART_ALWAYS = \"always\"\nOPTION_CDART_NEVER = \"never\"\nOPTION_CDART_NOALBUMART = \"noalbumart\"\n\n\ndef cover_sort_key(cover):\n    \"\"\"For sorting a list of cover arts by likes.\"\"\"\n    try:\n        return int(cover[\"likes\"]) if \"likes\" in cover else 0\n    except ValueError:\n        return 0\n\n\ndef encode_queryarg(arg):\n    return bytes(QUrl.toPercentEncoding(arg)).decode()\n\n\nclass FanartTvOptionsPage(ProviderOptions):\n\n    _options_ui = Ui_FanartTvOptionsPage\n\n    options = [\n        TextOption(\"setting\", \"fanarttv_client_key\", \"\"),\n        TextOption(\"setting\", \"fanarttv_use_cdart\", OPTION_CDART_NOALBUMART),\n    ]\n\n    def load(self):\n        setting = config.setting\n        self.ui.fanarttv_client_key.setText(setting[\"fanarttv_client_key\"])\n        if setting[\"fanarttv_use_cdart\"] == OPTION_CDART_ALWAYS:\n            self.ui.fanarttv_cdart_use_always.setChecked(True)\n        elif setting[\"fanarttv_use_cdart\"] == OPTION_CDART_NEVER:\n            self.ui.fanarttv_cdart_use_never.setChecked(True)\n        elif setting[\"fanarttv_use_cdart\"] == OPTION_CDART_NOALBUMART:\n            self.ui.fanarttv_cdart_use_if_no_albumcover.setChecked(True)\n\n    def save(self):\n        setting = config.setting\n        setting[\"fanarttv_client_key\"] = self.ui.fanarttv_client_key.text()\n        if self.ui.fanarttv_cdart_use_always.isChecked():\n            setting[\"fanarttv_use_cdart\"] = OPTION_CDART_ALWAYS\n        elif self.ui.fanarttv_cdart_use_never.isChecked():\n            setting[\"fanarttv_use_cdart\"] = OPTION_CDART_NEVER\n        elif self.ui.fanarttv_cdart_use_if_no_albumcover.isChecked():\n            setting[\"fanarttv_use_cdart\"] = OPTION_CDART_NOALBUMART\n\n\nclass FanartTvCoverArtImage(CoverArtImage):\n\n    \"\"\"Image from fanart.tv\"\"\"\n\n    support_types = True\n    sourceprefix = \"FATV\"\n\n\nclass CoverArtProviderFanartTv(CoverArtProvider):\n\n    \"\"\"Use fanart.tv to get cover art\"\"\"\n\n    NAME = \"fanart.tv\"\n    TITLE = \"fanart.tv\"\n    OPTIONS = FanartTvOptionsPage\n\n    def enabled(self):\n        return (self._client_key and super().enabled()\n                and not self.coverart.front_image_found)\n\n    def queue_images(self):\n        release_group_id = self.metadata[\"musicbrainz_releasegroupid\"]\n        path = \"/v3/music/albums/%s\" % (release_group_id, )\n        queryargs = {\n            \"api_key\": encode_queryarg(FANART_APIKEY),\n            \"client_key\": encode_queryarg(self._client_key),\n        }\n        log.debug(\"CoverArtProviderFanartTv.queue_downloads: %s\" % path)\n        self.album.tagger.webservice.get(\n            FANART_HOST,\n            FANART_PORT,\n            path,\n            partial(self._json_downloaded, release_group_id),\n            priority=True,\n            important=False,\n            parse_response_type='json',\n            queryargs=queryargs)\n        self.album._requests += 1\n        return CoverArtProvider.WAIT\n\n    @property\n    def _client_key(self):\n        return config.setting[\"fanarttv_client_key\"]\n\n    def _json_downloaded(self, release_group_id, data, reply, error):\n        self.album._requests -= 1\n\n        if error:\n            if error != QNetworkReply.ContentNotFoundError:\n                error_level = log.error\n            else:\n                error_level = log.debug\n            error_level(\"Problem requesting metadata in fanart.tv plugin: %s\",\n                        error)\n        else:\n            try:\n                release = data[\"albums\"][release_group_id]\n                has_cover = \"albumcover\" in release\n                has_cdart = \"cdart\" in release\n                use_cdart = config.setting[\"fanarttv_use_cdart\"]\n\n                if has_cover:\n                    covers = release[\"albumcover\"]\n                    self._select_and_add_cover_art(covers, [\"front\"])\n\n                if has_cdart and (use_cdart == OPTION_CDART_ALWAYS\n                                  or (use_cdart == OPTION_CDART_NOALBUMART\n                                      and not has_cover)):\n                    covers = release[\"cdart\"]\n                    types = [\"medium\"]\n                    if not has_cover:\n                        types.append(\"front\")\n                    self._select_and_add_cover_art(covers, types)\n            except (AttributeError, KeyError, TypeError):\n                log.error(\"Problem processing downloaded metadata in fanart.tv plugin\", exc_info=True)\n\n        self.next_in_queue()\n\n    def _select_and_add_cover_art(self, covers, types):\n        covers = sorted(covers, key=cover_sort_key, reverse=True)\n        url = covers[0][\"url\"]\n        log.debug(\"CoverArtProviderFanartTv found artwork %s\" % url)\n        self.queue_put(FanartTvCoverArtImage(url, types=types))\n\n\nregister_cover_art_provider(CoverArtProviderFanartTv)\n"
  },
  {
    "path": "plugins/fanarttv/ui_options_fanarttv.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/fanarttv/ui_options_fanarttv.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.4\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_FanartTvOptionsPage(object):\n    def setupUi(self, FanartTvOptionsPage):\n        FanartTvOptionsPage.setObjectName(\"FanartTvOptionsPage\")\n        FanartTvOptionsPage.resize(340, 317)\n        self.vboxlayout = QtWidgets.QVBoxLayout(FanartTvOptionsPage)\n        self.vboxlayout.setContentsMargins(9, 9, 9, 9)\n        self.vboxlayout.setSpacing(6)\n        self.vboxlayout.setObjectName(\"vboxlayout\")\n        self.groupBox = QtWidgets.QGroupBox(FanartTvOptionsPage)\n        self.groupBox.setObjectName(\"groupBox\")\n        self.vboxlayout1 = QtWidgets.QVBoxLayout(self.groupBox)\n        self.vboxlayout1.setContentsMargins(9, 9, 9, 9)\n        self.vboxlayout1.setSpacing(2)\n        self.vboxlayout1.setObjectName(\"vboxlayout1\")\n        self.label = QtWidgets.QLabel(self.groupBox)\n        self.label.setTextFormat(QtCore.Qt.RichText)\n        self.label.setWordWrap(True)\n        self.label.setOpenExternalLinks(True)\n        self.label.setObjectName(\"label\")\n        self.vboxlayout1.addWidget(self.label)\n        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.vboxlayout1.addItem(spacerItem)\n        self.label_2 = QtWidgets.QLabel(self.groupBox)\n        self.label_2.setObjectName(\"label_2\")\n        self.vboxlayout1.addWidget(self.label_2)\n        self.fanarttv_client_key = QtWidgets.QLineEdit(self.groupBox)\n        self.fanarttv_client_key.setObjectName(\"fanarttv_client_key\")\n        self.vboxlayout1.addWidget(self.fanarttv_client_key)\n        self.vboxlayout.addWidget(self.groupBox)\n        self.verticalGroupBox = QtWidgets.QGroupBox(FanartTvOptionsPage)\n        self.verticalGroupBox.setObjectName(\"verticalGroupBox\")\n        self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalGroupBox)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.fanarttv_cdart_use_always = QtWidgets.QRadioButton(self.verticalGroupBox)\n        self.fanarttv_cdart_use_always.setObjectName(\"fanarttv_cdart_use_always\")\n        self.verticalLayout.addWidget(self.fanarttv_cdart_use_always)\n        self.fanarttv_cdart_use_if_no_albumcover = QtWidgets.QRadioButton(self.verticalGroupBox)\n        self.fanarttv_cdart_use_if_no_albumcover.setObjectName(\"fanarttv_cdart_use_if_no_albumcover\")\n        self.verticalLayout.addWidget(self.fanarttv_cdart_use_if_no_albumcover)\n        self.fanarttv_cdart_use_never = QtWidgets.QRadioButton(self.verticalGroupBox)\n        self.fanarttv_cdart_use_never.setObjectName(\"fanarttv_cdart_use_never\")\n        self.verticalLayout.addWidget(self.fanarttv_cdart_use_never)\n        self.vboxlayout.addWidget(self.verticalGroupBox)\n        spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.vboxlayout.addItem(spacerItem1)\n        spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.vboxlayout.addItem(spacerItem2)\n\n        self.retranslateUi(FanartTvOptionsPage)\n        QtCore.QMetaObject.connectSlotsByName(FanartTvOptionsPage)\n\n    def retranslateUi(self, FanartTvOptionsPage):\n        _translate = QtCore.QCoreApplication.translate\n        self.groupBox.setTitle(_translate(\"FanartTvOptionsPage\", \"fanart.tv cover art\"))\n        self.label.setText(_translate(\"FanartTvOptionsPage\", \"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.0//EN\\\" \\\"http://www.w3.org/TR/REC-html40/strict.dtd\\\">\\n\"\n\"<html><head><meta name=\\\"qrichtext\\\" content=\\\"1\\\" /></head><body>\\n\"\n\"<p>This plugin loads cover art from <a href=\\\"http://fanart.tv/\\\">fanart.tv</a>. If you want to improve the results of this plugin please contribute.</p>\\n\"\n\"<p>In order to use this plugin you have to register a personal API key on<br /><a href=\\\"https://fanart.tv/get-an-api-key/\\\">https://fanart.tv/get-an-api-key/</a></p></body></html>\"))\n        self.label_2.setText(_translate(\"FanartTvOptionsPage\", \"Enter your personal API key here:\"))\n        self.verticalGroupBox.setTitle(_translate(\"FanartTvOptionsPage\", \"Medium images\"))\n        self.fanarttv_cdart_use_always.setText(_translate(\"FanartTvOptionsPage\", \"Always load medium images\"))\n        self.fanarttv_cdart_use_if_no_albumcover.setText(_translate(\"FanartTvOptionsPage\", \"Load only if no front cover is available\"))\n        self.fanarttv_cdart_use_never.setText(_translate(\"FanartTvOptionsPage\", \"Never load medium images\"))\n"
  },
  {
    "path": "plugins/fanarttv/ui_options_fanarttv.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>FanartTvOptionsPage</class>\n <widget class=\"QWidget\" name=\"FanartTvOptionsPage\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>340</width>\n    <height>317</height>\n   </rect>\n  </property>\n  <layout class=\"QVBoxLayout\">\n   <property name=\"spacing\">\n    <number>6</number>\n   </property>\n   <property name=\"leftMargin\">\n    <number>9</number>\n   </property>\n   <property name=\"topMargin\">\n    <number>9</number>\n   </property>\n   <property name=\"rightMargin\">\n    <number>9</number>\n   </property>\n   <property name=\"bottomMargin\">\n    <number>9</number>\n   </property>\n   <item>\n    <widget class=\"QGroupBox\" name=\"groupBox\">\n     <property name=\"title\">\n      <string>fanart.tv cover art</string>\n     </property>\n     <layout class=\"QVBoxLayout\">\n      <property name=\"spacing\">\n       <number>2</number>\n      </property>\n      <property name=\"leftMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"topMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"rightMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"bottomMargin\">\n       <number>9</number>\n      </property>\n      <item>\n       <widget class=\"QLabel\" name=\"label\">\n        <property name=\"text\">\n         <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;\n&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;/head&gt;&lt;body&gt;\n&lt;p&gt;This plugin loads cover art from &lt;a href=&quot;http://fanart.tv/&quot;&gt;fanart.tv&lt;/a&gt;. If you want to improve the results of this plugin please contribute.&lt;/p&gt;\n&lt;p&gt;In order to use this plugin you have to register a personal API key on&lt;br /&gt;&lt;a href=&quot;https://fanart.tv/get-an-api-key/&quot;&gt;https://fanart.tv/get-an-api-key/&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n        </property>\n        <property name=\"textFormat\">\n         <enum>Qt::RichText</enum>\n        </property>\n        <property name=\"wordWrap\">\n         <bool>true</bool>\n        </property>\n        <property name=\"openExternalLinks\">\n         <bool>true</bool>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <spacer name=\"verticalSpacer\">\n        <property name=\"orientation\">\n         <enum>Qt::Vertical</enum>\n        </property>\n        <property name=\"sizeType\">\n         <enum>QSizePolicy::Expanding</enum>\n        </property>\n        <property name=\"sizeHint\" stdset=\"0\">\n         <size>\n          <width>20</width>\n          <height>40</height>\n         </size>\n        </property>\n       </spacer>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"label_2\">\n        <property name=\"text\">\n         <string>Enter your personal API key here:</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLineEdit\" name=\"fanarttv_client_key\"/>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <widget class=\"QGroupBox\" name=\"verticalGroupBox\">\n     <property name=\"title\">\n      <string>Medium images</string>\n     </property>\n     <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n      <item>\n       <widget class=\"QRadioButton\" name=\"fanarttv_cdart_use_always\">\n        <property name=\"text\">\n         <string>Always load medium images</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QRadioButton\" name=\"fanarttv_cdart_use_if_no_albumcover\">\n        <property name=\"text\">\n         <string>Load only if no front cover is available</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QRadioButton\" name=\"fanarttv_cdart_use_never\">\n        <property name=\"text\">\n         <string>Never load medium images</string>\n        </property>\n       </widget>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <spacer name=\"verticalSpacer_3\">\n     <property name=\"orientation\">\n      <enum>Qt::Vertical</enum>\n     </property>\n     <property name=\"sizeHint\" stdset=\"0\">\n      <size>\n       <width>20</width>\n       <height>40</height>\n      </size>\n     </property>\n    </spacer>\n   </item>\n   <item>\n    <spacer name=\"verticalSpacer_2\">\n     <property name=\"orientation\">\n      <enum>Qt::Vertical</enum>\n     </property>\n     <property name=\"sizeHint\" stdset=\"0\">\n      <size>\n       <width>20</width>\n       <height>40</height>\n      </size>\n     </property>\n    </spacer>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/featartist/featartist.py",
    "content": "PLUGIN_NAME = 'Feat. Artists Removed'\nPLUGIN_AUTHOR = 'Lukas Lalinsky, Bryan Toth'\nPLUGIN_DESCRIPTION = 'Removes feat. artists from track titles.  Substitution is case insensitive.'\nPLUGIN_VERSION = \"0.4\"\nPLUGIN_API_VERSIONS = [\"0.9.0\", \"0.10\", \"0.15\", \"0.16\", \"2.0\"]\n\nfrom picard.metadata import register_track_metadata_processor\nimport re\n\n_feat_re = re.compile(r\"\\s+\\(feat\\. [^)]*\\)\", re.IGNORECASE)\n\n\ndef remove_featartists(tagger, metadata, track, release):\n    metadata[\"title\"] = _feat_re.sub(\"\", metadata[\"title\"])\n\nregister_track_metadata_processor(remove_featartists)\n"
  },
  {
    "path": "plugins/featartistsintitles/featartistsintitles.py",
    "content": "PLUGIN_NAME = 'Feat. Artists in Titles'\nPLUGIN_AUTHOR = 'Lukas Lalinsky, Michael Wiencek, Bryan Toth, JeromyNix (NobahdiAtoll)'\nPLUGIN_DESCRIPTION = 'Move \"feat.\" from artist names to album and track titles. Match is case insensitive.'\nPLUGIN_VERSION = \"0.5\"\nPLUGIN_API_VERSIONS = [\"0.9.0\", \"0.10\", \"0.15\", \"0.16\", \"2.0\"]\n\nfrom picard.metadata import register_album_metadata_processor, register_track_metadata_processor\nimport re\n\n_feat_re = re.compile(r\"([\\s\\S]+) feat\\.([\\s\\S]+)\", re.IGNORECASE)\n\n\ndef move_album_featartists(tagger, metadata, release):\n    match = _feat_re.match(metadata[\"albumartist\"])\n    if match:\n        metadata[\"albumartist\"] = match.group(1)\n        metadata[\"album\"] += \" (feat.%s)\" % match.group(2)\n    match = _feat_re.match(metadata[\"albumartistsort\"])\n    if match:\n        metadata[\"albumartistsort\"] = match.group(1)\n\n\ndef move_track_featartists(tagger, metadata, track, release):\n    match = _feat_re.match(metadata[\"artist\"])\n    if match:\n        metadata[\"artist\"] = match.group(1)\n        metadata[\"title\"] += \" (feat.%s)\" % match.group(2)\n    match = _feat_re.match(metadata[\"artistsort\"])\n    if match:\n        metadata[\"artistsort\"] = match.group(1)\n\nregister_album_metadata_processor(move_album_featartists)\nregister_track_metadata_processor(move_track_featartists)\n"
  },
  {
    "path": "plugins/fix_tracknums/fix_tracknums.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\n# Fix Track Numbers plugin for MusicBrainz Picard\n# Copyright (C) 2017 Jonathan Bradley Whited\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nPLUGIN_NAME = 'Fix Track Numbers'\nPLUGIN_AUTHOR = 'Jonathan Bradley Whited'\nPLUGIN_DESCRIPTION = '''\nFix the track numbers in a cluster by either using the track titles (1) or sequential order (2).\n\n<ol>\n  <li>\n    The title should contain something like \"#-#\" (number dash number) and be unique.<br />\n    All non-numbers and non-dashes will be removed when comparing the titles.<br />\n    This is especially useful for Language Audio Lessons, like this:\n    <pre>- Title: \"Unit 1 - Lesson 10\"</pre>\n    For example, take the following titles and track numbers:\n    <pre>\n- Title: \"Unit 1 - Lesson 1\"  - Track #1\n- Title: \"Unit 1 - Lesson 2\"  - Track #1\n- Title: \"Unit 2 - Lesson 10\" - Track #2\n- Title: \"Unit 2 - Lesson 1\"  - Track #2\n</pre>\n    The track numbers will be changed to:  1, 2, 4, 3<br />\n    The 3rd one will be changed to Track #4 because Lesson 1 &lt; Lesson 10.<br />\n    The titles will remain unchanged.\n  </li>\n\n  <li>The track numbers will be set based on the sequential order they appear within the cluster.</li>\n</ol>\n\nHow to use:\n<ol>\n  <li>Cluster a group of files</li>\n  <li>Right click on the cluster</li>\n  <li>\n    Then click one:\n    <ul>\n      <li>Plugins => Fix track numbers using titles</li>\n      <li>Plugins => Fix track numbers using sequence</li>\n    </ul>\n  </li>\n</ol>\n'''\nPLUGIN_VERSION = '0.2.1'\nPLUGIN_API_VERSIONS = ['0.15', '1.0', '2.0']\nPLUGIN_LICENSE = 'GPL-3.0-or-later'\nPLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl.txt'\n\nfrom picard import log\nfrom picard.cluster import Cluster\nfrom picard.ui.itemviews import BaseAction, register_cluster_action\n\nimport re\n\n# FIXME: for Python 3.0 and Picard 2.0, use String.format(...) instead of %?\n\n\nclass FixedTrack:\n\n    def __init__(self, tracknumber=0, title=None, title_num1=0, title_num2=0):\n        self.title = title\n        self.title_num1 = title_num1\n        self.title_num2 = title_num2\n        self.tracknumber = tracknumber\n\n\nclass FixTrackNumsUsingTitles(BaseAction):\n    NAME = 'Fix track numbers using titles'\n    TITLE_REGEX = re.compile(r\"[^\\d\\-]+\")  # Only digits and '-' allowed\n\n    def callback(self, objs):\n        log.debug('[FixTrackNumsUsingTitles]')\n\n        for cluster in objs:\n            if not isinstance(cluster, Cluster) or not cluster.files:\n                continue\n\n            tracks = []  # Sorted list of FixedTrack\n\n            for i, f in enumerate(cluster.files):\n                if not f or not f.metadata or 'title' not in f.metadata:\n                    log.debug('No file/metadata/title for [%i]' % (i))\n                    continue\n\n                track = FixedTrack(i + 1, f.metadata['title'])\n\n                if not track.title:\n                    log.debug('No title for [%i]' % (i))\n                    continue\n\n                title_nums = self.TITLE_REGEX.sub('', track.title)\n                dash_index = title_nums.find('-')\n\n                if dash_index < 0:\n                    log.debug('No dash in [%i][%s]' % (i, title_nums))\n                    continue\n\n                try:\n                    track.title_num1 = int(title_nums[:dash_index])\n                    track.title_num2 = int(title_nums[dash_index + 1:])\n                except ValueError:\n                    log.debug('Invalid ints in [%i][%s]' % (i, title_nums))\n                    continue\n\n                was_added = False\n\n                # Not empty?\n                if tracks:\n                    # Justin Timberlake?\n                    for j, t in enumerate(tracks):\n                        if was_added:\n                            # Increment all track numbers above last added one\n                            t.tracknumber += 1\n                        # Don't do \"<=\" on title_num2 to preserve sequence\n                        # - Case 1: \"2-10\" < \"3-1\"\n                        # - Case 2: \"2-1\"  < \"2-10\"\n                        elif ((track.title_num1 < t.title_num1) or\n                              (track.title_num1 == t.title_num1 and\n                               track.title_num2 < t.title_num2)):\n                            # t.tracknumber will be updated in next loop cycle\n                            track.tracknumber = t.tracknumber\n                            tracks.insert(j, track)\n                            was_added = True  # Don't break\n\n                if not was_added:\n                    tracks.append(track)\n\n            # Let's build a dictionary of the new (fixed) track numbers\n            new_tracks = {}\n\n            for i, t in enumerate(tracks):\n                # Assume title is unique\n                new_tracks[t.title] = str(t.tracknumber)\n\n            for i, f in enumerate(cluster.files):\n                if not f or not f.metadata or 'title' not in f.metadata:\n                    # Already logged\n                    continue\n\n                key = f.metadata['title']\n\n                if not key:\n                    # Already logged\n                    continue\n                if key not in new_tracks:\n                    log.debug('No new track for [%i][%s]' % (i, key))\n                    continue\n\n                new_track = new_tracks[key]\n\n                log.debug('Change [%s]=>[%s]' % (key, new_track))\n\n                f.metadata['tracknumber'] = new_track\n                f.metadata.changed = True\n                f.update(signal=True)\n\n            cluster.update()\n\n\nclass FixTrackNumsUsingSeq(BaseAction):\n    NAME = 'Fix track numbers using sequence'\n\n    def callback(self, objs):\n        log.debug('[FixTrackNumsUsingSeq]')\n\n        for cluster in objs:\n            if not isinstance(cluster, Cluster) or not cluster.files:\n                continue\n\n            for i, f in enumerate(cluster.files):\n                if not f or not f.metadata:\n                    log.debug('No file/metadata for [%i]' % (i))\n                    continue\n\n                new_track = str(i + 1)\n\n                if 'title' in f.metadata and f.metadata['title']:\n                    log.debug('Change [%s]=>[%s]' %\n                              (f.metadata['title'], new_track))\n\n                f.metadata['tracknumber'] = new_track\n                f.metadata.changed = True\n                f.update(signal=True)\n\n            cluster.update()\n\nregister_cluster_action(FixTrackNumsUsingTitles())\nregister_cluster_action(FixTrackNumsUsingSeq())\n"
  },
  {
    "path": "plugins/format_performer_tags/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2018 Bob Swift (rdswift)\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = 'Format Performer Tags'\nPLUGIN_AUTHOR = 'Bob Swift, Philipp Wolfer'\nPLUGIN_DESCRIPTION = '''\nThis plugin provides options with respect to the formatting of performer\ntags.  It has been developed using the 'Standardise Performers' plugin by\nSophist as the basis for retrieving and processing the performer data for\neach of the tracks.  The format of the resulting tags can be customized\nin the option settings page.\n'''\n\nPLUGIN_VERSION = \"0.8.2\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nPLUGIN_USER_GUIDE_URL = \"https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/format_performer_tags/docs/README.md\"\n\nimport re\nfrom picard import config, log\nfrom picard.metadata import Metadata, register_track_metadata_processor\nfrom picard.plugin import PluginPriority\nfrom picard.ui.options import register_options_page, OptionsPage\nfrom picard.plugins.format_performer_tags.ui_options_format_performer_tags import Ui_FormatPerformerTagsOptionsPage\n\nperformers_split = re.compile(r\", | and \").split\n\nWORD_LIST = ['guest', 'solo', 'additional']\n\n\ndef get_word_dict(settings):\n    word_dict = {}\n    for word in WORD_LIST:\n        word_dict[word] = settings['format_group_' + word]\n    return word_dict\n\n\ndef rewrite_tag(key, values, metadata, word_dict, settings):\n    if ':' not in key:\n        mainkey = key\n        subkey = ''\n    else:\n        mainkey, subkey = key.split(':', 1)\n    log.debug(\"%s: Removing key: '%s'\", PLUGIN_NAME, key,)\n    metadata.delete(key)\n    log.debug(\"%s: Formatting Performer [%s: %s]\", PLUGIN_NAME, subkey, values,)\n    if not subkey:\n        instruments = []\n    else:\n        instruments = performers_split(subkey)\n    if instruments:\n        for instrument in instruments:\n            groups = { 1: [], 2: [], 3: [], 4: [], }\n            vocals = ''\n            if instrument:\n                instrument_key = ''\n                words = instrument.split()\n                for word in words[:]:\n                    if word in WORD_LIST:\n                        groups[word_dict[word]].append(word)\n                        words.remove(word)\n                display_group = {}\n                for group_number in range(1, 5):\n                    if groups[group_number]:\n                        group_separator = settings[\"format_group_{0}_sep_char\".format(group_number)]\n                        if not group_separator:\n                            group_separator = \" \"\n                        display_group[group_number] = settings[\"format_group_{0}_start_char\".format(group_number)] \\\n                            + group_separator.join(groups[group_number]) \\\n                            + settings[\"format_group_{0}_end_char\".format(group_number)]\n                    else:\n                        display_group[group_number] = \"\"\n                if words:\n                    instrument_key = ' '.join(words)\n                    if (len(words) > 1) and (words[-1] in [\"vocal\", \"vocals\",]):\n                        vocals = \" \".join(words[:-1])\n                        instrument_key = words[-1]\n                else:\n                    instrument_key = ''\n                if vocals:\n                    group_number = settings[\"format_group_vocals\"]\n                    temp_group = groups[group_number][:]\n                    if group_number < 2:\n                        temp_group.append(vocals)\n                    else:\n                        temp_group.insert(0, vocals)\n                    group_separator = settings[\"format_group_{0}_sep_char\".format(group_number)]\n                    if not group_separator:\n                        group_separator = \" \"\n                    display_group[group_number] = settings[\"format_group_{0}_start_char\".format(group_number)] \\\n                        + group_separator.join(temp_group) \\\n                        + settings[\"format_group_{0}_end_char\".format(group_number)]\n            newkey = ('%s:%s%s%s%s' % (mainkey, display_group[1], instrument_key, display_group[2], display_group[3],))\n            log.debug(\"%s: newkey: %s\", PLUGIN_NAME, newkey,)\n            for value in values:\n                metadata.add_unique(newkey, (value + display_group[4]))\n    else:\n        newkey = '%s:' % (mainkey,)\n        log.debug(\"%s: newkey: %s\", PLUGIN_NAME, newkey,)\n        for value in values:\n            metadata.add_unique(newkey, value)\n\n\ndef format_performer_tags(album, metadata, *args):\n    word_dict = get_word_dict(config.setting)\n    for key, values in list(filter(lambda filter_tuple: filter_tuple[0].startswith('performer') or filter_tuple[0].startswith('~performersort'), metadata.rawitems())):\n        rewrite_tag(key, values, metadata, word_dict, config.setting)\n\n\nclass FormatPerformerTagsOptionsPage(OptionsPage):\n\n    NAME = \"format_performer_tags\"\n    TITLE = \"Format Performer Tags\"\n    PARENT = \"plugins\"\n    HELP_URL = \"https://github.com/metabrainz/picard-plugins/blob/2.0/plugins/format_performer_tags/docs/README.md\"\n\n    options = [\n        config.IntOption(\"setting\", \"format_group_additional\", 3),\n        config.IntOption(\"setting\", \"format_group_guest\", 4),\n        config.IntOption(\"setting\", \"format_group_solo\", 3),\n        config.IntOption(\"setting\", \"format_group_vocals\", 2),\n        config.TextOption(\"setting\", \"format_group_1_start_char\", ''),\n        config.TextOption(\"setting\", \"format_group_1_end_char\", ' '),\n        config.TextOption(\"setting\", \"format_group_1_sep_char\", ''),\n        config.TextOption(\"setting\", \"format_group_2_start_char\", ', '),\n        config.TextOption(\"setting\", \"format_group_2_end_char\", ''),\n        config.TextOption(\"setting\", \"format_group_2_sep_char\", ''),\n        config.TextOption(\"setting\", \"format_group_3_start_char\", ' ('),\n        config.TextOption(\"setting\", \"format_group_3_end_char\", ')'),\n        config.TextOption(\"setting\", \"format_group_3_sep_char\", ''),\n        config.TextOption(\"setting\", \"format_group_4_start_char\", ' ('),\n        config.TextOption(\"setting\", \"format_group_4_end_char\", ')'),\n        config.TextOption(\"setting\", \"format_group_4_sep_char\", ''),\n    ]\n\n    def __init__(self, parent=None):\n        super(FormatPerformerTagsOptionsPage, self).__init__(parent)\n        self.ui = Ui_FormatPerformerTagsOptionsPage()\n        self.ui.setupUi(self)\n        self.ui.additional_rb_1.clicked.connect(self.update_examples)\n        self.ui.additional_rb_2.clicked.connect(self.update_examples)\n        self.ui.additional_rb_3.clicked.connect(self.update_examples)\n        self.ui.additional_rb_4.clicked.connect(self.update_examples)\n        self.ui.guest_rb_1.clicked.connect(self.update_examples)\n        self.ui.guest_rb_2.clicked.connect(self.update_examples)\n        self.ui.guest_rb_3.clicked.connect(self.update_examples)\n        self.ui.guest_rb_4.clicked.connect(self.update_examples)\n        self.ui.solo_rb_1.clicked.connect(self.update_examples)\n        self.ui.solo_rb_2.clicked.connect(self.update_examples)\n        self.ui.solo_rb_3.clicked.connect(self.update_examples)\n        self.ui.solo_rb_4.clicked.connect(self.update_examples)\n        self.ui.vocals_rb_1.clicked.connect(self.update_examples)\n        self.ui.vocals_rb_2.clicked.connect(self.update_examples)\n        self.ui.vocals_rb_3.clicked.connect(self.update_examples)\n        self.ui.vocals_rb_4.clicked.connect(self.update_examples)\n        self.ui.format_group_1_start_char.editingFinished.connect(self.update_examples)\n        self.ui.format_group_2_start_char.editingFinished.connect(self.update_examples)\n        self.ui.format_group_3_start_char.editingFinished.connect(self.update_examples)\n        self.ui.format_group_4_start_char.editingFinished.connect(self.update_examples)\n        self.ui.format_group_1_sep_char.editingFinished.connect(self.update_examples)\n        self.ui.format_group_2_sep_char.editingFinished.connect(self.update_examples)\n        self.ui.format_group_3_sep_char.editingFinished.connect(self.update_examples)\n        self.ui.format_group_4_sep_char.editingFinished.connect(self.update_examples)\n        self.ui.format_group_1_end_char.editingFinished.connect(self.update_examples)\n        self.ui.format_group_2_end_char.editingFinished.connect(self.update_examples)\n        self.ui.format_group_3_end_char.editingFinished.connect(self.update_examples)\n        self.ui.format_group_4_end_char.editingFinished.connect(self.update_examples)\n\n    def load(self):\n        # Enable external link\n        self.ui.format_description.setOpenExternalLinks(True)\n\n        # Settings for Keyword: additional\n        temp = config.setting[\"format_group_additional\"]\n        if temp > 3:\n            self.ui.additional_rb_4.setChecked(True)\n        elif temp > 2:\n            self.ui.additional_rb_3.setChecked(True)\n        elif temp > 1:\n            self.ui.additional_rb_2.setChecked(True)\n        else:\n            self.ui.additional_rb_1.setChecked(True)\n\n        # Settings for Keyword: guest\n        temp = config.setting[\"format_group_guest\"]\n        if temp > 3:\n            self.ui.guest_rb_4.setChecked(True)\n        elif temp > 2:\n            self.ui.guest_rb_3.setChecked(True)\n        elif temp > 1:\n            self.ui.guest_rb_2.setChecked(True)\n        else:\n            self.ui.guest_rb_1.setChecked(True)\n\n        # Settings for Keyword: solo\n        temp = config.setting[\"format_group_solo\"]\n        if temp > 3:\n            self.ui.solo_rb_4.setChecked(True)\n        elif temp > 2:\n            self.ui.solo_rb_3.setChecked(True)\n        elif temp > 1:\n            self.ui.solo_rb_2.setChecked(True)\n        else:\n            self.ui.solo_rb_1.setChecked(True)\n\n        # Settings for all vocal keywords\n        temp = config.setting[\"format_group_vocals\"]\n        if temp > 3:\n            self.ui.vocals_rb_4.setChecked(True)\n        elif temp > 2:\n            self.ui.vocals_rb_3.setChecked(True)\n        elif temp > 1:\n            self.ui.vocals_rb_2.setChecked(True)\n        else:\n            self.ui.vocals_rb_1.setChecked(True)\n\n        # Settings for word group 1\n        self.ui.format_group_1_start_char.setText(config.setting[\"format_group_1_start_char\"])\n        self.ui.format_group_1_end_char.setText(config.setting[\"format_group_1_end_char\"])\n        self.ui.format_group_1_sep_char.setText(config.setting[\"format_group_1_sep_char\"])\n\n        # Settings for word group 2\n        self.ui.format_group_2_start_char.setText(config.setting[\"format_group_2_start_char\"])\n        self.ui.format_group_2_end_char.setText(config.setting[\"format_group_2_end_char\"])\n        self.ui.format_group_2_sep_char.setText(config.setting[\"format_group_2_sep_char\"])\n\n        # Settings for word group 3\n        self.ui.format_group_3_start_char.setText(config.setting[\"format_group_3_start_char\"])\n        self.ui.format_group_3_end_char.setText(config.setting[\"format_group_3_end_char\"])\n        self.ui.format_group_3_sep_char.setText(config.setting[\"format_group_3_sep_char\"])\n\n        # Settings for word group 4\n        self.ui.format_group_4_start_char.setText(config.setting[\"format_group_4_start_char\"])\n        self.ui.format_group_4_end_char.setText(config.setting[\"format_group_4_end_char\"])\n        self.ui.format_group_4_sep_char.setText(config.setting[\"format_group_4_sep_char\"])\n        self.update_examples()\n\n        # TODO: Modify self.format_description in ui_options_format_performer_tags.py to include a placeholder\n        #       such as {user_guide_url} so that the translated string can be formatted to include the value\n        #       of PLUGIN_USER_GUIDE_URL to dynamically set the link while not requiring retranslation if the\n        #       link changes.  Preliminary code something like:\n        #\n        #       temp = (self.ui.format_description.text).format(user_guide_url=PLUGIN_USER_GUIDE_URL,)\n        #       self.ui.format_description.setText(temp)\n\n    def save(self):\n        self._set_settings(config.setting)\n\n    def restore_defaults(self):\n        super().restore_defaults()\n        self.update_examples()\n\n    def _set_settings(self, settings):\n\n        # Process 'additional' keyword settings\n        temp = 1\n        if self.ui.additional_rb_2.isChecked(): temp = 2\n        if self.ui.additional_rb_3.isChecked(): temp = 3\n        if self.ui.additional_rb_4.isChecked(): temp = 4\n        settings[\"format_group_additional\"] = temp\n\n        # Process 'guest' keyword settings\n        temp = 1\n        if self.ui.guest_rb_2.isChecked(): temp = 2\n        if self.ui.guest_rb_3.isChecked(): temp = 3\n        if self.ui.guest_rb_4.isChecked(): temp = 4\n        settings[\"format_group_guest\"] = temp\n\n        # Process 'solo' keyword settings\n        temp = 1\n        if self.ui.solo_rb_2.isChecked(): temp = 2\n        if self.ui.solo_rb_3.isChecked(): temp = 3\n        if self.ui.solo_rb_4.isChecked(): temp = 4\n        settings[\"format_group_solo\"] = temp\n\n        # Process all vocal keyword settings\n        temp = 1\n        if self.ui.vocals_rb_2.isChecked(): temp = 2\n        if self.ui.vocals_rb_3.isChecked(): temp = 3\n        if self.ui.vocals_rb_4.isChecked(): temp = 4\n        settings[\"format_group_vocals\"] = temp\n\n        # Settings for word group 1\n        settings[\"format_group_1_start_char\"] = self.ui.format_group_1_start_char.text()\n        settings[\"format_group_1_end_char\"] = self.ui.format_group_1_end_char.text()\n        settings[\"format_group_1_sep_char\"] = self.ui.format_group_1_sep_char.text()\n\n        # Settings for word group 2\n        settings[\"format_group_2_start_char\"] = self.ui.format_group_2_start_char.text()\n        settings[\"format_group_2_end_char\"] = self.ui.format_group_2_end_char.text()\n        settings[\"format_group_2_sep_char\"] = self.ui.format_group_2_sep_char.text()\n\n        # Settings for word group 3\n        settings[\"format_group_3_start_char\"] = self.ui.format_group_3_start_char.text()\n        settings[\"format_group_3_end_char\"] = self.ui.format_group_3_end_char.text()\n        settings[\"format_group_3_sep_char\"] = self.ui.format_group_3_sep_char.text()\n\n        # Settings for word group 4\n        settings[\"format_group_4_start_char\"] = self.ui.format_group_4_start_char.text()\n        settings[\"format_group_4_end_char\"] = self.ui.format_group_4_end_char.text()\n        settings[\"format_group_4_sep_char\"] = self.ui.format_group_4_sep_char.text()\n\n    def update_examples(self):\n        settings = {}\n        self._set_settings(settings)\n        word_dict = get_word_dict(settings)\n\n        instruments_credits = {\n            \"guitar\": [\"Johnny Flux\", \"John Watson\"],\n            \"guest guitar\": [\"Jimmy Page\"],\n            \"additional guest solo guitar\": [\"Jimmy Page\"],\n        }\n        instruments_example = self.build_example(instruments_credits, word_dict, settings)\n        self.ui.example_instruments.setText(instruments_example)\n\n        vocals_credits = {\n            \"additional solo lead vocals\": [\"Robert Plant\"],\n            \"additional solo guest lead vocals\": [\"Sandy Denny\"],\n        }\n        vocals_example = self.build_example(vocals_credits, word_dict, settings)\n        self.ui.example_vocals.setText(vocals_example)\n\n    @staticmethod\n    def build_example(credits, word_dict, settings):\n        prefix = \"performer:\"\n        metadata = Metadata()\n        for key, values in credits.items():\n            rewrite_tag(prefix + key, values, metadata, word_dict, settings)\n\n        examples = []\n        for key, values in metadata.rawitems():\n            credit = \"%s: %s\" % (key, \", \".join(values))\n            if credit.startswith(prefix):\n                credit = credit[len(prefix):]\n            examples.append(credit)\n        return \"\\n\".join(examples)\n\n\n# Register the plugin to run at a LOW priority.\nregister_track_metadata_processor(format_performer_tags, priority=PluginPriority.LOW)\nregister_options_page(FormatPerformerTagsOptionsPage)\n"
  },
  {
    "path": "plugins/format_performer_tags/docs/HISTORY.md",
    "content": "# Format Performer Tags\n\n## Contributors\n\nThe following people have contributed to the development of this plugin.\n\n* Bob Swift ([rdswift](https://github.com/rdswift/))\n* Philipp Wolfer ([phw](https://github.com/phw/))\n\n---\n\n## Revision History\n\nThe following identifies the development history of the plugin, in reverse chronological order.  Each version lists the changes made for that version, along with the author of each change.\n\n### Version 0.7\n\n* Test that instrument is in the list before trying to remove it.  (Issue #256: ValueError Exception in Format Performer Tags) \\[rdswift\\]\n* Properly handle unspecified performer types (i.e.: with no instrument, vocal, etc. specified). \\[rdswift\\]\n\n### Version 0.6\n\n* Update the user interface.  Add live examples when settings are changed. \\[phw\\]\n* Update plugin metadata. \\[phw\\]\n* Add `HISTORY.md` file containing the contributors list and revision history. \\[rdswift\\]\n\n### Version 0.5\n\n* Reformat long lines. \\[rdswift\\]\n* Add TODO note about language translation. \\[rdswift\\]\n\n### Version 0.4\n\n* Remove code to strip extra whitespace from key and value strings. \\[rdswift\\]\n\n### Version 0.3\n\n* Update to use four user-defined sections. \\[rdswift\\]\n* Add user guide. \\[rdswift\\]\n\n### Version 0.2\n\n* Fix bug that caused some performer records to be missed in the processing. \\[rdswift\\]\n* Add vocals performer processing. \\[rdswift\\]\n\n### Version 0.1\n\n* Initial testing release. \\[rdswift\\]\n\n---\n"
  },
  {
    "path": "plugins/format_performer_tags/docs/README.md",
    "content": "# Format Performer Tags \\[[Download](https://github.com/rdswift/picard-plugins/raw/2.0_RDS_Plugins/plugins/format_performer_tags/format_performer_tags.zip)\\]\n\n## Overview\n\nThis plugin allows the user to configure the way that instrument and vocal performer tags are written. Once\ninstalled a settings page will be added to Picard's options, which is where the plugin is configured.\n\n---\n\n## What it Does\n\nThis plugin serves two purposes.\n \n### First:\n \nPicard will by default try to order the performer/instrument credits by the name of the performers, summing up all instruments for that performer in one line.\nThis plugin will order the performer/instrument credits by instrument, summing up all performers that play them.\n \nSo instead of this:\n\n``` \nbackground vocals and drums: Wayne Gretzky\nbass and background vocals: Leslie Nielsen\nguitar, keyboard and synthesizer: Edward Hopper\nguitar and vocals: Vladimir Nabokov\nkeyboard and lead vocals: Bianca Castafiore\n```\n\nIt will be displayed like this:\n\n``` \nbackground vocals: Wayne Gretzky, Leslie Nielsen\nbass: Leslie Nielsen\ndrums: Wayne Gretzky\nguitar: Edward Hopper, Vladimir Nabokov\nkeyboard: Edward Hopper, Bianca Castafiore\nlead vocals: Bianca Castafiore\nsynthesizer: Edward Hopper\nvocals: Vladimir Nabokov\n```\n \n### Second:\n \nThis plugin allows fine-tuned organization of how instruments, performers and additional descriptions (keywords) are displayed in the instrument/performer tags.\n \nHere is some background information about these keywords:\n \nMusicBrainz' database allows to add and store keywords (attributes) that will refine or additionally describe the role of a performer for a recording.\nFor an artist performing music on an instrument, these are the three attributes (keywords) that MusicBrainz can store, and offer Picard:\n \n* additional\n* guest\n* solo\n \nFor an artist performing with his/her voice, MusicBrainz has this restricted list of keywords describing the role or the register of the voice:\n \n* background vocals\n* choir vocals\n* lead vocals\n  * alto vocals\n  * baritone vocals\n  * bass vocals\n  * bass-baritone vocals\n  * contralto vocals\n  * countertenor vocals\n  * mezzo-soprano vocals\n  * soprano vocals\n  * tenor vocals\n  * treble vocals\n* other vocals\n  * spoken vocals\n\n \nPicard can retrieve and display these keywords and will list them all together in front of the performer.\nThe result will be something like this:\n\n```\nguitar and solo guitar: Bob 'Swift' Fingers\nadditional drums: Rob Reiner (guest)\nadditional baritone vocals: Hermann Rorschach\nguest soprano vocals: Bianca Castafiore\n```\n\nThe problem with this is that it is a bit indistinct if these keywords say something about the instrument, the artist and their performing role, the voice's register, or the persons relation to the group/orchestra.\nFor instance:\n \n* 'additional' is referring to instrumentation. For example 'additional percussion'.\n* 'guest' is referring to the performer as a person. Indicating that they are a guest in that band/orchestra instead of a regular member.\n* 'solo' is referring to a specific role a musician performs in a composition. For example a musician performing a guitar solo.\n* 'soprano vocals' is saying something about the register of a performer's voice.\n \nSo you might want to attach 'solo' to the instrument, 'baritone' to the vocals, and 'guest' to the performer.\nThis plugin allows you to do that, so you could have something like this as a result:\n\n```\nguitar [solo]: Bob 'Swift' Fingers\ndrums ‹additional› : Rob Reiner (guest)\nvocals, baritone ‹additional› : Hermann Rorschach\nvocals, soprano: Bianca Castafiore (guest)\n```\n\n---\n\n## How it Works\n\nThis is the concept behind the workings of this plugin:\n \nThe basic structure of a performer tag such as Picard produces it is:\n\n    [Keywords] Instrument / Vocals: Performer\n \nThis plugin makes four different 'Sections' at fixed positions in these tags available.\nTheir positions are:\n\n    [Section 1]Instrument / Vocals[Section 2][Section 3]: Performer[Section 4]\n \nIn the settings panel you can define in what section (at what location) you want each of the available keywords to be displayed.\nYou can do that by simply selecting the section number for that (group of) keyword(s).\n \nYou can also define what characters you want to use for delimiting or surrounding the keywords.\nFor some situations you might want to use the more common parenthesis brackets ( ), or maybe you prefer less common brackets such as \\[ \\] or ‹ ›.\nNote that using default parenthesis might confuse possible subsequent tagging formulas in the music player/manager of your choice. \nYou can also just leave them blank, or use commas, spaces, etc.\n \nNote that the plugin does not add any spaces as separators by default, so you will need to define those to your personal liking.\n \n---\n\n## Settings\n\nThe first group of settings is the **Keyword Section Assignments**.  This is where you select the section in\nwhich each of the keywords will be displayed.  Selection is made by clicking the radio button corresponding\nto the desired section for each of the keywords.\n\nThe second group of settings is the **Section Display Settings**.  This is where you configure the text\nincluded at the beginning and end of each section displayed, and the characters used to separate multiple\nitems within a section.  Note that leading or trailing spaces must be included in the settings and will not\nbe automatically added.  If no separator characters are entered, the items will be automatically separated\nby a single space.\n\nThe initial default settings are:\n\n<!---\n```\nKeyword 'additional':  Section 3\nKeyword 'guest':       Section 4\nKeyword 'solo':        Section 3\nAll 'vocals' keywords: Section 2\n\nSection 1 starting text:   ''\nSection 1 ending text:     ''\nSection 1 separator text:  ''\n\nSection 2 starting text:   ', '\nSection 2 ending text:     ''\nSection 2 separator text:  ''\n\nSection 3 starting text:   ' ('\nSection 3 ending text:     ''\nSection 3 separator text:  ')'\n\nSection 4 starting text:   ' ('\nSection 4 ending text:     ''\nSection 4 separator text:  ')'\n```\n--->\n![default settings image](default_settings.jpg)\n\nThese settings will produce tags such as:\n\n```\nrhodes piano (solo): Billy Preston (guest)\npercussion: Steve Berlin (guest), Kris MacFarlane, Séan McCann\nvocals, background: Jeen (guest)\n```\n\n---\n\n## Examples\n\nThe following are some examples using actual information from MusicBrainz:\n\n### Example 1:\n\n(add example)\n\n### Example 2:\n\n(add example)\n\n### Example 3:\n\n(add example)\n\n---\n\n## Credits\n\nSpecial thank-you to [hiccup](https://musicbrainz.org/user/hiccup) for improvement suggestions and extensive testing during\nthe development of this plugin, and for providing the write-up and examples that formed the basis for this User Guide.\n\n\n\n<!---\n## Description\n\nThis plugin allows the user to configure the way that instrument and vocal performer tags are written. Once\ninstalled a settings page will be added to Picard's options, which is where the plugin is configured.\n\nThese settings will determine the format for any Performer tags prepared. The format is divided into six\nparts: the performer; the instrument or vocals; and four user selectable sections for the extra\ninformation. This is set out as:\n\n\\[Section 1\\]Instrument/Vocals\\[Section 2\\]\\[Section 3\\]: Performer\\[Section 4\\]\n\nYou can select the section in which each of the extra information words appears.  These extra information\nwords are \"additional\", \"guest\", \"solo\" and type of vocal.\n\nFor each of the sections you can select the starting character(s), the character(s) separating entries, and\nthe ending character(s).  Note that leading or trailing spaces must be included in the settings and will not\nbe automatically added.  If no separator characters are entered, the items will be automatically separated\nby a single space.\n\nNote that sections that don't contain any entries for a givien performer tag will not be included in the\ntag, including any start or end text configured for the section.\n\nFor example, some of the ways that a performer relationship for Billy Preston playing a guest solo on the\nRhodes piano could be configured to be saved include:\n\n* Performer [guest solo rhodes piano]: Billy Preston\n* Performer [solo rhodes piano]: Billy Preston (guest)\n* Performer [rhodes piano]: Billy Preston (guest solo)\n* Performer [rhodes piano, guest solo]: Billy Preston\n* Performer [rhodes piano, solo]: Billy Preston (guest)\n* Performer [rhodes piano, guest]: Billy Preston (solo)\n\nThis shows only a few examples of the many possible displays that can be configured.\n\n## Settings\n\nThe first group of settings is the **Keyword Section Assignments**.  This is where you select the section in\nwhich each of the keywords will be displayed.  Selection is made by clicking the radio button corresponding\nto the desired section for each of the keywords.\n\nThe second group of settings is the **Section Display Settings**.  This is where you configure the text\nincluded at the beginning and end of each section displayed, and the characters used to separate multiple\nitems within a section.  Note that leading or trailing spaces must be included in the settings and will not\nbe automatically added.  If no separator characters are entered, the items will be automatically separated\nby a single space.\n\nThe initial default settings are:\n\n```\nKeyword 'additional':  Section 3\nKeyword 'guest':       Section 4\nKeyword 'solo':        Section 3\nAll 'vocals' keywords: Section 2\n\nSection 1 starting text:   ''\nSection 1 ending text:     ''\nSection 1 separator text:  ''\n\nSection 2 starting text:   ', '\nSection 2 ending text:     ''\nSection 2 separator text:  ''\n\nSection 3 starting text:   ' ('\nSection 3 ending text:     ''\nSection 3 separator text:  ')'\n\nSection 4 starting text:   ' ('\nSection 4 ending text:     ''\nSection 4 separator text:  ')'\n```\n\nThese settings will produce tags such as:\n\n* Performer [rhodes piano (solo)]: Billy Preston (guest)\n* Performer [percussion]: Steve Berlin (guest), Kris MacFarlane, Séan McCann\n* Performer [vocal, background]: Jeen (guest)\n\n--->\n"
  },
  {
    "path": "plugins/format_performer_tags/ui_options_format_performer_tags.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/format_performer_tags/ui_options_format_performer_tags.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.4\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_FormatPerformerTagsOptionsPage(object):\n    def setupUi(self, FormatPerformerTagsOptionsPage):\n        FormatPerformerTagsOptionsPage.setObjectName(\"FormatPerformerTagsOptionsPage\")\n        FormatPerformerTagsOptionsPage.resize(561, 802)\n        FormatPerformerTagsOptionsPage.setMinimumSize(QtCore.QSize(360, 0))\n        self.verticalLayout = QtWidgets.QVBoxLayout(FormatPerformerTagsOptionsPage)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.scrollArea = QtWidgets.QScrollArea(FormatPerformerTagsOptionsPage)\n        self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame)\n        self.scrollArea.setWidgetResizable(True)\n        self.scrollArea.setObjectName(\"scrollArea\")\n        self.scrollAreaWidgetContents = QtWidgets.QWidget()\n        self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, -13, 529, 848))\n        self.scrollAreaWidgetContents.setObjectName(\"scrollAreaWidgetContents\")\n        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents)\n        self.verticalLayout_2.setObjectName(\"verticalLayout_2\")\n        self.gb_description = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)\n        self.gb_description.setObjectName(\"gb_description\")\n        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.gb_description)\n        self.verticalLayout_3.setObjectName(\"verticalLayout_3\")\n        self.format_description = QtWidgets.QLabel(self.gb_description)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.format_description.sizePolicy().hasHeightForWidth())\n        self.format_description.setSizePolicy(sizePolicy)\n        self.format_description.setWordWrap(True)\n        self.format_description.setObjectName(\"format_description\")\n        self.verticalLayout_3.addWidget(self.format_description)\n        self.verticalLayout_2.addWidget(self.gb_description)\n        self.gb_word_groups = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)\n        self.gb_word_groups.setObjectName(\"gb_word_groups\")\n        self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.gb_word_groups)\n        self.verticalLayout_5.setObjectName(\"verticalLayout_5\")\n        self.group_additonal = QtWidgets.QGroupBox(self.gb_word_groups)\n        self.group_additonal.setObjectName(\"group_additonal\")\n        self.horizontalLayout = QtWidgets.QHBoxLayout(self.group_additonal)\n        self.horizontalLayout.setContentsMargins(1, 1, 1, 1)\n        self.horizontalLayout.setObjectName(\"horizontalLayout\")\n        self.additional_rb_1 = QtWidgets.QRadioButton(self.group_additonal)\n        self.additional_rb_1.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.additional_rb_1.setObjectName(\"additional_rb_1\")\n        self.horizontalLayout.addWidget(self.additional_rb_1)\n        self.additional_rb_2 = QtWidgets.QRadioButton(self.group_additonal)\n        self.additional_rb_2.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.additional_rb_2.setObjectName(\"additional_rb_2\")\n        self.horizontalLayout.addWidget(self.additional_rb_2)\n        self.additional_rb_3 = QtWidgets.QRadioButton(self.group_additonal)\n        self.additional_rb_3.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.additional_rb_3.setObjectName(\"additional_rb_3\")\n        self.horizontalLayout.addWidget(self.additional_rb_3)\n        self.additional_rb_4 = QtWidgets.QRadioButton(self.group_additonal)\n        self.additional_rb_4.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.additional_rb_4.setObjectName(\"additional_rb_4\")\n        self.horizontalLayout.addWidget(self.additional_rb_4)\n        self.verticalLayout_5.addWidget(self.group_additonal)\n        self.group_guest = QtWidgets.QGroupBox(self.gb_word_groups)\n        self.group_guest.setObjectName(\"group_guest\")\n        self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.group_guest)\n        self.horizontalLayout_2.setContentsMargins(1, 1, 1, 1)\n        self.horizontalLayout_2.setObjectName(\"horizontalLayout_2\")\n        self.guest_rb_1 = QtWidgets.QRadioButton(self.group_guest)\n        self.guest_rb_1.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.guest_rb_1.setObjectName(\"guest_rb_1\")\n        self.horizontalLayout_2.addWidget(self.guest_rb_1)\n        self.guest_rb_2 = QtWidgets.QRadioButton(self.group_guest)\n        self.guest_rb_2.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.guest_rb_2.setObjectName(\"guest_rb_2\")\n        self.horizontalLayout_2.addWidget(self.guest_rb_2)\n        self.guest_rb_3 = QtWidgets.QRadioButton(self.group_guest)\n        self.guest_rb_3.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.guest_rb_3.setObjectName(\"guest_rb_3\")\n        self.horizontalLayout_2.addWidget(self.guest_rb_3)\n        self.guest_rb_4 = QtWidgets.QRadioButton(self.group_guest)\n        self.guest_rb_4.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.guest_rb_4.setObjectName(\"guest_rb_4\")\n        self.horizontalLayout_2.addWidget(self.guest_rb_4)\n        self.verticalLayout_5.addWidget(self.group_guest)\n        self.group_solo = QtWidgets.QGroupBox(self.gb_word_groups)\n        self.group_solo.setObjectName(\"group_solo\")\n        self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.group_solo)\n        self.horizontalLayout_3.setContentsMargins(1, 1, 1, 1)\n        self.horizontalLayout_3.setObjectName(\"horizontalLayout_3\")\n        self.solo_rb_1 = QtWidgets.QRadioButton(self.group_solo)\n        self.solo_rb_1.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.solo_rb_1.setObjectName(\"solo_rb_1\")\n        self.horizontalLayout_3.addWidget(self.solo_rb_1)\n        self.solo_rb_2 = QtWidgets.QRadioButton(self.group_solo)\n        self.solo_rb_2.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.solo_rb_2.setObjectName(\"solo_rb_2\")\n        self.horizontalLayout_3.addWidget(self.solo_rb_2)\n        self.solo_rb_3 = QtWidgets.QRadioButton(self.group_solo)\n        self.solo_rb_3.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.solo_rb_3.setObjectName(\"solo_rb_3\")\n        self.horizontalLayout_3.addWidget(self.solo_rb_3)\n        self.solo_rb_4 = QtWidgets.QRadioButton(self.group_solo)\n        self.solo_rb_4.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.solo_rb_4.setObjectName(\"solo_rb_4\")\n        self.horizontalLayout_3.addWidget(self.solo_rb_4)\n        self.verticalLayout_5.addWidget(self.group_solo)\n        self.group_vocals = QtWidgets.QGroupBox(self.gb_word_groups)\n        self.group_vocals.setObjectName(\"group_vocals\")\n        self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.group_vocals)\n        self.horizontalLayout_4.setContentsMargins(1, 1, 1, 1)\n        self.horizontalLayout_4.setObjectName(\"horizontalLayout_4\")\n        self.vocals_rb_1 = QtWidgets.QRadioButton(self.group_vocals)\n        self.vocals_rb_1.setMinimumSize(QtCore.QSize(40, 20))\n        self.vocals_rb_1.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.vocals_rb_1.setObjectName(\"vocals_rb_1\")\n        self.horizontalLayout_4.addWidget(self.vocals_rb_1)\n        self.vocals_rb_2 = QtWidgets.QRadioButton(self.group_vocals)\n        self.vocals_rb_2.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.vocals_rb_2.setObjectName(\"vocals_rb_2\")\n        self.horizontalLayout_4.addWidget(self.vocals_rb_2)\n        self.vocals_rb_3 = QtWidgets.QRadioButton(self.group_vocals)\n        self.vocals_rb_3.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.vocals_rb_3.setObjectName(\"vocals_rb_3\")\n        self.horizontalLayout_4.addWidget(self.vocals_rb_3)\n        self.vocals_rb_4 = QtWidgets.QRadioButton(self.group_vocals)\n        self.vocals_rb_4.setMaximumSize(QtCore.QSize(40, 16777215))\n        self.vocals_rb_4.setObjectName(\"vocals_rb_4\")\n        self.horizontalLayout_4.addWidget(self.vocals_rb_4)\n        self.verticalLayout_5.addWidget(self.group_vocals)\n        self.verticalLayout_2.addWidget(self.gb_word_groups)\n        self.gb_group_settings = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)\n        self.gb_group_settings.setObjectName(\"gb_group_settings\")\n        self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.gb_group_settings)\n        self.verticalLayout_6.setObjectName(\"verticalLayout_6\")\n        self.gridLayout = QtWidgets.QGridLayout()\n        self.gridLayout.setVerticalSpacing(1)\n        self.gridLayout.setObjectName(\"gridLayout\")\n        self.label_2 = QtWidgets.QLabel(self.gb_group_settings)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_2.sizePolicy().hasHeightForWidth())\n        self.label_2.setSizePolicy(sizePolicy)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.label_2.setFont(font)\n        self.label_2.setObjectName(\"label_2\")\n        self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1, QtCore.Qt.AlignLeft)\n        self.label_3 = QtWidgets.QLabel(self.gb_group_settings)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_3.sizePolicy().hasHeightForWidth())\n        self.label_3.setSizePolicy(sizePolicy)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.label_3.setFont(font)\n        self.label_3.setObjectName(\"label_3\")\n        self.gridLayout.addWidget(self.label_3, 3, 0, 1, 1, QtCore.Qt.AlignLeft)\n        self.format_group_1_start_char = QtWidgets.QLineEdit(self.gb_group_settings)\n        self.format_group_1_start_char.setMaximumSize(QtCore.QSize(50, 16777215))\n        self.format_group_1_start_char.setObjectName(\"format_group_1_start_char\")\n        self.gridLayout.addWidget(self.format_group_1_start_char, 1, 1, 1, 1, QtCore.Qt.AlignHCenter)\n        self.label = QtWidgets.QLabel(self.gb_group_settings)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())\n        self.label.setSizePolicy(sizePolicy)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.label.setFont(font)\n        self.label.setObjectName(\"label\")\n        self.gridLayout.addWidget(self.label, 1, 0, 1, 1, QtCore.Qt.AlignLeft)\n        self.label_4 = QtWidgets.QLabel(self.gb_group_settings)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth())\n        self.label_4.setSizePolicy(sizePolicy)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.label_4.setFont(font)\n        self.label_4.setObjectName(\"label_4\")\n        self.gridLayout.addWidget(self.label_4, 4, 0, 1, 1, QtCore.Qt.AlignLeft)\n        self.format_group_1_sep_char = QtWidgets.QLineEdit(self.gb_group_settings)\n        self.format_group_1_sep_char.setMaximumSize(QtCore.QSize(50, 16777215))\n        self.format_group_1_sep_char.setObjectName(\"format_group_1_sep_char\")\n        self.gridLayout.addWidget(self.format_group_1_sep_char, 1, 2, 1, 1, QtCore.Qt.AlignHCenter)\n        self.format_group_1_end_char = QtWidgets.QLineEdit(self.gb_group_settings)\n        self.format_group_1_end_char.setMaximumSize(QtCore.QSize(50, 16777215))\n        self.format_group_1_end_char.setObjectName(\"format_group_1_end_char\")\n        self.gridLayout.addWidget(self.format_group_1_end_char, 1, 3, 1, 1, QtCore.Qt.AlignHCenter)\n        self.label_5 = QtWidgets.QLabel(self.gb_group_settings)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.label_5.setFont(font)\n        self.label_5.setObjectName(\"label_5\")\n        self.gridLayout.addWidget(self.label_5, 0, 1, 1, 1, QtCore.Qt.AlignHCenter)\n        self.label_6 = QtWidgets.QLabel(self.gb_group_settings)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.label_6.setFont(font)\n        self.label_6.setObjectName(\"label_6\")\n        self.gridLayout.addWidget(self.label_6, 0, 2, 1, 1, QtCore.Qt.AlignHCenter)\n        self.label_7 = QtWidgets.QLabel(self.gb_group_settings)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.label_7.setFont(font)\n        self.label_7.setObjectName(\"label_7\")\n        self.gridLayout.addWidget(self.label_7, 0, 3, 1, 1, QtCore.Qt.AlignHCenter)\n        self.format_group_2_start_char = QtWidgets.QLineEdit(self.gb_group_settings)\n        self.format_group_2_start_char.setMaximumSize(QtCore.QSize(50, 16777215))\n        self.format_group_2_start_char.setObjectName(\"format_group_2_start_char\")\n        self.gridLayout.addWidget(self.format_group_2_start_char, 2, 1, 1, 1, QtCore.Qt.AlignHCenter)\n        self.format_group_3_start_char = QtWidgets.QLineEdit(self.gb_group_settings)\n        self.format_group_3_start_char.setMaximumSize(QtCore.QSize(50, 16777215))\n        self.format_group_3_start_char.setObjectName(\"format_group_3_start_char\")\n        self.gridLayout.addWidget(self.format_group_3_start_char, 3, 1, 1, 1, QtCore.Qt.AlignHCenter)\n        self.format_group_4_start_char = QtWidgets.QLineEdit(self.gb_group_settings)\n        self.format_group_4_start_char.setMaximumSize(QtCore.QSize(50, 16777215))\n        self.format_group_4_start_char.setObjectName(\"format_group_4_start_char\")\n        self.gridLayout.addWidget(self.format_group_4_start_char, 4, 1, 1, 1, QtCore.Qt.AlignHCenter)\n        self.format_group_2_sep_char = QtWidgets.QLineEdit(self.gb_group_settings)\n        self.format_group_2_sep_char.setMaximumSize(QtCore.QSize(50, 16777215))\n        self.format_group_2_sep_char.setObjectName(\"format_group_2_sep_char\")\n        self.gridLayout.addWidget(self.format_group_2_sep_char, 2, 2, 1, 1, QtCore.Qt.AlignHCenter)\n        self.format_group_3_sep_char = QtWidgets.QLineEdit(self.gb_group_settings)\n        self.format_group_3_sep_char.setMaximumSize(QtCore.QSize(50, 16777215))\n        self.format_group_3_sep_char.setObjectName(\"format_group_3_sep_char\")\n        self.gridLayout.addWidget(self.format_group_3_sep_char, 3, 2, 1, 1, QtCore.Qt.AlignHCenter)\n        self.format_group_4_sep_char = QtWidgets.QLineEdit(self.gb_group_settings)\n        self.format_group_4_sep_char.setMaximumSize(QtCore.QSize(50, 16777215))\n        self.format_group_4_sep_char.setObjectName(\"format_group_4_sep_char\")\n        self.gridLayout.addWidget(self.format_group_4_sep_char, 4, 2, 1, 1, QtCore.Qt.AlignHCenter)\n        self.format_group_2_end_char = QtWidgets.QLineEdit(self.gb_group_settings)\n        self.format_group_2_end_char.setMaximumSize(QtCore.QSize(50, 16777215))\n        self.format_group_2_end_char.setObjectName(\"format_group_2_end_char\")\n        self.gridLayout.addWidget(self.format_group_2_end_char, 2, 3, 1, 1, QtCore.Qt.AlignHCenter)\n        self.format_group_3_end_char = QtWidgets.QLineEdit(self.gb_group_settings)\n        self.format_group_3_end_char.setMaximumSize(QtCore.QSize(50, 16777215))\n        self.format_group_3_end_char.setObjectName(\"format_group_3_end_char\")\n        self.gridLayout.addWidget(self.format_group_3_end_char, 3, 3, 1, 1, QtCore.Qt.AlignHCenter)\n        self.format_group_4_end_char = QtWidgets.QLineEdit(self.gb_group_settings)\n        self.format_group_4_end_char.setMaximumSize(QtCore.QSize(50, 16777215))\n        self.format_group_4_end_char.setObjectName(\"format_group_4_end_char\")\n        self.gridLayout.addWidget(self.format_group_4_end_char, 4, 3, 1, 1, QtCore.Qt.AlignHCenter)\n        self.verticalLayout_6.addLayout(self.gridLayout)\n        self.verticalLayout_2.addWidget(self.gb_group_settings)\n        self.gb_examples = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)\n        self.gb_examples.setObjectName(\"gb_examples\")\n        self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.gb_examples)\n        self.verticalLayout_4.setObjectName(\"verticalLayout_4\")\n        self.example_instruments = QtWidgets.QLabel(self.gb_examples)\n        self.example_instruments.setText(\"\")\n        self.example_instruments.setObjectName(\"example_instruments\")\n        self.verticalLayout_4.addWidget(self.example_instruments)\n        self.example_vocals = QtWidgets.QLabel(self.gb_examples)\n        self.example_vocals.setText(\"\")\n        self.example_vocals.setObjectName(\"example_vocals\")\n        self.verticalLayout_4.addWidget(self.example_vocals)\n        self.verticalLayout_2.addWidget(self.gb_examples)\n        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.verticalLayout_2.addItem(spacerItem)\n        self.scrollArea.setWidget(self.scrollAreaWidgetContents)\n        self.verticalLayout.addWidget(self.scrollArea)\n\n        self.retranslateUi(FormatPerformerTagsOptionsPage)\n        QtCore.QMetaObject.connectSlotsByName(FormatPerformerTagsOptionsPage)\n\n    def retranslateUi(self, FormatPerformerTagsOptionsPage):\n        _translate = QtCore.QCoreApplication.translate\n        FormatPerformerTagsOptionsPage.setWindowTitle(_translate(\"FormatPerformerTagsOptionsPage\", \"Form\"))\n        self.gb_description.setTitle(_translate(\"FormatPerformerTagsOptionsPage\", \"Format Performer Tags\"))\n        self.format_description.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"<html><head/><body><p>These settings will determine the format for any <span style=\\\" font-weight:600;\\\">Performer</span> tags prepared. The format is divided into six parts: the performer; the instrument or &quot;vocals&quot;; and four user selectable sections for the extra information. This is set out as:</p><p align=\\\"center\\\"><span style=\\\" font-weight:600;\\\">[Section 1]</span>Instrument/Vocals<span style=\\\" font-weight:600;\\\">[Section 2][Section 3]</span>: Performer<span style=\\\" font-weight:600;\\\">[Section 4]</span></p><p>You can select the section in which each of the extra information words appears.</p><p>For each of the sections you can select the starting character(s), the character(s) separating entries, and the ending character(s). Note that leading or trailing spaces must be included in the settings and will not be automatically added. If no separator characters are entered, the items within a section will be automatically separated by a single space.</p><p>Please visit the repository on GitHub for <a href=\\\"https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/format_performer_tags/docs/README.md\\\"><span style=\\\" text-decoration: underline; color:#0000ff;\\\">additional information</span></a>.</p></body></html>\"))\n        self.gb_word_groups.setTitle(_translate(\"FormatPerformerTagsOptionsPage\", \"Keyword Sections Assignment\"))\n        self.group_additonal.setTitle(_translate(\"FormatPerformerTagsOptionsPage\", \"Keyword: additional\"))\n        self.additional_rb_1.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"1\"))\n        self.additional_rb_2.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"2\"))\n        self.additional_rb_3.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"3\"))\n        self.additional_rb_4.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"4\"))\n        self.group_guest.setTitle(_translate(\"FormatPerformerTagsOptionsPage\", \"Keyword: guest\"))\n        self.guest_rb_1.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"1\"))\n        self.guest_rb_2.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"2\"))\n        self.guest_rb_3.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"3\"))\n        self.guest_rb_4.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"4\"))\n        self.group_solo.setTitle(_translate(\"FormatPerformerTagsOptionsPage\", \"Keyword: solo\"))\n        self.solo_rb_1.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"1\"))\n        self.solo_rb_2.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"2\"))\n        self.solo_rb_3.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"3\"))\n        self.solo_rb_4.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"4\"))\n        self.group_vocals.setTitle(_translate(\"FormatPerformerTagsOptionsPage\", \"All vocal type keywords\"))\n        self.vocals_rb_1.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"1\"))\n        self.vocals_rb_2.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"2\"))\n        self.vocals_rb_3.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"3\"))\n        self.vocals_rb_4.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"4\"))\n        self.gb_group_settings.setTitle(_translate(\"FormatPerformerTagsOptionsPage\", \"Section Display Settings\"))\n        self.label_2.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"Section 2\"))\n        self.label_3.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"Section 3\"))\n        self.format_group_1_start_char.setPlaceholderText(_translate(\"FormatPerformerTagsOptionsPage\", \"(blank)\"))\n        self.label.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"Section 1\"))\n        self.label_4.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"Section 4\"))\n        self.format_group_1_sep_char.setPlaceholderText(_translate(\"FormatPerformerTagsOptionsPage\", \"(blank)\"))\n        self.format_group_1_end_char.setText(_translate(\"FormatPerformerTagsOptionsPage\", \" \"))\n        self.format_group_1_end_char.setPlaceholderText(_translate(\"FormatPerformerTagsOptionsPage\", \"(blank)\"))\n        self.label_5.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"Start Char(s)\"))\n        self.label_6.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"Sep Char(s)\"))\n        self.label_7.setText(_translate(\"FormatPerformerTagsOptionsPage\", \"End Char(s)\"))\n        self.format_group_2_start_char.setText(_translate(\"FormatPerformerTagsOptionsPage\", \", \"))\n        self.format_group_2_start_char.setPlaceholderText(_translate(\"FormatPerformerTagsOptionsPage\", \"(blank)\"))\n        self.format_group_3_start_char.setText(_translate(\"FormatPerformerTagsOptionsPage\", \" (\"))\n        self.format_group_3_start_char.setPlaceholderText(_translate(\"FormatPerformerTagsOptionsPage\", \"(blank)\"))\n        self.format_group_4_start_char.setText(_translate(\"FormatPerformerTagsOptionsPage\", \" (\"))\n        self.format_group_4_start_char.setPlaceholderText(_translate(\"FormatPerformerTagsOptionsPage\", \"(blank)\"))\n        self.format_group_2_sep_char.setPlaceholderText(_translate(\"FormatPerformerTagsOptionsPage\", \"(blank)\"))\n        self.format_group_3_sep_char.setPlaceholderText(_translate(\"FormatPerformerTagsOptionsPage\", \"(blank)\"))\n        self.format_group_4_sep_char.setPlaceholderText(_translate(\"FormatPerformerTagsOptionsPage\", \"(blank)\"))\n        self.format_group_2_end_char.setPlaceholderText(_translate(\"FormatPerformerTagsOptionsPage\", \"(blank)\"))\n        self.format_group_3_end_char.setText(_translate(\"FormatPerformerTagsOptionsPage\", \")\"))\n        self.format_group_3_end_char.setPlaceholderText(_translate(\"FormatPerformerTagsOptionsPage\", \"(blank)\"))\n        self.format_group_4_end_char.setText(_translate(\"FormatPerformerTagsOptionsPage\", \")\"))\n        self.format_group_4_end_char.setPlaceholderText(_translate(\"FormatPerformerTagsOptionsPage\", \"(blank)\"))\n        self.gb_examples.setTitle(_translate(\"FormatPerformerTagsOptionsPage\", \"Examples\"))\n"
  },
  {
    "path": "plugins/format_performer_tags/ui_options_format_performer_tags.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>FormatPerformerTagsOptionsPage</class>\n <widget class=\"QWidget\" name=\"FormatPerformerTagsOptionsPage\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>561</width>\n    <height>802</height>\n   </rect>\n  </property>\n  <property name=\"minimumSize\">\n   <size>\n    <width>360</width>\n    <height>0</height>\n   </size>\n  </property>\n  <property name=\"windowTitle\">\n   <string>Form</string>\n  </property>\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n   <item>\n    <widget class=\"QScrollArea\" name=\"scrollArea\">\n     <property name=\"frameShape\">\n      <enum>QFrame::NoFrame</enum>\n     </property>\n     <property name=\"widgetResizable\">\n      <bool>true</bool>\n     </property>\n     <widget class=\"QWidget\" name=\"scrollAreaWidgetContents\">\n      <property name=\"geometry\">\n       <rect>\n        <x>0</x>\n        <y>-13</y>\n        <width>529</width>\n        <height>848</height>\n       </rect>\n      </property>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\n       <item>\n        <widget class=\"QGroupBox\" name=\"gb_description\">\n         <property name=\"title\">\n          <string>Format Performer Tags</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_3\">\n          <item>\n           <widget class=\"QLabel\" name=\"format_description\">\n            <property name=\"sizePolicy\">\n             <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Minimum\">\n              <horstretch>0</horstretch>\n              <verstretch>0</verstretch>\n             </sizepolicy>\n            </property>\n            <property name=\"text\">\n             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;These settings will determine the format for any &lt;span style=&quot; font-weight:600;&quot;&gt;Performer&lt;/span&gt; tags prepared. The format is divided into six parts: the performer; the instrument or &amp;quot;vocals&amp;quot;; and four user selectable sections for the extra information. This is set out as:&lt;/p&gt;&lt;p align=&quot;center&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;[Section 1]&lt;/span&gt;Instrument/Vocals&lt;span style=&quot; font-weight:600;&quot;&gt;[Section 2][Section 3]&lt;/span&gt;: Performer&lt;span style=&quot; font-weight:600;&quot;&gt;[Section 4]&lt;/span&gt;&lt;/p&gt;&lt;p&gt;You can select the section in which each of the extra information words appears.&lt;/p&gt;&lt;p&gt;For each of the sections you can select the starting character(s), the character(s) separating entries, and the ending character(s). Note that leading or trailing spaces must be included in the settings and will not be automatically added. If no separator characters are entered, the items within a section will be automatically separated by a single space.&lt;/p&gt;&lt;p&gt;Please visit the repository on GitHub for &lt;a href=&quot;https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/format_performer_tags/docs/README.md&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;additional information&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n            </property>\n            <property name=\"wordWrap\">\n             <bool>true</bool>\n            </property>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QGroupBox\" name=\"gb_word_groups\">\n         <property name=\"title\">\n          <string>Keyword Sections Assignment</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_5\">\n          <item>\n           <widget class=\"QGroupBox\" name=\"group_additonal\">\n            <property name=\"title\">\n             <string>Keyword: additional</string>\n            </property>\n            <layout class=\"QHBoxLayout\" name=\"horizontalLayout\">\n             <property name=\"leftMargin\">\n              <number>1</number>\n             </property>\n             <property name=\"topMargin\">\n              <number>1</number>\n             </property>\n             <property name=\"rightMargin\">\n              <number>1</number>\n             </property>\n             <property name=\"bottomMargin\">\n              <number>1</number>\n             </property>\n             <item>\n              <widget class=\"QRadioButton\" name=\"additional_rb_1\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>1</string>\n               </property>\n              </widget>\n             </item>\n             <item>\n              <widget class=\"QRadioButton\" name=\"additional_rb_2\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>2</string>\n               </property>\n              </widget>\n             </item>\n             <item>\n              <widget class=\"QRadioButton\" name=\"additional_rb_3\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>3</string>\n               </property>\n              </widget>\n             </item>\n             <item>\n              <widget class=\"QRadioButton\" name=\"additional_rb_4\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>4</string>\n               </property>\n              </widget>\n             </item>\n            </layout>\n           </widget>\n          </item>\n          <item>\n           <widget class=\"QGroupBox\" name=\"group_guest\">\n            <property name=\"title\">\n             <string>Keyword: guest</string>\n            </property>\n            <layout class=\"QHBoxLayout\" name=\"horizontalLayout_2\">\n             <property name=\"leftMargin\">\n              <number>1</number>\n             </property>\n             <property name=\"topMargin\">\n              <number>1</number>\n             </property>\n             <property name=\"rightMargin\">\n              <number>1</number>\n             </property>\n             <property name=\"bottomMargin\">\n              <number>1</number>\n             </property>\n             <item>\n              <widget class=\"QRadioButton\" name=\"guest_rb_1\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>1</string>\n               </property>\n              </widget>\n             </item>\n             <item>\n              <widget class=\"QRadioButton\" name=\"guest_rb_2\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>2</string>\n               </property>\n              </widget>\n             </item>\n             <item>\n              <widget class=\"QRadioButton\" name=\"guest_rb_3\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>3</string>\n               </property>\n              </widget>\n             </item>\n             <item>\n              <widget class=\"QRadioButton\" name=\"guest_rb_4\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>4</string>\n               </property>\n              </widget>\n             </item>\n            </layout>\n           </widget>\n          </item>\n          <item>\n           <widget class=\"QGroupBox\" name=\"group_solo\">\n            <property name=\"title\">\n             <string>Keyword: solo</string>\n            </property>\n            <layout class=\"QHBoxLayout\" name=\"horizontalLayout_3\">\n             <property name=\"leftMargin\">\n              <number>1</number>\n             </property>\n             <property name=\"topMargin\">\n              <number>1</number>\n             </property>\n             <property name=\"rightMargin\">\n              <number>1</number>\n             </property>\n             <property name=\"bottomMargin\">\n              <number>1</number>\n             </property>\n             <item>\n              <widget class=\"QRadioButton\" name=\"solo_rb_1\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>1</string>\n               </property>\n              </widget>\n             </item>\n             <item>\n              <widget class=\"QRadioButton\" name=\"solo_rb_2\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>2</string>\n               </property>\n              </widget>\n             </item>\n             <item>\n              <widget class=\"QRadioButton\" name=\"solo_rb_3\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>3</string>\n               </property>\n              </widget>\n             </item>\n             <item>\n              <widget class=\"QRadioButton\" name=\"solo_rb_4\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>4</string>\n               </property>\n              </widget>\n             </item>\n            </layout>\n           </widget>\n          </item>\n          <item>\n           <widget class=\"QGroupBox\" name=\"group_vocals\">\n            <property name=\"title\">\n             <string>All vocal type keywords</string>\n            </property>\n            <layout class=\"QHBoxLayout\" name=\"horizontalLayout_4\">\n             <property name=\"leftMargin\">\n              <number>1</number>\n             </property>\n             <property name=\"topMargin\">\n              <number>1</number>\n             </property>\n             <property name=\"rightMargin\">\n              <number>1</number>\n             </property>\n             <property name=\"bottomMargin\">\n              <number>1</number>\n             </property>\n             <item>\n              <widget class=\"QRadioButton\" name=\"vocals_rb_1\">\n               <property name=\"minimumSize\">\n                <size>\n                 <width>40</width>\n                 <height>20</height>\n                </size>\n               </property>\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>1</string>\n               </property>\n              </widget>\n             </item>\n             <item>\n              <widget class=\"QRadioButton\" name=\"vocals_rb_2\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>2</string>\n               </property>\n              </widget>\n             </item>\n             <item>\n              <widget class=\"QRadioButton\" name=\"vocals_rb_3\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>3</string>\n               </property>\n              </widget>\n             </item>\n             <item>\n              <widget class=\"QRadioButton\" name=\"vocals_rb_4\">\n               <property name=\"maximumSize\">\n                <size>\n                 <width>40</width>\n                 <height>16777215</height>\n                </size>\n               </property>\n               <property name=\"text\">\n                <string>4</string>\n               </property>\n              </widget>\n             </item>\n            </layout>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QGroupBox\" name=\"gb_group_settings\">\n         <property name=\"title\">\n          <string>Section Display Settings</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_6\">\n          <item>\n           <layout class=\"QGridLayout\" name=\"gridLayout\">\n            <property name=\"verticalSpacing\">\n             <number>1</number>\n            </property>\n            <item row=\"2\" column=\"0\" alignment=\"Qt::AlignLeft\">\n             <widget class=\"QLabel\" name=\"label_2\">\n              <property name=\"sizePolicy\">\n               <sizepolicy hsizetype=\"Minimum\" vsizetype=\"Preferred\">\n                <horstretch>0</horstretch>\n                <verstretch>0</verstretch>\n               </sizepolicy>\n              </property>\n              <property name=\"font\">\n               <font>\n                <weight>75</weight>\n                <bold>true</bold>\n               </font>\n              </property>\n              <property name=\"text\">\n               <string>Section 2</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"3\" column=\"0\" alignment=\"Qt::AlignLeft\">\n             <widget class=\"QLabel\" name=\"label_3\">\n              <property name=\"sizePolicy\">\n               <sizepolicy hsizetype=\"Minimum\" vsizetype=\"Preferred\">\n                <horstretch>0</horstretch>\n                <verstretch>0</verstretch>\n               </sizepolicy>\n              </property>\n              <property name=\"font\">\n               <font>\n                <weight>75</weight>\n                <bold>true</bold>\n               </font>\n              </property>\n              <property name=\"text\">\n               <string>Section 3</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"1\" column=\"1\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLineEdit\" name=\"format_group_1_start_char\">\n              <property name=\"maximumSize\">\n               <size>\n                <width>50</width>\n                <height>16777215</height>\n               </size>\n              </property>\n              <property name=\"placeholderText\">\n               <string>(blank)</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"1\" column=\"0\" alignment=\"Qt::AlignLeft\">\n             <widget class=\"QLabel\" name=\"label\">\n              <property name=\"sizePolicy\">\n               <sizepolicy hsizetype=\"Minimum\" vsizetype=\"Preferred\">\n                <horstretch>0</horstretch>\n                <verstretch>0</verstretch>\n               </sizepolicy>\n              </property>\n              <property name=\"font\">\n               <font>\n                <weight>75</weight>\n                <bold>true</bold>\n               </font>\n              </property>\n              <property name=\"text\">\n               <string>Section 1</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"4\" column=\"0\" alignment=\"Qt::AlignLeft\">\n             <widget class=\"QLabel\" name=\"label_4\">\n              <property name=\"sizePolicy\">\n               <sizepolicy hsizetype=\"Minimum\" vsizetype=\"Preferred\">\n                <horstretch>0</horstretch>\n                <verstretch>0</verstretch>\n               </sizepolicy>\n              </property>\n              <property name=\"font\">\n               <font>\n                <weight>75</weight>\n                <bold>true</bold>\n               </font>\n              </property>\n              <property name=\"text\">\n               <string>Section 4</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"1\" column=\"2\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLineEdit\" name=\"format_group_1_sep_char\">\n              <property name=\"maximumSize\">\n               <size>\n                <width>50</width>\n                <height>16777215</height>\n               </size>\n              </property>\n              <property name=\"placeholderText\">\n               <string>(blank)</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"1\" column=\"3\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLineEdit\" name=\"format_group_1_end_char\">\n              <property name=\"maximumSize\">\n               <size>\n                <width>50</width>\n                <height>16777215</height>\n               </size>\n              </property>\n              <property name=\"text\">\n               <string> </string>\n              </property>\n              <property name=\"placeholderText\">\n               <string>(blank)</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"0\" column=\"1\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLabel\" name=\"label_5\">\n              <property name=\"font\">\n               <font>\n                <weight>75</weight>\n                <bold>true</bold>\n               </font>\n              </property>\n              <property name=\"text\">\n               <string>Start Char(s)</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"0\" column=\"2\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLabel\" name=\"label_6\">\n              <property name=\"font\">\n               <font>\n                <weight>75</weight>\n                <bold>true</bold>\n               </font>\n              </property>\n              <property name=\"text\">\n               <string>Sep Char(s)</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"0\" column=\"3\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLabel\" name=\"label_7\">\n              <property name=\"font\">\n               <font>\n                <weight>75</weight>\n                <bold>true</bold>\n               </font>\n              </property>\n              <property name=\"text\">\n               <string>End Char(s)</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"2\" column=\"1\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLineEdit\" name=\"format_group_2_start_char\">\n              <property name=\"maximumSize\">\n               <size>\n                <width>50</width>\n                <height>16777215</height>\n               </size>\n              </property>\n              <property name=\"text\">\n               <string>, </string>\n              </property>\n              <property name=\"placeholderText\">\n               <string>(blank)</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"3\" column=\"1\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLineEdit\" name=\"format_group_3_start_char\">\n              <property name=\"maximumSize\">\n               <size>\n                <width>50</width>\n                <height>16777215</height>\n               </size>\n              </property>\n              <property name=\"text\">\n               <string> (</string>\n              </property>\n              <property name=\"placeholderText\">\n               <string>(blank)</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"4\" column=\"1\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLineEdit\" name=\"format_group_4_start_char\">\n              <property name=\"maximumSize\">\n               <size>\n                <width>50</width>\n                <height>16777215</height>\n               </size>\n              </property>\n              <property name=\"text\">\n               <string> (</string>\n              </property>\n              <property name=\"placeholderText\">\n               <string>(blank)</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"2\" column=\"2\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLineEdit\" name=\"format_group_2_sep_char\">\n              <property name=\"maximumSize\">\n               <size>\n                <width>50</width>\n                <height>16777215</height>\n               </size>\n              </property>\n              <property name=\"placeholderText\">\n               <string>(blank)</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"3\" column=\"2\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLineEdit\" name=\"format_group_3_sep_char\">\n              <property name=\"maximumSize\">\n               <size>\n                <width>50</width>\n                <height>16777215</height>\n               </size>\n              </property>\n              <property name=\"placeholderText\">\n               <string>(blank)</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"4\" column=\"2\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLineEdit\" name=\"format_group_4_sep_char\">\n              <property name=\"maximumSize\">\n               <size>\n                <width>50</width>\n                <height>16777215</height>\n               </size>\n              </property>\n              <property name=\"placeholderText\">\n               <string>(blank)</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"2\" column=\"3\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLineEdit\" name=\"format_group_2_end_char\">\n              <property name=\"maximumSize\">\n               <size>\n                <width>50</width>\n                <height>16777215</height>\n               </size>\n              </property>\n              <property name=\"placeholderText\">\n               <string>(blank)</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"3\" column=\"3\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLineEdit\" name=\"format_group_3_end_char\">\n              <property name=\"maximumSize\">\n               <size>\n                <width>50</width>\n                <height>16777215</height>\n               </size>\n              </property>\n              <property name=\"text\">\n               <string>)</string>\n              </property>\n              <property name=\"placeholderText\">\n               <string>(blank)</string>\n              </property>\n             </widget>\n            </item>\n            <item row=\"4\" column=\"3\" alignment=\"Qt::AlignHCenter\">\n             <widget class=\"QLineEdit\" name=\"format_group_4_end_char\">\n              <property name=\"maximumSize\">\n               <size>\n                <width>50</width>\n                <height>16777215</height>\n               </size>\n              </property>\n              <property name=\"text\">\n               <string>)</string>\n              </property>\n              <property name=\"placeholderText\">\n               <string>(blank)</string>\n              </property>\n             </widget>\n            </item>\n           </layout>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QGroupBox\" name=\"gb_examples\">\n         <property name=\"title\">\n          <string>Examples</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_4\">\n          <item>\n           <widget class=\"QLabel\" name=\"example_instruments\">\n            <property name=\"text\">\n             <string/>\n            </property>\n           </widget>\n          </item>\n          <item>\n           <widget class=\"QLabel\" name=\"example_vocals\">\n            <property name=\"text\">\n             <string/>\n            </property>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <spacer name=\"verticalSpacer\">\n         <property name=\"orientation\">\n          <enum>Qt::Vertical</enum>\n         </property>\n         <property name=\"sizeHint\" stdset=\"0\">\n          <size>\n           <width>20</width>\n           <height>40</height>\n          </size>\n         </property>\n        </spacer>\n       </item>\n      </layout>\n     </widget>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/genre_mapper/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2022-2024 Bob Swift (rdswift)\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = 'Genre Mapper'\nPLUGIN_AUTHOR = 'Bob Swift'\nPLUGIN_DESCRIPTION = '''\nThis plugin provides the ability to standardize genres in the \"genre\"\ntag by matching the genres as found to a standard genre as defined in\nthe genre replacement mapping configuration option. Once installed a\nsettings page will be added to Picard's options, which is where the\nplugin is configured.\n<br /><br />\nPlease see the <a href=\"https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/genre_mapper/docs/README.md\">user guide</a> on GitHub for more information.\n'''\nPLUGIN_VERSION = '0.6'\nPLUGIN_API_VERSIONS = ['2.0', '2.1', '2.2', '2.3', '2.6', '2.7', '2.8', '2.9', '2.10', '2.11']\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.txt\"\n\nimport re\n\nfrom picard import (\n    config,\n    log,\n)\nfrom picard.metadata import (\n    MULTI_VALUED_JOINER,\n    register_track_metadata_processor,\n)\nfrom picard.plugin import PluginPriority\nfrom picard.plugins.genre_mapper.ui_options_genre_mapper import (\n    Ui_GenreMapperOptionsPage,\n)\n\nfrom picard.ui.options import (\n    OptionsPage,\n    register_options_page,\n)\n\n\npairs_split = re.compile(r\"\\r\\n|\\n\\r|\\n\").split\n\nOPT_GENRE_SEPARATOR = 'join_genres'\nOPT_MATCH_ENABLED = 'genre_mapper_enabled'\nOPT_MATCH_PAIRS = 'genre_mapper_replacement_pairs'\nOPT_MATCH_FIRST = 'genre_mapper_apply_first_match_only'\nOPT_MATCH_REGEX = 'genre_mapper_use_regex'\n\n\nclass GenreMappingPairs():\n    pairs = []\n\n    @classmethod\n    def refresh(cls):\n        log.debug(\"%s: Refreshing the genre replacement maps processing pairs using '%s' translation.\",\n            PLUGIN_NAME, 'RegEx' if config.Option.exists(\"setting\", OPT_MATCH_REGEX) and config.setting[OPT_MATCH_REGEX] else 'Simple',)\n        if not config.Option.exists(\"setting\", OPT_MATCH_PAIRS):\n            log.warning(\"%s: Unable to read the '%s' setting.\", PLUGIN_NAME, OPT_MATCH_PAIRS,)\n            return\n\n        def _make_re(map_string):\n            # Replace period with temporary placeholder character (newline)\n            re_string = str(map_string).strip().replace('.', '\\n')\n            # Convert wildcard characters to regular expression equivalents\n            re_string = re_string.replace('*', '.*').replace('?', '.')\n            # Escape carat and dollar sign for regular expression\n            re_string = re_string.replace('^', '\\\\^').replace('$', '\\\\$')\n            # Replace temporary placeholder characters with escaped periods\n            re_string = '^' + re_string.replace('\\n', '\\\\.') + '$'\n            # Return regular expression with carat and dollar sign to force match condition on full string\n            return re_string\n\n        cls.pairs = []\n        for pair in pairs_split(config.setting[OPT_MATCH_PAIRS]):\n            if \"=\" not in pair:\n                continue\n            original, replacement = pair.split('=', 1)\n            original = original.strip()\n            if not original:\n                continue\n            replacement = replacement.strip()\n            cls.pairs.append((original if config.setting[OPT_MATCH_REGEX] else _make_re(original), replacement))\n            log.debug('%s: Add genre mapping pair: \"%s\" = \"%s\"', PLUGIN_NAME, original, replacement,)\n        if not cls.pairs:\n            log.debug(\"%s: No genre replacement maps defined.\", PLUGIN_NAME,)\n\n\nclass GenreMapperOptionsPage(OptionsPage):\n\n    NAME = \"genre_mapper\"\n    TITLE = \"Genre Mapper\"\n    PARENT = \"plugins\"\n\n    options = [\n        config.TextOption(\"setting\", OPT_MATCH_PAIRS, ''),\n        config.BoolOption(\"setting\", OPT_MATCH_FIRST, False),\n        config.BoolOption(\"setting\", OPT_MATCH_ENABLED, False),\n        config.BoolOption(\"setting\", OPT_MATCH_REGEX, False),\n    ]\n\n    def __init__(self, parent=None):\n        super().__init__(parent)\n        self.ui = Ui_GenreMapperOptionsPage()\n        self.ui.setupUi(self)\n\n    def load(self):\n        # Enable external link\n        self.ui.format_description.setOpenExternalLinks(True)\n\n        self.ui.genre_mapper_replacement_pairs.setPlainText(config.setting[OPT_MATCH_PAIRS])\n        self.ui.genre_mapper_first_match_only.setChecked(config.setting[OPT_MATCH_FIRST])\n        self.ui.cb_enable_genre_mapping.setChecked(config.setting[OPT_MATCH_ENABLED])\n        self.ui.cb_use_regex.setChecked(config.setting[OPT_MATCH_REGEX])\n\n        self.ui.cb_enable_genre_mapping.stateChanged.connect(self._set_enabled_state)\n        self._set_enabled_state()\n\n    def save(self):\n        config.setting[OPT_MATCH_PAIRS] = self.ui.genre_mapper_replacement_pairs.toPlainText()\n        config.setting[OPT_MATCH_FIRST] = self.ui.genre_mapper_first_match_only.isChecked()\n        config.setting[OPT_MATCH_ENABLED] = self.ui.cb_enable_genre_mapping.isChecked()\n        config.setting[OPT_MATCH_REGEX] = self.ui.cb_use_regex.isChecked()\n\n        GenreMappingPairs.refresh()\n\n    def _set_enabled_state(self, *args):\n        self.ui.gm_replacement_pairs.setEnabled(self.ui.cb_enable_genre_mapping.isChecked())\n\n\ndef track_genre_mapper(album, metadata, *args):\n    if not config.setting[OPT_MATCH_ENABLED]:\n        return\n    if 'genre' not in metadata or not metadata['genre']:\n        log.debug('%s: No genres found for: \"%s\"', PLUGIN_NAME, metadata['title'],)\n        return\n    genre_joiner = config.setting[OPT_GENRE_SEPARATOR] if config.setting[OPT_GENRE_SEPARATOR] else MULTI_VALUED_JOINER\n    genres = set()\n    metadata_genres = str(metadata['genre']).split(genre_joiner)\n    for genre in metadata_genres:\n        for (original, replacement) in GenreMappingPairs.pairs:\n            try:\n                if genre and re.search(original, genre, re.IGNORECASE):\n                    genre = replacement\n                    if config.setting[OPT_MATCH_FIRST]:\n                        break\n            except re.error:\n                log.error('%s: Invalid regular expression ignored: \"%s\"', PLUGIN_NAME, original,)\n        if genre:\n            genres.add(genre.title())\n    genres = sorted(genres)\n    log.debug('%s: Genres updated from %s to %s', PLUGIN_NAME, metadata_genres, genres,)\n    metadata['genre'] = genres\n\n\n# Register the plugin to run at a LOW priority.\nregister_track_metadata_processor(track_genre_mapper, priority=PluginPriority.LOW)\nregister_options_page(GenreMapperOptionsPage)\n\nGenreMappingPairs.refresh()\n"
  },
  {
    "path": "plugins/genre_mapper/options_genre_mapper.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>GenreMapperOptionsPage</class>\n <widget class=\"QWidget\" name=\"GenreMapperOptionsPage\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>398</width>\n    <height>568</height>\n   </rect>\n  </property>\n  <property name=\"sizePolicy\">\n   <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n    <horstretch>0</horstretch>\n    <verstretch>0</verstretch>\n   </sizepolicy>\n  </property>\n  <layout class=\"QVBoxLayout\">\n   <property name=\"spacing\">\n    <number>16</number>\n   </property>\n   <item>\n    <layout class=\"QVBoxLayout\" name=\"verticalLayout_4\">\n     <item>\n      <widget class=\"QGroupBox\" name=\"gm_description\">\n       <property name=\"sizePolicy\">\n        <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Minimum\">\n         <horstretch>0</horstretch>\n         <verstretch>0</verstretch>\n        </sizepolicy>\n       </property>\n       <property name=\"minimumSize\">\n        <size>\n         <width>0</width>\n         <height>50</height>\n        </size>\n       </property>\n       <property name=\"font\">\n        <font>\n         <weight>75</weight>\n         <bold>true</bold>\n        </font>\n       </property>\n       <property name=\"title\">\n        <string>Genre Mapper</string>\n       </property>\n       <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n        <property name=\"leftMargin\">\n         <number>9</number>\n        </property>\n        <property name=\"topMargin\">\n         <number>9</number>\n        </property>\n        <property name=\"rightMargin\">\n         <number>9</number>\n        </property>\n        <property name=\"bottomMargin\">\n         <number>1</number>\n        </property>\n        <item>\n         <widget class=\"QLabel\" name=\"format_description\">\n          <property name=\"sizePolicy\">\n           <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Minimum\">\n            <horstretch>0</horstretch>\n            <verstretch>0</verstretch>\n           </sizepolicy>\n          </property>\n          <property name=\"font\">\n           <font>\n            <weight>50</weight>\n            <bold>false</bold>\n           </font>\n          </property>\n          <property name=\"text\">\n           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;These are the original / replacement pairs used to map one genre entry to another. Each pair must be entered on a separate line in the form:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;[genre match test string]=[replacement genre]&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Unless the &amp;quot;regular expressions&amp;quot; option is enabled, supported wildcards in the test string part of the mapping include '*' and '?' to match any number of characters and a single character respectively. An example for mapping all types of Rock genres (e.g. Country Rock, Hard Rock, Progressive Rock) to &amp;quot;Rock&amp;quot; would be done using the following line:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-family:'Courier New'; font-size:10pt; font-weight:600;&quot;&gt;*rock*=Rock&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Blank lines and lines beginning with an equals sign (=) will be ignored. Case-insensitive tests are used when matching. Replacements will be made in the order they are found in the list.&lt;/p&gt;&lt;p&gt;For more information please see the &lt;a href=&quot;https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/genre_mapper/docs/README.md&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;User Guide&lt;/span&gt;&lt;/a&gt; on GitHub.&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n          </property>\n          <property name=\"alignment\">\n           <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>\n          </property>\n          <property name=\"wordWrap\">\n           <bool>true</bool>\n          </property>\n         </widget>\n        </item>\n       </layout>\n      </widget>\n     </item>\n     <item>\n      <widget class=\"QCheckBox\" name=\"cb_enable_genre_mapping\">\n       <property name=\"text\">\n        <string>Enable genre mapping</string>\n       </property>\n      </widget>\n     </item>\n     <item>\n      <widget class=\"QGroupBox\" name=\"gm_replacement_pairs\">\n       <property name=\"enabled\">\n        <bool>true</bool>\n       </property>\n       <property name=\"sizePolicy\">\n        <sizepolicy hsizetype=\"Preferred\" vsizetype=\"MinimumExpanding\">\n         <horstretch>0</horstretch>\n         <verstretch>0</verstretch>\n        </sizepolicy>\n       </property>\n       <property name=\"minimumSize\">\n        <size>\n         <width>0</width>\n         <height>50</height>\n        </size>\n       </property>\n       <property name=\"font\">\n        <font>\n         <weight>75</weight>\n         <bold>true</bold>\n        </font>\n       </property>\n       <property name=\"title\">\n        <string>Replacement Pairs</string>\n       </property>\n       <layout class=\"QVBoxLayout\" name=\"verticalLayout_3\">\n        <item>\n         <widget class=\"QCheckBox\" name=\"cb_use_regex\">\n          <property name=\"font\">\n           <font>\n            <weight>50</weight>\n            <bold>false</bold>\n           </font>\n          </property>\n          <property name=\"text\">\n           <string>Match tests are entered as regular expressions</string>\n          </property>\n         </widget>\n        </item>\n        <item>\n         <widget class=\"QCheckBox\" name=\"genre_mapper_first_match_only\">\n          <property name=\"font\">\n           <font>\n            <weight>50</weight>\n            <bold>false</bold>\n           </font>\n          </property>\n          <property name=\"text\">\n           <string>Apply only the first matching replacement</string>\n          </property>\n          <property name=\"checked\">\n           <bool>false</bool>\n          </property>\n         </widget>\n        </item>\n        <item>\n         <widget class=\"QPlainTextEdit\" name=\"genre_mapper_replacement_pairs\">\n          <property name=\"sizePolicy\">\n           <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Minimum\">\n            <horstretch>0</horstretch>\n            <verstretch>0</verstretch>\n           </sizepolicy>\n          </property>\n          <property name=\"minimumSize\">\n           <size>\n            <width>0</width>\n            <height>50</height>\n           </size>\n          </property>\n          <property name=\"font\">\n           <font>\n            <family>Courier New</family>\n            <pointsize>10</pointsize>\n           </font>\n          </property>\n          <property name=\"tabChangesFocus\">\n           <bool>true</bool>\n          </property>\n          <property name=\"lineWrapMode\">\n           <enum>QPlainTextEdit::NoWrap</enum>\n          </property>\n          <property name=\"placeholderText\">\n           <string>Enter replacement pairs (one per line)</string>\n          </property>\n         </widget>\n        </item>\n       </layout>\n      </widget>\n     </item>\n    </layout>\n   </item>\n  </layout>\n </widget>\n <tabstops>\n  <tabstop>cb_enable_genre_mapping</tabstop>\n  <tabstop>cb_use_regex</tabstop>\n  <tabstop>genre_mapper_first_match_only</tabstop>\n  <tabstop>genre_mapper_replacement_pairs</tabstop>\n </tabstops>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/genre_mapper/ui_options_genre_mapper.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file '.\\plugins\\genre_mapper\\options_genre_mapper.ui'\n#\n# Created by: PyQt5 UI code generator 5.14.1\n#\n# WARNING! All changes made in this file will be lost!\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_GenreMapperOptionsPage(object):\n    def setupUi(self, GenreMapperOptionsPage):\n        GenreMapperOptionsPage.setObjectName(\"GenreMapperOptionsPage\")\n        GenreMapperOptionsPage.resize(398, 568)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(GenreMapperOptionsPage.sizePolicy().hasHeightForWidth())\n        GenreMapperOptionsPage.setSizePolicy(sizePolicy)\n        self.vboxlayout = QtWidgets.QVBoxLayout(GenreMapperOptionsPage)\n        self.vboxlayout.setSpacing(16)\n        self.vboxlayout.setObjectName(\"vboxlayout\")\n        self.verticalLayout_4 = QtWidgets.QVBoxLayout()\n        self.verticalLayout_4.setObjectName(\"verticalLayout_4\")\n        self.gm_description = QtWidgets.QGroupBox(GenreMapperOptionsPage)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.gm_description.sizePolicy().hasHeightForWidth())\n        self.gm_description.setSizePolicy(sizePolicy)\n        self.gm_description.setMinimumSize(QtCore.QSize(0, 50))\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.gm_description.setFont(font)\n        self.gm_description.setObjectName(\"gm_description\")\n        self.verticalLayout = QtWidgets.QVBoxLayout(self.gm_description)\n        self.verticalLayout.setContentsMargins(9, 9, 9, 1)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.format_description = QtWidgets.QLabel(self.gm_description)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.format_description.sizePolicy().hasHeightForWidth())\n        self.format_description.setSizePolicy(sizePolicy)\n        font = QtGui.QFont()\n        font.setBold(False)\n        font.setWeight(50)\n        self.format_description.setFont(font)\n        self.format_description.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)\n        self.format_description.setWordWrap(True)\n        self.format_description.setObjectName(\"format_description\")\n        self.verticalLayout.addWidget(self.format_description)\n        self.verticalLayout_4.addWidget(self.gm_description)\n        self.cb_enable_genre_mapping = QtWidgets.QCheckBox(GenreMapperOptionsPage)\n        self.cb_enable_genre_mapping.setObjectName(\"cb_enable_genre_mapping\")\n        self.verticalLayout_4.addWidget(self.cb_enable_genre_mapping)\n        self.gm_replacement_pairs = QtWidgets.QGroupBox(GenreMapperOptionsPage)\n        self.gm_replacement_pairs.setEnabled(True)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.MinimumExpanding)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.gm_replacement_pairs.sizePolicy().hasHeightForWidth())\n        self.gm_replacement_pairs.setSizePolicy(sizePolicy)\n        self.gm_replacement_pairs.setMinimumSize(QtCore.QSize(0, 50))\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.gm_replacement_pairs.setFont(font)\n        self.gm_replacement_pairs.setObjectName(\"gm_replacement_pairs\")\n        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.gm_replacement_pairs)\n        self.verticalLayout_3.setObjectName(\"verticalLayout_3\")\n        self.cb_use_regex = QtWidgets.QCheckBox(self.gm_replacement_pairs)\n        font = QtGui.QFont()\n        font.setBold(False)\n        font.setWeight(50)\n        self.cb_use_regex.setFont(font)\n        self.cb_use_regex.setObjectName(\"cb_use_regex\")\n        self.verticalLayout_3.addWidget(self.cb_use_regex)\n        self.genre_mapper_first_match_only = QtWidgets.QCheckBox(self.gm_replacement_pairs)\n        font = QtGui.QFont()\n        font.setBold(False)\n        font.setWeight(50)\n        self.genre_mapper_first_match_only.setFont(font)\n        self.genre_mapper_first_match_only.setChecked(False)\n        self.genre_mapper_first_match_only.setObjectName(\"genre_mapper_first_match_only\")\n        self.verticalLayout_3.addWidget(self.genre_mapper_first_match_only)\n        self.genre_mapper_replacement_pairs = QtWidgets.QPlainTextEdit(self.gm_replacement_pairs)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.genre_mapper_replacement_pairs.sizePolicy().hasHeightForWidth())\n        self.genre_mapper_replacement_pairs.setSizePolicy(sizePolicy)\n        self.genre_mapper_replacement_pairs.setMinimumSize(QtCore.QSize(0, 50))\n        font = QtGui.QFont()\n        font.setFamily(\"Courier New\")\n        font.setPointSize(10)\n        self.genre_mapper_replacement_pairs.setFont(font)\n        self.genre_mapper_replacement_pairs.setTabChangesFocus(True)\n        self.genre_mapper_replacement_pairs.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)\n        self.genre_mapper_replacement_pairs.setObjectName(\"genre_mapper_replacement_pairs\")\n        self.verticalLayout_3.addWidget(self.genre_mapper_replacement_pairs)\n        self.verticalLayout_4.addWidget(self.gm_replacement_pairs)\n        self.vboxlayout.addLayout(self.verticalLayout_4)\n\n        self.retranslateUi(GenreMapperOptionsPage)\n        QtCore.QMetaObject.connectSlotsByName(GenreMapperOptionsPage)\n        GenreMapperOptionsPage.setTabOrder(self.cb_enable_genre_mapping, self.cb_use_regex)\n        GenreMapperOptionsPage.setTabOrder(self.cb_use_regex, self.genre_mapper_first_match_only)\n        GenreMapperOptionsPage.setTabOrder(self.genre_mapper_first_match_only, self.genre_mapper_replacement_pairs)\n\n    def retranslateUi(self, GenreMapperOptionsPage):\n        _translate = QtCore.QCoreApplication.translate\n        self.gm_description.setTitle(_translate(\"GenreMapperOptionsPage\", \"Genre Mapper\"))\n        self.format_description.setText(_translate(\"GenreMapperOptionsPage\", \"<html><head/><body><p>These are the original / replacement pairs used to map one genre entry to another. Each pair must be entered on a separate line in the form:</p><p><span style=\\\" font-weight:600;\\\">[genre match test string]=[replacement genre]</span></p><p>Unless the &quot;regular expressions&quot; option is enabled, supported wildcards in the test string part of the mapping include \\'*\\' and \\'?\\' to match any number of characters and a single character respectively. An example for mapping all types of Rock genres (e.g. Country Rock, Hard Rock, Progressive Rock) to &quot;Rock&quot; would be done using the following line:</p><p><span style=\\\" font-family:\\'Courier New\\'; font-size:10pt; font-weight:600;\\\">*rock*=Rock</span></p><p>Blank lines and lines beginning with an equals sign (=) will be ignored. Case-insensitive tests are used when matching. Replacements will be made in the order they are found in the list.</p><p>For more information please see the <a href=\\\"https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/genre_mapper/docs/README.md\\\"><span style=\\\" text-decoration: underline; color:#0000ff;\\\">User Guide</span></a> on GitHub.<br/></p></body></html>\"))\n        self.cb_enable_genre_mapping.setText(_translate(\"GenreMapperOptionsPage\", \"Enable genre mapping\"))\n        self.gm_replacement_pairs.setTitle(_translate(\"GenreMapperOptionsPage\", \"Replacement Pairs\"))\n        self.cb_use_regex.setText(_translate(\"GenreMapperOptionsPage\", \"Match tests are entered as regular expressions\"))\n        self.genre_mapper_first_match_only.setText(_translate(\"GenreMapperOptionsPage\", \"Apply only the first matching replacement\"))\n        self.genre_mapper_replacement_pairs.setPlaceholderText(_translate(\"GenreMapperOptionsPage\", \"Enter replacement pairs (one per line)\"))\n"
  },
  {
    "path": "plugins/haikuattrs/haikuattrs.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (c) 2019, 2021 Philipp Wolfer\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = 'Haiku BFS Attributes'\nPLUGIN_AUTHOR = 'Philipp Wolfer'\nPLUGIN_DESCRIPTION = 'Save and load metadata to/from Haiku BFS attributes.'\nPLUGIN_VERSION = \"1.3\"\nPLUGIN_API_VERSIONS = [\"2.2\", \"2.3\", \"2.4\", \"2.5\", \"2.6\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\n\nimport os\nimport struct\nimport sys\nfrom collections import namedtuple\nfrom ctypes import (CDLL, POINTER, Structure, byref, c_char_p, c_int, c_long,\n                    c_longlong, c_size_t, c_ssize_t, c_uint32, c_void_p, cast)\nfrom ctypes.util import find_library\nfrom functools import partial\n\nfrom picard import log\nfrom picard.file import (\n    register_file_post_load_processor,\n    register_file_post_save_processor,\n    )\nfrom picard.util import thread\n\nif sys.platform[:5] == 'haiku':\n\n    class AttrInfo(Structure):\n        _fields_ = [\n            ('type', c_uint32),\n            ('size', c_size_t)]\n\n        def __repr__(self):\n            return 'AttrInfo(type=%r, size=%r)' % (self.type, self.size)\n\n    try:\n        libbe_path = find_library('be') or 'libbe.so'\n        be = CDLL(libbe_path, use_errno=True)\n        be.fs_stat_attr.restype = c_int\n        be.fs_stat_attr.argtypes = [c_int, c_char_p, POINTER(AttrInfo)]\n        be.fs_read_attr.restype = c_ssize_t\n        be.fs_read_attr.argtypes = [c_int, c_char_p, c_uint32, c_size_t,\n                                    c_void_p, c_size_t]\n        be.fs_remove_attr.restype = c_int\n        be.fs_remove_attr.argtypes = [c_int, c_char_p]\n        be.fs_write_attr.restype = c_ssize_t\n        be.fs_write_attr.argtypes = [c_int, c_char_p, c_uint32, c_size_t,\n                                     c_void_p, c_size_t]\n    except OSError:\n        log.error('haikuattrs: unable to load libbe', exc_info=True)\n        be = None\nelse:\n    log.warning('haikuattrs: this plugin is only for the Haiku operating system')\n    be = None\n\nif be:\n    Attr = namedtuple('Attr', ['type', 'name'])\n    attr_map = {\n        'artist'     : Attr(b'CSTR', b'Audio:Artist'),\n        'album'      : Attr(b'CSTR', b'Audio:Album'),\n        'title'      : Attr(b'CSTR', b'Media:Title'),\n        'date'       : Attr(b'LONG', b'Media:Year'),\n        'comment:'   : Attr(b'CSTR', b'Media:Comment'),\n        'tracknumber': Attr(b'LONG', b'Audio:Track'),\n        'genre'      : Attr(b'CSTR', b'Media:Genre'),\n        'composer'   : Attr(b'CSTR', b'Audio:Composer'),\n        '~rating'    : Attr(b'LONG', b'Media:Rating'),\n        '~length'    : Attr(b'LLNG', b'Media:Length'),\n        '~bitrate'   : Attr(b'CSTR', b'Audio:Bitrate'),\n    }\n\n    def get_numeric_type(attr):\n        return struct.unpack('>I', attr.type)[0]\n\n    def read_attr(fd, attr):\n        info = AttrInfo()\n        if be.fs_stat_attr(fd, attr.name, byref(info)) == -1:\n            return None\n\n        buffer = b'\\0' * info.size\n        bytes_read = be.fs_read_attr(fd, attr.name, info.type, 0,\n                                     buffer, len(buffer))\n\n        result = None\n        if bytes_read > -1:\n            buffer = buffer[:bytes_read]\n            if attr.type == b'LONG':\n                result = str(struct.unpack('=l', buffer)[0])\n            elif attr.type == b'LLNG':\n                result = str(struct.unpack('=q', buffer)[0])\n            elif attr.type == b'CSTR':\n                result = buffer.decode('utf-8', errors='replace')\n            else:\n                raise ValueError('Unsupported attribute type %s' % attr.type)\n\n        log.debug(\"haikuattrs: fs_read_attr(%r, %r, %r, %r, %r, %r) -> %r: %r\" % (\n            fd, attr.name, info.type, 0, buffer, len(buffer), bytes_read, result))\n        return result\n\n    def remove_attr(fd, attr):\n        return be.fs_remove_attr(fd, attr.name) == 0\n\n    def write_attr(fd, attr, attr_value):\n        if attr.type in (b'LONG', b'LLNG'):\n            try:\n                attr_value = int(attr_value)\n            except ValueError:\n                return False\n            length = 4\n            c_type = c_long if attr.type == b'LONG' else c_longlong\n            buffer = byref(c_type(attr_value))\n        elif attr.type == b'CSTR':\n            attr_value = attr_value.encode('utf-8')\n            length = len(attr_value)\n            buffer = cast(attr_value, c_char_p)\n        else:\n            raise ValueError('Unsupported attribute type %s' % attr.type)\n\n        int_type = get_numeric_type(attr)\n        ret_val = be.fs_write_attr(fd, attr.name, int_type, 0, buffer, length)\n        log.debug(\"haikuattrs: fs_write_attr(%r, %r, %r, %r, %r, %r) -> %r\" % (\n            fd, attr.name, int_type, 0, buffer, length, ret_val))\n        return ret_val >= 0\n\n    def get_attribute_value(tag, file):\n        metadata = file.orig_metadata\n        value = metadata[tag]\n        if value:\n            if tag == 'date':\n                value = value[:4]\n            elif tag == '~length':\n                # Haiku expects the Media:Length attribute in nanoseconds\n                value = metadata.length * 1000\n            elif tag == '~bitrate':\n                value = '%s kbps' % value\n            elif tag == '~rating':\n                # Haiku's ratings are internally repesented by integers between 1 and 10\n                value = int(value * 2)\n        return value\n\n    def set_attrs_from_metadata(file):\n        log.debug('haikuattrs: setting attributes for %s' % file.filename)\n        fd = os.open(file.filename, os.O_RDWR)\n        try:\n            for tag, attr in attr_map.items():\n                try:\n                    value = get_attribute_value(tag, file)\n                except (TypeError, ValueError) as err:\n                    log.warning(\n                        'haikuattrs: failed converting tag %s with value %r for %s: %r',\n                        tag, value, file.filename, err)\n                    continue\n                if value:\n                    if not write_attr(fd, attr, value):\n                        log.error('haikuattrs: setting %s=%s for %s failed' % (\n                            attr.name, value, file.filename))\n                else:\n                    remove_attr(fd, attr)\n        finally:\n            os.close(fd)\n\n    def set_attrs_from_metadata_finished(file, result=None, error=None):\n        if error:\n            log.error('haikuattrs: setting attributes for %s failed: %r' % (\n                file.filename, error))\n        else:\n            log.debug('haikuattrs: attributes set for %s' % file.filename)\n\n    def load_attrs_to_metadata(file):\n        fd = os.open(file.filename, os.O_RDONLY)\n        filename, _ = os.path.splitext(file.base_filename)\n        try:\n            for tag, attr in attr_map.items():\n                # Technical variables that are loaded directly from the file\n                if tag in ('~bitrate', '~length'):\n                    continue\n                # Ignore tags for which metadata is included in the file.\n                # But ignore the special case where the title has been set to\n                # the filename.\n                value = file.metadata[tag]\n                if value and not (tag == 'title' and value == filename):\n                    continue\n                value = read_attr(fd, attr)\n                if value:\n                    file.metadata[tag] = value\n                    file.orig_metadata[tag] = value\n        finally:\n            os.close(fd)\n\n    def load_attrs_to_metadata_finished(file, result=None, error=None):\n        if error:\n            log.error('haikuattrs: loading attributes for %s failed: %r' % (\n                file.filename, error))\n        else:\n            log.debug('haikuattrs: attributes loaded for %s' % file.filename)\n            file.update()\n\n    def on_file_load_processor(file):\n        thread.run_task(\n            partial(load_attrs_to_metadata, file),\n            partial(load_attrs_to_metadata_finished, file))\n\n    def on_file_save_processor(file):\n        thread.run_task(\n            partial(set_attrs_from_metadata, file),\n            partial(set_attrs_from_metadata_finished, file))\n\n    register_file_post_load_processor(on_file_load_processor)\n    register_file_post_save_processor(on_file_save_processor)\n"
  },
  {
    "path": "plugins/happidev_lyrics/happidev_lyrics.py",
    "content": "from functools import partial\nfrom urllib.parse import (\n    quote,\n    urlencode,\n    urlparse,\n)\n\nfrom PyQt5 import QtWidgets\nfrom PyQt5.QtNetwork import QNetworkRequest\n\nfrom picard import (\n    config,\n    log,\n)\nfrom picard.config import TextOption\nfrom picard.metadata import register_track_metadata_processor\nfrom picard.ui.options import (\n    OptionsPage,\n    register_options_page,\n)\nfrom picard.webservice import ratecontrol\n\nPLUGIN_NAME = 'Happi.dev Lyrics'\nPLUGIN_AUTHOR = 'Andrea Avallone, Philipp Wolfer'\nPLUGIN_DESCRIPTION = 'Fetch lyrics from Happi.dev Lyrics, which provides millions of lyrics from artist all around the world. ' \\\n                     'Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed.<br /><br />' \\\n                     'In order to use Happi.dev you need to get a free API key at <a href=\"https://happi.dev\">happi.dev</a>'\nPLUGIN_VERSION = '2.1.2'\nPLUGIN_API_VERSIONS = ['2.0', '2.1', '2.2', '2.3', '2.4', '2.5', '2.6']\nPLUGIN_LICENSE = 'MIT'\nPLUGIN_LICENSE_URL = 'https://opensource.org/licenses/MIT'\n\n\nclass HappidevLyricsMetadataProcessor:\n\n    happidev_host = 'api.happi.dev'\n    happidev_port = 443\n    happidev_delay = int(60 * 1000 / 60)  # 60 requests per minute\n\n    def __init__(self):\n        super().__init__()\n        ratecontrol.set_minimum_delay(\n            (self.happidev_host, self.happidev_port), self.happidev_delay)\n\n    def process_metadata(self, album, metadata, track, release):\n        if not config.setting['happidev_apikey']:\n            error = 'API key is missing, please provide a valid value'\n            log.warning('{}: {}'.format(PLUGIN_NAME, error))\n            return\n\n        artist = metadata['artist']\n        title = metadata['title']\n        if not (artist and title):\n            log.debug(\n                '{}: both artist and title are required to obtain lyrics'.format(PLUGIN_NAME))\n            return\n\n        path = '/v1/music'\n        queryargs = {\n            'q': '\"{}\" \"{}\"'.format(artist, title),\n            'lyrics': 'true',\n            'type': 'track',\n        }\n        album._requests += 1\n        log.debug('{}: GET {}?{}'.format(PLUGIN_NAME, quote(path), urlencode(queryargs)))\n        self._request(album.tagger.webservice, path,\n            partial(self.process_search_response, album, metadata), queryargs)\n\n    def _request(self, ws, path, callback, queryargs=None, important=False):\n        if not queryargs:\n            queryargs = {}\n\n        queryargs['apikey'] = config.setting['happidev_apikey']\n        ws.get(self.happidev_host, self.happidev_port, path, callback,\n            parse_response_type='json', priority=True, important=important,\n            queryargs=queryargs, cacheloadcontrol=QNetworkRequest.PreferCache)\n\n    def process_search_response(self, album, metadata, response, reply, error):\n        if self._handle_error(album, error, response):\n            log.warning('{}: lyrics NOT found for track \"{}\" by {}'.format(\n                PLUGIN_NAME, metadata['title'], metadata['artist']))\n            return\n\n        try:\n            lyrics_url = response['result'][0]['api_lyrics']\n            log.debug('{}: lyrics found for track \"{}\" by {} at {}'.format(\n                PLUGIN_NAME, metadata['title'], metadata['artist'], lyrics_url))\n            path = urlparse(lyrics_url).path\n\n        except (TypeError, KeyError, ValueError):\n            log.warn('{}: failed parsing search response for \"{}\" by {}'.format(\n                PLUGIN_NAME, metadata['title'], metadata['artist']), exc_info=True)\n            album._requests -= 1\n            album._finalize_loading(None)\n            return\n\n        self._request(album.tagger.webservice, path,\n            partial(self.process_lyrics_response, album, metadata),\n            important=True)\n\n    def process_lyrics_response(self, album, metadata, response, reply, error):\n        if self._handle_error(album, error, response):\n            log.warning('{}: lyrics NOT loaded for track \"{}\" by {}'.format(\n                PLUGIN_NAME, metadata['title'], metadata['artist']))\n            return\n\n        try:\n            lyrics = response['result']['lyrics']\n            metadata['lyrics'] = lyrics\n            log.debug('{}: lyrics loaded for track \"{}\" by {}'.format(\n                PLUGIN_NAME, metadata['title'], metadata['artist']))\n\n        except (TypeError, KeyError):\n            log.warn('{}: failed parsing search response for \"{}\" by {}'.format(\n                PLUGIN_NAME, metadata['title'], metadata['artist']), exc_info=True)\n\n        finally:\n            album._requests -= 1\n            album._finalize_loading(None)\n\n    @staticmethod\n    def _handle_error(album, error, response):\n        if error or (response and (not response.get('success', False) or not response.get('length', 0))):\n            album._requests -= 1\n            album._finalize_loading(None)\n            return True\n\n        return False\n\n\nclass HappidevLyricsOptionsPage(OptionsPage):\n\n    NAME = 'apiseeds_lyrics'\n    TITLE = 'Happi.dev Lyrics'\n    PARENT = 'plugins'\n\n    options = [TextOption('setting', 'happidev_apikey', '')]\n\n    def __init__(self, parent=None):\n\n        super().__init__(parent)\n        self.box = QtWidgets.QVBoxLayout(self)\n\n        self.label = QtWidgets.QLabel(self)\n        self.label.setText('Apiseeds API key')\n        self.box.addWidget(self.label)\n\n        self.description = QtWidgets.QLabel(self)\n        self.description.setText('Happi.dev Music provides millions of lyrics from artist all around the world. '\n                                 'Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed. '\n                                 'In order to use Happi.dev Music you need to get a free API key <a href=\"https://happi.dev\">here</a>.')\n        self.description.setOpenExternalLinks(True)\n        self.box.addWidget(self.description)\n\n        self.input = QtWidgets.QLineEdit(self)\n        self.box.addWidget(self.input)\n\n        self.spacer = QtWidgets.QSpacerItem(\n            0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.box.addItem(self.spacer)\n\n    def load(self):\n        self.input.setText(config.setting['happidev_apikey'])\n\n    def save(self):\n        config.setting['happidev_apikey'] = self.input.text()\n\n\nregister_track_metadata_processor(HappidevLyricsMetadataProcessor().process_metadata)\nregister_options_page(HappidevLyricsOptionsPage)"
  },
  {
    "path": "plugins/hyphen_unicode/hyphen_unicode.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Copyright (C) 2016 Anderson Mesquita <andersonvom@gmail.com>\n# Copyright (C) 2019 Alan Swanson <reiver@improbability.net>\n#\n# This program is free software: you can redistribute it and/or modify it under\n# the terms of the GNU General Public License as published by the Free Software\n# Foundation, either version 3 of the License, or (at your option) any later\n# version.\n#\n# This program is distributed in the hope that it will be useful, but WITHOUT\n# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS\n# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more\n# details.\n#\n# You should have received a copy of the GNU General Public License along with\n# this program. If not, see <http://www.gnu.org/licenses/>.\n\nfrom picard import metadata\n\nPLUGIN_NAME = \"Hyphen unicode\"\nPLUGIN_AUTHOR = \"Alan Swanson <revier@improbability.net>\"\nPLUGIN_VERSION = \"1.0.1\"\nPLUGIN_API_VERSIONS = [\"0.9\", \"0.10\", \"0.11\", \"0.15\", \"2.0\"]\nPLUGIN_LICENSE = \"GPL-3.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://gnu.org/licenses/gpl.html\"\nPLUGIN_DESCRIPTION = '''Replaces unicode character HYPHEN (U+2010) [0xE2 0x80\n0x90] with typographically identical HYPHEN-MINUS (U+002D) [0x2D] for fonts\nthat do not support HYPHEN and to prevent visually duplicate filenames\ndifferentiated only by their hyphens.\n\nUnicode duplicated hyphen from ASCII as an unambiguous way to designate a\nhyphen from a minus whilst still being typographically indentical. Since\ntext processing on music tags is rare so choice is purely pedantic esepcially\nas keyboards only have HYPHEN-MINUS.\n\nReplaces character on \"album\", \"title\", \"artist\", \"artists\", \"artistsort\",\n\"albumartist\", \"albumartists\" and \"albumartistsort\" tags.'''\n\n# Based on Non-ASCII Equivalents plugin.\n# Musicbrainz form discussion on HYPHEN versus HYPHEN-MINUS at;\n# https://community.metabrainz.org/t/correct-hyphen-unicode-hyphen-or-hyphen-minus/19610\n\nCHAR_TABLE = {\n    # HYPHEN to HYPHEN-MINUS\n    \"‐\": \"-\",\n}\n\nFILTER_TAGS = [\n    \"title\",\n    \"artist\",\n    \"artists\",\n    \"artistsort\",\n    \"album\",\n    \"albumsort\",\n    \"albumartist\",\n    \"albumartists\",\n    \"albumartistsort\",\n]\n\n\ndef sanitize(char):\n    if char in CHAR_TABLE:\n        return CHAR_TABLE[char]\n    return char\n\n\ndef ascii(word):\n    return \"\".join(sanitize(char) for char in word)\n\n\ndef main(tagger, metadata, *args):\n    for name, value in metadata.rawitems():\n        if name in FILTER_TAGS:\n            metadata[name] = [ascii(x) for x in value]\n\n\nmetadata.register_track_metadata_processor(main)\nmetadata.register_album_metadata_processor(main)\n"
  },
  {
    "path": "plugins/instruments/instruments.py",
    "content": "# MusicBrainz Picard plugin to add an ~instruments tag.\n# Copyright (C) 2019  David Mandelberg\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nPLUGIN_NAME = 'Instruments'\nPLUGIN_AUTHOR = 'David Mandelberg'\nPLUGIN_DESCRIPTION = \"\"\"\\\n  Adds a multi-valued tag (~instruments) containing all the instruments (including vocals), \n  for use in scripts.\n  \"\"\"\nPLUGIN_VERSION = '1.0.1'\nPLUGIN_API_VERSIONS = ['2.0']\nPLUGIN_LICENSE = 'GPL-3.0-or-later'\nPLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl-3.0.html'\n\nfrom typing import Generator, Optional\n\nfrom picard import metadata\nfrom picard import plugin\n\n\ndef _iterate_instruments(instrument_list: str) -> Generator[str, None, None]:\n  \"\"\"Yields individual instruments from a string listing them.\n\n  Args:\n    instrument_list: List of instruments in the form 'A, B and C'.\n  \"\"\"\n  remaining = instrument_list\n  while remaining:\n    instrument, _, remaining = remaining.partition(', ')\n    if not remaining:\n      instrument, _, remaining = instrument.partition(' and ')\n      if ' and ' in remaining:\n        raise ValueError('Instrument list contains multiple \\'and\\'s: {!r}'\n                         .format(instrument_list))\n    yield instrument\n\n\ndef _strip_instrument_prefixes(instrument: str) -> Optional[str]:\n  \"\"\"Returns the instrument name without qualifying prefixes, or None.\n\n  Args:\n    instrument: Potentially prefixed instrument name, e.g., 'solo bassoon'.\n\n  Returns:\n    The instrument name with all prefixes stripped, or None if there's nothing\n    other than prefixes. The all-prefixes case can happen with relationships\n    like 'guest performer'.\n  \"\"\"\n  instrument_prefixes = {\n      'additional',\n      'guest',\n      'solo',\n  }\n  remaining = instrument\n  while remaining:\n    prefix, sep, remaining = remaining.partition(' ')\n    if prefix not in instrument_prefixes:\n      return ''.join((prefix, sep, remaining))\n  return None\n\n\ndef add_instruments(tagger, metadata_, *args):\n  key_prefix = 'performer:'\n  instruments = set()\n  for key in metadata_.keys():\n    if not key.startswith(key_prefix):\n      continue\n    for instrument in _iterate_instruments(key[len(key_prefix):]):\n      instrument = _strip_instrument_prefixes(instrument)\n      if instrument:\n        instruments.add(instrument)\n  metadata_['~instruments'] = list(instruments)\n\n\nmetadata.register_track_metadata_processor(\n    add_instruments, priority=plugin.PluginPriority.HIGH)\n"
  },
  {
    "path": "plugins/keep/keep.py",
    "content": "PLUGIN_NAME = \"Keep tags\"\nPLUGIN_AUTHOR = \"Wieland Hoffmann\"\nPLUGIN_DESCRIPTION = \"\"\"\nAdds a $keep() function to delete all tags except the ones that you want.\nTags beginning with `musicbrainz_` are kept automatically, as are tags\nbeginning with `_`.\n\nTo keep all tags that can have a description (like `comment`, lyrics` and\n`performer`), add `&lt;tagname without description&gt;` (not including `:`) to the\nlist of tags to keep.\"\"\"\n\nPLUGIN_VERSION = \"1.2.1\"\nPLUGIN_API_VERSIONS = [\"0.15.0\", \"0.15.1\", \"0.16.0\", \"1.0.0\", \"1.1.0\", \"1.2.0\",\n                       \"1.3.0\", \"2.0\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom picard.script import register_script_function\n\n\n@register_script_function\ndef keep(parser, *keeptags):\n    tags = list(parser.context.keys())\n    for tag in tags:\n        if (tag in keeptags or\n            tag.startswith(\"musicbrainz_\") or\n            tag.startswith(\"~\")):\n            continue\n        if \":\" in tag:\n            tag_without_description = tag.split(\":\")[0]\n            if tag_without_description in keeptags:\n                continue\n        parser.context.pop(tag, None)\n    return \"\"\n"
  },
  {
    "path": "plugins/key_wheel_converter/key_wheel_converter.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Key Wheel Converter Plugin\n\"\"\"\n#\n# Copyright (C) 2022-2025 Bob Swift (rdswift)\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\n# pylint: disable=C0413     (wrong-import-position)\n# pylint: disable=W0613     (unused-argument)\n\nPLUGIN_NAME = 'Key Wheel Converter'\nPLUGIN_AUTHOR = 'Bob Swift'\nPLUGIN_DESCRIPTION = '''\nAdds functions to convert between 'standard', 'camelot', 'open key' and 'traktor' key formats.\n'''\nPLUGIN_VERSION = '1.2'\nPLUGIN_API_VERSIONS = ['2.3', '2.4', '2.6', '2.7', '2.13']\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.txt\"\n\nimport re\n\n\ntry:\n    from picard import log\nexcept ModuleNotFoundError:\n    class log():\n        \"\"\"Mocked logger for testing\n        \"\"\"\n        # pylint: disable=invalid-name\n        # pylint: disable=too-few-public-methods\n        @staticmethod\n        def debug(*args, **kwargs):\n            \"\"\"Mocked degug logger\n            \"\"\"\n            return\ntry:\n    from picard.script import register_script_function\nexcept ModuleNotFoundError:\n    def register_script_function(*args, **kwargs):\n        \"\"\"Mocked function for testing\n        \"\"\"\n        return\n\n\nclass KeyMap():\n    \"\"\"\n        Class to hold the mapping dictionary.  The dictionary is\n        stored as a class variable so that it is only generated once.\n    \"\"\"\n    # pylint: disable=too-few-public-methods\n\n    # Circle of Fifths reference:\n    # https://www.circleoffifths.com\n\n    # Key Wheel references:\n    # https://i.imgur.com/p9Kdevi.jpg\n    # http://www.quanta.com.br/wp-content/uploads/2013/07/traktor-key-wheel_alta.jpg\n\n    # List of tuples of:\n    #   'camelot key',\n    #   'open key',\n    #   'standard key /w symbols',\n    #   'standard key /w text'\n    #   'traktor key'\n    _keys = [\n        ('1A', '6m', 'A♭ Minor', 'A-Flat Minor', 'Abm'),\n        ('1B', '6d', 'B Major', 'B Major', 'B'),\n        ('2A', '7m', 'E♭ Minor', 'E-Flat Minor', 'Ebm'),\n        ('2B', '7d', 'F# Major', 'F-Sharp Major', 'F#'),\n        ('3A', '8m', 'B♭ Minor', 'B-Flat Minor', 'Bbm'),\n        ('3B', '8d', 'D♭ Major', 'D-Flat Major', 'Db'),\n        ('4A', '9m', 'F Minor', 'F Minor', 'Fm'),\n        ('4B', '9d', 'A♭ Major', 'A-Flat Major', 'Ab'),\n        ('5A', '10m', 'C Minor', 'C Minor', 'Cm'),\n        ('5B', '10d', 'E♭ Major', 'E-Flat Major', 'Eb'),\n        ('6A', '11m', 'G Minor', 'G Minor', 'Gm'),\n        ('6B', '11d', 'B♭ Major', 'B-Flat Major', 'Bb'),\n        ('7A', '12m', 'D Minor', 'D Minor', 'Dm'),\n        ('7B', '12d', 'F Major', 'F Major', 'F'),\n        ('8A', '1m', 'A Minor', 'A Minor', 'Am'),\n        ('8B', '1d', 'C Major', 'C Major', 'C'),\n        ('9A', '2m', 'E Minor', 'E Minor', 'Em'),\n        ('9B', '2d', 'G Major', 'G Major', 'G'),\n        ('10A', '3m', 'B Minor', 'B Minor', 'Bm'),\n        ('10B', '3d', 'D Major', 'D Major', 'D'),\n        ('11A', '4m', 'G♭ Minor', 'G-Flat Minor', 'Gbm'),\n        ('11B', '4d', 'A Major', 'A Major', 'A'),\n        ('12A', '5m', 'D♭ Minor', 'D-Flat Minor', 'Dbm'),\n        ('12B', '5d', 'E Major', 'E Major', 'E'),\n    ]\n\n    # Build mapping dictionary with 'camelot', 'standard with text'\n    # and 'traktor' keys.\n    keys = {}\n    for item in _keys:\n        for i in [0, 3, 4]:\n            keys[item[i]] = {\n                'camelot': item[0],\n                'open': item[1],\n                'standard_s': item[2],\n                'standard_t': item[3],\n                'traktor': item[4],\n            }\n\n    # Alternate mapping for standard keys\n    s_alt = {\n        'G-Flat Major': 'F-Sharp Major',\n        'D-Sharp Minor': 'E-Flat Minor',\n    }\n\n    # Alternate mapping for traktor keys\n    t_alt = {\n        'G#': 'Ab',\n        'A#': 'Bb',\n        'C#': 'Db',\n        'D#': 'Eb',\n        'G#m': 'Abm',\n        'A#m': 'Bbm',\n        'C#m': 'Dbm',\n        'D#m': 'Ebm',\n        'F#m': 'Gbm',\n    }\n\n\ndef _matcher(text, out_type):\n    \"\"\"Helper function that performs the actual key lookup.\n\n    Args:\n        text (str): Key provided by the user.\n        out_type (str): Output format to use for the return value\n\n    Returns:\n        str: Value mapped to the key for the specified output type\n    \"\"\"\n    # pylint: disable=consider-using-f-string\n    match_text = _parse_input(text)\n    if match_text not in KeyMap.keys:\n        log.debug(\"{0}: Unable to match key: '{1}'\".format(PLUGIN_NAME, text,))\n        return ''\n    return KeyMap.keys[match_text][out_type]\n\n\ndef _parse_input(text):\n    \"\"\"Helper function to parse the input argument to try to match\n    one of the supported formats used for the mapping keys.\n\n    Args:\n        text (str): Input argument provided by the user\n\n    Returns:\n        str: Argument converted to supported key format (if possible)\n    \"\"\"\n    # pylint: disable=too-many-return-statements\n\n    text = text.strip()\n    if not text:\n        return ''\n\n    if re.match(\"[0-9]{1,2}[ABab]$\", text):\n        # Matches camelot key.  Fix capitalization for lookup.\n        return text.upper()\n\n    if re.match(\"[0-9]{1,2}[dmDM]$\", text):\n        # Matches open key format.  Convert to camelot key for lookup.\n        temp = int(text[0:-1])\n        if 0 < temp < 13:\n            _num = ((temp + 6) % 12) + 1\n            _char = text[-1:].lower().replace('m', 'A').replace('d', 'B')\n            return \"{0}{1}\".format(_num, _char,)\n\n    # if re.match(\"[a-gA-G][#bB♭]?[mM]?$\", text):\n    if re.match(\"[a-gA-G][#Bb]?[mM]?$\", text):\n        # Matches Traktor key format.  Fix capitalization for lookup.\n        temp = text[0:1].upper() + text[1:].replace('♭', 'b').lower()\n        # Handle cases where there are multiple entries for the item\n        if temp in KeyMap.t_alt:\n            return KeyMap.t_alt[temp]\n        return temp\n\n    # Parse as standard key\n    # Add missing hyphens before 'Flat' and 'Sharp'\n    text = text.lower().replace(' s', '-s').replace(' f', '-f')\n    # Convert symbols to text for lookup.\n    parts = text.replace('♭', '-Flat').replace('#', '-Sharp').split()\n    for (i, part) in enumerate(parts):\n        parts[i] = part[0:1].upper() + part[1:]\n    temp = ' '.join(parts).replace('-s', '-S').replace('-f', '-F')\n    # Handle cases where there are multiple entries for the item\n    if temp in KeyMap.s_alt:\n        return KeyMap.s_alt[temp]\n    return temp\n\n\ndef key2camelot(parser, text):\n    \"\"\"Any key to camelot format converter.\n\n    Args:\n        parser (object): Picard parser object\n        text (str): Key to convert\n\n    Returns:\n        str: Converted key value\n\n    Tests:\n\n    >>> key2camelot(None, '1A')\n    '1A'\n    >>> key2camelot(None, '6m')\n    '1A'\n    >>> key2camelot(None, 'A♭ Minor')\n    '1A'\n    >>> key2camelot(None, 'A-Flat Minor')\n    '1A'\n    >>> key2camelot(None, '1a')\n    '1A'\n    >>> key2camelot(None, '6M')\n    '1A'\n    >>> key2camelot(None, 'A-FLAT MINOR')\n    '1A'\n    >>> key2camelot(None, 'a-flat minor')\n    '1A'\n    >>> key2camelot(None, ' 1A')\n    '1A'\n    >>> key2camelot(None, '1A ')\n    '1A'\n    >>> key2camelot(None, 'ABm')\n    '1A'\n    >>> key2camelot(None, '')\n    ''\n    >>> key2camelot(None, 'A-Flat Minor x')\n    ''\n    >>> key2camelot(None, 'H-Flat Minor')\n    ''\n    >>> key2camelot(None, '1-Flat Minor')\n    ''\n    >>> key2camelot(None, 'A-Flat')\n    ''\n    >>> key2camelot(None, 'A♭')\n    ''\n    >>> key2camelot(None, '0a')\n    ''\n    >>> key2camelot(None, '13a')\n    ''\n    >>> key2camelot(None, '0m')\n    ''\n    >>> key2camelot(None, '13m')\n    ''\n    >>> key2camelot(None, '1x')\n    ''\n\n    >>> key2camelot(None, 'A#')\n    '6B'\n    >>> key2camelot(None, 'A#M')\n    '3A'\n    >>> key2camelot(None, 'C#')\n    '3B'\n    >>> key2camelot(None, 'C#M')\n    '12A'\n    >>> key2camelot(None, 'D#')\n    '5B'\n    >>> key2camelot(None, 'D#M')\n    '2A'\n    >>> key2camelot(None, 'F#M')\n    '11A'\n    >>> key2camelot(None, 'G#')\n    '4B'\n    >>> key2camelot(None, 'G#M')\n    '1A'\n\n    >>> key2camelot(None, 'c')\n    '8B'\n    >>> key2camelot(None, 'dB')\n    '3B'\n    >>> key2camelot(None, 'd#M')\n    '2A'\n    >>> key2camelot(None, 'D#M')\n    '2A'\n    \"\"\"\n    return _matcher(text, 'camelot')\n\n\ndef key2openkey(parser, text):\n    \"\"\"Any key to open key format converter.\n\n    Args:\n        parser (object): Picard parser object\n        text (str): Key to convert\n\n    Returns:\n        str: Converted key value\n\n    Tests:\n\n    >>> key2openkey(None, '1A')\n    '6m'\n    >>> key2openkey(None, '6m')\n    '6m'\n    >>> key2openkey(None, 'A♭ Minor')\n    '6m'\n    >>> key2openkey(None, 'A-Flat Minor')\n    '6m'\n    >>> key2openkey(None, '1a')\n    '6m'\n    >>> key2openkey(None, '6M')\n    '6m'\n    >>> key2openkey(None, 'A-FLAT MINOR')\n    '6m'\n    >>> key2openkey(None, 'a-flat minor')\n    '6m'\n    >>> key2openkey(None, ' 1A')\n    '6m'\n    >>> key2openkey(None, '1A ')\n    '6m'\n    >>> key2openkey(None, '')\n    ''\n    >>> key2openkey(None, 'A-Flat Minor x')\n    ''\n    >>> key2openkey(None, 'H-Flat Minor')\n    ''\n    >>> key2openkey(None, '1-Flat Minor')\n    ''\n    >>> key2openkey(None, 'A-Flat')\n    ''\n    >>> key2openkey(None, 'A♭')\n    ''\n    >>> key2openkey(None, '0a')\n    ''\n    >>> key2openkey(None, '13a')\n    ''\n    >>> key2openkey(None, '0m')\n    ''\n    >>> key2openkey(None, '13m')\n    ''\n    >>> key2openkey(None, '1x')\n    ''\n    \"\"\"\n    return _matcher(text, 'open')\n\n\ndef key2standard(parser, text, use_symbol=''):\n    \"\"\"Any key to standard key format converter.\n\n    Args:\n        parser (object): Picard parser object\n        text (str): Key to convert\n        use_symbol (str, optional): Use '♭' and '#' symbols. Defaults to False.\n\n    Returns:\n        str: Converted key value\n\n    Tests:\n\n    >>> key2standard(None, '1A')\n    'A-Flat Minor'\n    >>> key2standard(None, '6m')\n    'A-Flat Minor'\n    >>> key2standard(None, 'A♭ Minor')\n    'A-Flat Minor'\n    >>> key2standard(None, 'A-Flat Minor')\n    'A-Flat Minor'\n    >>> key2standard(None, '1a')\n    'A-Flat Minor'\n    >>> key2standard(None, '6M')\n    'A-Flat Minor'\n    >>> key2standard(None, 'A-FLAT MINOR')\n    'A-Flat Minor'\n    >>> key2standard(None, 'a-flat minor')\n    'A-Flat Minor'\n    >>> key2standard(None, ' 1A')\n    'A-Flat Minor'\n    >>> key2standard(None, '1A ')\n    'A-Flat Minor'\n    >>> key2standard(None, '')\n    ''\n    >>> key2standard(None, 'A-Flat Minor x')\n    ''\n    >>> key2standard(None, 'H-Flat Minor')\n    ''\n    >>> key2standard(None, '1-Flat Minor')\n    ''\n    >>> key2standard(None, 'A-Flat')\n    ''\n    >>> key2standard(None, 'A♭')\n    ''\n    >>> key2standard(None, '0a')\n    ''\n    >>> key2standard(None, '13a')\n    ''\n    >>> key2standard(None, '0m')\n    ''\n    >>> key2standard(None, '13m')\n    ''\n    >>> key2standard(None, '1x')\n    ''\n\n    >>> key2standard(None, '1A', 'x')\n    'A♭ Minor'\n    >>> key2standard(None, '6m', 'x')\n    'A♭ Minor'\n    >>> key2standard(None, 'A♭ Minor', 'x')\n    'A♭ Minor'\n    >>> key2standard(None, 'A-Flat Minor', 'x')\n    'A♭ Minor'\n    >>> key2standard(None, '1a', 'x')\n    'A♭ Minor'\n    >>> key2standard(None, '6M', 'x')\n    'A♭ Minor'\n    >>> key2standard(None, 'A-FLAT MINOR', 'x')\n    'A♭ Minor'\n    >>> key2standard(None, 'a-flat minor', 'x')\n    'A♭ Minor'\n    >>> key2standard(None, ' 1A', 'x')\n    'A♭ Minor'\n    >>> key2standard(None, '1A ', 'x')\n    'A♭ Minor'\n    >>> key2standard(None, '1A ', ' ')\n    'A♭ Minor'\n    >>> key2standard(None, '', 'x')\n    ''\n    >>> key2standard(None, 'A-Flat Minor x', 'x')\n    ''\n    >>> key2standard(None, 'H-Flat Minor', 'x')\n    ''\n    >>> key2standard(None, '1-Flat Minor', 'x')\n    ''\n    >>> key2standard(None, 'A-Flat', 'x')\n    ''\n    >>> key2standard(None, 'A♭', 'x')\n    ''\n    >>> key2standard(None, '0a', 'x')\n    ''\n    >>> key2standard(None, '13a', 'x')\n    ''\n    >>> key2standard(None, '0m', 'x')\n    ''\n    >>> key2standard(None, '13m', 'x')\n    ''\n    >>> key2standard(None, '1x', 'x')\n    ''\n\n    >>> key2standard(None, 'A Flat Minor')\n    'A-Flat Minor'\n    >>> key2standard(None, 'F Sharp Major')\n    'F-Sharp Major'\n    >>> key2standard(None, 'G♭ Major')\n    'F-Sharp Major'\n    >>> key2standard(None, 'D# Minor')\n    'E-Flat Minor'\n    \"\"\"\n    if use_symbol:\n        return _matcher(text, 'standard_s')\n    return _matcher(text, 'standard_t')\n\n\ndef key2traktor(parser, text):\n    \"\"\"Any key to traktor key format converter.\n\n    Args:\n        parser (object): Picard parser object\n        text (str): Key to convert\n\n    Returns:\n        str: Converted key value\n\n    Tests:\n\n    >>> key2traktor(None, '1A')\n    'Abm'\n    >>> key2traktor(None, '6m')\n    'Abm'\n    >>> key2traktor(None, 'A♭ Minor')\n    'Abm'\n    >>> key2traktor(None, 'A-Flat Minor')\n    'Abm'\n    >>> key2traktor(None, 'c#')\n    'Db'\n    >>> key2traktor(None, 'gBM')\n    'Gbm'\n    >>> key2traktor(None, 'gbM')\n    'Gbm'\n    >>> key2traktor(None, 'g♭M')\n    ''\n    >>> key2traktor(None, '')\n    ''\n    \"\"\"\n    return _matcher(text, 'traktor')\n\n\nregister_script_function(\n    key2camelot,\n    name='key2camelot',\n    documentation=\"\"\"`$key2camelot(key)`\n\nReturns the key string `key` in camelot key format.\n\nThe `key` argument can be entered in any of the supported formats, such as\n'2B' (camelot), '6d' (open key), 'A♭ Minor' (standard with symbols),\n'A-Flat Minor' (standard with text) or 'C#' (traktor).  If the `key` argument\nis not recognized as one of the standard keys in the supported formats, then\nan empty string will be returned.\"\"\")\n\nregister_script_function(\n    key2openkey,\n    name='key2openkey',\n    documentation=\"\"\"`$key2openkey(key)`\n\nReturns the key string `key` in open key format.\n\nThe `key` argument can be entered in any of the supported formats, such as\n'2B' (camelot), '6d' (open key), 'A♭ Minor' (standard with symbols),\n'A-Flat Minor' (standard with text) or 'C#' (traktor).  If the `key` argument\nis not recognized as one of the standard keys in the supported formats, then\nan empty string will be returned.\"\"\")\n\nregister_script_function(\n    key2standard,\n    name='key2standard',\n    documentation=\"\"\"`$key2standard(key[,symbols])`\n\nReturns the key string `key` in standard key format.  If the optional argument\n`symbols` is set, then the '♭' and '#' symbols will be used, rather than\nspelling out '-Flat' and '-Sharp'.\n\nThe `key` argument can be entered in any of the supported formats, such as\n'2B' (camelot), '6d' (open key), 'A♭ Minor' (standard with symbols),\n'A-Flat Minor' (standard with text) or 'C#' (traktor).  If the `key` argument\nis not recognized as one of the standard keys in the supported formats, then\nan empty string will be returned.\"\"\")\n\nregister_script_function(\n    key2traktor,\n    name='key2traktor',\n    documentation=\"\"\"`$key2traktor(key)`\n\nReturns the key string `key` in traktor key format.\n\nThe `key` argument can be entered in any of the supported formats, such as\n'2B' (camelot), '6d' (open key), 'A♭ Minor' (standard with symbols),\n'A-Flat Minor' (standard with text) or 'C#' (traktor).  If the `key` argument\nis not recognized as one of the standard keys in the supported formats, then\nan empty string will be returned.\"\"\")\n\nif __name__ == \"__main__\":\n    import doctest\n    doctest.testmod()\n"
  },
  {
    "path": "plugins/lastfm/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = 'Last.fm'\nPLUGIN_AUTHOR = 'Lukáš Lalinský, Philipp Wolfer'\nPLUGIN_DESCRIPTION = 'Use tags from Last.fm as genre.'\nPLUGIN_VERSION = \"0.10.1\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\n\nimport re\nfrom functools import partial\nfrom PyQt5 import QtCore\nfrom picard import config, log\nfrom picard.config import BoolOption, IntOption, TextOption\nfrom picard.metadata import register_track_metadata_processor\nfrom picard.plugins.lastfm.ui_options_lastfm import Ui_LastfmOptionsPage\nfrom picard.ui.options import register_options_page, OptionsPage\nfrom picard.util import build_qurl\nfrom picard.webservice import ratecontrol\n\nLASTFM_HOST = 'ws.audioscrobbler.com'\nLASTFM_PORT = 80\nLASTFM_PATH = '/2.0/'\nLASTFM_API_KEY = '0a210a4a6741f2ec8f27a791b9d5d971'\n\n# From https://www.last.fm/api/tos, 2018-09-04\n# 4.4 […] You will not make more than 5 requests per originating IP address per\n# second, averaged over a 5 minute period, without prior written consent. […]\nratecontrol.set_minimum_delay((LASTFM_HOST, LASTFM_PORT), 200)\n\n# Cache for Tags to avoid re-requesting tags within same Picard session\n_cache = {}\n\n# Keeps track of requests for tags made to webservice API but not yet returned\n# (to avoid re-requesting the same URIs)\n_pending_requests = {}\n\n# TODO: move this to an options page\nTRANSLATE_TAGS = {\n    \"hip hop\": \"Hip-Hop\",\n    \"synth-pop\": \"Synthpop\",\n    \"electronica\": \"Electronic\",\n}\nTITLE_CASE = True\n\n\ndef parse_ignored_tags(ignore_tags_setting):\n    ignore_tags = []\n    for tag in ignore_tags_setting.lower().split(','):\n        tag = tag.strip()\n        if tag.startswith('/') and tag.endswith('/'):\n            try:\n                tag = re.compile(tag[1:-1])\n            except re.error:\n                log.error(\n                    'Error parsing ignored tag \"%s\"', tag, exc_info=True)\n        ignore_tags.append(tag)\n    return ignore_tags\n\n\ndef matches_ignored(ignore_tags, tag):\n    tag = tag.lower().strip()\n    for pattern in ignore_tags:\n        if hasattr(pattern, 'match'):\n            match = pattern.match(tag)\n        else:\n            match = pattern == tag\n        if match:\n            return True\n    return False\n\n\ndef _tags_finalize(album, metadata, tags, next_):\n    if next_:\n        next_(tags)\n    else:\n        tags = list(set(tags))\n        if tags:\n            join_tags = config.setting[\"lastfm_join_tags\"]\n            if join_tags:\n                tags = join_tags.join(tags)\n            metadata[\"genre\"] = tags\n\n\ndef _tags_downloaded(album, metadata, min_usage, ignore, next_, current, data,\n                     reply, error):\n    if error:\n        album._requests -= 1\n        album._finalize_loading(None)\n        return\n\n    try:\n        try:\n            intags = data.lfm[0].toptags[0].tag\n        except AttributeError:\n            intags = []\n        tags = []\n        for tag in intags:\n            name = tag.name[0].text.strip()\n            try:\n                count = int(tag.count[0].text.strip())\n            except ValueError:\n                count = 0\n            if count < min_usage:\n                break\n            try:\n                name = TRANSLATE_TAGS[name]\n            except KeyError:\n                pass\n            if not matches_ignored(ignore, name):\n                tags.append(name.title())\n        url = reply.url().toString()\n        _cache[url] = tags\n        _tags_finalize(album, metadata, current + tags, next_)\n\n        # Process any pending requests for the same URL\n        if url in _pending_requests:\n            pending = _pending_requests[url]\n            del _pending_requests[url]\n            for delayed_call in pending:\n                delayed_call()\n\n    except Exception:\n        log.error('Problem processing download tags', exc_info=True)\n    finally:\n        album._requests -= 1\n        album._finalize_loading(None)\n\n\ndef get_tags(album, metadata, queryargs, min_usage, ignore, next_, current):\n    \"\"\"Get tags from an URL.\"\"\"\n    url = build_qurl(\n        LASTFM_HOST, LASTFM_PORT, LASTFM_PATH, queryargs).toString()\n    if url in _cache:\n        _tags_finalize(album, metadata, current + _cache[url], next_)\n    else:\n        # If we have already sent a request for this URL, delay this call\n        if url in _pending_requests:\n            _pending_requests[url].append(\n                partial(get_tags, album, metadata, queryargs, min_usage,\n                        ignore, next_, current))\n        else:\n            _pending_requests[url] = []\n            album._requests += 1\n            album.tagger.webservice.get(\n                LASTFM_HOST, LASTFM_PORT, LASTFM_PATH,\n                partial(_tags_downloaded, album, metadata, min_usage, ignore,\n                        next_, current),\n                queryargs=queryargs, parse_response_type='xml',\n                priority=True, important=True)\n\n\ndef encode_str(s):\n    s = QtCore.QUrl.toPercentEncoding(s)\n    return bytes(s).decode()\n\n\ndef get_queryargs(queryargs):\n    queryargs = {k: encode_str(v) for (k, v) in queryargs.items()}\n    queryargs['api_key'] = LASTFM_API_KEY\n    return queryargs\n\n\ndef get_track_tags(album, metadata, artist, track, min_usage,\n                   ignore, next_, current):\n    \"\"\"Get track top tags.\"\"\"\n    queryargs = get_queryargs({\n        'method': 'Track.getTopTags',\n        'artist': artist,\n        'track': track,\n    })\n    get_tags(album, metadata, queryargs, min_usage, ignore, next_, current)\n\n\ndef get_artist_tags(album, metadata, artist, min_usage,\n                    ignore, next_, current):\n    \"\"\"Get artist top tags.\"\"\"\n    queryargs = get_queryargs({\n        'method': 'Artist.getTopTags',\n        'artist': artist,\n    })\n    get_tags(album, metadata, queryargs, min_usage, ignore, next_, current)\n\n\ndef process_track(album, metadata, track, release):\n    use_track_tags = config.setting[\"lastfm_use_track_tags\"]\n    use_artist_tags = config.setting[\"lastfm_use_artist_tags\"]\n    min_tag_usage = config.setting[\"lastfm_min_tag_usage\"]\n    ignore_tags = parse_ignored_tags(config.setting[\"lastfm_ignore_tags\"])\n    if use_track_tags or use_artist_tags:\n        artist = metadata[\"artist\"]\n        title = metadata[\"title\"]\n        if artist:\n            if use_artist_tags:\n                get_artist_tags_func = partial(get_artist_tags, album,\n                                               metadata, artist, min_tag_usage,\n                                               ignore_tags, None)\n            else:\n                get_artist_tags_func = None\n            if title and use_track_tags:\n                get_track_tags(album, metadata, artist, title, min_tag_usage,\n                               ignore_tags, get_artist_tags_func, [])\n            elif get_artist_tags_func:\n                get_artist_tags_func([])\n\n\nclass LastfmOptionsPage(OptionsPage):\n\n    NAME = \"lastfm\"\n    TITLE = \"Last.fm\"\n    PARENT = \"plugins\"\n\n    options = [\n        BoolOption(\"setting\", \"lastfm_use_track_tags\", False),\n        BoolOption(\"setting\", \"lastfm_use_artist_tags\", False),\n        IntOption(\"setting\", \"lastfm_min_tag_usage\", 90),\n        TextOption(\"setting\", \"lastfm_ignore_tags\",\n                   \"seen live, favorites, /\\\\d+ of \\\\d+ stars/\"),\n        TextOption(\"setting\", \"lastfm_join_tags\", \"\"),\n    ]\n\n    def __init__(self, parent=None):\n        super(LastfmOptionsPage, self).__init__(parent)\n        self.ui = Ui_LastfmOptionsPage()\n        self.ui.setupUi(self)\n\n    def load(self):\n        setting = config.setting\n        self.ui.use_track_tags.setChecked(setting[\"lastfm_use_track_tags\"])\n        self.ui.use_artist_tags.setChecked(setting[\"lastfm_use_artist_tags\"])\n        self.ui.min_tag_usage.setValue(setting[\"lastfm_min_tag_usage\"])\n        self.ui.ignore_tags.setText(setting[\"lastfm_ignore_tags\"])\n        self.ui.join_tags.setEditText(setting[\"lastfm_join_tags\"])\n\n    def save(self):\n        global _cache\n        setting = config.setting\n        if setting[\"lastfm_min_tag_usage\"] != self.ui.min_tag_usage.value() \\\n           or setting[\"lastfm_ignore_tags\"] != str(self.ui.ignore_tags.text()):\n            _cache = {}\n        setting[\"lastfm_use_track_tags\"] = self.ui.use_track_tags.isChecked()\n        setting[\"lastfm_use_artist_tags\"] = self.ui.use_artist_tags.isChecked()\n        setting[\"lastfm_min_tag_usage\"] = self.ui.min_tag_usage.value()\n        setting[\"lastfm_ignore_tags\"] = str(self.ui.ignore_tags.text())\n        setting[\"lastfm_join_tags\"] = str(self.ui.join_tags.currentText())\n\n\nregister_track_metadata_processor(process_track)\nregister_options_page(LastfmOptionsPage)\n"
  },
  {
    "path": "plugins/lastfm/ui_options_lastfm.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/lastfm/ui_options_lastfm.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.4\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_LastfmOptionsPage(object):\n    def setupUi(self, LastfmOptionsPage):\n        LastfmOptionsPage.setObjectName(\"LastfmOptionsPage\")\n        LastfmOptionsPage.resize(305, 317)\n        self.vboxlayout = QtWidgets.QVBoxLayout(LastfmOptionsPage)\n        self.vboxlayout.setContentsMargins(9, 9, 9, 9)\n        self.vboxlayout.setSpacing(6)\n        self.vboxlayout.setObjectName(\"vboxlayout\")\n        self.rename_files = QtWidgets.QGroupBox(LastfmOptionsPage)\n        self.rename_files.setObjectName(\"rename_files\")\n        self.vboxlayout1 = QtWidgets.QVBoxLayout(self.rename_files)\n        self.vboxlayout1.setContentsMargins(9, 9, 9, 9)\n        self.vboxlayout1.setSpacing(2)\n        self.vboxlayout1.setObjectName(\"vboxlayout1\")\n        self.use_track_tags = QtWidgets.QCheckBox(self.rename_files)\n        self.use_track_tags.setObjectName(\"use_track_tags\")\n        self.vboxlayout1.addWidget(self.use_track_tags)\n        self.use_artist_tags = QtWidgets.QCheckBox(self.rename_files)\n        self.use_artist_tags.setObjectName(\"use_artist_tags\")\n        self.vboxlayout1.addWidget(self.use_artist_tags)\n        self.vboxlayout.addWidget(self.rename_files)\n        self.rename_files_2 = QtWidgets.QGroupBox(LastfmOptionsPage)\n        self.rename_files_2.setObjectName(\"rename_files_2\")\n        self.vboxlayout2 = QtWidgets.QVBoxLayout(self.rename_files_2)\n        self.vboxlayout2.setContentsMargins(9, 9, 9, 9)\n        self.vboxlayout2.setSpacing(2)\n        self.vboxlayout2.setObjectName(\"vboxlayout2\")\n        self.ignore_tags_2 = QtWidgets.QLabel(self.rename_files_2)\n        self.ignore_tags_2.setObjectName(\"ignore_tags_2\")\n        self.vboxlayout2.addWidget(self.ignore_tags_2)\n        self.ignore_tags = QtWidgets.QLineEdit(self.rename_files_2)\n        self.ignore_tags.setObjectName(\"ignore_tags\")\n        self.vboxlayout2.addWidget(self.ignore_tags)\n        self.hboxlayout = QtWidgets.QHBoxLayout()\n        self.hboxlayout.setContentsMargins(0, 0, 0, 0)\n        self.hboxlayout.setSpacing(6)\n        self.hboxlayout.setObjectName(\"hboxlayout\")\n        self.ignore_tags_4 = QtWidgets.QLabel(self.rename_files_2)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(4)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.ignore_tags_4.sizePolicy().hasHeightForWidth())\n        self.ignore_tags_4.setSizePolicy(sizePolicy)\n        self.ignore_tags_4.setObjectName(\"ignore_tags_4\")\n        self.hboxlayout.addWidget(self.ignore_tags_4)\n        self.join_tags = QtWidgets.QComboBox(self.rename_files_2)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(1)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.join_tags.sizePolicy().hasHeightForWidth())\n        self.join_tags.setSizePolicy(sizePolicy)\n        self.join_tags.setEditable(True)\n        self.join_tags.setObjectName(\"join_tags\")\n        self.join_tags.addItem(\"\")\n        self.join_tags.setItemText(0, \"\")\n        self.join_tags.addItem(\"\")\n        self.join_tags.addItem(\"\")\n        self.hboxlayout.addWidget(self.join_tags)\n        self.vboxlayout2.addLayout(self.hboxlayout)\n        self.hboxlayout1 = QtWidgets.QHBoxLayout()\n        self.hboxlayout1.setContentsMargins(0, 0, 0, 0)\n        self.hboxlayout1.setSpacing(6)\n        self.hboxlayout1.setObjectName(\"hboxlayout1\")\n        self.label_4 = QtWidgets.QLabel(self.rename_files_2)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth())\n        self.label_4.setSizePolicy(sizePolicy)\n        self.label_4.setObjectName(\"label_4\")\n        self.hboxlayout1.addWidget(self.label_4)\n        self.min_tag_usage = QtWidgets.QSpinBox(self.rename_files_2)\n        self.min_tag_usage.setMaximum(100)\n        self.min_tag_usage.setObjectName(\"min_tag_usage\")\n        self.hboxlayout1.addWidget(self.min_tag_usage)\n        self.vboxlayout2.addLayout(self.hboxlayout1)\n        self.vboxlayout.addWidget(self.rename_files_2)\n        spacerItem = QtWidgets.QSpacerItem(263, 21, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.vboxlayout.addItem(spacerItem)\n        self.label_4.setBuddy(self.min_tag_usage)\n\n        self.retranslateUi(LastfmOptionsPage)\n        QtCore.QMetaObject.connectSlotsByName(LastfmOptionsPage)\n        LastfmOptionsPage.setTabOrder(self.use_track_tags, self.ignore_tags)\n\n    def retranslateUi(self, LastfmOptionsPage):\n        _translate = QtCore.QCoreApplication.translate\n        self.rename_files.setTitle(_translate(\"LastfmOptionsPage\", \"Last.fm\"))\n        self.use_track_tags.setText(_translate(\"LastfmOptionsPage\", \"Use track tags\"))\n        self.use_artist_tags.setText(_translate(\"LastfmOptionsPage\", \"Use artist tags\"))\n        self.rename_files_2.setTitle(_translate(\"LastfmOptionsPage\", \"Tags\"))\n        self.ignore_tags_2.setText(_translate(\"LastfmOptionsPage\", \"Ignore tags:\"))\n        self.ignore_tags_4.setText(_translate(\"LastfmOptionsPage\", \"Join multiple tags with:\"))\n        self.join_tags.setItemText(1, _translate(\"LastfmOptionsPage\", \" / \"))\n        self.join_tags.setItemText(2, _translate(\"LastfmOptionsPage\", \", \"))\n        self.label_4.setText(_translate(\"LastfmOptionsPage\", \"Minimal tag usage:\"))\n        self.min_tag_usage.setSuffix(_translate(\"LastfmOptionsPage\", \" %\"))\n"
  },
  {
    "path": "plugins/lastfm/ui_options_lastfm.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>LastfmOptionsPage</class>\n <widget class=\"QWidget\" name=\"LastfmOptionsPage\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>305</width>\n    <height>317</height>\n   </rect>\n  </property>\n  <layout class=\"QVBoxLayout\">\n   <property name=\"spacing\">\n    <number>6</number>\n   </property>\n   <property name=\"leftMargin\">\n    <number>9</number>\n   </property>\n   <property name=\"topMargin\">\n    <number>9</number>\n   </property>\n   <property name=\"rightMargin\">\n    <number>9</number>\n   </property>\n   <property name=\"bottomMargin\">\n    <number>9</number>\n   </property>\n   <item>\n    <widget class=\"QGroupBox\" name=\"rename_files\">\n     <property name=\"title\">\n      <string>Last.fm</string>\n     </property>\n     <layout class=\"QVBoxLayout\">\n      <property name=\"spacing\">\n       <number>2</number>\n      </property>\n      <property name=\"leftMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"topMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"rightMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"bottomMargin\">\n       <number>9</number>\n      </property>\n      <item>\n       <widget class=\"QCheckBox\" name=\"use_track_tags\">\n        <property name=\"text\">\n         <string>Use track tags</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QCheckBox\" name=\"use_artist_tags\">\n        <property name=\"text\">\n         <string>Use artist tags</string>\n        </property>\n       </widget>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <widget class=\"QGroupBox\" name=\"rename_files_2\">\n     <property name=\"title\">\n      <string>Tags</string>\n     </property>\n     <layout class=\"QVBoxLayout\">\n      <property name=\"spacing\">\n       <number>2</number>\n      </property>\n      <property name=\"leftMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"topMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"rightMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"bottomMargin\">\n       <number>9</number>\n      </property>\n      <item>\n       <widget class=\"QLabel\" name=\"ignore_tags_2\">\n        <property name=\"text\">\n         <string>Ignore tags:</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLineEdit\" name=\"ignore_tags\"/>\n      </item>\n      <item>\n       <layout class=\"QHBoxLayout\">\n        <property name=\"spacing\">\n         <number>6</number>\n        </property>\n        <property name=\"leftMargin\">\n         <number>0</number>\n        </property>\n        <property name=\"topMargin\">\n         <number>0</number>\n        </property>\n        <property name=\"rightMargin\">\n         <number>0</number>\n        </property>\n        <property name=\"bottomMargin\">\n         <number>0</number>\n        </property>\n        <item>\n         <widget class=\"QLabel\" name=\"ignore_tags_4\">\n          <property name=\"sizePolicy\">\n           <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n            <horstretch>4</horstretch>\n            <verstretch>0</verstretch>\n           </sizepolicy>\n          </property>\n          <property name=\"text\">\n           <string>Join multiple tags with:</string>\n          </property>\n         </widget>\n        </item>\n        <item>\n         <widget class=\"QComboBox\" name=\"join_tags\">\n          <property name=\"sizePolicy\">\n           <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Fixed\">\n            <horstretch>1</horstretch>\n            <verstretch>0</verstretch>\n           </sizepolicy>\n          </property>\n          <property name=\"editable\">\n           <bool>true</bool>\n          </property>\n          <item>\n           <property name=\"text\">\n            <string/>\n           </property>\n          </item>\n          <item>\n           <property name=\"text\">\n            <string> / </string>\n           </property>\n          </item>\n          <item>\n           <property name=\"text\">\n            <string>, </string>\n           </property>\n          </item>\n         </widget>\n        </item>\n       </layout>\n      </item>\n      <item>\n       <layout class=\"QHBoxLayout\">\n        <property name=\"spacing\">\n         <number>6</number>\n        </property>\n        <property name=\"leftMargin\">\n         <number>0</number>\n        </property>\n        <property name=\"topMargin\">\n         <number>0</number>\n        </property>\n        <property name=\"rightMargin\">\n         <number>0</number>\n        </property>\n        <property name=\"bottomMargin\">\n         <number>0</number>\n        </property>\n        <item>\n         <widget class=\"QLabel\" name=\"label_4\">\n          <property name=\"sizePolicy\">\n           <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Preferred\">\n            <horstretch>0</horstretch>\n            <verstretch>0</verstretch>\n           </sizepolicy>\n          </property>\n          <property name=\"text\">\n           <string>Minimal tag usage:</string>\n          </property>\n          <property name=\"buddy\">\n           <cstring>min_tag_usage</cstring>\n          </property>\n         </widget>\n        </item>\n        <item>\n         <widget class=\"QSpinBox\" name=\"min_tag_usage\">\n          <property name=\"suffix\">\n           <string> %</string>\n          </property>\n          <property name=\"maximum\">\n           <number>100</number>\n          </property>\n         </widget>\n        </item>\n       </layout>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <spacer>\n     <property name=\"orientation\">\n      <enum>Qt::Vertical</enum>\n     </property>\n     <property name=\"sizeHint\" stdset=\"0\">\n      <size>\n       <width>263</width>\n       <height>21</height>\n      </size>\n     </property>\n    </spacer>\n   </item>\n  </layout>\n </widget>\n <tabstops>\n  <tabstop>use_track_tags</tabstop>\n  <tabstop>ignore_tags</tabstop>\n </tabstops>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/loadasnat/loadasnat.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2017, 2019, 2021 Philipp Wolfer\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = \"Load as non-album track\"\nPLUGIN_AUTHOR = \"Philipp Wolfer\"\nPLUGIN_DESCRIPTION = (\"Allows loading selected tracks as non-album tracks. \"\n                      \"Useful for tagging single tracks where you do not care \"\n                      \"about the album.\")\nPLUGIN_VERSION = \"0.4\"\nPLUGIN_API_VERSIONS = [\"1.4.0\", \"2.0\", \"2.1\", \"2.2\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom picard import log\nfrom picard.track import Track\nfrom picard.ui.itemviews import (\n    BaseAction,\n    register_track_action,\n)\n\n\nclass LoadAsNat(BaseAction):\n    NAME = \"Load as non-album track...\"\n\n    def callback(self, objs):\n        for track in (t for t in objs if isinstance(t, Track)):\n            nat = self.tagger.load_nat(\n                track.metadata['musicbrainz_recordingid'])\n            for file in list(track.linked_files):\n                file.move(nat)\n                metadata = file.metadata\n                metadata.delete('albumartist')\n                metadata.delete('albumartistsort')\n                metadata.delete('albumsort')\n                metadata.delete('asin')\n                metadata.delete('barcode')\n                metadata.delete('catalognumber')\n                metadata.delete('discnumber')\n                metadata.delete('discsubtitle')\n                metadata.delete('media')\n                metadata.delete('musicbrainz_albumartistid')\n                metadata.delete('musicbrainz_albumid')\n                metadata.delete('musicbrainz_discid')\n                metadata.delete('musicbrainz_releasegroupid')\n                metadata.delete('releasecountry')\n                metadata.delete('releasestatus')\n                metadata.delete('releasetype')\n                metadata.delete('totaldiscs')\n                metadata.delete('totaltracks')\n                metadata.delete('tracknumber')\n                log.debug(\"[LoadAsNat] deleted tags: %r\", metadata.deleted_tags)\n\n\nregister_track_action(LoadAsNat())\n"
  },
  {
    "path": "plugins/losslessfuncs/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2014, 2017, 2021, 2023-2024 Philipp Wolfer\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n\nPLUGIN_NAME = 'Tagger script functions $is_lossless() and $is_lossy()'\nPLUGIN_AUTHOR = 'Philipp Wolfer'\nPLUGIN_DESCRIPTION = 'Tagger script functions to detect if a file is lossless or lossy'\nPLUGIN_VERSION = \"0.3\"\nPLUGIN_API_VERSIONS = [\"2.0\", \"2.1\", \"2.2\", \"2.3\", \"2.4\", \"2.5\", \"2.6\", \"2.7\", \"2.8\", \"2.9\", \"2.10\", \"2.11\"]\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\n\nLOSSLESS_EXTENSIONS = {'flac', 'oggflac', 'ape', 'ofr', 'tak', 'wv', 'tta',\n                       'aiff', 'aif', 'wav'}\n\nfrom picard.script import register_script_function\n\n\ndef is_lossless(parser):\n    \"\"\"\n    Returns true, if the file processed is a lossless audio format.\n    Note: This depends mainly on the file extension and does not look inside the\n    file to detect the codec.\n    \"\"\"\n    if parser.context['~extension'] in LOSSLESS_EXTENSIONS:\n        return \"1\"\n    elif parser.context['~extension'] == 'm4a':\n        # Mutagen < 1.26 fails to detect the bitrate for Apple Lossless Audio Codec.\n        if not parser.context['~bitrate']:\n            return \"1\"\n        else:\n            try:\n                bitrate = float(parser.context['~bitrate'])\n                return \"1\" if bitrate > 1000 else \"\"\n            except (TypeError, ValueError):\n                return \"\"\n    else:\n        return \"\"\n\n\ndef is_lossy(parser):\n    \"\"\"Returns true, if the file processed is a lossy audio format.\"\"\"\n    return \"\" if is_lossless(parser) == \"1\" else \"1\"\n\n\nregister_script_function(is_lossless)\nregister_script_function(is_lossy)\n"
  },
  {
    "path": "plugins/lrclib_lyrics/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2024 Giorgio Fontanive (twodoorcoupe)\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n\nPLUGIN_NAME = \"Lrclib Lyrics\"\nPLUGIN_AUTHOR = \"Giorgio Fontanive\"\nPLUGIN_DESCRIPTION = \"\"\"\nFetches lyrics from lrclib.net\n\nAlso allows to export lyrics to an .lrc file or import them from one.\n\"\"\"\nPLUGIN_VERSION = \"0.3\"\nPLUGIN_API_VERSIONS = [\"2.12\"]\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nimport os\nimport re\nfrom functools import partial\n\nfrom picard import config, log\nfrom picard.file import register_file_post_save_processor, register_file_post_addition_to_track_processor\nfrom picard.track import Track\nfrom picard.ui.itemviews import BaseAction, register_track_action\nfrom picard.ui.options import OptionsPage, register_options_page\nfrom picard.webservice import ratecontrol\n\nfrom .option_lrclib_lyrics import Ui_OptionLrclibLyrics\n\n\nURL = \"https://lrclib.net/api/get\"\nREQUESTS_DELAY = 100\n\n# Options\nADD_UNSYNCED_LYRICS = \"add_unsynced_lyrics\"\nADD_SYNCED_LYRICS = \"add_synced_lyrics\"\nNEVER_REPLACE_LYRICS = \"never_replace_lyrics\"\nLRC_FILENAME = \"exported_lrc_filename\"\nLRC_AS_SIDECAR = \"lrc_as_sidecar\"\nEXPORT_LRC = \"exported_lrc\"\nNEVER_REPLACE_LRC = \"never_replace_lrc\"\n\nlyrics_cache = {}\nsynced_lyrics_pattern = re.compile(r\"(\\[\\d\\d:\\d\\d\\.\\d\\d\\d]|<\\d\\d:\\d\\d\\.\\d\\d\\d>)\")\ntags_pattern = re.compile(r\"%(\\w+)%\")\nextra_file_variables = {\n    \"filepath\": lambda file: file,\n    \"folderpath\": lambda file: os.path.dirname(file),  # pylint: disable=unnecessary-lambda\n    \"filename\": lambda file: os.path.splitext(os.path.basename(file))[0],\n    \"filename_ext\": lambda file: os.path.basename(file),  # pylint: disable=unnecessary-lambda\n    \"directory\": lambda file: os.path.basename(os.path.dirname(file))\n}\n\n\ndef get_lyrics(track, file):\n    album = track.album\n    metadata = file.metadata\n    if not (config.setting[ADD_UNSYNCED_LYRICS] or config.setting[ADD_SYNCED_LYRICS]):\n        return\n    if not (metadata.get(\"title\") and metadata.get(\"artist\")):\n        log.debug(f\"Skipping fetching lyrics for track in {album} as both title and artist are required\")\n        return\n    if config.setting[NEVER_REPLACE_LYRICS] and metadata.get(\"lyrics\"):\n        log.debug(f\"Skipping fetching lyrics for {metadata['title']} as lyrics are already embedded\")\n        return\n    args = {\n        \"track_name\": metadata[\"title\"],\n        \"artist_name\": metadata[\"artist\"],\n    }\n    if metadata.get(\"album\"):\n        args[\"album_name\"] = metadata[\"album\"]\n    handler = partial(response_handler, metadata)\n    album.tagger.webservice.get_url(\n        method=\"GET\",\n        handler=handler,\n        parse_response_type='json',\n        url=URL,\n        unencoded_queryargs=args\n    )\n\n\ndef response_handler(metadata, document, reply, error):\n    if document and not error:\n        unsynced_lyrics = document.get(\"plainLyrics\")\n        synced_lyrics = document.get(\"syncedLyrics\")\n        if unsynced_lyrics:\n            lyrics_cache[metadata[\"title\"]] = unsynced_lyrics\n            if ((not config.setting[ADD_UNSYNCED_LYRICS]) or\n                    (config.setting[NEVER_REPLACE_LYRICS] and metadata.get(\"lyrics\"))):\n                return\n            metadata[\"lyrics\"] = unsynced_lyrics\n        if synced_lyrics:\n            lyrics_cache[metadata[\"title\"]] = synced_lyrics\n            # Support for the syncedlyrics tag is not available yet\n            # if (not config.setting[ADD_SYNCED_LYRICS] or\n            #         (config.setting[NEVER_REPLACE_LYRICS] and metadata.get(\"syncedlyrics\"))):\n            #     return\n            # metadata[\"syncedlyrics\"] = syncedlyrics\n    else:\n        log.debug(f\"Could not fetch lyrics for {metadata['title']}\")\n\n\ndef get_lrc_file_name(file):\n    filename = f\"{tags_pattern.sub('{}', config.setting[LRC_FILENAME])}\"\n    # If sidecar option is selected, override any pattern\n    if config.setting[LRC_AS_SIDECAR]:\n        filename = f\"{os.path.splitext(file.filename)[0]}.lrc\"\n        log.debug(f\"LRC sidecar filename for {file.metadata['title']}: {filename}\")\n        return filename\n    # Otherwise, parse the pattern\n    tags = tags_pattern.findall(config.setting[LRC_FILENAME])\n    values = []\n    for tag in tags:\n        if tag in extra_file_variables:\n            values.append(extra_file_variables[tag](file.filename))\n        else:\n            values.append(file.metadata.get(tag, f\"%{tag}%\"))\n    return filename.format(*values)\n\n\ndef export_lrc_file(file):\n    if config.setting[EXPORT_LRC]:\n        metadata = file.metadata\n        # If no lyrics were downloaded, try to export the lyrics already embedded\n        lyrics = lyrics_cache.pop(metadata[\"title\"], metadata.get(\"lyrics\"))\n        if lyrics:\n            filename = get_lrc_file_name(file)\n            if config.setting[NEVER_REPLACE_LRC] and os.path.exists(filename):\n                return\n            try:\n                with open(filename, 'w') as file:\n                    file.write(lyrics)\n                log.debug(f\"Created new lyrics file at {filename}\")\n            except OSError:\n                log.debug(f\"Could not create the lrc file for {metadata['title']}\")\n        else:\n            log.debug(f\"Could not export any lyrics for {metadata['title']}\")\n\n\nclass ImportLrc(BaseAction):\n    NAME = 'Import lyrics from lrc files'\n\n    def callback(self, objs):\n        for track in objs:\n            if isinstance(track, Track):\n                file = track.files[0]\n                filename = get_lrc_file_name(file)\n                try:\n                    with open(filename, 'r') as lyrics_file:\n                        lyrics = lyrics_file.read()\n                        if synced_lyrics_pattern.search(lyrics):\n                            # Support for syncedlyrics is not available yet\n                            # file.metadata[\"syncedlyrics\"] = lyrics\n                            pass\n                        else:\n                            file.metadata[\"lyrics\"] = lyrics\n                except FileNotFoundError:\n                    log.debug(f\"Could not find matching lrc file for {file.metadata['title']}\")\n\n\nclass LrclibLyricsOptions(OptionsPage):\n\n    NAME = \"lrclib_lyrics\"\n    TITLE = \"Lrclib Lyrics\"\n    PARENT = \"plugins\"\n\n    # By default, use a path for the LRC file in the same folder as\n    # the music file so as not to store the LRC files \"somewhere\"\n    __default_naming = f\"%folderpath%{os.sep}%filename%.lrc\"\n\n    options = [\n        config.BoolOption(\"setting\", ADD_UNSYNCED_LYRICS, True),\n        config.BoolOption(\"setting\", ADD_SYNCED_LYRICS, False),\n        config.BoolOption(\"setting\", NEVER_REPLACE_LYRICS, False),\n        config.BoolOption(\"setting\", LRC_AS_SIDECAR, True),\n        config.TextOption(\"setting\", LRC_FILENAME, __default_naming),\n        config.BoolOption(\"setting\", EXPORT_LRC, False),\n        config.BoolOption(\"setting\", NEVER_REPLACE_LRC, False),\n    ]\n\n    def __init__(self, parent=None):\n        super(LrclibLyricsOptions, self).__init__(parent)\n        self.ui = Ui_OptionLrclibLyrics()\n        self.ui.setupUi(self)\n\n    def load(self):\n        self.ui.lyrics.setChecked(config.setting[ADD_UNSYNCED_LYRICS])\n        self.ui.syncedlyrics.setChecked(config.setting[ADD_SYNCED_LYRICS])\n        self.ui.replace_embedded.setChecked(config.setting[NEVER_REPLACE_LYRICS])\n        self.ui.lrc_name.setText(config.setting[LRC_FILENAME])\n        self.ui.lrc_as_sidecar.setChecked(config.setting[LRC_AS_SIDECAR])\n        self.ui.export_lyrics.setChecked(config.setting[EXPORT_LRC])\n        self.ui.replace_exported.setChecked(config.setting[NEVER_REPLACE_LRC])\n\n        # Initialize the LRC filename field state\n        self.update_lrc_name_field_state()\n        # Connect the sidecar checkbox to update the filename field state\n        self.ui.lrc_as_sidecar.toggled.connect(self.update_lrc_name_field_state())\n\n    def save(self):\n        config.setting[ADD_UNSYNCED_LYRICS] = self.ui.lyrics.isChecked()\n        config.setting[ADD_SYNCED_LYRICS] = self.ui.syncedlyrics.isChecked()\n        config.setting[NEVER_REPLACE_LYRICS] = self.ui.replace_embedded.isChecked()\n        config.setting[LRC_FILENAME] = self.ui.lrc_name.text()\n        config.setting[LRC_AS_SIDECAR] = self.ui.lrc_as_sidecar.isChecked()\n        config.setting[EXPORT_LRC] = self.ui.export_lyrics.isChecked()\n        config.setting[NEVER_REPLACE_LRC] = self.ui.replace_exported.isChecked()\n\n    def update_lrc_name_field_state(self):\n        \"\"\"Enable or disable the LRC filename field based on the sidecar option.\"\"\"\n        self.ui.lrc_name.setEnabled(not self.ui.lrc_as_sidecar.isChecked())\n\n\nratecontrol.set_minimum_delay_for_url(URL, REQUESTS_DELAY)\nregister_file_post_addition_to_track_processor(get_lyrics)\nregister_file_post_save_processor(export_lrc_file)\nregister_track_action(ImportLrc())\nregister_options_page(LrclibLyricsOptions)\n"
  },
  {
    "path": "plugins/lrclib_lyrics/option_lrclib_lyrics.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'option_lrclib_lyrics.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.10\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtWidgets\n\n\nclass Ui_OptionLrclibLyrics(object):\n    def setupUi(self, OptionLrclibLyrics):\n        OptionLrclibLyrics.setObjectName(\"OptionLrclibLyrics\")\n        OptionLrclibLyrics.resize(432, 368)\n        self.verticalLayout = QtWidgets.QVBoxLayout(OptionLrclibLyrics)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.groupBox = QtWidgets.QGroupBox(OptionLrclibLyrics)\n        self.groupBox.setObjectName(\"groupBox\")\n        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.groupBox)\n        self.verticalLayout_2.setObjectName(\"verticalLayout_2\")\n        self.lyrics = QtWidgets.QCheckBox(self.groupBox)\n        self.lyrics.setObjectName(\"lyrics\")\n        self.verticalLayout_2.addWidget(self.lyrics)\n        self.syncedlyrics = QtWidgets.QCheckBox(self.groupBox)\n        self.syncedlyrics.setEnabled(False)\n        self.syncedlyrics.setCheckable(False)\n        self.syncedlyrics.setObjectName(\"syncedlyrics\")\n        self.verticalLayout_2.addWidget(self.syncedlyrics)\n        self.replace_embedded = QtWidgets.QCheckBox(self.groupBox)\n        self.replace_embedded.setObjectName(\"replace_embedded\")\n        self.verticalLayout_2.addWidget(self.replace_embedded)\n        self.verticalLayout.addWidget(self.groupBox)\n        self.groupBox_2 = QtWidgets.QGroupBox(OptionLrclibLyrics)\n        self.groupBox_2.setObjectName(\"groupBox_2\")\n        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.groupBox_2)\n        self.verticalLayout_3.setObjectName(\"verticalLayout_3\")\n        self.export_lyrics = QtWidgets.QCheckBox(self.groupBox_2)\n        self.export_lyrics.setObjectName(\"export_lyrics\")\n        self.verticalLayout_3.addWidget(self.export_lyrics)\n        self.lrc_as_sidecar = QtWidgets.QCheckBox(self.groupBox_2)\n        self.lrc_as_sidecar.setObjectName(\"lrc_as_sidecar\")\n        self.verticalLayout_3.addWidget(self.lrc_as_sidecar)\n        self.label = QtWidgets.QLabel(self.groupBox_2)\n        self.label.setObjectName(\"label\")\n        self.verticalLayout_3.addWidget(self.label)\n        self.lrc_name = QtWidgets.QLineEdit(self.groupBox_2)\n        self.lrc_name.setObjectName(\"lrc_name\")\n        self.verticalLayout_3.addWidget(self.lrc_name)\n        self.replace_exported = QtWidgets.QCheckBox(self.groupBox_2)\n        self.replace_exported.setObjectName(\"replace_exported\")\n        self.verticalLayout_3.addWidget(self.replace_exported)\n        self.verticalLayout.addWidget(self.groupBox_2)\n        spacerItem = QtWidgets.QSpacerItem(20, 23, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.verticalLayout.addItem(spacerItem)\n\n        self.retranslateUi(OptionLrclibLyrics)\n        QtCore.QMetaObject.connectSlotsByName(OptionLrclibLyrics)\n\n    def retranslateUi(self, OptionLrclibLyrics):\n        _translate = QtCore.QCoreApplication.translate\n        OptionLrclibLyrics.setWindowTitle(_translate(\"OptionLrclibLyrics\", \"Form\"))\n        self.groupBox.setTitle(_translate(\"OptionLrclibLyrics\", \"Embedded Lyrics Options\"))\n        self.lyrics.setText(_translate(\"OptionLrclibLyrics\", \"Download and embed unsynced lyrics\"))\n        self.syncedlyrics.setText(_translate(\"OptionLrclibLyrics\", \"Download and embed synced lyrics\"))\n        self.replace_embedded.setText(_translate(\"OptionLrclibLyrics\", \"Never replace any embedded lyrics if already present\"))\n        self.groupBox_2.setTitle(_translate(\"OptionLrclibLyrics\", \"Lrc Files Options\"))\n        self.export_lyrics.setText(_translate(\"OptionLrclibLyrics\", \"Export lyrics to lrc file when saving (priority to synced lyrics)\"))\n        self.lrc_as_sidecar.setText(_translate(\"OptionLrclibLyrics\", \"Save the LRC file as a sidecar file to the audio file or\"))\n        self.label.setText(_translate(\"OptionLrclibLyrics\", \"use the following name pattern for lrc files:\"))\n        self.replace_exported.setText(_translate(\"OptionLrclibLyrics\", \"Never replace lrc files if already present\"))\n"
  },
  {
    "path": "plugins/lrclib_lyrics/option_lrclib_lyrics.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>OptionLrclibLyrics</class>\n <widget class=\"QWidget\" name=\"OptionLrclibLyrics\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>432</width>\n    <height>368</height>\n   </rect>\n  </property>\n  <property name=\"windowTitle\">\n   <string>Form</string>\n  </property>\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n   <item>\n    <widget class=\"QGroupBox\" name=\"groupBox\">\n     <property name=\"title\">\n      <string>Embedded Lyrics Options</string>\n     </property>\n     <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\n      <item>\n       <widget class=\"QCheckBox\" name=\"lyrics\">\n        <property name=\"text\">\n         <string>Download and embed unsynced lyrics</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QCheckBox\" name=\"syncedlyrics\">\n        <property name=\"enabled\">\n         <bool>false</bool>\n        </property>\n        <property name=\"text\">\n         <string>Download and embed synced lyrics</string>\n        </property>\n        <property name=\"checkable\">\n         <bool>false</bool>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QCheckBox\" name=\"replace_embedded\">\n        <property name=\"text\">\n         <string>Never replace any embedded lyrics if already present</string>\n        </property>\n       </widget>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <widget class=\"QGroupBox\" name=\"groupBox_2\">\n     <property name=\"title\">\n      <string>Lrc Files Options</string>\n     </property>\n     <layout class=\"QVBoxLayout\" name=\"verticalLayout_3\">\n      <item>\n       <widget class=\"QCheckBox\" name=\"export_lyrics\">\n        <property name=\"text\">\n         <string>Export lyrics to lrc file when saving (priority to synced lyrics)</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QCheckBox\" name=\"lrc_as_sidecar\">\n        <property name=\"text\">\n         <string>Save the LRC file as a sidecar file to the audio file or</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"label\">\n        <property name=\"text\">\n         <string>use the following name pattern for lrc files:</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLineEdit\" name=\"lrc_name\"/>\n      </item>\n      <item>\n       <widget class=\"QCheckBox\" name=\"replace_exported\">\n        <property name=\"text\">\n         <string>Never replace lrc files if already present</string>\n        </property>\n       </widget>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <spacer name=\"verticalSpacer\">\n     <property name=\"orientation\">\n      <enum>Qt::Vertical</enum>\n     </property>\n     <property name=\"sizeHint\" stdset=\"0\">\n      <size>\n       <width>20</width>\n       <height>23</height>\n      </size>\n     </property>\n    </spacer>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/matroska_tagger/matroska_tagger.py",
    "content": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = \"Matroska Tagger\"\nPLUGIN_AUTHOR = \"Ben McLean\"\nPLUGIN_VERSION = \"0.1\"\nPLUGIN_API_VERSIONS = [\n\t\"2.0\",\n\t\"2.1\",\n\t\"2.2\",\n\t\"2.3\",\n\t\"2.4\",\n\t\"2.5\",\n\t\"2.6\",\n\t\"2.7\",\n\t\"2.8\",\n\t\"2.9\",\n\t\"2.10\",\n\t\"2.11\",\n\t\"2.12\",\n\t\"2.13\",\n]\nPLUGIN_DESCRIPTION = (\n\t\"Adds support for reading and writing tags in Matroska (.mkv, .mka) files via MKVToolNix \"\n\t\"(https://mkvtoolnix.download).\"\n)\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nimport json\nimport os\nimport subprocess\nimport sys\nimport tempfile\nfrom picard import config, log\nfrom picard.config import BoolOption, TextOption\nfrom picard.file import File\nfrom picard.formats import register_format\nfrom picard.metadata import Metadata\nfrom picard.ui.options import register_options_page, OptionsPage\n\nfrom PyQt5.QtWidgets import (\n\tQCheckBox,\n\tQFileDialog,\n\tQFormLayout,\n\tQHBoxLayout,\n\tQLabel,\n\tQLineEdit,\n\tQPushButton,\n\tQVBoxLayout,\n)\nfrom PyQt5.QtCore import (\n\tQBuffer,\n\tQFile,\n\tQIODevice,\n\tQXmlStreamReader,\n\tQXmlStreamWriter,\n\tQt,\n)\n\n_AlignTop = Qt.AlignTop\n\n\n# ---------------------------------------------------------------------------\n# Tag mappings\n# ---------------------------------------------------------------------------\n\n# Picard internal name → Matroska tag name, for track-level tags (TargetTypeValue=30)\n_TRACK_TAGS = {\n\t\"title\": \"TITLE\",\n\t\"artist\": \"ARTIST\",\n\t\"comment\": \"COMMENT\",\n\t\"lyrics\": \"LYRICS\",\n\t\"isrc\": \"ISRC\",\n\t\"bpm\": \"BPM\",\n\t\"key\": \"INITIAL_KEY\",\n\t\"language\": \"LANGUAGE\",  # undocumented in spec but used by MP3Tag\n\t\"subtitle\": \"SUBTITLE\",\n\t\"remixer\": \"REMIXED_BY\",\n\t\"conductor\": \"CONDUCTOR\",\n\t\"lyricist\": \"LYRICIST\",\n\t\"composer\": \"COMPOSER\",\n\t\"engineer\": \"SOUND_ENGINEER\",\n\t\"mixer\": \"MIXED_BY\",\n\t\"djmixer\": \"DJMIXED_BY\",\n\t\"producer\": \"PRODUCER\",\n\t\"mood\": \"MOOD\",\n\t\"artists\": \"ARTISTS\",\n\t\"grouping\": \"GROUPING\",\n\t# titlesort/artistsort/composersort emitted as SORT_WITH nested inside TITLE/ARTIST/COMPOSER\n\t\"website\": \"WEBSITE\",\n\t\"license\": \"LICENSE\",\n\t\"copyright\": \"COPYRIGHT\",\n\t# originalartist emitted as ORIGINAL/ARTIST nested structure\n\t\"musicbrainz_recordingid\": \"MUSICBRAINZ_RECORDINGID\",\n\t\"musicbrainz_trackid\": \"MUSICBRAINZ_RELEASETRACKID\",\n\t\"musicbrainz_workid\": \"MUSICBRAINZ_WORKID\",\n\t\"musicbrainz_artistid\": \"MUSICBRAINZ_ARTISTID\",\n\t\"musicbrainz_composerid\": \"MUSICBRAINZ_COMPOSERID\",\n\t\"musicbrainz_originalartistid\": \"MUSICBRAINZ_ORIGINALARTISTID\",\n\t\"acoustid_id\": \"ACOUSTID_ID\",\n\t\"acoustid_fingerprint\": \"ACOUSTID_FINGERPRINT\",\n\t\"replaygain_track_gain\": \"REPLAYGAIN_GAIN\",\n\t\"replaygain_track_peak\": \"REPLAYGAIN_PEAK\",\n\t\"replaygain_track_range\": \"REPLAYGAIN_TRACK_RANGE\",\n\t\"genre\": \"GENRE\",\n}\n\n# Picard internal name → Matroska tag name, for album-level tags (TargetTypeValue=50)\n_ALBUM_TAGS = {\n\t\"album\": \"TITLE\",\n\t\"albumartist\": \"ARTIST\",\n\t\"date\": \"DATE_RELEASED\",\n\t\"label\": \"LABEL\",\n\t\"catalognumber\": \"CATALOG_NUMBER\",\n\t\"barcode\": \"BARCODE\",\n\t\"script\": \"SCRIPT\",\n\t\"media\": \"ORIGINAL_MEDIA_TYPE\",\n\t\"releasestatus\": \"RELEASE_STATUS\",\n\t\"releasecountry\": \"RELEASE_COUNTRY\",\n\t\"releasetype\": \"RELEASE_TYPE\",\n\t\"asin\": \"ASIN\",\n\t\"compilation\": \"COMPILATION\",\n\t# albumsort/albumartistsort emitted as SORT_WITH nested inside TITLE/ARTIST\n\t\"musicbrainz_albumid\": \"MUSICBRAINZ_ALBUMID\",\n\t\"musicbrainz_releasegroupid\": \"MUSICBRAINZ_RELEASEGROUPID\",\n\t\"musicbrainz_albumartistid\": \"MUSICBRAINZ_ALBUMARTISTID\",\n\t\"musicbrainz_discid\": \"MUSICBRAINZ_DISCID\",\n\t\"musicbrainz_originalalbumid\": \"MUSICBRAINZ_ORIGINALALBUMID\",\n\t\"replaygain_album_gain\": \"REPLAYGAIN_GAIN\",\n\t\"replaygain_album_peak\": \"REPLAYGAIN_PEAK\",\n\t\"replaygain_album_range\": \"REPLAYGAIN_ALBUM_RANGE\",\n\t\"replaygain_reference_loudness\": \"REPLAYGAIN_REFERENCE_LOUDNESS\",\n}\n\n# Picard internal name → Matroska tag name, for disc-level tags (TargetTypeValue=60)\n_DISC_TAGS = {\n\t\"discnumber\": \"PART_NUMBER\",\n\t\"totaldiscs\": \"TOTAL_PARTS\",\n}\n\n# Sort companions: maps the main Picard field to its sort field, per level.\n# These are emitted as SORT_WITH nested inside the parent Simple element.\n_TRACK_SORT = {\"title\": \"titlesort\", \"artist\": \"artistsort\", \"composer\": \"composersort\"}\n_ALBUM_SORT = {\"album\": \"albumsort\", \"albumartist\": \"albumartistsort\"}\n\n# Reverse maps for loading: Matroska tag name → Picard internal name (per level)\n_R_TRACK_TAGS = {v: k for k, v in _TRACK_TAGS.items()}\n_R_ALBUM_TAGS = {v: k for k, v in _ALBUM_TAGS.items()}\n_R_DISC_TAGS = {v: k for k, v in _DISC_TAGS.items()}\n\n# All Picard internal tag names that this format can store.\n_SUPPORTED_TAGS = (\n\tset(_TRACK_TAGS)\n\t| set(_ALBUM_TAGS)\n\t| set(_DISC_TAGS)\n\t| set(_TRACK_SORT.values())  # titlesort, artistsort, composersort\n\t| set(_ALBUM_SORT.values())  # albumsort, albumartistsort\n\t| {\"tracknumber\", \"totaltracks\", \"originalartist\", \"originaldate\"}\n)\n\n\n# ---------------------------------------------------------------------------\n# Tool path helpers\n# ---------------------------------------------------------------------------\n\n\ndef _tool_path(name, tooldir):\n\t\"\"\"Return the full path to a MKVToolNix executable.\"\"\"\n\tif sys.platform == \"win32\":\n\t\tname = name + \".exe\"\n\tif tooldir:\n\t\treturn os.path.join(tooldir, name)\n\treturn name  # rely on PATH\n\n\ndef _subprocess_flags():\n\t\"\"\"Return extra kwargs for subprocess.run to suppress console windows on Windows.\"\"\"\n\tflags = {}\n\tif sys.platform == \"win32\":\n\t\tflags[\"creationflags\"] = subprocess.CREATE_NO_WINDOW\n\treturn flags\n\n\ndef _run(args, **kwargs):\n\t\"\"\"Run a subprocess, returning CompletedProcess. Raises on non-zero exit.\"\"\"\n\tlog.debug(\"MkvPlugin: running %r\", args)\n\talready_captured = \"capture_output\" in kwargs or \"stdout\" in kwargs\n\tif not already_captured:\n\t\tkwargs[\"capture_output\"] = True\n\tresult = subprocess.run(args, check=True, **_subprocess_flags(), **kwargs)\n\tif not already_captured:\n\t\tif result.stdout:\n\t\t\tfor line in result.stdout.decode(\"utf-8\", errors=\"replace\").splitlines():\n\t\t\t\tlog.debug(\"MkvPlugin stdout: %s\", line)\n\t\tif result.stderr:\n\t\t\tfor line in result.stderr.decode(\"utf-8\", errors=\"replace\").splitlines():\n\t\t\t\tlog.debug(\"MkvPlugin stderr: %s\", line)\n\treturn result\n\n\ndef _get_tooldir():\n\treturn config.setting[\"mkvtoolnix_dir\"].strip()\n\n\n# ---------------------------------------------------------------------------\n# XML generation (Picard Metadata → Matroska tags XML)\n# ---------------------------------------------------------------------------\n\n\ndef _write_simple(w, name, value):\n\tw.writeStartElement(\"Simple\")\n\tw.writeTextElement(\"Name\", name)\n\tw.writeTextElement(\"String\", str(value))\n\tw.writeEndElement()\n\n\ndef _write_simple_with_sort(w, name, value, sort_value=None):\n\t\"\"\"Write a Simple element, with an optional nested SORT_WITH child.\"\"\"\n\tw.writeStartElement(\"Simple\")\n\tw.writeTextElement(\"Name\", name)\n\tw.writeTextElement(\"String\", str(value))\n\tif sort_value:\n\t\tw.writeStartElement(\"Simple\")\n\t\tw.writeTextElement(\"Name\", \"SORT_WITH\")\n\t\tw.writeTextElement(\"String\", str(sort_value))\n\t\tw.writeEndElement()\n\tw.writeEndElement()\n\n\ndef _write_original_artist(w, value):\n\t\"\"\"Write originalartist as a nested ORIGINAL/ARTIST structure per spec.\"\"\"\n\tw.writeStartElement(\"Simple\")\n\tw.writeTextElement(\"Name\", \"ORIGINAL\")\n\tw.writeStartElement(\"Simple\")\n\tw.writeTextElement(\"Name\", \"ARTIST\")\n\tw.writeTextElement(\"String\", str(value))\n\tw.writeEndElement()\n\tw.writeEndElement()\n\n\ndef _sort_field_for(mkv_name, target_type_value):\n\t\"\"\"Return the Picard sort field for a SORT_WITH element nested under mkv_name.\"\"\"\n\tif target_type_value < 50:\n\t\treturn {\n\t\t\t\"TITLE\": \"titlesort\",\n\t\t\t\"ARTIST\": \"artistsort\",\n\t\t\t\"COMPOSER\": \"composersort\",\n\t\t}.get(mkv_name)\n\tif target_type_value < 60:\n\t\treturn {\"TITLE\": \"albumsort\", \"ARTIST\": \"albumartistsort\"}.get(mkv_name)\n\treturn None\n\n\ndef _tags_xml(metadata):\n\t\"\"\"Combined XML with level-50 (album), level-60 (disc), and level-30 (track) Tag blocks.\n\n\tAll blocks have no UID in their Targets, which is the spec-correct form for\n\ta single-track file.  mkvpropedit writes them via --tags global:, and FFmpeg\n\troutes all no-UID tags to s->metadata regardless of TargetTypeValue.\n\n\tBecause the level-30 block is written last, level-30 TITLE/ARTIST overwrite\n\tthe level-50 values in fctx->metadata — giving Kodi the track title and track\n\tartist in its title/artist fields as desired.\n\n\tALBUM, ALBUM_ARTIST, and DATE are Kodi compatibility aliases.  They have no\n\tlevel-30 counterparts, so they survive in fctx->metadata after level-30\n\tprocessing.  Kodi reads these case-insensitively and maps them to its album,\n\talbumartist, and date fields respectively.\n\n\tTargetType string is omitted from all blocks: FFmpeg uses it as a key prefix\n\ton global tags (e.g. \"ALBUM/TITLE\"), which breaks Kodi's exact-match lookup.\n\tTargetTypeValue is still written for spec-compliant readers.\n\t\"\"\"\n\tbuf = QBuffer()\n\tbuf.open(QIODevice.WriteOnly)\n\tw = QXmlStreamWriter(buf)\n\tw.setAutoFormatting(True)\n\tw.writeStartDocument()\n\n\tw.writeStartElement(\"Tags\")\n\n\t# --- Level 50: album ---\n\tw.writeStartElement(\"Tag\")\n\tw.writeStartElement(\"Targets\")\n\tw.writeTextElement(\"TargetTypeValue\", \"50\")\n\tw.writeEndElement()  # Targets\n\tfor picard_name, mkv_name in _ALBUM_TAGS.items():\n\t\tsort_key = _ALBUM_SORT.get(picard_name)\n\t\tsort_val = metadata.get(sort_key, \"\") if sort_key else \"\"\n\t\tfor value in metadata.getall(picard_name):\n\t\t\tif value:\n\t\t\t\t_write_simple_with_sort(w, mkv_name, value, sort_val or None)\n\tfor value in metadata.getall(\"totaltracks\"):\n\t\tif value:\n\t\t\t_write_simple(w, \"TOTAL_PARTS\", value)\n\t# Kodi compat aliases: no level-30 equivalents so they persist in fctx->metadata\n\tfor value in metadata.getall(\"album\"):\n\t\tif value:\n\t\t\t_write_simple(w, \"ALBUM\", value)\n\tfor value in metadata.getall(\"albumartist\"):\n\t\tif value:\n\t\t\t_write_simple(w, \"ALBUM_ARTIST\", value)\n\t_date_field = \"originaldate\" if config.setting[\"mkv_use_original_date\"] else \"date\"\n\tfor value in metadata.getall(_date_field):\n\t\tif value:\n\t\t\t_write_simple(w, \"DATE\", value)\n\tw.writeEndElement()  # Tag (album)\n\n\t# --- Level 60: disc (omitted when no disc data is present) ---\n\tdisc_num = metadata.get(\"discnumber\", \"\")\n\ttotal_discs = metadata.get(\"totaldiscs\", \"\")\n\tif disc_num or total_discs:\n\t\tw.writeStartElement(\"Tag\")\n\t\tw.writeStartElement(\"Targets\")\n\t\tw.writeTextElement(\"TargetTypeValue\", \"60\")\n\t\tw.writeEndElement()  # Targets\n\t\tif disc_num:\n\t\t\t_write_simple(w, \"PART_NUMBER\", disc_num)\n\t\tif total_discs:\n\t\t\t_write_simple(w, \"TOTAL_PARTS\", total_discs)\n\t\tw.writeEndElement()  # Tag (disc)\n\n\t# --- Level 30: track ---\n\tw.writeStartElement(\"Tag\")\n\tw.writeStartElement(\"Targets\")\n\tw.writeTextElement(\"TargetTypeValue\", \"30\")\n\tw.writeEndElement()  # Targets\n\tfor picard_name, mkv_name in _TRACK_TAGS.items():\n\t\tsort_key = _TRACK_SORT.get(picard_name)\n\t\tsort_val = metadata.get(sort_key, \"\") if sort_key else \"\"\n\t\tfor value in metadata.getall(picard_name):\n\t\t\tif value:\n\t\t\t\t_write_simple_with_sort(w, mkv_name, value, sort_val or None)\n\tfor value in metadata.getall(\"originalartist\"):\n\t\tif value:\n\t\t\t_write_original_artist(w, value)\n\tfor value in metadata.getall(\"tracknumber\"):\n\t\tif value:\n\t\t\t_write_simple(w, \"PART_NUMBER\", value)\n\tw.writeEndElement()  # Tag (track)\n\n\tw.writeEndElement()  # Tags\n\tw.writeEndDocument()\n\tbuf.close()\n\treturn bytes(buf.data()).decode(\"utf-8\")\n\n\n# ---------------------------------------------------------------------------\n# XML parsing (Matroska tags XML → Picard Metadata)\n# ---------------------------------------------------------------------------\n\n\ndef _parse_tags_xml(xml_path, metadata):\n\t\"\"\"Read a Matroska tags XML file and populate a Metadata object.\"\"\"\n\tf = QFile(xml_path)\n\tif not f.open(QIODevice.ReadOnly):\n\t\tlog.warning(\"MkvPlugin: failed to open tags XML %r\", xml_path)\n\t\treturn\n\n\treader = QXmlStreamReader(f)\n\ttarget_type_value = 50\n\tin_tag = False\n\tin_targets = False\n\tin_ttv = False\n\tttv_text = \"\"\n\t# Stack of open Simple elements; each frame: {name, value, reading_name, reading_string}\n\tsimple_stack = []\n\n\twhile not reader.atEnd():\n\t\ttoken = reader.readNext()\n\t\tif token == QXmlStreamReader.StartElement:\n\t\t\tel = reader.name()\n\t\t\tif el == \"Tag\":\n\t\t\t\tin_tag = True\n\t\t\t\ttarget_type_value = 50\n\t\t\telif el == \"Targets\" and in_tag:\n\t\t\t\tin_targets = True\n\t\t\telif el == \"TargetTypeValue\" and in_targets:\n\t\t\t\tin_ttv = True\n\t\t\t\tttv_text = \"\"\n\t\t\telif el == \"Simple\" and in_tag:\n\t\t\t\tsimple_stack.append(\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"\",\n\t\t\t\t\t\t\"value\": \"\",\n\t\t\t\t\t\t\"reading_name\": False,\n\t\t\t\t\t\t\"reading_string\": False,\n\t\t\t\t\t}\n\t\t\t\t)\n\t\t\telif el == \"Name\" and simple_stack:\n\t\t\t\tsimple_stack[-1][\"reading_name\"] = True\n\t\t\telif el == \"String\" and simple_stack:\n\t\t\t\tsimple_stack[-1][\"reading_string\"] = True\n\t\telif token == QXmlStreamReader.EndElement:\n\t\t\tel = reader.name()\n\t\t\tif el == \"Tag\":\n\t\t\t\tin_tag = False\n\t\t\t\tsimple_stack.clear()\n\t\t\telif el == \"Targets\":\n\t\t\t\tin_targets = False\n\t\t\telif el == \"TargetTypeValue\":\n\t\t\t\tin_ttv = False\n\t\t\t\ttry:\n\t\t\t\t\ttarget_type_value = int(ttv_text)\n\t\t\t\texcept ValueError:\n\t\t\t\t\tpass\n\t\t\telif el == \"Name\" and simple_stack:\n\t\t\t\tsimple_stack[-1][\"reading_name\"] = False\n\t\t\telif el == \"String\" and simple_stack:\n\t\t\t\tsimple_stack[-1][\"reading_string\"] = False\n\t\t\telif el == \"Simple\" and simple_stack:\n\t\t\t\tframe = simple_stack.pop()\n\t\t\t\tmkv_name = frame[\"name\"].upper()\n\t\t\t\tvalue = frame[\"value\"]\n\n\t\t\t\tif simple_stack:\n\t\t\t\t\t# Nested Simple: handle SORT_WITH and ORIGINAL/ARTIST\n\t\t\t\t\tparent_name = simple_stack[-1][\"name\"].upper()\n\t\t\t\t\tif mkv_name == \"SORT_WITH\" and value:\n\t\t\t\t\t\tsort_field = _sort_field_for(parent_name, target_type_value)\n\t\t\t\t\t\tif sort_field:\n\t\t\t\t\t\t\tmetadata.add(sort_field, value)\n\t\t\t\t\telif parent_name == \"ORIGINAL\" and mkv_name == \"ARTIST\" and value:\n\t\t\t\t\t\tmetadata.add(\"originalartist\", value)\n\t\t\t\telse:\n\t\t\t\t\t# Top-level Simple\n\t\t\t\t\tif target_type_value >= 60:\n\t\t\t\t\t\tif value and mkv_name in _R_DISC_TAGS:\n\t\t\t\t\t\t\tmetadata.add(_R_DISC_TAGS[mkv_name], value)\n\t\t\t\t\telif target_type_value >= 50:\n\t\t\t\t\t\tif value:\n\t\t\t\t\t\t\tif mkv_name == \"TOTAL_PARTS\":\n\t\t\t\t\t\t\t\tmetadata.add(\"totaltracks\", value)\n\t\t\t\t\t\t\telif mkv_name in _R_ALBUM_TAGS:\n\t\t\t\t\t\t\t\tmetadata.add(_R_ALBUM_TAGS[mkv_name], value)\n\t\t\t\t\telse:\n\t\t\t\t\t\tif mkv_name == \"PART_NUMBER\":\n\t\t\t\t\t\t\tif value:\n\t\t\t\t\t\t\t\tif \"/\" in value:\n\t\t\t\t\t\t\t\t\tparts = value.split(\"/\", 1)\n\t\t\t\t\t\t\t\t\tmetadata.add(\"tracknumber\", parts[0].strip())\n\t\t\t\t\t\t\t\t\tmetadata.add(\"totaltracks\", parts[1].strip())\n\t\t\t\t\t\t\t\telse:\n\t\t\t\t\t\t\t\t\tmetadata.add(\"tracknumber\", value)\n\t\t\t\t\t\telif value and mkv_name in _R_TRACK_TAGS:\n\t\t\t\t\t\t\tmetadata.add(_R_TRACK_TAGS[mkv_name], value)\n\n\t\telif token == QXmlStreamReader.Characters:\n\t\t\ttext = reader.text()\n\t\t\tif in_ttv:\n\t\t\t\tttv_text += text\n\t\t\telif simple_stack:\n\t\t\t\tframe = simple_stack[-1]\n\t\t\t\tif frame[\"reading_name\"]:\n\t\t\t\t\tframe[\"name\"] += text\n\t\t\t\telif frame[\"reading_string\"]:\n\t\t\t\t\tframe[\"value\"] += text\n\n\tf.close()\n\tif reader.hasError():\n\t\tlog.warning(\n\t\t\t\"MkvPlugin: failed to parse tags XML %r: %s\", xml_path, reader.errorString()\n\t\t)\n\n\n# ---------------------------------------------------------------------------\n# Matroska file format base class\n# ---------------------------------------------------------------------------\n\n\nclass _MatroskaFileBase(File):\n\t\"\"\"\n\tHandles both reading and writing of Matroska tags via MKVToolNix.\n\n\t_load()  uses mkvmerge --identify for duration/codec info and mkvextract tags for existing tag values.\n\t_save()  generates a Matroska tags XML and feeds it to mkvpropedit.\n\t\"\"\"\n\n\t@classmethod\n\tdef supports_tag(cls, name):\n\t\treturn name in _SUPPORTED_TAGS\n\n\tdef _load(self, filename):\n\t\tlog.debug(\"MkvPlugin: loading %r\", filename)\n\t\ttooldir = _get_tooldir()\n\t\tmetadata = Metadata()\n\n\t\t# Read file info (duration, codec) via mkvmerge --identify\n\t\tmkvmerge = _tool_path(\"mkvmerge\", tooldir)\n\t\ttry:\n\t\t\tresult = _run(\n\t\t\t\t[mkvmerge, \"--identify\", \"--identification-format\", \"json\", filename],\n\t\t\t\tcapture_output=True,\n\t\t\t\ttext=True,\n\t\t\t\tencoding=\"utf-8\",\n\t\t\t)\n\t\t\tinfo = json.loads(result.stdout)\n\t\t\tduration_ns = (\n\t\t\t\tinfo.get(\"container\", {}).get(\"properties\", {}).get(\"duration\", 0)\n\t\t\t)\n\t\t\tif duration_ns:\n\t\t\t\tmetadata.length = duration_ns // 1_000_000  # ns → ms\n\t\texcept FileNotFoundError:\n\t\t\traise Exception(\n\t\t\t\tf\"mkvmerge not found at {mkvmerge!r}. \"\n\t\t\t\t\"Install MKVToolNix and set the folder path in \"\n\t\t\t\t\"Options > Plugins > Matroska Tagger.\"\n\t\t\t)\n\t\texcept subprocess.CalledProcessError as e:\n\t\t\tlog.warning(\"MkvPlugin: mkvmerge failed on %r: %s\", filename, e)\n\t\texcept (json.JSONDecodeError, KeyError) as e:\n\t\t\tlog.warning(\n\t\t\t\t\"MkvPlugin: could not parse mkvmerge output for %r: %s\", filename, e\n\t\t\t)\n\n\t\t# Read existing tags via mkvextract tags\n\t\tmkvextract = _tool_path(\"mkvextract\", tooldir)\n\t\ttmp_fd, tmp_path = tempfile.mkstemp(suffix=\".xml\", prefix=\"picard_mkv_\")\n\t\tos.close(tmp_fd)\n\t\ttry:\n\t\t\t# mkvextract returns 0 (clean) or 1 (warnings but still valid output).\n\t\t\t# check=True would treat exit code 1 as failure, so we check manually.\n\t\t\tresult = subprocess.run(\n\t\t\t\t[mkvextract, filename, \"tags\", tmp_path],\n\t\t\t\tcapture_output=True,\n\t\t\t\t**_subprocess_flags(),\n\t\t\t)\n\t\t\tif result.returncode >= 2:\n\t\t\t\traise subprocess.CalledProcessError(result.returncode, result.args)\n\t\t\tif os.path.getsize(tmp_path) > 0:\n\t\t\t\t_parse_tags_xml(tmp_path, metadata)\n\t\texcept FileNotFoundError:\n\t\t\traise Exception(\n\t\t\t\tf\"mkvextract not found at {mkvextract!r}. \"\n\t\t\t\t\"Install MKVToolNix and set the folder path in \"\n\t\t\t\t\"Options > Plugins > Matroska Tagger.\"\n\t\t\t)\n\t\texcept subprocess.CalledProcessError as e:\n\t\t\tlog.warning(\"MkvPlugin: mkvextract failed on %r: %s\", filename, e)\n\t\tfinally:\n\t\t\ttry:\n\t\t\t\tos.unlink(tmp_path)\n\t\t\texcept OSError:\n\t\t\t\tpass\n\n\t\treturn metadata\n\n\tdef _save(self, filename, metadata):\n\t\tlog.debug(\"MkvPlugin: saving %r\", filename)\n\t\ttooldir = _get_tooldir()\n\t\tmkvpropedit = _tool_path(\"mkvpropedit\", tooldir)\n\n\t\ttags_xml = _tags_xml(metadata)\n\t\tlog.debug(\"MkvPlugin: tags XML: %s\", tags_xml)\n\n\t\ttags_fd, tags_path = tempfile.mkstemp(suffix=\".xml\", prefix=\"picard_mkv_\")\n\t\ttry:\n\t\t\twith os.fdopen(tags_fd, \"w\", encoding=\"utf-8\") as f:\n\t\t\t\tf.write(tags_xml)\n\n\t\t\ttry:\n\t\t\t\tif config.setting[\"mkv_strip_chapters\"]:\n\t\t\t\t\t_run([mkvpropedit, filename, \"--chapters\", \"\"])\n\n\t\t\t\ttitle = metadata.get(\"title\", \"\")\n\t\t\t\t_run(\n\t\t\t\t\t[\n\t\t\t\t\t\tmkvpropedit,\n\t\t\t\t\t\tfilename,\n\t\t\t\t\t\t\"--edit\",\n\t\t\t\t\t\t\"info\",\n\t\t\t\t\t\t\"--set\",\n\t\t\t\t\t\tf\"title={title}\",\n\t\t\t\t\t\t\"--edit\",\n\t\t\t\t\t\t\"track:a1\",\n\t\t\t\t\t\t\"--set\",\n\t\t\t\t\t\tf\"name={title}\",\n\t\t\t\t\t\t\"--tags\",\n\t\t\t\t\t\t\"all:\",\n\t\t\t\t\t\t\"--tags\",\n\t\t\t\t\t\tf\"global:{tags_path}\",\n\t\t\t\t\t]\n\t\t\t\t)\n\t\t\texcept FileNotFoundError:\n\t\t\t\traise Exception(\n\t\t\t\t\tf\"mkvpropedit not found at {mkvpropedit!r}. \"\n\t\t\t\t\t\"Install MKVToolNix and set the folder path in \"\n\t\t\t\t\t\"Options > Plugins > Matroska Tagger.\"\n\t\t\t\t)\n\t\t\texcept subprocess.CalledProcessError as e:\n\t\t\t\traise Exception(f\"mkvpropedit failed: {e}\")\n\t\tfinally:\n\t\t\ttry:\n\t\t\t\tos.unlink(tags_path)\n\t\t\texcept OSError:\n\t\t\t\tpass\n\n\n# ---------------------------------------------------------------------------\n# Concrete format classes — one per extension so Picard treats them separately\n# ---------------------------------------------------------------------------\n\n\nclass MKVFile(_MatroskaFileBase):\n\tNAME = \"Matroska Video (MKV)\"\n\tEXTENSIONS = [\".mkv\"]\n\n\nclass MKAFile(_MatroskaFileBase):\n\tNAME = \"Matroska Audio (MKA)\"\n\tEXTENSIONS = [\".mka\"]\n\n\n# ---------------------------------------------------------------------------\n# Options page\n# ---------------------------------------------------------------------------\n\n\nclass _MkvOptionsPage(OptionsPage):\n\tNAME = \"matroska_tagger\"\n\tTITLE = \"Matroska Tagger\"\n\tPARENT = \"plugins\"\n\tSORT_ORDER = 100\n\n\tdef __init__(self, parent=None):\n\t\tsuper().__init__(parent)\n\n\t\tlayout = QVBoxLayout(self)\n\t\tlayout.setAlignment(_AlignTop)\n\n\t\tdesc = QLabel(\n\t\t\t\"Path to the folder containing MKVToolNix executables \"\n\t\t\t\"(mkvpropedit, mkvmerge, mkvextract). \"\n\t\t\t\"Leave empty to use the system PATH.\"\n\t\t)\n\t\tdesc.setWordWrap(True)\n\t\tlayout.addWidget(desc)\n\n\t\tform = QFormLayout()\n\n\t\tpath_row = QHBoxLayout()\n\t\tself._path_edit = QLineEdit()\n\t\tif sys.platform == \"win32\":\n\t\t\tself._path_edit.setPlaceholderText(r\"e.g. C:\\Program Files\\MKVToolNix\")\n\t\telse:\n\t\t\tself._path_edit.setPlaceholderText(\"e.g. /usr/bin  (leave empty for PATH)\")\n\t\tpath_row.addWidget(self._path_edit)\n\n\t\tbrowse_btn = QPushButton(\"Browse…\")\n\t\tbrowse_btn.clicked.connect(self._browse)\n\t\tpath_row.addWidget(browse_btn)\n\n\t\tform.addRow(\"MKVToolNix folder:\", path_row)\n\t\tlayout.addLayout(form)\n\n\t\ttest_row = QHBoxLayout()\n\t\tself._test_btn = QPushButton(\"Test\")\n\t\tself._test_btn.clicked.connect(self._test_tools)\n\t\ttest_row.addWidget(self._test_btn)\n\t\tself._test_label = QLabel(\"\")\n\t\ttest_row.addWidget(self._test_label)\n\t\ttest_row.addStretch()\n\t\tlayout.addLayout(test_row)\n\n\t\tself._strip_chapters_cb = QCheckBox(\"Remove chapter data when saving\")\n\t\tlayout.addWidget(self._strip_chapters_cb)\n\n\t\tself._use_original_date_cb = QCheckBox(\n\t\t\t\"Use original release date as DATE tag (instead of this release's date)\"\n\t\t)\n\t\tlayout.addWidget(self._use_original_date_cb)\n\n\tdef _browse(self):\n\t\tfolder = QFileDialog.getExistingDirectory(\n\t\t\tself, \"Select MKVToolNix folder\", self._path_edit.text()\n\t\t)\n\t\tif folder:\n\t\t\tself._path_edit.setText(folder)\n\n\tdef _test_tools(self):\n\t\ttooldir = self._path_edit.text().strip()\n\t\tresults = []\n\t\tfor name in (\"mkvpropedit\", \"mkvmerge\", \"mkvextract\"):\n\t\t\tpath = _tool_path(name, tooldir)\n\t\t\ttry:\n\t\t\t\t_run([path, \"--version\"], capture_output=True)\n\t\t\t\tresults.append(f\"✓ {name}\")\n\t\t\texcept FileNotFoundError:\n\t\t\t\tresults.append(f\"✗ {name} not found\")\n\t\t\texcept subprocess.CalledProcessError as e:\n\t\t\t\tresults.append(f\"✗ {name} error ({e.returncode})\")\n\t\tself._test_label.setText(\"  \".join(results))\n\n\tdef load(self):\n\t\tself._path_edit.setText(config.setting[\"mkvtoolnix_dir\"])\n\t\tself._strip_chapters_cb.setChecked(config.setting[\"mkv_strip_chapters\"])\n\t\tself._use_original_date_cb.setChecked(config.setting[\"mkv_use_original_date\"])\n\n\tdef save(self):\n\t\tconfig.setting[\"mkvtoolnix_dir\"] = self._path_edit.text().strip()\n\t\tconfig.setting[\"mkv_strip_chapters\"] = self._strip_chapters_cb.isChecked()\n\t\tconfig.setting[\"mkv_use_original_date\"] = self._use_original_date_cb.isChecked()\n\n\n# ---------------------------------------------------------------------------\n# Registration\n# ---------------------------------------------------------------------------\n\n\ndef _default_tooldir():\n\tif sys.platform == \"win32\":\n\t\tprog_files = os.environ.get(\"ProgramFiles\", r\"C:\\Program Files\")\n\t\tcandidate = os.path.join(prog_files, \"MKVToolNix\")\n\t\tif os.path.isdir(candidate):\n\t\t\treturn candidate\n\treturn \"\"\n\n\nTextOption(\"setting\", \"mkvtoolnix_dir\", _default_tooldir())\nBoolOption(\"setting\", \"mkv_strip_chapters\", False)\nBoolOption(\"setting\", \"mkv_use_original_date\", True)\nregister_format(MKVFile)\nregister_format(MKAFile)\nregister_options_page(_MkvOptionsPage)\nlog.info(\"MkvPlugin: Matroska Tagger enabled (MKV + MKA)\")\n"
  },
  {
    "path": "plugins/mod/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2022, 2025 Philipp Wolfer\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\n# For Python 3.8 compatibility\nfrom __future__ import annotations\n\nPLUGIN_NAME = 'MOD files'\nPLUGIN_AUTHOR = 'Philipp Wolfer'\nPLUGIN_DESCRIPTION = (\n    'Support for loading and renaming various tracker module formats '\n    '(.mod, .xm, .it, .mptm, .ahx, .mtm, .med, .s3m, .ult, .699, .okt). '\n    'There is limited support for writing the title tag as track name for '\n    'some formats.'\n)\nPLUGIN_VERSION = \"0.2.1\"\nPLUGIN_API_VERSIONS = [\"2.8\"]\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom collections import namedtuple\nfrom enum import Enum\nfrom io import RawIOBase\nimport struct\n\nfrom mutagen._util import resize_bytes\n\nfrom picard import log\nfrom picard.file import File\nfrom picard.formats import register_format\nfrom picard.metadata import Metadata\nfrom picard.util.textencoding import asciipunct\n\n\nclass FieldAccess(Enum):\n    READ_ONLY = 0\n    READ_WRITE = 1\n\n\nStaticField = namedtuple('StaticField', 'name offset length access fillchar', defaults=(' ',))\n\n\nclass MagicBytes(bytes):\n    offset: int = 0\n\n    def __new__(cls, value, offset: int = 0):\n        self = super().__new__(cls, value)\n        self.offset = offset\n        return self\n\n\nclass ModuleFile(File):\n    # Allows to specify a magic number to identify the file format.\n    # Re-implement _ensure_format for more complex cases.\n    _magic_bytes: tuple[MagicBytes] = None\n\n    # Specify the encoding for the format.\n    # There is not much info about encoding, most files seem to be limited\n    # to ASCII. But cp850 seems to be a solid default for the few files that\n    # use 8-bit characters.\n    _encoding = 'cp850'  # or cp437?\n\n    # List of StaticField. Most fields in mod files\n    # have static position and fixed size.\n    _static_text_fields: tuple[StaticField] = ()\n\n    @classmethod\n    def supports_tag(cls, name: str) -> bool:\n        return name in {field.name for field in cls._static_text_fields}\n\n    def _load(self, filename: str) -> Metadata:\n        log.debug('Loading file %r', filename)\n        metadata = Metadata()\n        self._add_path_to_metadata(metadata)\n        metadata['~format'] = self.NAME\n        with open(filename, 'rb') as f:\n            magic = self._ensure_format(f)\n            self._parse_file(f, metadata, magic)\n        return metadata\n\n    def _save(self, filename: str, metadata: Metadata):\n        log.debug('Saving file %r', filename)\n        with open(filename, 'rb+') as f:\n            self._ensure_format(f)\n            self._write_file(f, metadata)\n\n    def _ensure_format(self, f: RawIOBase) -> MagicBytes:\n        if not self._magic_bytes:\n            raise NotImplementedError('_magic_bytes not set or method not implemented')\n        for magic in self._magic_bytes:\n            if self._magic_matches(f, magic):\n                return magic\n        # None of the magic byte sequences matched, fail loading\n        raise ValueError('Not a %s file' % self.NAME)\n\n    def _magic_matches(self, f: RawIOBase, magic: MagicBytes) -> bool:\n        f.seek(magic.offset)\n        file_id = f.read(len(magic))\n        return file_id == magic\n\n    def _parse_file(self, f: RawIOBase, metadata: Metadata, magic: MagicBytes):\n        for field in self._static_text_fields:\n            f.seek(field.offset)\n            metadata[field.name] = self._decode_text(f.read(field.length))\n\n    def _write_file(self, f: RawIOBase, metadata: Metadata):\n        for field in self._static_text_fields:\n            if field.access == FieldAccess.READ_WRITE and not field.name.startswith('~'):\n                f.seek(field.offset)\n                f.write(self._encode_text(metadata[field.name], field.length, field.fillchar))\n\n    def _decode_text(self, data: bytes) -> str:\n        return data.decode(self._encoding, errors='replace').strip().strip('\\0')\n\n    def _encode_text(self, text: str, length: int = None, fillchar: str = ' ') -> bytes:\n        if length:\n            text = text[:length].ljust(length, fillchar)\n        return asciipunct(text).encode(self._encoding, errors='replace')\n\n\nclass MODFile(ModuleFile):\n    EXTENSIONS = ['.mod']\n    NAME = 'MOD'\n\n    # https://www.ocf.berkeley.edu/~eek/index.html/tiny_examples/ptmod/ap12.html\n    _magic_bytes = (MagicBytes(b'M\\x2eK\\x2e', offset=1080),)\n    _static_text_fields = (\n        StaticField('title', 0, 20, FieldAccess.READ_WRITE),\n    )\n\n\nclass ExtendedModuleFile(ModuleFile):\n    EXTENSIONS = ['.xm']\n    NAME = 'Extended Module'\n\n    # https://github.com/milkytracker/MilkyTracker/blob/master/resources/reference/xm-form.txt\n    _magic_bytes = (MagicBytes(b'Extended Module: '),)\n    _static_text_fields = (\n        StaticField('title', 17, 20, FieldAccess.READ_WRITE),\n        StaticField('encodedby', 38, 20, FieldAccess.READ_WRITE),\n    )\n\n    def _parse_file(self, f: RawIOBase, metadata: Metadata, magic: MagicBytes):\n        super()._parse_file(f, metadata, magic)\n        # OpenMPT seems to use iso-8859-1 encoding.\n        if metadata['encodedby'].startswith('OpenMPT'):\n            self._encoding = 'iso-8859-1'\n            super()._parse_file(f, metadata, magic)\n        f.seek(68)\n        metadata['~channels'] = struct.unpack('<h', f.read(2))[0]\n\n\nclass ImpulseTrackerFile(ModuleFile):\n    EXTENSIONS = ['.it', '.mptm']\n    NAME = 'Impulse Tracker'\n\n    # https://fileformats.fandom.com/wiki/Impulse_tracker\n    # https://wiki.openmpt.org/Manual:_Module_formats#The_OpenMPT_format_.28.mptm.29\n    _magic_bytes = (MagicBytes(b'IMPM'),)\n    _static_text_fields = (\n        StaticField('title', 4, 26, FieldAccess.READ_WRITE, '\\0'),\n    )\n\n    def _parse_file(self, f: RawIOBase, metadata: Metadata, magic: MagicBytes):\n        if self._magic_matches(f, MagicBytes(b'OMPT', offset=60)):\n            self._encoding = 'iso-8859-1'\n            metadata['~format'] = 'OpenMPT'\n            # TODO: For OpenMPT enhanced format parse also the author and comment\n        super()._parse_file(f, metadata, magic)\n\n\nclass AHXFile(ModuleFile):\n    EXTENSIONS = ['.ahx']\n    NAME = 'AHX'\n\n    # http://lclevy.free.fr/exotica/ahx/ahxformat.txt\n    _magic_bytes = (MagicBytes(b'THX'),)\n\n    @classmethod\n    def supports_tag(cls, name: str) -> bool:\n        return name in {'title'}\n\n    def _parse_file(self, f: RawIOBase, metadata: Metadata, magic: MagicBytes):\n        self._seek_names_offset(f)\n        metadata['title'] = self._decode_text(self._read_string(f))\n\n    def _write_file(self, f: RawIOBase, metadata: Metadata):\n        # Write the title (first null terminated string after the samples)\n        names_offset = self._seek_names_offset(f)\n        old_title = self._read_string(f)\n        new_title = self._encode_text(metadata['title'])\n        resize_bytes(f, len(old_title), len(new_title), names_offset)\n        f.seek(names_offset)\n        f.write(new_title)\n\n    def _seek_names_offset(self, f: RawIOBase) -> int:\n        f.seek(6)\n        len_ = struct.unpack('>H', f.read(2))[0] & 0xfff\n        f.seek(10)\n        trl, trk, smp, ss = struct.unpack('BBBB', f.read(4))\n        samples_offset = 14 + ss*2 + len_*8 + (trk+1)*trl*3\n        f.seek(samples_offset)\n        self._skip_samples(f, count=smp)\n        return f.tell()\n\n    def _skip_samples(self, f: RawIOBase, count: int):\n        while count:\n            f.seek(f.tell() + 21)\n            plen = struct.unpack('B', f.read(1))[0]\n            f.seek(f.tell() + plen*4)\n            count -= 1\n\n    def _read_string(self, f: RawIOBase) -> bytes:\n        \"\"\"Reads a null terminated string from the current position.\"\"\"\n        result = b''\n        char = f.read(1)\n        while char and char != b'\\0':\n            result += char\n            char = f.read(1)\n        return result\n\n\nclass MEDFile(ModuleFile):\n    EXTENSIONS = ['.med']\n    NAME = 'MED'\n\n    # https://github.com/dv1/ion_player/blob/master/extern/uade-2.13/amigasrc/players/med/MMD_FileFormat.doc\n    _magic_bytes = (\n        MagicBytes(b'MMD0'),\n        MagicBytes(b'MMD1'),\n        MagicBytes(b'MMD2'),\n        MagicBytes(b'MMD3'),\n    )\n\n    def _parse_file(self, f: RawIOBase, metadata: Metadata, magic: MagicBytes):\n        # TODO: Extract songname\n        super()._parse_file(f, metadata, magic)\n        metadata['~format'] = '%s (%s)' % (self.NAME, self._decode_text(magic))\n\n\nclass MTMFile(ModuleFile):\n    EXTENSIONS = ['.mtm']\n    NAME = 'MTM'\n\n    # https://www.fileformat.info/format/mtm/corion.htm\n    _magic_bytes = (MagicBytes(b'MTM'),)\n    _static_text_fields = (\n        StaticField('title', 4, 20, FieldAccess.READ_WRITE, '\\0'),\n    )\n\n\nclass S3MFile(ModuleFile):\n    EXTENSIONS = ['.s3m']\n    NAME = 'S3M'\n\n    # https://www.fileformat.info/format/screamtracker/corion.htm\n    _magic_bytes = (MagicBytes(b'\\x1a', offset=28),)\n    _static_text_fields = (\n        StaticField('title', 0, 20, FieldAccess.READ_WRITE, '\\0'),\n        StaticField('encodedby', 20, 8, FieldAccess.READ_WRITE, '\\0'),\n    )\n\n\nclass ULTFile(ModuleFile):\n    EXTENSIONS = ['.ult']\n    NAME = 'ULT'\n\n    # http://www.textfiles.com/programming/FORMATS/ultform.pro\n    # http://www.textfiles.com/programming/FORMATS/ultform14.pro\n    _magic_bytes = (\n        MagicBytes(b'MAS_UTrack_V001'),\n        MagicBytes(b'MAS_UTrack_V002'),\n        MagicBytes(b'MAS_UTrack_V003'),\n        MagicBytes(b'MAS_UTrack_V004'),\n    )\n    _static_text_fields = (\n        StaticField('title', 15, 32, FieldAccess.READ_WRITE),\n    )\n\n    def _parse_file(self, f: RawIOBase, metadata: Metadata, magic: MagicBytes):\n        super()._parse_file(f, metadata, magic)\n        metadata['~format'] = '%s (%s)' % (self.NAME, self._decode_text(magic))\n\n\nclass Composer669File(ModuleFile):\n    EXTENSIONS = ['.669']\n    NAME = 'Composer 669'\n\n    # http://www.textfiles.com/programming/FORMATS/669-form.pro\n    _magic_bytes = (\n        MagicBytes(b'if'),\n        MagicBytes(b'JN'),  # Extended 669\n    )\n    _static_text_fields = (\n        StaticField('comment', 2, 108, FieldAccess.READ_WRITE),\n    )\n\n    def _parse_file(self, f: RawIOBase, metadata: Metadata, magic: MagicBytes):\n        super()._parse_file(f, metadata, magic)\n        if magic == b'JN':\n            metadata['~format'] = 'Extended Composer 669'\n\n\nclass OktalyzerFile(ModuleFile):\n    EXTENSIONS = ['.okt']\n    NAME = 'Oktalyzer'\n\n    # http://www.vgmpf.com/Wiki/index.php?title=OKT\n    _magic_bytes = (MagicBytes(b'OKTASONGCMOD'),)\n\n\nregister_format(MODFile)\nregister_format(ExtendedModuleFile)\nregister_format(ImpulseTrackerFile)\nregister_format(AHXFile)\nregister_format(MEDFile)\nregister_format(MTMFile)\nregister_format(S3MFile)\nregister_format(ULTFile)\nregister_format(Composer669File)\nregister_format(OktalyzerFile)\n"
  },
  {
    "path": "plugins/moodbars/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Changelog:\n# [2015-09-24] Initial version with support for Ogg Vorbis, FLAC, WAV and MP3, tested MP3 and FLAC\n# [2017-11-21] Amended to Python3 & Qt5\n# [2017-11-21] removed unicode, replaced str with string_ and untrusted input on check_call addressed\n\nPLUGIN_NAME = \"Moodbars\"\nPLUGIN_AUTHOR = \"Len Joubert, Sambhav Kothari\"\nPLUGIN_DESCRIPTION = \"\"\"Calculate Moodbars for selected files and albums.<br /><br />\nAccording to <a href=\"http://en.wikipedia.org/wiki/Moodbar\">WikiPedia</a>\na \"Moodbar is a computer visualization used for navigating within a piece of music or any other recording on a digital audio track.\nThis is done with a commonly horizontal bar that is divided into vertical stripes.\nEach stripe has a colour combination showing the \"mood\" within a short part of the audio track.\"<br /><br />\nTo use this plugin you will need to download special executables to create the moodbars -\nat the time of writing, executables are only available for various Linux distributions\n(see the <a href=\"http://userbase.kde.org/Amarok/Manual/Various/Moodbar\">Amarok Moodbar page<a> for details).\n\"\"\"\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\nPLUGIN_VERSION = \"2.3.3\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\n# PLUGIN_INCOMPATIBLE_PLATFORMS = [\n#    'win32', 'cygwyn', 'darwin', 'os2', 'os2emx', 'riscos', 'atheos']\n\nimport os.path\nfrom functools import partial\nfrom collections import defaultdict\nfrom subprocess import check_call\nfrom picard.album import Album, NatAlbum\nfrom picard.track import Track\nfrom picard.file import File\nfrom picard.util import encode_filename, decode_filename, thread\nfrom picard.ui.options import register_options_page, OptionsPage\nfrom picard.config import TextOption\nfrom picard.ui.itemviews import (BaseAction, register_file_action,\n                                 register_album_action)\nfrom picard.plugins.moodbars.ui_options_moodbar import Ui_MoodbarOptionsPage\n\n# Path to various moodbar tools. There must be a tool for every supported\n# audio file format.\nMOODBAR_COMMANDS = {\n    \"Ogg Vorbis\": (\"moodbar_vorbis_command\", \"moodbar_vorbis_options\"),\n    \"MPEG-1 Audio\": (\"moodbar_mp3_command\", \"moodbar_mp3_options\"),\n    \"FLAC\": (\"moodbar_flac_command\", \"moodbar_flac_options\"),\n    \"WavPack\": (\"moodbar_wav_command\", \"moodbar_wav_options\"),\n}\n\n\ndef generate_moodbar_for_files(files, format, tagger):\n    \"\"\"Generate the moodfiles for a list of files in album mode.\"\"\"\n    # python3 list for subprocess\n    command_to_execute = []\n    file_list = [files]\n    for mood_file in file_list:\n        new_filename = os.path.join(os.path.dirname(\n            mood_file), '.' + os.path.splitext(os.path.basename(mood_file))[0] + '.mood')\n        # file format to make it compaitble with Amarok and hidden in linux\n        file_list_mood = ['%s' % new_filename]\n        file_list_music = ['%s' % file_list[0]]\n\n    if format in MOODBAR_COMMANDS \\\n            and tagger.config.setting[MOODBAR_COMMANDS[format][0]]:\n        command = tagger.config.setting[MOODBAR_COMMANDS[format][0]]\n        options = tagger.config.setting[\n            MOODBAR_COMMANDS[format][1]].split(' ')\n        # tagger.log.debug('My debug file_list_mood >>>  %s' %\n        #    (file_list_mood))\n        #tagger.log.debug('My debug file_list >>>  %s' % (file_list_music))\n        #tagger.log.debug('My debug command >>>  %s' % (command))\n        #tagger.log.debug('My debug options >>>  %s' % (options))\n        # tagger.log.debug(\n        #    '%s %s %s %s' % (command, decode_filename(' '.join(file_list)), ' '.join(options), decode_filename(' '.join(file_list_mood))))\n        # command args order corrected for new moodbar\n        command_to_execute.append(command)\n        command_to_execute = command_to_execute + options\n        # need to decode the file string and appand to the list\n        command_to_execute.append(file_list_mood[0])\n        command_to_execute = command_to_execute + file_list_music\n        tagger.log.debug('My debug command to execute >>>  %s' %\n                         (command_to_execute))\n        check_call(command_to_execute, shell=False)\n        # check_call([command] + options + file_list_mood + file_list)\n    else:\n        raise Exception('Moodbar: Unsupported format %s' % (format))\n\n\nclass MoodBar(BaseAction):\n    NAME = N_(\"Generate Moodbar &file...\")\n\n    def _add_file_to_queue(self, file):\n        thread.run_task(\n            partial(self._generate_moodbar, file),\n            partial(self._moodbar_callback, file))\n\n    def callback(self, objs):\n        for obj in objs:\n            if isinstance(obj, Track):\n                for file_ in obj.linked_files:\n                    self._add_file_to_queue(file_)\n            elif isinstance(obj, File):\n                self._add_file_to_queue(obj)\n\n    def _generate_moodbar(self, file):\n        self.tagger.window.set_statusbar_message(\n            N_('Calculating moodbar for \"%(filename)s\"...'),\n            {'filename': file.filename}\n        )\n        generate_moodbar_for_files(file.filename, file.NAME, self.tagger)\n\n    def _moodbar_callback(self, file, result=None, error=None):\n        if not error:\n            self.tagger.window.set_statusbar_message(\n                N_('Moodbar for \"%(filename)s\" successfully generated.'),\n                {'filename': file.filename}\n            )\n        else:\n            self.tagger.window.set_statusbar_message(\n                N_('Could not generate moodbar for \"%(filename)s\".'),\n                {'filename': file.filename}\n            )\n\n\nclass MoodbarOptionsPage(OptionsPage):\n\n    NAME = \"Moodbars\"\n    TITLE = \"Moodbars\"\n    PARENT = \"plugins\"\n\n    options = [\n        TextOption(\"setting\", \"moodbar_vorbis_command\", \"moodbar\"),\n        TextOption(\"setting\", \"moodbar_vorbis_options\", \"-o\"),\n        TextOption(\"setting\", \"moodbar_mp3_command\", \"moodbar\"),\n        TextOption(\"setting\", \"moodbar_mp3_options\", \"-o\"),\n        TextOption(\"setting\", \"moodbar_flac_command\", \"moodbar\"),\n        TextOption(\"setting\", \"moodbar_flac_options\", \"-o\"),\n        TextOption(\"setting\", \"moodbar_wav_command\", \"moodbar\"),\n        TextOption(\"setting\", \"moodbar_wav_options\", \"-o\")\n    ]\n\n    def __init__(self, parent=None):\n        super(MoodbarOptionsPage, self).__init__(parent)\n        self.ui = Ui_MoodbarOptionsPage()\n        self.ui.setupUi(self)\n\n    def load(self):\n        self.ui.vorbis_command.setText(\n            self.config.setting[\"moodbar_vorbis_command\"])\n        self.ui.mp3_command.setText(\n            self.config.setting[\"moodbar_mp3_command\"])\n        self.ui.flac_command.setText(\n            self.config.setting[\"moodbar_flac_command\"])\n        self.ui.wav_command.setText(\n            self.config.setting[\"moodbar_wav_command\"])\n\n    def save(self):\n        self.config.setting[\"moodbar_vorbis_command\"] = self.ui.vorbis_command.text()\n        self.config.setting[\"moodbar_mp3_command\"] = self.ui.mp3_command.text()\n        self.config.setting[\"moodbar_flac_command\"] = self.ui.flac_command.text()\n        self.config.setting[\"moodbar_wav_command\"] = self.ui.wav_command.text()\n\nregister_file_action(MoodBar())\nregister_options_page(MoodbarOptionsPage)\n"
  },
  {
    "path": "plugins/moodbars/ui_options_moodbar.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/moodbars/ui_options_moodbar.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.4\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_MoodbarOptionsPage(object):\n    def setupUi(self, MoodbarOptionsPage):\n        MoodbarOptionsPage.setObjectName(\"MoodbarOptionsPage\")\n        MoodbarOptionsPage.resize(305, 317)\n        self.vboxlayout = QtWidgets.QVBoxLayout(MoodbarOptionsPage)\n        self.vboxlayout.setContentsMargins(9, 9, 9, 9)\n        self.vboxlayout.setSpacing(6)\n        self.vboxlayout.setObjectName(\"vboxlayout\")\n        self.moodbar_group = QtWidgets.QGroupBox(MoodbarOptionsPage)\n        self.moodbar_group.setObjectName(\"moodbar_group\")\n        self.vboxlayout1 = QtWidgets.QVBoxLayout(self.moodbar_group)\n        self.vboxlayout1.setContentsMargins(9, 9, 9, 9)\n        self.vboxlayout1.setSpacing(2)\n        self.vboxlayout1.setObjectName(\"vboxlayout1\")\n        self.label = QtWidgets.QLabel(self.moodbar_group)\n        self.label.setObjectName(\"label\")\n        self.vboxlayout1.addWidget(self.label)\n        self.vorbis_command = QtWidgets.QLineEdit(self.moodbar_group)\n        self.vorbis_command.setObjectName(\"vorbis_command\")\n        self.vboxlayout1.addWidget(self.vorbis_command)\n        self.label_2 = QtWidgets.QLabel(self.moodbar_group)\n        self.label_2.setObjectName(\"label_2\")\n        self.vboxlayout1.addWidget(self.label_2)\n        self.mp3_command = QtWidgets.QLineEdit(self.moodbar_group)\n        self.mp3_command.setObjectName(\"mp3_command\")\n        self.vboxlayout1.addWidget(self.mp3_command)\n        self.label_3 = QtWidgets.QLabel(self.moodbar_group)\n        self.label_3.setObjectName(\"label_3\")\n        self.vboxlayout1.addWidget(self.label_3)\n        self.flac_command = QtWidgets.QLineEdit(self.moodbar_group)\n        self.flac_command.setObjectName(\"flac_command\")\n        self.vboxlayout1.addWidget(self.flac_command)\n        self.label_4 = QtWidgets.QLabel(self.moodbar_group)\n        self.label_4.setObjectName(\"label_4\")\n        self.vboxlayout1.addWidget(self.label_4)\n        self.wav_command = QtWidgets.QLineEdit(self.moodbar_group)\n        self.wav_command.setObjectName(\"wav_command\")\n        self.vboxlayout1.addWidget(self.wav_command)\n        self.vboxlayout.addWidget(self.moodbar_group)\n        spacerItem = QtWidgets.QSpacerItem(263, 21, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.vboxlayout.addItem(spacerItem)\n\n        self.retranslateUi(MoodbarOptionsPage)\n        QtCore.QMetaObject.connectSlotsByName(MoodbarOptionsPage)\n\n    def retranslateUi(self, MoodbarOptionsPage):\n        _translate = QtCore.QCoreApplication.translate\n        self.moodbar_group.setTitle(_translate(\"MoodbarOptionsPage\", \"Moodbar\"))\n        self.label.setText(_translate(\"MoodbarOptionsPage\", \"Path to Ogg Vorbis moodbar tool:\"))\n        self.label_2.setText(_translate(\"MoodbarOptionsPage\", \"Path to MP3 moodbar tool:\"))\n        self.label_3.setText(_translate(\"MoodbarOptionsPage\", \"Path to FLAC moodbar tool:\"))\n        self.label_4.setText(_translate(\"MoodbarOptionsPage\", \"Path to WAV moodbar tool:\"))\n"
  },
  {
    "path": "plugins/moodbars/ui_options_moodbar.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>MoodbarOptionsPage</class>\n <widget class=\"QWidget\" name=\"MoodbarOptionsPage\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>305</width>\n    <height>317</height>\n   </rect>\n  </property>\n  <layout class=\"QVBoxLayout\">\n   <property name=\"spacing\">\n    <number>6</number>\n   </property>\n   <property name=\"margin\">\n    <number>9</number>\n   </property>\n   <item>\n    <widget class=\"QGroupBox\" name=\"moodbar_group\">\n     <property name=\"title\">\n      <string>Moodbar</string>\n     </property>\n     <layout class=\"QVBoxLayout\">\n      <property name=\"spacing\">\n       <number>2</number>\n      </property>\n      <property name=\"margin\">\n       <number>9</number>\n      </property>\n      <item>\n       <widget class=\"QLabel\" name=\"label\">\n        <property name=\"text\">\n         <string>Path to Ogg Vorbis moodbar tool:</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLineEdit\" name=\"vorbis_command\"/>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"label_2\">\n        <property name=\"text\">\n         <string>Path to MP3 moodbar tool:</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLineEdit\" name=\"mp3_command\"/>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"label_3\">\n        <property name=\"text\">\n         <string>Path to FLAC moodbar tool:</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLineEdit\" name=\"flac_command\"/>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"label_4\">\n        <property name=\"text\">\n         <string>Path to WAV moodbar tool:</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLineEdit\" name=\"wav_command\"/>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <spacer>\n     <property name=\"orientation\">\n      <enum>Qt::Vertical</enum>\n     </property>\n     <property name=\"sizeHint\" stdset=\"0\">\n      <size>\n       <width>263</width>\n       <height>21</height>\n      </size>\n     </property>\n    </spacer>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/musixmatch/README",
    "content": "Installation:\n        Obtain API key from Musixmatch (https://developer.musixmatch.com)\n"
  },
  {
    "path": "plugins/musixmatch/__init__.py",
    "content": "PLUGIN_NAME = 'Musixmatch Lyrics'\nPLUGIN_AUTHOR = 'm-yn, Sambhav Kothari, Philipp Wolfer'\nPLUGIN_DESCRIPTION = 'Fetch first 30% of lyrics from Musixmatch'\nPLUGIN_VERSION = '1.1.1'\nPLUGIN_API_VERSIONS = [\"2.0\"]\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\n\nfrom functools import partial\nfrom picard import config, log\nfrom picard.metadata import register_track_metadata_processor\nfrom picard.ui.options import register_options_page, OptionsPage\nfrom .ui_options_musixmatch import Ui_MusixmatchOptionsPage\n\n\nMUSIXMATCH_HOST = 'api.musixmatch.com'\nMUSIXMATCH_PORT = 80\n\n\ndef handle_result(album, metadata, data, reply, error):\n    try:\n        if error:\n            log.error(error)\n            return\n        message = data.get('message', {})\n        header = message.get('header')\n        if header.get('status_code') != 200:\n            log.warning('MusixMatch: Server returned no result: %s', data)\n            return\n        result = message.get('body', {}).get('lyrics')\n        if result:\n            lyrics = result.get('lyrics_body')\n            if lyrics:\n                metadata['lyrics:description'] = lyrics\n    except AttributeError:\n        log.error('MusixMatch: Error handling server response %s',\n                  data, exc_info=True)\n    finally:\n        album._requests -= 1\n        album._finalize_loading(error)\n\n\ndef process_track(album, metadata, track, release):\n    apikey = config.setting['musixmatch_api_key']\n    if not apikey:\n        log.warning('MusixMatch: No API key configured')\n        return\n    if metadata['language'] == 'zxx':\n        log.debug('MusixMatch: Track %s has no lyrics, skipping query',\n                  metadata['musicbrainz_recordingid'])\n        return\n    queryargs = {\n        'apikey': apikey,\n        'track_mbid': metadata['musicbrainz_recordingid']\n    }\n    album.tagger.webservice.get(\n        MUSIXMATCH_HOST,\n        MUSIXMATCH_PORT,\n        \"/ws/1.1/track.lyrics.get\",\n        partial(handle_result, album, metadata),\n        parse_response_type='json',\n        queryargs=queryargs\n    )\n    album._requests += 1\n\n\nclass MusixmatchOptionsPage(OptionsPage):\n    NAME = 'musixmatch'\n    TITLE = 'Musixmatch API Key'\n    PARENT = \"plugins\"\n    options = [\n        config.TextOption(\"setting\", \"musixmatch_api_key\", \"\")\n    ]\n\n    def __init__(self, parent=None):\n        super(MusixmatchOptionsPage, self).__init__(parent)\n        self.ui = Ui_MusixmatchOptionsPage()\n        self.ui.setupUi(self)\n\n    def load(self):\n        self.ui.api_key.setText(config.setting[\"musixmatch_api_key\"])\n\n    def save(self):\n        self.config.setting[\"musixmatch_api_key\"] = self.ui.api_key.text()\n\n\nregister_track_metadata_processor(process_track)\nregister_options_page(MusixmatchOptionsPage)\n"
  },
  {
    "path": "plugins/musixmatch/ui_options_musixmatch.py",
    "content": "from PyQt5 import QtCore, QtWidgets\n\n\nclass Ui_MusixmatchOptionsPage(object):\n\n    def setupUi(self, MusixmatchOptionsPage):\n        MusixmatchOptionsPage.setObjectName(\"MusixmatchOptionsPage\")\n        MusixmatchOptionsPage.resize(305, 317)\n        self.vboxlayout = QtWidgets.QVBoxLayout(MusixmatchOptionsPage)\n        self.vboxlayout.setContentsMargins(9, 9, 9, 9)\n        self.vboxlayout.setSpacing(6)\n        self.vboxlayout.setObjectName(\"vboxlayout\")\n        self.api_key_group = QtWidgets.QGroupBox(MusixmatchOptionsPage)\n        self.api_key_group.setObjectName(\"api_key_group\")\n        self.vboxlayout2 = QtWidgets.QVBoxLayout(self.api_key_group)\n        self.vboxlayout2.setContentsMargins(9, 9, 9, 9)\n        self.vboxlayout2.setSpacing(2)\n        self.vboxlayout2.setObjectName(\"vboxlayout2\")\n        self.api_key_label = QtWidgets.QLabel(self.api_key_group)\n        self.api_key_label.setObjectName(\"api_key_label\")\n        self.vboxlayout2.addWidget(self.api_key_label)\n        self.api_key = QtWidgets.QLineEdit(self.api_key_group)\n        self.api_key.setObjectName(\"api_key\")\n        self.vboxlayout2.addWidget(self.api_key)\n        self.vboxlayout.addWidget(self.api_key_group)\n        spacerItem = QtWidgets.QSpacerItem(263, 21, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.vboxlayout.addItem(spacerItem)\n\n        self.retranslateUi(MusixmatchOptionsPage)\n        QtCore.QMetaObject.connectSlotsByName(MusixmatchOptionsPage)\n\n    def retranslateUi(self, MusixmatchOptionsPage):\n        self.api_key_label.setText(_(\"Musixmatch API Key\"))\n"
  },
  {
    "path": "plugins/no_release/no_release.py",
    "content": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = 'No release'\nPLUGIN_AUTHOR = 'Johannes Weißl, Philipp Wolfer'\nPLUGIN_DESCRIPTION = '''Do not store specific release information in releases of unknown origin.'''\nPLUGIN_VERSION = '0.3'\nPLUGIN_API_VERSIONS = ['2.0']\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\nfrom picard import config\nfrom picard.album import Album\nfrom picard.metadata import register_album_metadata_processor, register_track_metadata_processor\nfrom picard.ui.options import register_options_page, OptionsPage\nfrom picard.ui.itemviews import BaseAction, register_album_action\nfrom picard.config import BoolOption, TextOption\n\n\nclass Ui_NoReleaseOptionsPage(object):\n\n    def setupUi(self, NoReleaseOptionsPage):\n        NoReleaseOptionsPage.setObjectName('NoReleaseOptionsPage')\n        NoReleaseOptionsPage.resize(394, 300)\n        self.verticalLayout = QtWidgets.QVBoxLayout(NoReleaseOptionsPage)\n        self.verticalLayout.setObjectName('verticalLayout')\n        self.groupBox = QtWidgets.QGroupBox(NoReleaseOptionsPage)\n        self.groupBox.setObjectName('groupBox')\n        self.vboxlayout = QtWidgets.QVBoxLayout(self.groupBox)\n        self.vboxlayout.setObjectName('vboxlayout')\n        self.norelease_enable = QtWidgets.QCheckBox(self.groupBox)\n        self.norelease_enable.setObjectName('norelease_enable')\n        self.vboxlayout.addWidget(self.norelease_enable)\n        self.label = QtWidgets.QLabel(self.groupBox)\n        self.label.setObjectName('label')\n        self.vboxlayout.addWidget(self.label)\n        self.horizontalLayout = QtWidgets.QHBoxLayout()\n        self.horizontalLayout.setObjectName('horizontalLayout')\n        self.norelease_strip_tags = QtWidgets.QLineEdit(self.groupBox)\n        self.norelease_strip_tags.setObjectName('norelease_strip_tags')\n        self.horizontalLayout.addWidget(self.norelease_strip_tags)\n        self.vboxlayout.addLayout(self.horizontalLayout)\n        self.verticalLayout.addWidget(self.groupBox)\n        spacerItem = QtWidgets.QSpacerItem(368, 187, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.verticalLayout.addItem(spacerItem)\n\n        self.retranslateUi(NoReleaseOptionsPage)\n        QtCore.QMetaObject.connectSlotsByName(NoReleaseOptionsPage)\n\n    def retranslateUi(self, NoReleaseOptionsPage):\n        self.groupBox.setTitle(QtWidgets.QApplication.translate('NoReleaseOptionsPage', 'No release'))\n        self.norelease_enable.setText(QtWidgets.QApplication.translate('NoReleaseOptionsPage', _('Enable plugin for all releases by default')))\n        self.label.setText(QtWidgets.QApplication.translate('NoReleaseOptionsPage', _('Tags to strip (comma-separated)')))\n\n\ndef strip_release_specific_metadata(metadata):\n    strip_tags = config.setting['norelease_strip_tags']\n    strip_tags = [tag.strip() for tag in strip_tags.split(',')]\n    for tag in strip_tags:\n        metadata.delete(tag)\n\n\nclass NoReleaseAction(BaseAction):\n    NAME = _('Remove specific release information...')\n\n    def callback(self, objs):\n        for album in objs:\n            if isinstance(album, Album):\n                strip_release_specific_metadata(album.metadata)\n                for track in album.tracks:\n                    strip_release_specific_metadata(track.metadata)\n                    for file in track.linked_files:\n                        track.update_file_metadata(file)\n                album.update()\n\n\nclass NoReleaseOptionsPage(OptionsPage):\n    NAME = 'norelease'\n    TITLE = 'No release'\n    PARENT = 'plugins'\n\n    options = [\n        BoolOption('setting', 'norelease_enable', False),\n        TextOption('setting', 'norelease_strip_tags', 'asin,barcode,catalognumber,date,label,media,releasecountry,releasestatus'),\n    ]\n\n    def __init__(self, parent=None):\n        super(NoReleaseOptionsPage, self).__init__(parent)\n        self.ui = Ui_NoReleaseOptionsPage()\n        self.ui.setupUi(self)\n\n    def load(self):\n        self.ui.norelease_strip_tags.setText(config.setting['norelease_strip_tags'])\n        self.ui.norelease_enable.setChecked(config.setting['norelease_enable'])\n\n    def save(self):\n        config.setting['norelease_strip_tags'] = str(self.ui.norelease_strip_tags.text())\n        config.setting['norelease_enable'] = self.ui.norelease_enable.isChecked()\n\n\ndef no_release_album_processor(tagger, metadata, release):\n    if config.setting['norelease_enable']:\n        strip_release_specific_metadata(metadata)\n\n\ndef no_release_track_processor(tagger, metadata, track, release):\n    if config.setting['norelease_enable']:\n        strip_release_specific_metadata(metadata)\n\n\nregister_album_metadata_processor(no_release_album_processor)\nregister_track_metadata_processor(no_release_track_processor)\nregister_album_action(NoReleaseAction())\nregister_options_page(NoReleaseOptionsPage)\n"
  },
  {
    "path": "plugins/non_ascii_equivalents/non_ascii_equivalents.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Copyright (C) 2016 Anderson Mesquita <andersonvom@gmail.com>\n#\n# This program is free software: you can redistribute it and/or modify it under\n# the terms of the GNU General Public License as published by the Free Software\n# Foundation, either version 3 of the License, or (at your option) any later\n# version.\n#\n# This program is distributed in the hope that it will be useful, but WITHOUT\n# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS\n# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more\n# details.\n#\n# You should have received a copy of the GNU General Public License along with\n# this program. If not, see <http://www.gnu.org/licenses/>.\n\nfrom picard import metadata\nfrom picard.util.textencoding import unaccent\n\nPLUGIN_NAME = \"Non-ASCII Equivalents\"\nPLUGIN_AUTHOR = \"Anderson Mesquita <andersonvom@trysometinghere>, Konrad Marciniak\"\nPLUGIN_VERSION = \"0.5\"\nPLUGIN_API_VERSIONS = [\"0.9\", \"0.10\", \"0.11\", \"0.15\", \"2.0\"]\nPLUGIN_LICENSE = \"GPL-3.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://gnu.org/licenses/gpl.html\"\nPLUGIN_DESCRIPTION = '''Replaces accented and otherwise non-ASCII characters\nwith a somewhat equivalent version of their ASCII counterparts. This allows old\ndevices to be able to display song artists and titles somewhat correctly,\ninstead of displaying weird or blank symbols. It's an attempt to do a little\nbetter than Musicbrainz's native \"Replace non-ASCII characters\" option.\n\nCurrently replaces characters on \"album\", \"albumartist\", \"albumartists\", \"albumartistsort\", \"albumsort\", \"artist\", \"artists\", \"artistsort\" and \"title\" tags.'''\n\nCHAR_TABLE = {\n    # Misc Letters\n    \"Å\": \"AA\",\n    \"å\": \"aa\",\n    \"Æ\": \"AE\",\n    \"æ\": \"ae\",\n    \"Œ\": \"OE\",\n    \"œ\": \"oe\",\n    \"ẞ\": \"ss\",\n    \"ß\": \"ss\",\n    \"Ø\": \"O\",\n    \"ø\": \"o\",\n    \"Ł\": \"L\",\n    \"ł\": \"l\",\n    \"Þ\": \"Th\", # Thorn\n    \"þ\": \"th\",\n    \"Ð\": \"D\", # Eth\n    \"ð\": \"d\",\n\n    # Punctuation\n    \"¡\": \"!\",\n    \"¿\": \"?\",\n    \"–\": \"--\",\n    \"—\": \"--\",\n    \"―\": \"--\",\n    \"«\": \"<<\",\n    \"»\": \">>\",\n    \"‘\": \"'\",\n    \"’\": \"'\",\n    \"‚\": \",\",\n    \"‛\": \"'\",\n    \"“\": '\"',\n    \"”\": '\"',\n    \"„\": \",,\",\n    \"‟\": '\"',\n    \"‹\": \"<\",\n    \"›\": \">\",\n    \"⹂\": \",,\",\n    \"「\": \"|-\",\n    \"」\": \"-|\",\n    \"『\": \"|-\",\n    \"』\": \"-|\",\n    \"〝\": '\"',\n    \"〞\": '\"',\n    \"〟\": \",,\",\n    \"﹁\": \"-|\",\n    \"﹂\": \"|-\",\n    \"﹃\": \"-|\",\n    \"﹄\": \"|-\",\n    \"｢\": \"|-\",\n    \"｣\": \"-|\",\n    \"・\": \".\", # Katakana middle dot\n\n    # Mathematics\n    \"≠\": \"!=\",\n    \"≤\": \"<=\",\n    \"≥\": \">=\",\n    \"±\": \"+-\",\n    \"∓\": \"-+\",\n    \"×\": \"x\",\n    \"·\": \".\",\n    \"÷\": \"/\",\n    \"√\": \"\\\\/\",\n    \"∑\": \"E\",\n    \"≪\": \"<<\", # these are different\n    \"≫\": \">>\", # from the quotation marks\n\n    # Misc\n    \"°\": \"o\",\n    \"µ\": \"u\",\n    \"ı\": \"i\",\n    \"†\": \"t\",\n    \"©\": \"(c)\",\n    \"®\": \"(R)\",\n    \"♥\": \"<3\",\n    \"→\": \"-->\",\n    \"☆\": \"*\",\n    \"★\": \"*\",\n}\n\nFILTER_TAGS = [\n    \"album\",\n    \"albumartist\",\n    \"albumartists\",\n    \"albumartistsort\",\n    \"albumsort\",\n    \"artist\",\n    \"artists\",\n    \"artistsort\",\n    \"title\",\n]\n\n\ndef sanitize(char):\n    if char in CHAR_TABLE:\n        return CHAR_TABLE[char]\n    return unaccent(char)\n\n\ndef to_ascii(word):\n    return \"\".join(sanitize(char) for char in word)\n\n\ndef main(tagger, metadata, *args):\n    for name, value in metadata.rawitems():\n        if name in FILTER_TAGS:\n            metadata[name] = [to_ascii(x) for x in value]\n\n\nmetadata.register_track_metadata_processor(main)\nmetadata.register_album_metadata_processor(main)\n"
  },
  {
    "path": "plugins/padded/padded.py",
    "content": "PLUGIN_NAME = \"Padded disc and tracknumbers\"\nPLUGIN_AUTHOR = \"Wieland Hoffmann\"\nPLUGIN_DESCRIPTION = \"\"\"\nAdds padded disc- and tracknumbers so the length of all disc- and tracknumbers\nis the same. They are stored in the `_paddedtracknumber` and `_paddeddiscnumber`\ntags.\"\"\"\n\nPLUGIN_VERSION = \"1.0.1\"\nPLUGIN_API_VERSIONS = [\"0.15.0\", \"0.15.1\", \"0.16.0\", \"1.0.0\", \"1.1.0\", \"1.2.0\",\n                       \"1.3.0\", \"2.0\", ]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom picard.metadata import register_track_metadata_processor\n\n\n@register_track_metadata_processor\ndef add_padded_tn(album, metadata, track, release):\n    maxlength = len(metadata[\"totaltracks\"])\n    islength = len(metadata[\"tracknumber\"])\n    metadata[\"~paddedtracknumber\"] = (int(maxlength - islength) * \"0\" +\n                                      metadata[\"tracknumber\"])\n\n\n@register_track_metadata_processor\ndef add_padded_dn(album, metadata, track, release):\n    maxlength = len(metadata[\"totaldiscs\"])\n    islength = len(metadata[\"discnumber\"])\n    metadata[\"~paddeddiscnumber\"] = (int(maxlength - islength) * \"0\" +\n                                     metadata[\"discnumber\"])\n"
  },
  {
    "path": "plugins/papercdcase/papercdcase.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2015, 2019 Philipp Wolfer\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = 'Paper CD case'\nPLUGIN_AUTHOR = 'Philipp Wolfer, Sambhav Kothari'\nPLUGIN_DESCRIPTION = ('Create a paper CD case from an album or cluster '\n                      'using http://papercdcase.com')\nPLUGIN_VERSION = \"1.2.1\"\nPLUGIN_API_VERSIONS = [\"2.0\", \"2.1\", \"2.2\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\n\nfrom PyQt5.QtCore import QUrl, QUrlQuery\nfrom picard.album import Album\nfrom picard.cluster import Cluster\nfrom picard.ui.itemviews import (\n    BaseAction,\n    register_album_action,\n    register_cluster_action,\n)\nfrom picard.util import webbrowser2\nfrom picard.util import textencoding\n\n\nPAPERCDCASE_URL = 'http://papercdcase.com/advanced.php'\nURLENCODE_ASCII_CHARS = [ord(' '), ord('%'), ord('&'), ord('/'), ord('?')]\n\n\ndef urlencode(s):\n    chars = []\n    for c in s:\n        n = ord(c)\n        if (n < 128 or n > 255) and n not in URLENCODE_ASCII_CHARS:\n            chars.append(c)\n        else:\n            chars.append('%' + hex(n)[2:].upper())\n    return ''.join(chars)\n\n\ndef build_papercdcase_url(artist, album, tracks):\n    url = QUrl(PAPERCDCASE_URL)\n    # papercdcase.com does not deal well with unicode characters :(\n    url_query = QUrlQuery()\n    url_query.addQueryItem('artist',\n                           urlencode(textencoding.asciipunct(artist)))\n    url_query.addQueryItem('title',\n                           urlencode(textencoding.asciipunct(album)))\n    i = 1\n    for track in tracks:\n        url_query.addQueryItem('track' + str(i),\n                               urlencode(textencoding.asciipunct(track)))\n        i += 1\n    url.setQuery(url_query)\n    return url.toString(QUrl.FullyEncoded)\n\n\nclass PaperCdCase(BaseAction):\n    NAME = 'Create paper CD case'\n\n    def callback(self, objs):\n        for obj in objs:\n            if isinstance(obj, Album) or isinstance(obj, Cluster):\n                artist = obj.metadata['albumartist']\n                album = obj.metadata['album']\n                if isinstance(obj, Album):\n                    tracks = [track.metadata['title'] for track in obj.tracks]\n                else:\n                    tracks = [file.metadata['title'] for file in obj.files]\n                url = build_papercdcase_url(artist, album, tracks)\n                webbrowser2.open(url)\n\n\npaperCdCase = PaperCdCase()\nregister_album_action(paperCdCase)\nregister_cluster_action(paperCdCase)\n"
  },
  {
    "path": "plugins/performer_tag_replace/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2018-2025 Bob Swift (rdswift)\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nimport re\n\nfrom picard import config, log\nfrom picard.metadata import register_track_metadata_processor\nfrom picard.plugins.performer_tag_replace.ui_options_performer_tag_replace import \\\n    Ui_PerformerTagReplaceOptionsPage\nfrom picard.ui.options import OptionsPage, register_options_page\n\nPLUGIN_NAME = 'Performer Tag Replace'\nPLUGIN_AUTHOR = 'Bob Swift (rdswift)'\nPLUGIN_DESCRIPTION = '''\nThis plugin provides the ability to replace text in performer tags.  It\nhas been developed using the 'Standardise Performers' plugin by Sophist\nas the basis for retrieving and processing the performer data for each\nof the tracks.  The original/replacement pairs used can be customized\nin the option settings page.\n<br /><br />\nPlease see the <a href=\"https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/performer_tag_replace/docs/README.md\">user\nguide</a> on GitHub for more information.\n'''\n\nPLUGIN_VERSION = \"0.03\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\nPLUGIN_LICENSE = \"GPL-2.0 or later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nPLUGIN_USER_GUIDE_URL = \"https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/performer_tag_replace/docs/README.md\"\n\nDEV_TESTING = False\n\npairs_split = re.compile(r\"\\r\\n|\\n\\r|\\n\").split\n\n\ndef _update_track_metadata(track_metadata, replacements):\n    if 'recording' not in track_metadata or 'relations' not in track_metadata['recording']:\n        return\n\n    relations = []\n    for relation in track_metadata['recording']['relations']:\n        if 'type' in relation and relation['type'] in ['instrument', 'vocal']:\n\n            if 'attributes' in relation:\n                attributes = []\n                for attribute in relation['attributes']:\n                    for (original, replacement) in replacements:\n                        attribute = attribute.replace(original, replacement)\n                    attributes.append(attribute)\n                relation['attributes'] = attributes\n\n            if 'attribute-ids' in relation:\n                attribute_ids = {}\n                for key, value in relation['attribute-ids'].items():\n                    for (original, replacement) in replacements:\n                        key = key.replace(original, replacement)\n                    attribute_ids[key] = value\n                relation['attribute-ids'] = attribute_ids\n\n            if 'attribute-credits' in relation:\n                attribute_credits = {}\n                for key, value in relation['attribute-credits'].items():\n                    for (original, replacement) in replacements:\n                        key = key.replace(original, replacement)\n                    attribute_credits[key] = value\n                relation['attribute-credits'] = attribute_credits\n\n        relations.append(relation)\n\n    track_metadata['recording']['relations'] = relations\n\n    return\n\n\ndef performer_tag_replace(album, metadata, track_metadata, *args):\n    replacements = []\n    for pair in pairs_split(config.setting[\"performer_tag_replacement_pairs\"]):\n        if \"=\" not in pair:\n            continue\n        original, replacement = pair.split('=', 1)\n        if original:\n            replacements.append((original, replacement))\n            if DEV_TESTING:\n                log.debug(\"%s: Add pair: '%s' = '%s'\", PLUGIN_NAME, original, replacement,)\n    if replacements:\n        _update_track_metadata(track_metadata, replacements)\n        for key, values in list(metadata.rawitems()):\n            if not key.startswith('performer:') and not key.startswith('~performersort:'):\n                continue\n            mainkey, subkey = key.split(':', 1)\n            if not subkey:\n                continue\n            log.debug(\"%s: Original key: '%s'\", PLUGIN_NAME, subkey,)\n            for (original, replacement) in replacements:\n                subkey = subkey.replace(original, replacement)\n                if DEV_TESTING:\n                    log.debug(\"%s: Applying pair: '%s' = '%s'\", PLUGIN_NAME, original, replacement,)\n                    log.debug(\"%s: Updated key: '%s'\", PLUGIN_NAME, subkey,)\n            log.debug(\"%s: Replacement key: '%s'\", PLUGIN_NAME, subkey,)\n            del metadata[key]\n            newkey = ('%s:%s' % (mainkey, subkey,)).strip()\n            for value in values:\n                if config.setting[\"performer_tag_replace_performers\"]:\n                    log.debug(\"%s: Original value: '%s'\", PLUGIN_NAME, subkey,)\n                    for (original, replacement) in replacements:\n                        value = value.replace(original, replacement)\n                        if DEV_TESTING:\n                            log.debug(\"%s: Applying pair: '%s' = '%s'\", PLUGIN_NAME, original, replacement,)\n                            log.debug(\"%s: Updated value: '%s'\", PLUGIN_NAME, value,)\n                    log.debug(\"%s: Replacement value: '%s'\", PLUGIN_NAME, value,)\n                metadata.add_unique(newkey, value)\n    else:\n        log.debug(\"%s: No replacement pairs found.\", PLUGIN_NAME,)\n\n\nclass PerformerTagReplaceOptionsPage(OptionsPage):\n\n    NAME = \"performer_tag_replace\"\n    TITLE = \"Performer Tag Replacement\"\n    PARENT = \"plugins\"\n\n    options = [\n        config.TextOption(\"setting\", \"performer_tag_replacement_pairs\", ''),\n        config.BoolOption(\"setting\", \"performer_tag_replace_performers\", False),\n    ]\n\n    def __init__(self, parent=None):\n        super(PerformerTagReplaceOptionsPage, self).__init__(parent)\n        self.ui = Ui_PerformerTagReplaceOptionsPage()\n        self.ui.setupUi(self)\n\n    def load(self):\n        # Enable external link\n        self.ui.format_description.setOpenExternalLinks(True)\n\n        # Replacement settings\n        self.ui.performer_tag_replacement_pairs.setPlainText(config.setting[\"performer_tag_replacement_pairs\"])\n        self.ui.performer_tag_replace_performers.setChecked(config.setting[\"performer_tag_replace_performers\"])\n\n    def save(self):\n        # Replacement settings\n        config.setting[\"performer_tag_replacement_pairs\"] = self.ui.performer_tag_replacement_pairs.toPlainText()\n        config.setting[\"performer_tag_replace_performers\"] = self.ui.performer_tag_replace_performers.isChecked()\n\n\n# Register the plugin to run at a priority slightly higher than NORMAL to help ensure that\n# the replacements are applied before most other metadata processing plugins are executed.\nregister_track_metadata_processor(performer_tag_replace, priority=10)\nregister_options_page(PerformerTagReplaceOptionsPage)\n"
  },
  {
    "path": "plugins/performer_tag_replace/options_performer_tag_replace.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>PerformerTagReplaceOptionsPage</class>\n <widget class=\"QWidget\" name=\"PerformerTagReplaceOptionsPage\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>398</width>\n    <height>568</height>\n   </rect>\n  </property>\n  <property name=\"sizePolicy\">\n   <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n    <horstretch>0</horstretch>\n    <verstretch>0</verstretch>\n   </sizepolicy>\n  </property>\n  <layout class=\"QVBoxLayout\">\n   <property name=\"spacing\">\n    <number>16</number>\n   </property>\n   <item alignment=\"Qt::AlignTop\">\n    <widget class=\"QScrollArea\" name=\"scrollArea\">\n     <property name=\"sizePolicy\">\n      <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Fixed\">\n       <horstretch>0</horstretch>\n       <verstretch>0</verstretch>\n      </sizepolicy>\n     </property>\n     <property name=\"minimumSize\">\n      <size>\n       <width>380</width>\n       <height>550</height>\n      </size>\n     </property>\n     <property name=\"frameShape\">\n      <enum>QFrame::NoFrame</enum>\n     </property>\n     <property name=\"widgetResizable\">\n      <bool>true</bool>\n     </property>\n     <property name=\"alignment\">\n      <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>\n     </property>\n     <widget class=\"QWidget\" name=\"scrollAreaWidgetContents\">\n      <property name=\"geometry\">\n       <rect>\n        <x>0</x>\n        <y>0</y>\n        <width>380</width>\n        <height>550</height>\n       </rect>\n      </property>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\n       <item>\n        <widget class=\"QGroupBox\" name=\"gb_description\">\n         <property name=\"sizePolicy\">\n          <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Minimum\">\n           <horstretch>0</horstretch>\n           <verstretch>0</verstretch>\n          </sizepolicy>\n         </property>\n         <property name=\"minimumSize\">\n          <size>\n           <width>0</width>\n           <height>50</height>\n          </size>\n         </property>\n         <property name=\"font\">\n          <font>\n           <weight>75</weight>\n           <bold>true</bold>\n          </font>\n         </property>\n         <property name=\"title\">\n          <string>Performer Tag Replacement</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n          <property name=\"leftMargin\">\n           <number>9</number>\n          </property>\n          <property name=\"topMargin\">\n           <number>9</number>\n          </property>\n          <property name=\"rightMargin\">\n           <number>9</number>\n          </property>\n          <property name=\"bottomMargin\">\n           <number>1</number>\n          </property>\n          <item>\n           <widget class=\"QLabel\" name=\"format_description\">\n            <property name=\"sizePolicy\">\n             <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Minimum\">\n              <horstretch>0</horstretch>\n              <verstretch>0</verstretch>\n             </sizepolicy>\n            </property>\n            <property name=\"font\">\n             <font>\n              <weight>50</weight>\n              <bold>false</bold>\n             </font>\n            </property>\n            <property name=\"text\">\n             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;These are the original / replacement pairs used to modify the keys for the performer tags. Each pair must be entered on a separate line in the form:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;original character string=replacement character string&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Blank lines and lines beginning with an equals sign (=) will be ignored. Replacements will be made in the order they are found in the list. An example for removing &amp;quot;family&amp;quot; from instrument names would be done using the following two lines:&lt;/p&gt;&lt;pre style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Courier New'; font-size:10pt; font-weight:600;&quot;&gt;s family=ses&lt;br/&gt; family=s&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;Note that the second line begins with a single space.&lt;/p&gt;&lt;p&gt;For more information please see the &lt;a href=&quot;https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/performer_tag_replace/docs/README.md&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;User Guide&lt;/span&gt;&lt;/a&gt; on GitHub.&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n            </property>\n            <property name=\"alignment\">\n             <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>\n            </property>\n            <property name=\"wordWrap\">\n             <bool>true</bool>\n            </property>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item alignment=\"Qt::AlignTop\">\n        <widget class=\"QGroupBox\" name=\"gb_replacement_pairs\">\n         <property name=\"font\">\n          <font>\n           <weight>75</weight>\n           <bold>true</bold>\n          </font>\n         </property>\n         <property name=\"title\">\n          <string>Replacement Pairs</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_3\">\n          <item>\n           <widget class=\"QPlainTextEdit\" name=\"performer_tag_replacement_pairs\">\n            <property name=\"font\">\n             <font>\n              <family>Courier New</family>\n              <pointsize>10</pointsize>\n             </font>\n            </property>\n            <property name=\"placeholderText\">\n             <string>Enter replacement pairs (one per line)</string>\n            </property>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QCheckBox\" name=\"performer_tag_replace_performers\">\n         <property name=\"text\">\n          <string>Apply replacements to performers as well as instruments &amp;&amp; vocals</string>\n         </property>\n         <property name=\"checked\">\n          <bool>false</bool>\n         </property>\n        </widget>\n       </item>\n       <item>\n        <spacer name=\"verticalSpacer\">\n         <property name=\"orientation\">\n          <enum>Qt::Vertical</enum>\n         </property>\n         <property name=\"sizeHint\" stdset=\"0\">\n          <size>\n           <width>20</width>\n           <height>0</height>\n          </size>\n         </property>\n        </spacer>\n       </item>\n      </layout>\n     </widget>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <tabstops>\n  <tabstop>scrollArea</tabstop>\n </tabstops>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/performer_tag_replace/ui_options_performer_tag_replace.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Automatically generated - don't edit.\n# Use `python setup.py build_ui` to update it.\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\nclass Ui_PerformerTagReplaceOptionsPage(object):\n    def setupUi(self, PerformerTagReplaceOptionsPage):\n        PerformerTagReplaceOptionsPage.setObjectName(\"PerformerTagReplaceOptionsPage\")\n        PerformerTagReplaceOptionsPage.resize(398, 568)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(PerformerTagReplaceOptionsPage.sizePolicy().hasHeightForWidth())\n        PerformerTagReplaceOptionsPage.setSizePolicy(sizePolicy)\n        self.vboxlayout = QtWidgets.QVBoxLayout(PerformerTagReplaceOptionsPage)\n        self.vboxlayout.setSpacing(16)\n        self.vboxlayout.setObjectName(\"vboxlayout\")\n        self.scrollArea = QtWidgets.QScrollArea(PerformerTagReplaceOptionsPage)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth())\n        self.scrollArea.setSizePolicy(sizePolicy)\n        self.scrollArea.setMinimumSize(QtCore.QSize(380, 550))\n        self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame)\n        self.scrollArea.setWidgetResizable(True)\n        self.scrollArea.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)\n        self.scrollArea.setObjectName(\"scrollArea\")\n        self.scrollAreaWidgetContents = QtWidgets.QWidget()\n        self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 380, 550))\n        self.scrollAreaWidgetContents.setObjectName(\"scrollAreaWidgetContents\")\n        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents)\n        self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout_2.setObjectName(\"verticalLayout_2\")\n        self.gb_description = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.gb_description.sizePolicy().hasHeightForWidth())\n        self.gb_description.setSizePolicy(sizePolicy)\n        self.gb_description.setMinimumSize(QtCore.QSize(0, 50))\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.gb_description.setFont(font)\n        self.gb_description.setObjectName(\"gb_description\")\n        self.verticalLayout = QtWidgets.QVBoxLayout(self.gb_description)\n        self.verticalLayout.setContentsMargins(9, 9, 9, 1)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.format_description = QtWidgets.QLabel(self.gb_description)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.format_description.sizePolicy().hasHeightForWidth())\n        self.format_description.setSizePolicy(sizePolicy)\n        font = QtGui.QFont()\n        font.setBold(False)\n        font.setWeight(50)\n        self.format_description.setFont(font)\n        self.format_description.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)\n        self.format_description.setWordWrap(True)\n        self.format_description.setObjectName(\"format_description\")\n        self.verticalLayout.addWidget(self.format_description)\n        self.verticalLayout_2.addWidget(self.gb_description)\n        self.gb_replacement_pairs = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.gb_replacement_pairs.setFont(font)\n        self.gb_replacement_pairs.setObjectName(\"gb_replacement_pairs\")\n        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.gb_replacement_pairs)\n        self.verticalLayout_3.setObjectName(\"verticalLayout_3\")\n        self.performer_tag_replacement_pairs = QtWidgets.QPlainTextEdit(self.gb_replacement_pairs)\n        font = QtGui.QFont()\n        font.setFamily(\"Courier New\")\n        font.setPointSize(10)\n        self.performer_tag_replacement_pairs.setFont(font)\n        self.performer_tag_replacement_pairs.setObjectName(\"performer_tag_replacement_pairs\")\n        self.verticalLayout_3.addWidget(self.performer_tag_replacement_pairs)\n        self.verticalLayout_2.addWidget(self.gb_replacement_pairs, 0, QtCore.Qt.AlignTop)\n        self.performer_tag_replace_performers = QtWidgets.QCheckBox(self.scrollAreaWidgetContents)\n        self.performer_tag_replace_performers.setChecked(False)\n        self.performer_tag_replace_performers.setObjectName(\"performer_tag_replace_performers\")\n        self.verticalLayout_2.addWidget(self.performer_tag_replace_performers)\n        spacerItem = QtWidgets.QSpacerItem(20, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.verticalLayout_2.addItem(spacerItem)\n        self.scrollArea.setWidget(self.scrollAreaWidgetContents)\n        self.vboxlayout.addWidget(self.scrollArea, 0, QtCore.Qt.AlignTop)\n\n        self.retranslateUi(PerformerTagReplaceOptionsPage)\n        QtCore.QMetaObject.connectSlotsByName(PerformerTagReplaceOptionsPage)\n\n    def retranslateUi(self, PerformerTagReplaceOptionsPage):\n        _translate = QtCore.QCoreApplication.translate\n        self.gb_description.setTitle(_(\"Performer Tag Replacement\"))\n        self.format_description.setText(_(\"<html><head/><body><p>These are the original / replacement pairs used to modify the keys for the performer tags. Each pair must be entered on a separate line in the form:</p><p><span style=\\\" font-weight:600;\\\">original character string=replacement character string</span></p><p>Blank lines and lines beginning with an equals sign (=) will be ignored. Replacements will be made in the order they are found in the list. An example for removing &quot;family&quot; from instrument names would be done using the following two lines:</p><pre style=\\\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\\\"><span style=\\\" font-family:\\'Courier New\\'; font-size:10pt; font-weight:600;\\\">s family=ses<br/> family=s</span></pre><p>Note that the second line begins with a single space.</p><p>For more information please see the <a href=\\\"https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/performer_tag_replace/docs/README.md\\\"><span style=\\\" text-decoration: underline; color:#0000ff;\\\">User Guide</span></a> on GitHub.<br/></p></body></html>\"))\n        self.gb_replacement_pairs.setTitle(_(\"Replacement Pairs\"))\n        self.performer_tag_replacement_pairs.setPlaceholderText(_(\"Enter replacement pairs (one per line)\"))\n        self.performer_tag_replace_performers.setText(_(\"Apply replacements to performers as well as instruments && vocals\"))\n\n"
  },
  {
    "path": "plugins/persistent_variables/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2022 Bob Swift (rdswift)\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\n# \"Persistent variables display\" context is based on the code from\n# the \"View Script Variables\" plugin.\n\nPLUGIN_NAME = 'Persistent Variables'\nPLUGIN_AUTHOR = 'Bob Swift (rdswift)'\nPLUGIN_DESCRIPTION = '''\n<p>\nThis plugin provides the ability to store and retrieve script variables that persist across tracks and albums.\nThis allows things like finding and storing the earliest recording date of all of the tracks on an album.\n</p><p>\nThere are two types of persistent variables maintained - album variables and session variables. Album variables\npersist across all tracks on an album.  Each album's information is stored separately, and is reset when the\nalbum is refreshed. The information is cleared when an album is removed.  Session variables persist across all\nalbums and tracks, and are cleared when Picard is shut down or restarted.\n</p><p>\nThis plugin adds eight new scripting functions to allow management of persistent script variables:\n<ul>\n<li>$set_a(name,value) : Sets the album persistent variable name to value.</li>\n<li>$unset_a(name) : Unsets the album persistent variable name.</li>\n<li>$get_a(name) : Gets the album persistent variable name.</li>\n<li>$clear_a() : Clears all album persistent variables.</li>\n<li>$set_s(name,value) : Sets the session persistent variable name to value.</li>\n<li>$unset_s(name) : Unsets the session persistent variable name.</li>\n<li>$get_s(name) : Gets the session persistent variable name.</li>\n<li>$clear_s() : Clears all session persistent variables.</li>\n</ul>\n</p><p>\nPlease see the <a href=\"https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/persistent_variables/docs/README.md\">user guide</a> on GitHub for more information.\n</p>\n'''\nPLUGIN_VERSION = '1.1'\nPLUGIN_API_VERSIONS = ['2.0', '2.1', '2.2', '2.3', '2.4', '2.6', '2.7']\nPLUGIN_LICENSE = 'GPL-2.0-or-later'\nPLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl-2.0.html'\n\nPLUGIN_USER_GUIDE_URL = 'https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/persistent_variables/docs/README.md'\n\nfrom PyQt5 import QtWidgets\n\nfrom picard import log\nfrom picard.album import (\n    Album,\n    register_album_post_removal_processor,\n)\nfrom picard.file import File\nfrom picard.metadata import register_album_metadata_processor\nfrom picard.plugin import PluginPriority\nfrom picard.plugins.persistent_variables.ui_variables_dialog import (\n    Ui_VariablesDialog,\n)\nfrom picard.script import register_script_function\nfrom picard.script.parser import normalize_tagname\nfrom picard.track import Track\n\nfrom picard.ui.itemviews import (\n    BaseAction,\n    register_album_action,\n    register_file_action,\n    register_track_action,\n)\n\n\nclass PersistentVariables:\n    album_variables = {}\n    session_variables = {}\n\n    @classmethod\n    def clear_album_vars(cls, album):\n        if album:\n            cls.album_variables[album] = {}\n\n    @classmethod\n    def set_album_var(cls, album, key, value):\n        if album:\n            if album not in cls.album_variables:\n                cls.clear_album_vars(album)\n            if key:\n                cls.album_variables[album][key] = value\n\n    @classmethod\n    def unset_album_var(cls, album, key):\n        if album and album in cls.album_variables:\n            cls.album_variables[album].pop(key, None)\n\n    @classmethod\n    def unset_album_dict(cls, album):\n        if album:\n            cls.album_variables.pop(album, None)\n\n    @classmethod\n    def get_album_var(cls, album, key):\n        if album in cls.album_variables:\n            return cls.album_variables[album][key] if key in cls.album_variables[album] else \"\"\n        return \"\"\n\n    @classmethod\n    def clear_session_vars(cls):\n        cls.session_variables = {}\n\n    @classmethod\n    def set_session_var(cls, key, value):\n        if key:\n            cls.session_variables[key] = value\n\n    @classmethod\n    def unset_session_var(cls, key):\n        cls.session_variables.pop(key, None)\n\n    @classmethod\n    def get_session_var(cls, key):\n        return cls.session_variables[key] if key in cls.session_variables else \"\"\n\n    @classmethod\n    def get_album_dict(cls, album):\n        if album and album in cls.album_variables:\n            return cls.album_variables[album]\n        return {}\n\n    @classmethod\n    def get_session_dict(cls):\n        return cls.session_variables\n\n\ndef _get_album_id(parser):\n    file = parser.file\n    if file:\n        if file.parent and hasattr(file.parent, 'album') and file.parent.album:\n            return str(file.parent.album.id)\n        else:\n            return \"\"\n    # Fall back to parser context to allow processing on albums newly retrieved from MusicBrainz\n    return parser.context['musicbrainz_albumid']\n\n\ndef func_set_s(parser, name, value):\n    if value:\n        PersistentVariables.set_session_var(normalize_tagname(name), value)\n    else:\n        func_unset_s(parser, name)\n    return \"\"\n\n\ndef func_unset_s(parser, name):\n    PersistentVariables.unset_session_var(normalize_tagname(name))\n    return \"\"\n\n\ndef func_get_s(parser, name):\n    return PersistentVariables.get_session_var(normalize_tagname(name))\n\n\ndef func_clear_s(parser):\n    PersistentVariables.clear_session_vars()\n    return \"\"\n\n\ndef func_unset_a(parser, name):\n    album_id = _get_album_id(parser)\n    log.debug(\"{0}: Unsetting album '{1}' variable '{2}'\".format(PLUGIN_NAME, album_id, normalize_tagname(name),))\n    if album_id:\n        PersistentVariables.unset_album_var(album_id, normalize_tagname(name))\n    return \"\"\n\n\ndef func_set_a(parser, name, value):\n    album_id = _get_album_id(parser)\n    log.debug(\"{0}: Setting album '{1}' persistent variable '{2}' to '{3}'\".format(PLUGIN_NAME, album_id, normalize_tagname(name), value,))\n    if album_id:\n        PersistentVariables.set_album_var(album_id, normalize_tagname(name), value)\n    return \"\"\n\n\ndef func_get_a(parser, name):\n    album_id = _get_album_id(parser)\n    log.debug(\"{0}: Getting album '{1}' persistent variable '{2}'\".format(PLUGIN_NAME, album_id, normalize_tagname(name),))\n    if album_id:\n        return PersistentVariables.get_album_var(album_id, normalize_tagname(name))\n    return \"\"\n\n\ndef func_clear_a(parser):\n    album_id = _get_album_id(parser)\n    log.debug(\"{0}: Clearing album '{1}' persistent variables dictionary\".format(PLUGIN_NAME, album_id,))\n    if album_id:\n        PersistentVariables.clear_album_vars(album_id)\n    return \"\"\n\n\ndef initialize_album_dict(album, album_metadata, release_metadata):\n    album_id = str(album.id)\n    log.debug(\"{0}: Initializing album '{1}' persistent variables dictionary\".format(PLUGIN_NAME, album_id,))\n    PersistentVariables.clear_album_vars(album_id)\n\n\ndef destroy_album_dict(album):\n    album_id = str(album.id)\n    log.debug(\"{0}: Destroying album '{1}' persistent variables dictionary\".format(PLUGIN_NAME, album_id,))\n    PersistentVariables.unset_album_dict(album_id)\n\n\nclass ViewVariables(BaseAction):\n    NAME = 'View persistent variables'\n\n    def callback(self, objs):\n        obj = objs[0]\n        files = self.tagger.get_files_from_objects(objs)\n        if files:\n            obj = files[0]\n        dialog = ViewVariablesDialog(obj)\n        dialog.exec_()\n\n\nclass ViewVariablesDialog(QtWidgets.QDialog):\n\n    def __init__(self, obj, parent=None):\n        QtWidgets.QDialog.__init__(self, parent)\n        self.ui = Ui_VariablesDialog()\n        self.ui.setupUi(self)\n        self.ui.buttonBox.accepted.connect(self.accept)\n        self.album_id = \"\"\n        self.setWindowTitle(\"Persistent Variables\")\n        if isinstance(obj, Album):\n            self.album_id = str(obj.id)\n        if isinstance(obj, File):\n            if obj.parent and hasattr(obj.parent, 'album') and obj.parent.album:\n                self.album_id = str(obj.parent.album.id)\n        elif isinstance(obj, Track):\n            if obj.album:\n                self.album_id = str(obj.album.id)\n        album_dict = PersistentVariables.get_album_dict(self.album_id)\n        album_count = len(album_dict)\n        session_dict = PersistentVariables.get_session_dict()\n        session_count = len(session_dict)\n\n        table = self.ui.metadata_table\n        key_example, value_example = self.get_table_items(table, 0)\n        self.key_flags = key_example.flags()\n        self.value_flags = value_example.flags()\n        table.setRowCount(album_count + session_count + 2)\n        i = 0\n        self.add_separator_row(table, i, \"Album Variables\", album_count)\n        i += 1\n        for key in sorted(album_dict.keys()):\n            key_item, value_item = self.get_table_items(table, i)\n            key_item.setText(key)\n            value_item.setText(album_dict[key])\n            i += 1\n        self.add_separator_row(table, i, \"Session Variables\", session_count)\n        i += 1\n        for key in sorted(session_dict.keys()):\n            key_item, value_item = self.get_table_items(table, i)\n            key_item.setText(key)\n            value_item.setText(session_dict[key])\n            i += 1\n\n    def add_separator_row(self, table, i, title, count):\n        key_item, value_item = self.get_table_items(table, i)\n        font = key_item.font()\n        font.setBold(True)\n        key_item.setFont(font)\n        key_item.setText(title)\n        value_item.setText(\"{0} item{1}\".format(count, \"\" if count == 1 else \"s\",))\n\n    def get_table_items(self, table, i):\n        key_item = table.item(i, 0)\n        value_item = table.item(i, 1)\n        if not key_item:\n            key_item = QtWidgets.QTableWidgetItem()\n            key_item.setFlags(self.key_flags)\n            table.setItem(i, 0, key_item)\n        if not value_item:\n            value_item = QtWidgets.QTableWidgetItem()\n            value_item.setFlags(self.value_flags)\n            table.setItem(i, 1, value_item)\n        return key_item, value_item\n\nviewer = ViewVariables()\n\n\n# Register the new functions\nregister_script_function(func_set_a, name='set_a',\n    documentation=\"\"\"`$set_a(name,value)`\n\nSets the album persistent variable `name` to `value`.\"\"\")\n\nregister_script_function(func_unset_a, name='unset_a',\n    documentation=\"\"\"`$unset_a(name)`\n\nUnsets the album persistent variable `name`.\"\"\")\n\nregister_script_function(func_get_a, name='get_a',\n    documentation=\"\"\"`$get_a(name)`\n\nGets the album persistent variable `name`.\"\"\")\n\nregister_script_function(func_clear_a, name='clear_a',\n    documentation=\"\"\"`$clear_a()`\n\nClears all album persistent variables.\"\"\")\n\nregister_script_function(func_set_s, name='set_s',\n    documentation=\"\"\"`$set_s(name,value)`\n\nSets the session persistent variable `name` to `value`.\"\"\")\n\nregister_script_function(func_unset_s, name='unset_s',\n    documentation=\"\"\"`$unset_s(name)`\n\nUnsets the session persistent variable `name`.\"\"\")\n\nregister_script_function(func_get_s, name='get_s',\n    documentation=\"\"\"`$get_s(name)`\n\nGets the session persistent variable `name`.\"\"\")\n\nregister_script_function(func_clear_s, name='clear_s',\n    documentation=\"\"\"`$clear_s()`\n\nClears all session persistent variables.\"\"\")\n\n\n# Register the processers\nregister_album_metadata_processor(initialize_album_dict, priority=PluginPriority.HIGH)\nregister_album_post_removal_processor(destroy_album_dict)\n\n\n# Register context actions\nregister_file_action(viewer)\nregister_track_action(viewer)\nregister_album_action(viewer)\n"
  },
  {
    "path": "plugins/persistent_variables/ui_variables_dialog.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form based on that used for the \"View Script Variables\" plugin\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_VariablesDialog(object):\n    def setupUi(self, VariablesDialog):\n        VariablesDialog.setObjectName(\"VariablesDialog\")\n        VariablesDialog.resize(600, 450)\n        self.verticalLayout = QtWidgets.QVBoxLayout(VariablesDialog)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.metadata_table = QtWidgets.QTableWidget(VariablesDialog)\n        self.metadata_table.setAutoFillBackground(False)\n        self.metadata_table.setSelectionMode(QtWidgets.QAbstractItemView.ContiguousSelection)\n        self.metadata_table.setRowCount(1)\n        self.metadata_table.setColumnCount(2)\n        self.metadata_table.setObjectName(\"metadata_table\")\n        item = QtWidgets.QTableWidgetItem()\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        item.setFont(font)\n        self.metadata_table.setHorizontalHeaderItem(0, item)\n        item = QtWidgets.QTableWidgetItem()\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        item.setFont(font)\n        self.metadata_table.setHorizontalHeaderItem(1, item)\n        item = QtWidgets.QTableWidgetItem()\n        item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled)\n        self.metadata_table.setItem(0, 0, item)\n        item = QtWidgets.QTableWidgetItem()\n        item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled)\n        self.metadata_table.setItem(0, 1, item)\n        self.metadata_table.horizontalHeader().setDefaultSectionSize(150)\n        self.metadata_table.horizontalHeader().setSortIndicatorShown(False)\n        self.metadata_table.horizontalHeader().setStretchLastSection(True)\n        self.metadata_table.verticalHeader().setVisible(False)\n        self.metadata_table.verticalHeader().setDefaultSectionSize(20)\n        self.metadata_table.verticalHeader().setMinimumSectionSize(20)\n        self.verticalLayout.addWidget(self.metadata_table)\n        self.buttonBox = QtWidgets.QDialogButtonBox(VariablesDialog)\n        self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Ok)\n        self.buttonBox.setObjectName(\"buttonBox\")\n        self.verticalLayout.addWidget(self.buttonBox)\n\n        self.retranslateUi(VariablesDialog)\n        QtCore.QMetaObject.connectSlotsByName(VariablesDialog)\n\n    def retranslateUi(self, VariablesDialog):\n        _translate = QtCore.QCoreApplication.translate\n        item = self.metadata_table.horizontalHeaderItem(0)\n        item.setText(_translate(\"VariablesDialog\", \"Variable\"))\n        item = self.metadata_table.horizontalHeaderItem(1)\n        item.setText(_translate(\"VariablesDialog\", \"Value\"))\n        __sortingEnabled = self.metadata_table.isSortingEnabled()\n        self.metadata_table.setSortingEnabled(False)\n        self.metadata_table.setSortingEnabled(__sortingEnabled)\n"
  },
  {
    "path": "plugins/playlist/playlist.py",
    "content": "#!/usr/bin/python\n# -*- coding: utf-8 -*-\n\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n\nPLUGIN_NAME = \"Generate M3U playlist\"\nPLUGIN_AUTHOR = \"Francis Chin, Sambhav Kothari, Chris Hylen\"\nPLUGIN_DESCRIPTION = \"\"\"Generate an Extended M3U playlist (.m3u8 file, UTF8\nencoded text). Relative pathnames are used where audio files are in the same\ndirectory as the playlist, otherwise absolute (full) pathnames are used.\"\"\"\nPLUGIN_VERSION = \"1.2.1\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nimport os.path\n\nfrom PyQt5 import QtCore, QtWidgets\nfrom picard import log\nfrom picard.const import VARIOUS_ARTISTS_ID\nfrom picard.util import find_existing_path, encode_filename\nfrom picard.ui.itemviews import BaseAction, register_album_action\n\n\n_debug_level = 0\n\ndef get_safe_filename(filename):\n    _valid_chars = \" .,_-:+&!()\"\n    _safe_filename = \"\".join(\n        c if (c.isalnum() or c in _valid_chars) else \"_\" for c in filename\n    ).rstrip()\n    return _safe_filename\n\n\nclass PlaylistEntry(list):\n\n    def __init__(self, playlist, index):\n        list.__init__(self)\n        self.playlist = playlist\n        self.index = index\n\n    def add(self, entry_row):\n        self.append(entry_row + \"\\n\")\n\n\nclass Playlist(object):\n\n    def __init__(self, filename):\n        self.filename = filename\n        self.entries = []\n        self.headers = []\n\n    def add_header(self, header):\n        self.headers.append(header + \"\\n\")\n\n    def write(self):\n        b_lines = []\n        for header in self.headers:\n            b_lines.append(header.encode(\"utf-8\"))\n        for entry in self.entries:\n            for row in entry:\n                b_lines.append(row.encode(\"utf-8\"))\n        with open(encode_filename(self.filename), \"wb\") as f:\n            f.writelines(b_lines)\n\n\nclass GeneratePlaylist(BaseAction):\n    NAME = \"Generate &Playlist...\"\n\n    def callback(self, objs):\n        # Find common path of all files to default where to save playlist\n        files = []\n        for album in objs:\n            for track in album.tracks:\n                if track.linked_files:\n                    files.append(track.linked_files[0].filename)\n\n        try:\n            current_directory = os.path.commonpath(files)\n        except ValueError:\n            current_directory = \"\"\n\n        # Default playlist filename set as \"%albumartist% - %album%.m3u8\",\n        # except where \"Various Artists\" is suppressed\n        if _debug_level > 1:\n            log.debug(\"{}: VARIOUS_ARTISTS_ID is {}, musicbrainz_albumartistid is {}\".format(\n                    PLUGIN_NAME, VARIOUS_ARTISTS_ID,\n                    objs[0].metadata[\"musicbrainz_albumartistid\"]))\n        if objs[0].metadata[\"musicbrainz_albumartistid\"] != VARIOUS_ARTISTS_ID:\n            default_filename = get_safe_filename(\n                objs[0].metadata[\"albumartist\"] + \" - \"\n                + objs[0].metadata[\"album\"] + \".m3u8\"\n            )\n        else:\n            default_filename = get_safe_filename(\n                objs[0].metadata[\"album\"] + \".m3u8\"\n            )\n        if _debug_level > 1:\n            log.debug(\"{}: default playlist filename sanitized to {}\".format(\n                    PLUGIN_NAME, default_filename))\n        filename, selected_format = QtWidgets.QFileDialog.getSaveFileName(\n            None, \"Save new playlist\",\n            os.path.join(current_directory, default_filename),\n            \"Playlist (*.m3u8 *.m3u)\"\n        )\n        if filename:\n            playlist = Playlist(filename)\n            playlist.add_header(\"#EXTM3U\")\n\n            for album in objs:\n                for track in album.tracks:\n                    if track.linked_files:\n                        entry = PlaylistEntry(playlist, len(playlist.entries))\n                        playlist.entries.append(entry)\n\n                        # M3U EXTINF row\n                        track_length_seconds = int(round(track.metadata.length / 1000.0))\n                        # EXTINF format assumed to be fixed as follows:\n                        entry.add(\"#EXTINF:{duration:d},{artist} - {title}\".format(\n                            duration=track_length_seconds,\n                            artist=track.metadata[\"artist\"],\n                            title=track.metadata[\"title\"]\n                            )\n                        )\n\n                        # M3U URL row - assumes only one file per track\n                        audio_filename = track.linked_files[0].filename\n                        if _debug_level > 1:\n                            for i, file in enumerate(track.linked_files):\n                                log.debug(\"{}: linked_file {}: {}\".format(\n                                    PLUGIN_NAME, i, str(file)))\n                        # If playlist is in same directory as audio files, then use\n                        # local (relative) pathname, otherwise use absolute pathname\n                        if _debug_level > 1:\n                            log.debug(\"{}: audio_filename: {}, selected dir: {}\".format(\n                                    PLUGIN_NAME, audio_filename, os.path.dirname(filename)))\n\n                        try:\n                            audio_filename = os.path.relpath(audio_filename, os.path.dirname(filename))\n                        except ValueError:\n                            pass\n                        entry.add(str(audio_filename))\n\n            playlist.write()\n\n\nregister_album_action(GeneratePlaylist())\n"
  },
  {
    "path": "plugins/post_tagging_actions/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2024 Giorgio Fontanive (twodoorcoupe)\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n\nPLUGIN_NAME = \"Post Tagging Actions\"\nPLUGIN_AUTHOR = \"Giorgio Fontanive\"\nPLUGIN_DESCRIPTION = \"\"\"\nThis plugin lets you set up actions that run with a context menu click. \nAn action consists in a command line executed for each album or each track along\nwith a few options to tweak the behaviour. \nThis can be used to run external programs and pass some variables to it. \n\"\"\"\nPLUGIN_VERSION = \"0.1\"\nPLUGIN_API_VERSIONS = [\"2.10\", \"2.11\"]\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\nPLUGIN_USER_GUIDE_URL = \"https://github.com/metabrainz/picard-plugins/tree/2.0/plugins/post_tagging_actions/docs/guide.md\"\n\nfrom picard.album import Album\nfrom picard.track import Track\nfrom picard.ui.options import OptionsPage, register_options_page\nfrom picard.ui.itemviews import BaseAction, register_album_action, register_track_action\nfrom picard.ui import mainwindow\nfrom picard import log, config\nfrom picard.const import sys\nfrom picard.util import thread\nfrom picard.script import parser\n\nfrom .options_post_tagging_actions import Ui_PostTaggingActions\nfrom .actions_status import Ui_ActionsStatus\nfrom PyQt5 import QtCore, QtWidgets, QtGui\n\nfrom collections import namedtuple\nfrom queue import PriorityQueue\nfrom threading import Thread, Lock\nfrom concurrent import futures\nfrom os import path, cpu_count\nimport re\nimport shlex\nimport subprocess  # nosec B404\nimport time\n\nWIDGET_UPDATE_INTERVAL = 0.5\n\n# Additional special variables.\nTRACK_SPECIAL_VARIABLES = {\n    \"filepath\": lambda file: file,\n    \"folderpath\": lambda file: path.dirname(file),  # pylint: disable=unnecessary-lambda\n    \"filename\": lambda file: path.splitext(path.basename(file))[0],\n    \"filename_ext\": lambda file: path.basename(file),  # pylint: disable=unnecessary-lambda\n    \"directory\": lambda file: path.basename(path.dirname(file))\n}\nALBUM_SPECIAL_VARIABLES = {\n    \"get_num_matched_tracks\",\n    \"get_num_unmatched_files\",\n    \"get_num_total_files\",\n    \"get_num_unsaved_files\",\n    \"is_complete\",\n    \"is_modified\"\n}\n\n# Settings.\nCANCEL = \"pta_cancel\"\nMAX_WORKERS = \"pta_max_workers\"\nOPTIONS = (\"pta_command\", \"pta_wait_for_exit\", \"pta_execute_for_tracks\", \"pta_refresh_tags\")\n\nOptions = namedtuple(\"Options\", (\"variables\", *[option[4:] for option in OPTIONS]))\nAction = namedtuple(\"Action\", (\"commands\", \"album\", \"options\"))\nPriorityAction = namedtuple(\"PriorityAction\", (\"priority\", \"counter\", \"action\"))\naction_queue = PriorityQueue()\nvariables_pattern = re.compile(r'%.*?%')\n\n\nclass ActionLoader:\n    \"\"\"Adds actions to the execution queue.\n\n    Attributes:\n        action_options (list): Stores the actions' information loaded from the options page.\n        action_counter (int): The count of actions that have been added to the queue, used for priority.\n    \"\"\"\n\n    def __init__(self):\n        self.action_options = []\n        self.action_counter = 0\n        self.load_actions()\n\n    def _create_options(self, command, *other_options):\n        \"\"\"Finds the variables in the command and adds the options to the action options list.\n        \"\"\"\n        variables = [parser.normalize_tagname(variable[1:-1]) for variable in variables_pattern.findall(command)]\n        command = variables_pattern.sub('{}', command)\n        options = Options(variables, command, *other_options)\n        self.action_options.append(options)\n\n    def _create_action(self, priority, commands, album, options):\n        \"\"\"Adds an action with the given parameters to the execution queue.\n\n        If the os is not windows, the command is split as suggested by the subprocess\n        module documentation.\n        \"\"\"\n        if not sys.IS_WIN:\n            commands = [shlex.split(command) for command in commands]\n        action = Action(commands, album, options)\n        priority_action = PriorityAction(priority, self.action_counter, action)\n        action_queue.put(priority_action)\n        self.action_counter += 1\n\n    def _replace_variables(self, variables, item):\n        \"\"\"Returns a list where each variable is replaced with its value.\n\n        Item is either an album or a track. For track special variables,\n        it uses the path of the first file of the given item.\n        If the variable is not found anywhere, it remains as in the original text.\n        \"\"\"\n        values = []\n        album = item.album if isinstance(item, Track) else item\n        first_file_path = next(item.iterfiles()).filename\n        for variable in variables:\n            if variable in ALBUM_SPECIAL_VARIABLES:\n                values.append(getattr(album, variable)())\n            elif variable in TRACK_SPECIAL_VARIABLES:\n                values.append(TRACK_SPECIAL_VARIABLES[variable](first_file_path))\n            else:\n                values.append(item.metadata.get(variable, f\"%{variable}%\"))\n        return values\n\n    def add_actions(self, album, tracks):\n        \"\"\"Adds one action to the execution queue for each tuple in the action options list.\n\n        Actions meant to be executed once for each track are considered as a single\n        action. This way, the other options are more consistent.\n        \"\"\"\n        for priority, options in enumerate(self.action_options):\n            if options.execute_for_tracks:\n                values_list = [self._replace_variables(options.variables, track) for track in tracks]\n            else:\n                values_list = [self._replace_variables(options.variables, album)]\n            commands = [options.command.format(*values) for values in values_list]\n            self._create_action(priority, commands, album, options)\n\n    def load_actions(self):\n        \"\"\"Loads the information from the options and saves it in the action options list.\n\n        This gets called when the plugin is loaded or when the user saves the options.\n        \"\"\"\n        self.action_options = []\n        option_tuples = zip(*[config.setting[name] for name in OPTIONS])\n        for option_tuple in option_tuples:\n            command = option_tuple[0]\n            other_options = [eval(option) for option in option_tuple[1:]]  # nosec B307\n            self._create_options(command, *other_options)\n\n\nclass ActionRunner:\n    \"\"\"Runs actions in the execution queue.\n\n    Attributes:\n        action_thread_pool (ThreadPoolExecutor): Pool used to run processes with the subprocess module.\n        refresh_tags_pool (ThreadPoolExecutor): Pool used to reload tags from files and refresh albums.\n        worker (Thread): Worker thread that picks actions from the execution queue.\n        update_widget (Thread): Thread that updates the number of pending actions in the status bar.\n    \"\"\"\n\n    def __init__(self):\n        self.action_thread_pool = futures.ThreadPoolExecutor(config.setting[MAX_WORKERS])\n        self.refresh_tags_pool = futures.ThreadPoolExecutor(1)\n        self.worker = Thread(target = self._execute)\n        self.update_widget = Thread(target = self._update_widget)\n        self.worker.start()\n\n        self.keep_updating = True\n        self.currently_executing = 0\n        self.currently_executing_lock = Lock()\n        self.status_widget = ActionsStatus()\n\n        # This is used to register functions that run when the application is being closed.\n        # The stop function makes the background threads return.\n        tagger = QtCore.QCoreApplication.instance()\n        tagger.register_cleanup(self.stop)\n\n        # This checks whether the tagger has already created the main window.\n        # It should happen only when the plugin is installed through the options menu,\n        # otherwise the plugins are loaded before the main window is created.\n        if hasattr(tagger, \"window\"):\n            self._create_widget(tagger.window)\n        else:\n            # This is used to register functions that run after the main window has finished loading.\n            mainwindow.register_ui_init(self._create_widget)\n\n    def _create_widget(self, window):\n        \"\"\"Adds the pending actions widget to the right of the other icons in the statusbar.\n        \"\"\"\n        window.statusBar().insertPermanentWidget(1, self.status_widget)\n        self.update_widget.start()\n\n    def _update_widget(self):\n        \"\"\"Updates the number of pending actions in the status bar at regular intervals.\n        \"\"\"\n        while self.keep_updating:\n            number_of_actions = action_queue.qsize() + self.currently_executing\n            thread.to_main(self.status_widget.update_actions_count, number_of_actions)\n            time.sleep(WIDGET_UPDATE_INTERVAL)\n\n    def _refresh_tags(self, future_objects, album):\n        \"\"\"Reloads tags from the album's files and refreshes the album.\n\n        First, it makes sure that the action has finished running. This is used for\n        when an external process changes a file's tags.\n        \"\"\"\n        futures.wait(future_objects, return_when = futures.ALL_COMPLETED)\n        for file in album.iterfiles():\n            file.set_pending()\n            file.load(lambda file: None)\n        thread.to_main(album.load, priority = True, refresh = True)\n\n    def _run_process(self, command):\n        \"\"\"Runs the process and waits for it to finish.\n        \"\"\"\n        process = subprocess.Popen(\n            command,\n            text = True,\n            stdout = subprocess.PIPE,\n            stderr = subprocess.PIPE\n        )  # nosec B603\n        answer = process.communicate()\n        if answer[0]:\n            log.info(\"Action output:\\n%s\", answer[0])\n        if answer[1]:\n            log.error(\"Action error:\\n%s\", answer[1])\n\n    def _update_executing_count(self, future_objects):\n        \"\"\"Decrements the count of executing actions once the given action finishes.\n        \"\"\"\n        futures.wait(future_objects, return_when = futures.ALL_COMPLETED)\n        with self.currently_executing_lock:\n            self.currently_executing -= 1\n\n    def _execute(self):\n        \"\"\"Takes actions from the execution queue and runs them.\n\n        If it finds an action with priority -1, the loop stops. When the loop\n        stops, both ThreadPoolExecutors are shutdown.\n        \"\"\"\n        while True:\n            priority_action = action_queue.get()\n            if priority_action.priority == -1:\n                break\n            with self.currently_executing_lock:\n                self.currently_executing += 1\n            next_action = priority_action.action\n            commands = next_action.commands\n            future_objects = {self.action_thread_pool.submit(self._run_process, command) for command in commands}\n\n            if next_action.options.wait_for_exit:\n                futures.wait(future_objects, return_when = futures.ALL_COMPLETED)\n            if next_action.options.refresh_tags:\n                self.refresh_tags_pool.submit(self._refresh_tags, future_objects, next_action.album)\n            self.refresh_tags_pool.submit(self._update_executing_count, future_objects)\n            action_queue.task_done()\n\n        self.action_thread_pool.shutdown(wait = False, cancel_futures = True)\n        self.refresh_tags_pool.shutdown(wait = False, cancel_futures = True)\n\n    def stop(self):\n        \"\"\"Makes the worker thread exit its loop.\n\n        This gets called when Picard is closed. It waits for the processes that\n        are still executing to finish before exiting.\n        \"\"\"\n        if not config.setting[CANCEL]:\n            action_queue.join()\n        action_queue.put(PriorityAction(-1, -1, None))\n        self.keep_updating = False\n        if self.update_widget.is_alive():\n            self.update_widget.join()\n        self.worker.join()\n\n\nclass ExecuteAlbumActions(BaseAction):\n\n    NAME = \"Run actions for highlighted albums\"\n\n    def callback(self, objs):\n        albums = {obj for obj in objs if isinstance(obj, Album)}\n        for album in albums:\n            action_loader.add_actions(album, album.tracks)\n\n\nclass ExecuteTrackActions(BaseAction):\n\n    NAME = \"Run actions for highlighted tracks\"\n\n    def callback(self, objs):\n        tracks = {obj for obj in objs if isinstance(obj, Track)}\n        albums = {track.album for track in tracks}\n        for album in albums:\n            album_tracks = tracks.intersection(album.tracks)\n            action_loader.add_actions(album, album_tracks)\n\n\nclass PostTaggingActionsOptions(OptionsPage):\n    \"\"\"Options page found under the \"plugins\" page.\n    \"\"\"\n\n    NAME = \"post_tagging_actions\"\n    TITLE = \"Post Tagging Actions\"\n    PARENT = \"plugins\"\n\n    action_options = [config.ListOption(\"setting\", name, []) for name in OPTIONS]\n    options = [\n        config.BoolOption(\"setting\", CANCEL, True),\n        config.IntOption(\"setting\", MAX_WORKERS, min(32, cpu_count() + 4)),\n        *action_options\n    ]\n\n    def __init__(self, parent = None):\n        super(PostTaggingActionsOptions, self).__init__(parent)\n        self.ui = Ui_PostTaggingActions()\n        self.ui.setupUi(self)\n        self._reset_ui()\n\n        header = self.ui.table.horizontalHeader()\n        header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch)\n        for column in range(1, header.count()):\n            header.setSectionResizeMode(column, QtWidgets.QHeaderView.ResizeMode.ResizeToContents)\n\n        self.ui.add_file_path.clicked.connect(self._open_file_dialog)\n        self.ui.add_action.clicked.connect(self._add_action_to_table)\n        self.ui.remove_action.clicked.connect(self._remove_action_from_table)\n        self.ui.up.clicked.connect(self._move_action_up)\n        self.ui.down.clicked.connect(self._move_action_down)\n\n        self.get_table_columns_values = [\n            self.ui.action.text,\n            self.ui.wait.isChecked,\n            self.ui.tracks.isChecked,\n            self.ui.refresh.isChecked\n        ]\n\n    def _open_file_dialog(self):\n        \"\"\"Adds the selected file's path to the command line text box.\n        \"\"\"\n        file = QtWidgets.QFileDialog.getOpenFileName(self)[0]\n        cursor_position = self.ui.action.cursorPosition()\n        current_text = self.ui.action.text()\n        if not sys.IS_WIN:\n            file = shlex.quote(file)\n        new_text = current_text[:cursor_position] + file + current_text[cursor_position:]\n        self.ui.action.setText(new_text)\n\n    def _reset_ui(self):\n        self.ui.action.setText(\"\")\n        self.ui.wait.setChecked(False)\n        self.ui.refresh.setChecked(False)\n        self.ui.albums.setChecked(True)\n\n    def _add_action_to_table(self):\n        if not self.ui.action.text():\n            return\n        row_position = self.ui.table.rowCount()\n        self.ui.table.insertRow(row_position)\n        for column in range(self.ui.table.columnCount()):\n            value = self.get_table_columns_values[column]()\n            value = str(value)\n            widget = QtWidgets.QTableWidgetItem(value)\n            self.ui.table.setItem(row_position, column, widget)\n        self._reset_ui()\n\n    def _remove_action_from_table(self):\n        current_row = self.ui.table.currentRow()\n        if current_row != -1:\n            self.ui.table.removeRow(current_row)\n\n    def _move_action_up(self):\n        current_row = self.ui.table.currentRow()\n        new_row = current_row - 1\n        if current_row > 0:\n            self._swap_table_rows(current_row, new_row)\n            self.ui.table.setCurrentCell(new_row, 0)\n\n    def _move_action_down(self):\n        current_row = self.ui.table.currentRow()\n        new_row = current_row + 1\n        if current_row < self.ui.table.rowCount() - 1:\n            self._swap_table_rows(current_row, new_row)\n            self.ui.table.setCurrentCell(new_row, 0)\n\n    def _swap_table_rows(self, row1, row2):\n        for column in range(self.ui.table.columnCount()):\n            item1 = self.ui.table.takeItem(row1, column)\n            item2 = self.ui.table.takeItem(row2, column)\n            self.ui.table.setItem(row1, column, item2)\n            self.ui.table.setItem(row2, column, item1)\n\n    def load(self):\n        \"\"\"Puts the plugin's settings into the actions table.\n        \"\"\"\n        settings = zip(*[config.setting[name] for name in OPTIONS])\n        for row, values in enumerate(settings):\n            self.ui.table.insertRow(row)\n            for column in range(self.ui.table.columnCount()):\n                widget = QtWidgets.QTableWidgetItem(values[column])\n                self.ui.table.setItem(row, column, widget)\n        self.ui.cancel.setChecked(config.setting[CANCEL])\n        self.ui.max_workers.setValue(config.setting[MAX_WORKERS])\n\n    def save(self):\n        \"\"\"Saves the actions table items in the settings.\n        \"\"\"\n        settings = []\n        for column in range(self.ui.table.columnCount()):\n            settings.append([])\n            for row in range(self.ui.table.rowCount()):\n                setting = self.ui.table.item(row, column).text()\n                settings[column].append(setting)\n            config.setting[OPTIONS[column]] = settings[column]\n        config.setting[CANCEL] = self.ui.cancel.isChecked()\n        config.setting[MAX_WORKERS] = self.ui.max_workers.value()\n        action_loader.load_actions()\n\n\nclass ActionsStatus(QtWidgets.QWidget, Ui_ActionsStatus):\n    \"\"\"An icon and a label that displays the number of pending actions.\n\n    The widget is only visible when there are pending actions. This is placed\n    in the statusbar to the left of the other icons.\n    \"\"\"\n\n    def __init__(self):\n        QtWidgets.QWidget.__init__(self)\n        Ui_ActionsStatus.__init__(self)\n        self.setupUi(self)\n        self.hide()\n\n        # Creates the icon to the right of the label.\n        size = QtCore.QSize(16, 16)\n        icon = QtGui.QIcon(\":/images/16x16/applications-system.png\")\n        self.label.setPixmap(icon.pixmap(size))\n\n    def update_actions_count(self, count):\n        self.actions_count.setText(f\"{count}\")\n        self.setVisible(count > 0)\n\n\naction_loader = ActionLoader()\naction_runner = ActionRunner()\nregister_album_action(ExecuteAlbumActions())\nregister_track_action(ExecuteTrackActions())\nregister_options_page(PostTaggingActionsOptions)\n"
  },
  {
    "path": "plugins/post_tagging_actions/actions_status.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/post_tagging_actions/actions_status.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.10\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtWidgets\n\n\nclass Ui_ActionsStatus(object):\n    def setupUi(self, ActionsStatus):\n        ActionsStatus.setObjectName(\"ActionsStatus\")\n        ActionsStatus.resize(94, 18)\n        self.horizontalLayout = QtWidgets.QHBoxLayout(ActionsStatus)\n        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)\n        self.horizontalLayout.setSpacing(2)\n        self.horizontalLayout.setObjectName(\"horizontalLayout\")\n        self.actions_count = QtWidgets.QLabel(ActionsStatus)\n        self.actions_count.setMinimumSize(QtCore.QSize(35, 0))\n        self.actions_count.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)\n        self.actions_count.setObjectName(\"actions_count\")\n        self.horizontalLayout.addWidget(self.actions_count)\n        self.label = QtWidgets.QLabel(ActionsStatus)\n        self.label.setText(\"\")\n        self.label.setObjectName(\"label\")\n        self.horizontalLayout.addWidget(self.label)\n\n        self.retranslateUi(ActionsStatus)\n        QtCore.QMetaObject.connectSlotsByName(ActionsStatus)\n\n    def retranslateUi(self, ActionsStatus):\n        _translate = QtCore.QCoreApplication.translate\n        ActionsStatus.setWindowTitle(_translate(\"ActionsStatus\", \"Form\"))\n        self.actions_count.setText(_translate(\"ActionsStatus\", \"0\"))\n        self.label.setToolTip(_translate(\"ActionsStatus\", \"Remaining Actions\"))\n"
  },
  {
    "path": "plugins/post_tagging_actions/actions_status.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>ActionsStatus</class>\n <widget class=\"QWidget\" name=\"ActionsStatus\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>94</width>\n    <height>18</height>\n   </rect>\n  </property>\n  <property name=\"windowTitle\">\n   <string>Form</string>\n  </property>\n  <layout class=\"QHBoxLayout\" name=\"horizontalLayout\">\n   <property name=\"spacing\">\n    <number>2</number>\n   </property>\n   <property name=\"leftMargin\">\n    <number>0</number>\n   </property>\n   <property name=\"topMargin\">\n    <number>0</number>\n   </property>\n   <property name=\"rightMargin\">\n    <number>0</number>\n   </property>\n   <property name=\"bottomMargin\">\n    <number>0</number>\n   </property>\n   <item>\n    <widget class=\"QLabel\" name=\"actions_count\">\n     <property name=\"minimumSize\">\n      <size>\n       <width>35</width>\n       <height>0</height>\n      </size>\n     </property>\n     <property name=\"text\">\n      <string>0</string>\n     </property>\n     <property name=\"alignment\">\n      <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>\n     </property>\n    </widget>\n   </item>\n   <item>\n    <widget class=\"QLabel\" name=\"label\">\n     <property name=\"toolTip\">\n      <string>Remaining Actions</string>\n     </property>\n     <property name=\"text\">\n      <string/>\n     </property>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/post_tagging_actions/docs/guide.md",
    "content": "# Post Tagging Actions\nThis plugin lets you set up actions that run with a context menu click.\nAn action consists in a command line executed for each album or each track along with a few options to tweak the behaviour. This can be used to run external programs and pass some variables to it. Environment variables do not work.\n\nTo run the actions,\n- First move the albums or tracks you are interested in to the right hand pane.\n- Then highlight all the items you want the actions to run for.\n- Right click, go to plugins, then click \"Run Actions for highlighted albums/tracks\".\n### Adding an action\nIn the options page, you will find \"Post Tagging Actions\" under \"Plugins\". You will be greeted by this:\n\n![options](options.png)\n\n1. Enter the command to run in the text box, choose your options, then click \"Add action\".\n2. You can click on \"Add file\" to search for a file and add its path to the text box.\n3. Once you add an action, it will appear in the table at the bottom of the page. You can reorder actions with the arrows above the table.\n## Options\n- `Wait for process to finish` will make the next command in the queue execute only after this one has finished.\n- `Refresh tags after process finishes` will reload the files once the command finishes. This is useful when an external program changes files' tags.\n- `Execute for albums/tracks` lets you choose whether the command will be executed once for each track or each album highlighted.\n\nThe order of the actions in the table represents the order of execution: the top most action will be executed first.\n## Variables\nYou can use variables in the commands just like in scripting. For example: \n```\npython /path/script.py --album %album% --artist %albumartist%\n```\nThe variables `%album%` and `%albumartist%` will be replaced with their value for each album.\n\nFor actions that execute once per album, only variables in the album's metadata can be used. Same thing for actions that execute once per track, only variables in the track's metadata can be used.\n### Special variables\nThere are also extra variables that can be used.\n\nThe following are used to get files' information: \n- `%filepath%`: The full path of the file.\n- `%folderpath%`: The path of the folder in which the file is located.\n- `%filename%`: The name of the file, without the extension.\n- `%filename_ext%`: The name of the file, with the extension.\n- `%directory%`: The name of the folder in which the file is located.\n\nWhen these are used with album actions,  the first file found is considered. For example, if you keep all tracks in the same folder, using `%folderpath%` will give you the path to that folder.\n\nThe following apply to each album:\n- `%gen_num_matched_tracks%`: The number of tracks which have a matching file.\n- `%gen_num_unmatched_tracks%`: The number of tracks without any matching files.\n- `%gen_num_total_files%`: The number of files added.\n- `%gen_num_unsaed_files%`: The number of files that have unsaved changes.\n- `%is_complete%`: \"True\" if every track is matched, \"False\" otherwise. (Shown with a gold disc in Picard)\n- `%is_modified%`: \"True\" if the album has some changes to apply, \"False\" otherwise. (Shown with a red star on the disc)\n\nWhen these are used with track actions, the album to which the track belongs to is considered.\n\n"
  },
  {
    "path": "plugins/post_tagging_actions/options_post_tagging_actions.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/post_tagging_actions/options_post_tagging_actions.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.10\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtWidgets\n\n\nclass Ui_PostTaggingActions(object):\n    def setupUi(self, PostTaggingActions):\n        PostTaggingActions.setObjectName(\"PostTaggingActions\")\n        PostTaggingActions.resize(638, 450)\n        PostTaggingActions.setMinimumSize(QtCore.QSize(100, 0))\n        self.verticalLayout = QtWidgets.QVBoxLayout(PostTaggingActions)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.scrollArea = QtWidgets.QScrollArea(PostTaggingActions)\n        self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame)\n        self.scrollArea.setWidgetResizable(True)\n        self.scrollArea.setObjectName(\"scrollArea\")\n        self.scrollAreaWidgetContents = QtWidgets.QWidget()\n        self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, -70, 606, 502))\n        self.scrollAreaWidgetContents.setObjectName(\"scrollAreaWidgetContents\")\n        self.vboxlayout = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents)\n        self.vboxlayout.setObjectName(\"vboxlayout\")\n        self.action_widget = QtWidgets.QWidget(self.scrollAreaWidgetContents)\n        self.action_widget.setObjectName(\"action_widget\")\n        self._2 = QtWidgets.QVBoxLayout(self.action_widget)\n        self._2.setObjectName(\"_2\")\n        self.widget_2 = QtWidgets.QWidget(self.action_widget)\n        self.widget_2.setObjectName(\"widget_2\")\n        self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.widget_2)\n        self.horizontalLayout_3.setObjectName(\"horizontalLayout_3\")\n        self.label_3 = QtWidgets.QLabel(self.widget_2)\n        self.label_3.setObjectName(\"label_3\")\n        self.horizontalLayout_3.addWidget(self.label_3)\n        self.add_file_path = QtWidgets.QPushButton(self.widget_2)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.add_file_path.sizePolicy().hasHeightForWidth())\n        self.add_file_path.setSizePolicy(sizePolicy)\n        self.add_file_path.setCheckable(False)\n        self.add_file_path.setObjectName(\"add_file_path\")\n        self.horizontalLayout_3.addWidget(self.add_file_path)\n        self._2.addWidget(self.widget_2)\n        self.action = QtWidgets.QLineEdit(self.action_widget)\n        self.action.setObjectName(\"action\")\n        self._2.addWidget(self.action)\n        self.wait = QtWidgets.QCheckBox(self.action_widget)\n        self.wait.setObjectName(\"wait\")\n        self._2.addWidget(self.wait)\n        self.refresh = QtWidgets.QCheckBox(self.action_widget)\n        self.refresh.setObjectName(\"refresh\")\n        self._2.addWidget(self.refresh)\n        self.widget_3 = QtWidgets.QWidget(self.action_widget)\n        self.widget_3.setObjectName(\"widget_3\")\n        self.horizontalLayout_6 = QtWidgets.QHBoxLayout(self.widget_3)\n        self.horizontalLayout_6.setObjectName(\"horizontalLayout_6\")\n        self.albums = QtWidgets.QRadioButton(self.widget_3)\n        self.albums.setObjectName(\"albums\")\n        self.horizontalLayout_6.addWidget(self.albums)\n        self.tracks = QtWidgets.QRadioButton(self.widget_3)\n        self.tracks.setObjectName(\"tracks\")\n        self.horizontalLayout_6.addWidget(self.tracks)\n        self._2.addWidget(self.widget_3)\n        self.vboxlayout.addWidget(self.action_widget)\n        self.table_commands = QtWidgets.QWidget(self.scrollAreaWidgetContents)\n        self.table_commands.setObjectName(\"table_commands\")\n        self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.table_commands)\n        self.horizontalLayout_2.setObjectName(\"horizontalLayout_2\")\n        self.widget = QtWidgets.QWidget(self.table_commands)\n        self.widget.setObjectName(\"widget\")\n        self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.widget)\n        self.horizontalLayout_5.setObjectName(\"horizontalLayout_5\")\n        self.add_action = QtWidgets.QPushButton(self.widget)\n        self.add_action.setObjectName(\"add_action\")\n        self.horizontalLayout_5.addWidget(self.add_action)\n        self.remove_action = QtWidgets.QPushButton(self.widget)\n        self.remove_action.setObjectName(\"remove_action\")\n        self.horizontalLayout_5.addWidget(self.remove_action)\n        self.horizontalLayout_2.addWidget(self.widget)\n        self.widget1 = QtWidgets.QWidget(self.table_commands)\n        self.widget1.setObjectName(\"widget1\")\n        self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.widget1)\n        self.horizontalLayout_4.setObjectName(\"horizontalLayout_4\")\n        self.up = QtWidgets.QToolButton(self.widget1)\n        self.up.setText(\"\")\n        self.up.setArrowType(QtCore.Qt.UpArrow)\n        self.up.setObjectName(\"up\")\n        self.horizontalLayout_4.addWidget(self.up)\n        self.down = QtWidgets.QToolButton(self.widget1)\n        self.down.setText(\"\")\n        self.down.setArrowType(QtCore.Qt.DownArrow)\n        self.down.setObjectName(\"down\")\n        self.horizontalLayout_4.addWidget(self.down)\n        self.horizontalLayout_2.addWidget(self.widget1, 0, QtCore.Qt.AlignRight)\n        self.vboxlayout.addWidget(self.table_commands)\n        self.table = QtWidgets.QTableWidget(self.scrollAreaWidgetContents)\n        self.table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)\n        self.table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\n        self.table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)\n        self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)\n        self.table.setObjectName(\"table\")\n        self.table.setColumnCount(4)\n        self.table.setRowCount(0)\n        item = QtWidgets.QTableWidgetItem()\n        self.table.setHorizontalHeaderItem(0, item)\n        item = QtWidgets.QTableWidgetItem()\n        self.table.setHorizontalHeaderItem(1, item)\n        item = QtWidgets.QTableWidgetItem()\n        self.table.setHorizontalHeaderItem(2, item)\n        item = QtWidgets.QTableWidgetItem()\n        self.table.setHorizontalHeaderItem(3, item)\n        self.table.horizontalHeader().setDefaultSectionSize(150)\n        self.vboxlayout.addWidget(self.table)\n        self.line = QtWidgets.QFrame(self.scrollAreaWidgetContents)\n        self.line.setFrameShadow(QtWidgets.QFrame.Sunken)\n        self.line.setFrameShape(QtWidgets.QFrame.HLine)\n        self.line.setObjectName(\"line\")\n        self.vboxlayout.addWidget(self.line)\n        self.frame = QtWidgets.QFrame(self.scrollAreaWidgetContents)\n        self.frame.setObjectName(\"frame\")\n        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame)\n        self.verticalLayout_2.setObjectName(\"verticalLayout_2\")\n        self.cancel = QtWidgets.QCheckBox(self.frame)\n        self.cancel.setObjectName(\"cancel\")\n        self.verticalLayout_2.addWidget(self.cancel)\n        self.widget_4 = QtWidgets.QWidget(self.frame)\n        self.widget_4.setObjectName(\"widget_4\")\n        self.horizontalLayout = QtWidgets.QHBoxLayout(self.widget_4)\n        self.horizontalLayout.setObjectName(\"horizontalLayout\")\n        self.max_workers = QtWidgets.QSpinBox(self.widget_4)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.max_workers.sizePolicy().hasHeightForWidth())\n        self.max_workers.setSizePolicy(sizePolicy)\n        self.max_workers.setMinimum(1)\n        self.max_workers.setMaximum(64)\n        self.max_workers.setObjectName(\"max_workers\")\n        self.horizontalLayout.addWidget(self.max_workers)\n        self.label_2 = QtWidgets.QLabel(self.widget_4)\n        self.label_2.setObjectName(\"label_2\")\n        self.horizontalLayout.addWidget(self.label_2)\n        self.verticalLayout_2.addWidget(self.widget_4)\n        self.label = QtWidgets.QLabel(self.frame)\n        self.label.setOpenExternalLinks(True)\n        self.label.setObjectName(\"label\")\n        self.verticalLayout_2.addWidget(self.label)\n        self.vboxlayout.addWidget(self.frame)\n        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.vboxlayout.addItem(spacerItem)\n        self.scrollArea.setWidget(self.scrollAreaWidgetContents)\n        self.verticalLayout.addWidget(self.scrollArea)\n\n        self.retranslateUi(PostTaggingActions)\n        QtCore.QMetaObject.connectSlotsByName(PostTaggingActions)\n\n    def retranslateUi(self, PostTaggingActions):\n        _translate = QtCore.QCoreApplication.translate\n        PostTaggingActions.setWindowTitle(_translate(\"PostTaggingActions\", \"Form\"))\n        self.label_3.setText(_translate(\"PostTaggingActions\", \"Insert action\"))\n        self.add_file_path.setToolTip(_translate(\"PostTaggingActions\", \"Add a file path to the command line.\"))\n        self.add_file_path.setText(_translate(\"PostTaggingActions\", \"Add file\"))\n        self.wait.setToolTip(_translate(\"PostTaggingActions\", \"If checked, the next action runs immediately.\"))\n        self.wait.setText(_translate(\"PostTaggingActions\", \" Wait for process to finish\"))\n        self.refresh.setToolTip(_translate(\"PostTaggingActions\", \"<html><head/><body><p>If checked, the album will &quot;refresh&quot; after this action finishes.</p></body></html>\"))\n        self.refresh.setText(_translate(\"PostTaggingActions\", \" Refresh tags after process finishes\"))\n        self.albums.setToolTip(_translate(\"PostTaggingActions\", \"Makes the action execute once for each album tagged.\"))\n        self.albums.setText(_translate(\"PostTaggingActions\", \"Execute for albums\"))\n        self.tracks.setToolTip(_translate(\"PostTaggingActions\", \"Makes the action run once for each track tagged.\"))\n        self.tracks.setText(_translate(\"PostTaggingActions\", \"Execute for tracks\"))\n        self.add_action.setToolTip(_translate(\"PostTaggingActions\", \"Add the action at the bottom of the queue.\"))\n        self.add_action.setText(_translate(\"PostTaggingActions\", \"Add action\"))\n        self.remove_action.setToolTip(_translate(\"PostTaggingActions\", \"Remove the selected action.\"))\n        self.remove_action.setText(_translate(\"PostTaggingActions\", \"Remove action\"))\n        self.table.setToolTip(_translate(\"PostTaggingActions\", \"Actions at the top of the list run first. Use the buttons on the right to reorder the selected action.\"))\n        item = self.table.horizontalHeaderItem(0)\n        item.setText(_translate(\"PostTaggingActions\", \"Action\"))\n        item = self.table.horizontalHeaderItem(1)\n        item.setText(_translate(\"PostTaggingActions\", \"   Wait for exit   \"))\n        item = self.table.horizontalHeaderItem(2)\n        item.setText(_translate(\"PostTaggingActions\", \"   Execute for tracks   \"))\n        item = self.table.horizontalHeaderItem(3)\n        item.setText(_translate(\"PostTaggingActions\", \"   Refresh tags   \"))\n        self.cancel.setToolTip(_translate(\"PostTaggingActions\", \"<html><head/><body><p>If <span style=\\\" font-weight:700;\\\">not </span>checked, when Picard is closed, it will wait for the actions to finish in the background.</p></body></html>\"))\n        self.cancel.setText(_translate(\"PostTaggingActions\", \"Cancel actions in the queue when Picard is closed\"))\n        self.label_2.setToolTip(_translate(\"PostTaggingActions\", \"Sets the number of background threads executing the actions\"))\n        self.label_2.setText(_translate(\"PostTaggingActions\", \"   Maximum number of worker threads (Requires Picard restart)\"))\n        self.label.setText(_translate(\"PostTaggingActions\", \"<html><head/><body><p>Hover over each item to know more, or take a peek at the user guide <a href=\\\"https://github.com/metabrainz/picard-plugins/tree/2.0/plugins/post_tagging_actions/docs/guide.md\\\"><span style=\\\" text-decoration: underline; color:#3584e4;\\\">here.</span></a></p></body></html>\"))\n"
  },
  {
    "path": "plugins/post_tagging_actions/options_post_tagging_actions.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<ui version=\"4.0\">\r\n <class>PostTaggingActions</class>\r\n <widget class=\"QWidget\" name=\"PostTaggingActions\">\r\n  <property name=\"geometry\">\r\n   <rect>\r\n    <x>0</x>\r\n    <y>0</y>\r\n    <width>638</width>\r\n    <height>450</height>\r\n   </rect>\r\n  </property>\r\n  <property name=\"minimumSize\">\r\n   <size>\r\n    <width>100</width>\r\n    <height>0</height>\r\n   </size>\r\n  </property>\r\n  <property name=\"windowTitle\">\r\n   <string>Form</string>\r\n  </property>\r\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\r\n   <item>\r\n    <widget class=\"QScrollArea\" name=\"scrollArea\">\r\n     <property name=\"frameShape\">\r\n      <enum>QFrame::NoFrame</enum>\r\n     </property>\r\n     <property name=\"widgetResizable\">\r\n      <bool>true</bool>\r\n     </property>\r\n     <widget class=\"QWidget\" name=\"scrollAreaWidgetContents\">\r\n      <property name=\"geometry\">\r\n       <rect>\r\n        <x>0</x>\r\n        <y>-70</y>\r\n        <width>606</width>\r\n        <height>502</height>\r\n       </rect>\r\n      </property>\r\n      <layout class=\"QVBoxLayout\">\r\n       <item>\r\n        <widget class=\"QWidget\" name=\"action_widget\" native=\"true\">\r\n         <layout class=\"QVBoxLayout\" name=\"_2\">\r\n          <item>\r\n           <widget class=\"QWidget\" name=\"widget_2\" native=\"true\">\r\n            <layout class=\"QHBoxLayout\" name=\"horizontalLayout_3\">\r\n             <item>\r\n              <widget class=\"QLabel\" name=\"label_3\">\r\n               <property name=\"text\">\r\n                <string>Insert action</string>\r\n               </property>\r\n              </widget>\r\n             </item>\r\n             <item>\r\n              <widget class=\"QPushButton\" name=\"add_file_path\">\r\n               <property name=\"sizePolicy\">\r\n                <sizepolicy hsizetype=\"Maximum\" vsizetype=\"Fixed\">\r\n                 <horstretch>0</horstretch>\r\n                 <verstretch>0</verstretch>\r\n                </sizepolicy>\r\n               </property>\r\n               <property name=\"toolTip\">\r\n                <string>Add a file path to the command line.</string>\r\n               </property>\r\n               <property name=\"text\">\r\n                <string>Add file</string>\r\n               </property>\r\n               <property name=\"checkable\">\r\n                <bool>false</bool>\r\n               </property>\r\n              </widget>\r\n             </item>\r\n            </layout>\r\n           </widget>\r\n          </item>\r\n          <item>\r\n           <widget class=\"QLineEdit\" name=\"action\"/>\r\n          </item>\r\n          <item>\r\n           <widget class=\"QCheckBox\" name=\"wait\">\r\n            <property name=\"toolTip\">\r\n             <string>If checked, the next action runs immediately.</string>\r\n            </property>\r\n            <property name=\"text\">\r\n             <string> Wait for process to finish</string>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n          <item>\r\n           <widget class=\"QCheckBox\" name=\"refresh\">\r\n            <property name=\"toolTip\">\r\n             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, the album will &amp;quot;refresh&amp;quot; after this action finishes.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\r\n            </property>\r\n            <property name=\"text\">\r\n             <string> Refresh tags after process finishes</string>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n          <item>\r\n           <widget class=\"QWidget\" name=\"widget_3\" native=\"true\">\r\n            <layout class=\"QHBoxLayout\" name=\"horizontalLayout_6\">\r\n             <item>\r\n              <widget class=\"QRadioButton\" name=\"albums\">\r\n               <property name=\"toolTip\">\r\n                <string>Makes the action execute once for each album tagged.</string>\r\n               </property>\r\n               <property name=\"text\">\r\n                <string>Execute for albums</string>\r\n               </property>\r\n              </widget>\r\n             </item>\r\n             <item>\r\n              <widget class=\"QRadioButton\" name=\"tracks\">\r\n               <property name=\"toolTip\">\r\n                <string>Makes the action run once for each track tagged.</string>\r\n               </property>\r\n               <property name=\"text\">\r\n                <string>Execute for tracks</string>\r\n               </property>\r\n              </widget>\r\n             </item>\r\n            </layout>\r\n           </widget>\r\n          </item>\r\n         </layout>\r\n        </widget>\r\n       </item>\r\n       <item>\r\n        <widget class=\"QWidget\" name=\"table_commands\" native=\"true\">\r\n         <layout class=\"QHBoxLayout\" name=\"horizontalLayout_2\">\r\n          <item>\r\n           <widget class=\"QWidget\" name=\"widget\" native=\"true\">\r\n            <layout class=\"QHBoxLayout\" name=\"horizontalLayout_5\">\r\n             <item>\r\n              <widget class=\"QPushButton\" name=\"add_action\">\r\n               <property name=\"toolTip\">\r\n                <string>Add the action at the bottom of the queue.</string>\r\n               </property>\r\n               <property name=\"text\">\r\n                <string>Add action</string>\r\n               </property>\r\n              </widget>\r\n             </item>\r\n             <item>\r\n              <widget class=\"QPushButton\" name=\"remove_action\">\r\n               <property name=\"toolTip\">\r\n                <string>Remove the selected action.</string>\r\n               </property>\r\n               <property name=\"text\">\r\n                <string>Remove action</string>\r\n               </property>\r\n              </widget>\r\n             </item>\r\n            </layout>\r\n           </widget>\r\n          </item>\r\n          <item alignment=\"Qt::AlignRight\">\r\n           <widget class=\"QWidget\" name=\"widget\" native=\"true\">\r\n            <layout class=\"QHBoxLayout\" name=\"horizontalLayout_4\">\r\n             <item>\r\n              <widget class=\"QToolButton\" name=\"up\">\r\n               <property name=\"text\">\r\n                <string/>\r\n               </property>\r\n               <property name=\"arrowType\">\r\n                <enum>Qt::UpArrow</enum>\r\n               </property>\r\n              </widget>\r\n             </item>\r\n             <item>\r\n              <widget class=\"QToolButton\" name=\"down\">\r\n               <property name=\"text\">\r\n                <string/>\r\n               </property>\r\n               <property name=\"arrowType\">\r\n                <enum>Qt::DownArrow</enum>\r\n               </property>\r\n              </widget>\r\n             </item>\r\n            </layout>\r\n           </widget>\r\n          </item>\r\n         </layout>\r\n        </widget>\r\n       </item>\r\n       <item>\r\n        <widget class=\"QTableWidget\" name=\"table\">\r\n         <property name=\"toolTip\">\r\n          <string>Actions at the top of the list run first. Use the buttons on the right to reorder the selected action.</string>\r\n         </property>\r\n         <property name=\"sizeAdjustPolicy\">\r\n          <enum>QAbstractScrollArea::AdjustToContents</enum>\r\n         </property>\r\n         <property name=\"editTriggers\">\r\n          <set>QAbstractItemView::NoEditTriggers</set>\r\n         </property>\r\n         <property name=\"selectionMode\">\r\n          <enum>QAbstractItemView::SingleSelection</enum>\r\n         </property>\r\n         <property name=\"selectionBehavior\">\r\n          <enum>QAbstractItemView::SelectRows</enum>\r\n         </property>\r\n         <attribute name=\"horizontalHeaderDefaultSectionSize\">\r\n          <number>150</number>\r\n         </attribute>\r\n         <column>\r\n          <property name=\"text\">\r\n           <string>Action</string>\r\n          </property>\r\n         </column>\r\n         <column>\r\n          <property name=\"text\">\r\n           <string>   Wait for exit   </string>\r\n          </property>\r\n         </column>\r\n         <column>\r\n          <property name=\"text\">\r\n           <string>   Execute for tracks   </string>\r\n          </property>\r\n         </column>\r\n         <column>\r\n          <property name=\"text\">\r\n           <string>   Refresh tags   </string>\r\n          </property>\r\n         </column>\r\n        </widget>\r\n       </item>\r\n       <item>\r\n        <widget class=\"Line\" name=\"line\">\r\n         <property name=\"frameShadow\">\r\n          <enum>QFrame::Sunken</enum>\r\n         </property>\r\n         <property name=\"orientation\">\r\n          <enum>Qt::Horizontal</enum>\r\n         </property>\r\n        </widget>\r\n       </item>\r\n       <item>\r\n        <widget class=\"QFrame\" name=\"frame\">\r\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\r\n          <item>\r\n           <widget class=\"QCheckBox\" name=\"cancel\">\r\n            <property name=\"toolTip\">\r\n             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If &lt;span style=&quot; font-weight:700;&quot;&gt;not &lt;/span&gt;checked, when Picard is closed, it will wait for the actions to finish in the background.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\r\n            </property>\r\n            <property name=\"text\">\r\n             <string>Cancel actions in the queue when Picard is closed</string>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n          <item>\r\n           <widget class=\"QWidget\" name=\"widget_4\" native=\"true\">\r\n            <layout class=\"QHBoxLayout\" name=\"horizontalLayout\">\r\n             <item>\r\n              <widget class=\"QSpinBox\" name=\"max_workers\">\r\n               <property name=\"sizePolicy\">\r\n                <sizepolicy hsizetype=\"Maximum\" vsizetype=\"Fixed\">\r\n                 <horstretch>0</horstretch>\r\n                 <verstretch>0</verstretch>\r\n                </sizepolicy>\r\n               </property>\r\n               <property name=\"minimum\">\r\n                <number>1</number>\r\n               </property>\r\n               <property name=\"maximum\">\r\n                <number>64</number>\r\n               </property>\r\n              </widget>\r\n             </item>\r\n             <item>\r\n              <widget class=\"QLabel\" name=\"label_2\">\r\n               <property name=\"toolTip\">\r\n                <string>Sets the number of background threads executing the actions</string>\r\n               </property>\r\n               <property name=\"text\">\r\n                <string>   Maximum number of worker threads (Requires Picard restart)</string>\r\n               </property>\r\n              </widget>\r\n             </item>\r\n            </layout>\r\n           </widget>\r\n          </item>\r\n          <item>\r\n           <widget class=\"QLabel\" name=\"label\">\r\n            <property name=\"text\">\r\n             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Hover over each item to know more, or take a peek at the user guide &lt;a href=&quot;https://github.com/metabrainz/picard-plugins/tree/2.0/plugins/post_tagging_actions/docs/guide.md&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#3584e4;&quot;&gt;here.&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\r\n            </property>\r\n            <property name=\"openExternalLinks\">\r\n             <bool>true</bool>\r\n            </property>\r\n           </widget>\r\n          </item>\r\n         </layout>\r\n        </widget>\r\n       </item>\r\n       <item>\r\n        <spacer name=\"verticalSpacer\">\r\n         <property name=\"orientation\">\r\n          <enum>Qt::Vertical</enum>\r\n         </property>\r\n         <property name=\"sizeHint\" stdset=\"0\">\r\n          <size>\r\n           <width>20</width>\r\n           <height>40</height>\r\n          </size>\r\n         </property>\r\n        </spacer>\r\n       </item>\r\n      </layout>\r\n     </widget>\r\n    </widget>\r\n   </item>\r\n  </layout>\r\n </widget>\r\n <resources/>\r\n <connections/>\r\n</ui>\r\n"
  },
  {
    "path": "plugins/release_type/release_type.py",
    "content": "PLUGIN_NAME = 'Release Type'\nPLUGIN_AUTHOR = 'Elliot Chance'\nPLUGIN_DESCRIPTION = 'Appends information to EPs and Singles'\nPLUGIN_VERSION = '1.4'\nPLUGIN_API_VERSIONS = [\"0.9.0\", \"0.10\", \"0.15\", \"2.0\"]\n\nfrom picard.metadata import register_album_metadata_processor\n\nRELEASE_TYPE_MAPPING = {\n    \"ep\": \" EP\",\n    \"single\": \" (single)\"\n}\n\n\ndef add_release_type(tagger, metadata, release):\n\n    # make sure \"EP\" (or \"single\", ...) is not already a word in the name\n    words = metadata[\"album\"].lower().split(\" \")\n    for word in [\"ep\", \"e.p.\", \"single\", \"(single)\"]:\n        if word in words:\n            return\n\n    # check release type\n    for releasetype, text in RELEASE_TYPE_MAPPING.items():\n        if (metadata[\"~primaryreleasetype\"] == releasetype) or (\n                metadata[\"releasetype\"].startswith(releasetype)):\n            rs = text\n            break\n    else:\n        rs = \"\"\n\n  # append title\n    metadata[\"album\"] = metadata[\"album\"] + rs\n\nregister_album_metadata_processor(add_release_type)\n"
  },
  {
    "path": "plugins/releasetag_aggregations/releasetag_aggregations.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2021 Philipp Wolfer\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = 'Release tag aggregation functions'\nPLUGIN_AUTHOR = 'Philipp Wolfer'\nPLUGIN_DESCRIPTION = ('Add functions to aggregate tags on a release:'\n                      '<ul>'\n                      '<li>$album_all(name)</li>'\n                      '<li>‎$album_avg(name, precision=2)</li>'\n                      '<li>‎$album_max(name, precision=2)</li>'\n                      '<li>‎$album_min(name, precision=2)</li>'\n                      '<li>‎$album_mode(name)</li>'\n                      '<li>‎$album_distinct(name, separator=; )</li>'\n                      '<li>‎$album_multi_avg(name, precision=2)</li>'\n                      '<li>‎$album_multi_max(name, precision=2)</li>'\n                      '<li>‎$album_multi_min(name, precision=2)</li>'\n                      '<li>‎$album_multi_mode(name)</li>'\n                      '<li>‎$album_multi_distinct(name, separator=; )</li>'\n                      '</ul>'\n                      '<b>The functions work only in file naming scripts and '\n                      'the files should either be part of a release or cluster!</b>')\nPLUGIN_VERSION = \"0.4\"\nPLUGIN_API_VERSIONS = [\"2.5\", \"2.6\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom functools import partial\n\nfrom picard.cluster import Cluster\nfrom picard.metadata import MULTI_VALUED_JOINER\nfrom picard.script import script_function\nfrom picard.script.parser import normalize_tagname\n\n\ndef get_parent_release(file):\n    if file.parent:\n        if isinstance(file.parent, Cluster):\n            return file.parent\n        elif hasattr(file.parent, 'album') and file.parent.album:\n            return file.parent.album\n    return None\n\n\ndef iter_release_values(name, file):\n    parent = get_parent_release(file)\n    if parent:\n        for file in parent.iterfiles(save=True):\n            yield file.metadata.get(name, '')\n    else:\n        yield file.metadata.get(name, '')\n\n\ndef iter_release_values_multi(name, file):\n    parent = get_parent_release(file)\n    if parent:\n        for file in parent.iterfiles(save=True):\n            yield from file.metadata.getall(name)\n    else:\n        yield from file.metadata.getall(name)\n\n\ndef try_iter_numeric(values, skip_non_numeric=False):\n    for value in values:\n        # First try treating the value as an integer, if this fails try float\n        try:\n            yield int(value)\n        except (TypeError, ValueError):\n            try:\n                yield float(value)\n            except (TypeError, ValueError):\n                if not skip_non_numeric:\n                    yield value\n\n\ndef aggregate_release_tags(parser, name, aggregate_func, multi=False):\n    name = normalize_tagname(name)\n    file = parser.file\n    if file:\n        iter_func = iter_release_values_multi if multi else iter_release_values\n        return aggregate_func(iter_func(name, file))\n    else:  # Nothing to aggregate, return base value\n        return parser.context.get(name, '')\n\n\ndef format_number(value, precision=2):\n    try:\n        precision = int(precision)\n    except (TypeError, ValueError):\n        precision = 0\n    if isinstance(value, int):\n        return str(value)\n    elif isinstance(value, float):\n        fmt = '{:.' + str(precision) + 'f}'\n        return fmt.format(value)\n    elif not value:\n        return ''\n    else:\n        return str(value)\n\n\ndef mode(values):\n    \"\"\"Returns the mode, the value with the most occurrences, from values.\"\"\"\n    l = list(values)\n    if not l:\n        return ''\n    return max(set(l), key=l.count)\n\n\ndef average(values, precision=2):\n    \"\"\"Returns the arithmetic average of all numeric elements in values.\n\n    Non-numeric elements are ignored.\n    \"\"\"\n    numbers = list(try_iter_numeric(values, skip_non_numeric=True))\n    if not numbers:\n        return ''\n    return format_number(sum(numbers) / len(numbers), precision=precision)\n\n\ndef natsort_min(values, precision=2):\n    \"\"\"Returns the smallest value from values, treats numeric strings as numbers.\"\"\"\n    return format_number(max(try_iter_numeric(values)), precision=precision)\n\n\ndef natsort_max(values, precision=2):\n    \"\"\"Returns the largest value from values, treats numeric strings as numbers.\"\"\"\n    return format_number(max(try_iter_numeric(values)), precision=precision)\n\n\ndef distinct(values, separator=MULTI_VALUED_JOINER):\n    return separator.join(set(values))\n\n\n@script_function(documentation=\"\"\"`$album_all(name)`\n\nReturns the value of the tag name if all files on the album have the same\nvalue for this tag. Otherwise returns empty.\n**Only works in File Naming scripts.**\"\"\")\ndef func_album_all(parser, name):\n    def aggregate_func(values):\n        common_value = ''\n        for value in values:\n            if not common_value:\n                common_value = value\n            if common_value != value:\n                return ''\n        return common_value\n\n    return aggregate_release_tags(parser, name, aggregate_func)\n\n\n@script_function(documentation=\"\"\"`$album_mode(name)`\n\nReturns the value with the most occurrences over all the files on the release for the given tag.\n**Only works in File Naming scripts.**\"\"\")\ndef func_album_mode(parser, name):\n    return aggregate_release_tags(parser, name, mode)\n\n\n@script_function(documentation=\"\"\"`$album_multi_mode(name)`\n\nReturns the value with the most occurrences over all the files on the release for the given tag.\nSimilar to $album_mode(), but considers each value of multi-value tags separately.\n**Only works in File Naming scripts.**\"\"\")\ndef func_album_multi_mode(parser, name):\n    return aggregate_release_tags(parser, name, mode, multi=True)\n\n\n@script_function(documentation=\"\"\"`$album_min(name, precision=2)`\n\nReturns the minimum value of all the files on the release for the given tag.\n**Only works in File Naming scripts.**\"\"\")\ndef func_album_min(parser, name, precision=\"2\"):\n    aggregate_func = partial(natsort_min, precision=precision)\n    return aggregate_release_tags(parser, name, aggregate_func)\n\n\n@script_function(documentation=\"\"\"`$album_multi_min(name, precision=2)`\n\nReturns the minimum value of all the files on the release for the given tag.\nSimilar to $album_min(), but considers each value of multi-value tags separately.\n**Only works in File Naming scripts.**\"\"\")\ndef releasetag_multi_min(parser, name, precision=\"2\"):\n    aggregate_func = partial(natsort_min, precision=precision)\n    return aggregate_release_tags(parser, name, aggregate_func, multi=True)\n\n\n@script_function(documentation=\"\"\"`$album_max(name, precision=2)`\n\nReturns the maximum value of all the files on the release for the given tag.\n**Only works in File Naming scripts.**\"\"\")\ndef func_album_max(parser, name, precision=\"2\"):\n    aggregate_func = partial(natsort_max, precision=precision)\n    return aggregate_release_tags(parser, name, aggregate_func)\n\n\n@script_function(documentation=\"\"\"`$album_multi_max(name, precision=2)`\n\nReturns the maximum value of all the files on the release for the given tag.\nSimilar to $album_max(), but considers each value of multi-value tags separately.\n**Only works in File Naming scripts.**\"\"\")\ndef releasetag_multi_max(parser, name, precision=\"2\"):\n    aggregate_func = partial(natsort_max, precision=precision)\n    return aggregate_release_tags(parser, name, aggregate_func, multi=True)\n\n\n@script_function(documentation=\"\"\"`$album_avg(name, precision=2)`\n\nReturns the arithmetical average value of all the files on the release for the given tag.\nNon-numeric values are ignored. Returns empty if no numeric value is present.\n**Only works in File Naming scripts.**\"\"\")\ndef func_album_avg(parser, name, precision=\"2\"):\n    aggregate_func = partial(average, precision=precision)\n    return aggregate_release_tags(parser, name, aggregate_func)\n\n\n@script_function(documentation=\"\"\"`$album_multi_avg(name, precision=2)`\n\nReturns the arithmetical average value of all the files on the release for the given tag.\nNon-numeric values are ignored. Returns empty if no numeric value is present.\nSimilar to $album_avg(), but considers each value of multi-value tags separately.\n**Only works in File Naming scripts.**\"\"\")\ndef func_album_multi_avg(parser, name, precision=\"2\"):\n    aggregate_func = partial(average, precision=precision)\n    return aggregate_release_tags(parser, name, aggregate_func, multi=True)\n\n\n@script_function(documentation=\"\"\"`$album_distinct(name, separator=; )`\n\nReturns a multi-value tag with all distinct values of tag across all the files on the release.\n**Only works in File Naming scripts.**\"\"\")\ndef func_album_distinct(parser, name, separator=MULTI_VALUED_JOINER):\n    aggregate_func = partial(distinct, separator=separator)\n    return aggregate_release_tags(parser, name, aggregate_func)\n\n\n@script_function(documentation=\"\"\"`$album_multi_distinct(name, separator=; )`\n\nReturns a multi-value tag with all distinct values of tag across all the files on the release.\nSimilar to $album_distinct(), but considers each value of multi-value tags separately.\n**Only works in File Naming scripts.**\"\"\")\ndef func_album_multi_distinct(parser, name, separator=MULTI_VALUED_JOINER):\n    aggregate_func = partial(distinct, separator=separator)\n    return aggregate_release_tags(parser, name, aggregate_func, multi=True)\n"
  },
  {
    "path": "plugins/remove_perfect_albums/remove_perfect_albums.py",
    "content": "PLUGIN_NAME = 'Remove Perfect Albums'\nPLUGIN_AUTHOR = 'ichneumon, hrglgrmpf'\nPLUGIN_DESCRIPTION = '''Remove all perfectly matched albums from the selection.'''\nPLUGIN_VERSION = '0.3'\nPLUGIN_API_VERSIONS = ['2.0', '2.1', '2.2', '2.3']\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom PyQt5.QtCore import QCoreApplication\n\nfrom picard.album import Album\nfrom picard.ui.itemviews import BaseAction, register_album_action\n\n\nclass RemovePerfectAlbums(BaseAction):\n    NAME = 'Remove perfect albums'\n\n    def callback(self, objs):\n        for album in objs:\n            if (isinstance(album, Album) and album.loaded\n               and album.is_complete() and album.get_num_unsaved_files() == 0):\n                self.tagger.remove_album(album)\n            QCoreApplication.processEvents()\n\n\nregister_album_action(RemovePerfectAlbums())\n"
  },
  {
    "path": "plugins/reorder_sides/reorder_sides.py",
    "content": "# MusicBrainz Picard plugin to re-order sides of a release.\n# Copyright (C) 2016  David Mandelberg\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nPLUGIN_NAME = 'Re-order sides of a release'\nPLUGIN_AUTHOR = 'David Mandelberg, Sambhav Kothari'\nPLUGIN_DESCRIPTION = \"\"\"\\\n  Split mediums and re-order sides to match side order rather than\n  medium order. E.g., if a release has two mediums with track numbers\n  <em>A1, A2, ..., D1, D2, ...</em> and <em>B1, B2, ..., C1, C2,\n  ...</em>, this plugin will split the release into four mediums and\n  reorder the new mediums so that the track numbers are <em>A1, A2,\n  ..., B1, B2, ..., C1, C2, ..., D1, D2, ...</em>\n\n  This is primarily intended to make vinyl records designed for record\n  changers\n  (https://en.wikipedia.org/wiki/Record_changer#Automatic_sequencing)\n  play in the correct order.\"\"\"\nPLUGIN_VERSION = '1.2'\nPLUGIN_API_VERSIONS = ['2.0']\nPLUGIN_LICENSE = 'GPL-3.0-or-later'\nPLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl-3.0.html'\n\nimport collections\n\nfrom picard.metadata import \\\n  register_album_metadata_processor, register_track_metadata_processor\n\n\n# List of strings that represent the medium side of a track when the\n# track number is one of these strings followed by digits. The order of\n# this list specifies the order of the sides.\nSIDE_PREFIXES = [\n  'A',\n  'B',\n  'C',\n  'D',\n  'E',\n  'F',\n  'G',\n  'H',\n  'I',\n  'J',\n  'K',\n  'L',\n  'M',\n  'N',\n  'O',\n  'P',\n  'Q',\n  'R',\n  'S',\n  'T',\n  'U',\n  'V',\n  'W',\n  'X',\n  'Y',\n  'Z',\n  ]\n\n# Map from release MBID to information that we can use to re-group and\n# re-order the tracks. The per-release info (value of this map) is an\n# OrderedDict mapping side name to a sequence of (discnumber of the\n# side, first tracknumber of the side, last tracknumber of the side).\n# The order of the OrderedDict specifies the intended order of the\n# sides.\nrelease_to_side_info = {}\n\ndef tracknumber_to_side(tracknumber):\n  \"\"\"Given a track \"number\", return the side, or None\"\"\"\n\n  for side_prefix in SIDE_PREFIXES:\n    if not tracknumber.startswith(side_prefix):\n      continue\n\n    number_in_side = tracknumber[len(side_prefix):]\n    try:\n      int(number_in_side)\n    except ValueError:\n      continue\n\n    return side_prefix\n\n  return None\n\ndef get_side_info(release):\n  \"\"\"Return side info (see release_to_side_info), or None if we don't\n  want to reorder the sides.\"\"\"\n\n  side_info = collections.OrderedDict()\n\n  for medium in release['media']:\n    current_side = None\n\n    for track in medium['tracks']:\n      tracknumber = track['number']\n      trackside = tracknumber_to_side(tracknumber)\n\n      try:\n        int_tracknumber = int(track['position'])\n      except ValueError:\n        # Non-integer tracknumber, so give up.\n        return None\n\n      if trackside is None:\n        # If any track has no side information, we don't reorder\n        # anything.\n        return None\n\n      if current_side is not None and current_side == trackside:\n        # Another track of the same side as before, so just update the\n        # last tracknumber for this side.\n        if int_tracknumber > side_info[current_side][2]:\n          side_info[current_side][2] = int_tracknumber\n        continue\n\n      # At this point, we're on the first track of a new side.\n\n      current_side = trackside\n\n      if current_side in side_info:\n        # We've already seen this side somewhere else, so give up.\n        return None\n\n      try:\n        side_info[current_side] = [\n          int(medium['position']),\n          int_tracknumber,\n          int_tracknumber,\n          ]\n      except ValueError:\n        # Non-integer position, so give up.\n        return None\n\n  # At this point, we know that all tracknumbers include side\n  # information, and no sides appear in more than one place (every\n  # side is contiguous).\n\n  sides_in_order_of_appearance = list(side_info.keys())\n  sides_in_intended_order = sorted(\n    sides_in_order_of_appearance,\n    key=lambda s: SIDE_PREFIXES.index(s),\n    )\n  if sides_in_order_of_appearance == sides_in_intended_order:\n    # Sides are already in the right order, so we don't need to do\n    # anything.\n    return None\n\n  # Re-order side_info to match the intended order.\n  side_info_ordered = collections.OrderedDict()\n  for side in sides_in_intended_order:\n    side_info_ordered[side] = side_info[side]\n  side_info = side_info_ordered\n\n  return side_info\n\ndef find_side(side_info, metadata):\n  \"\"\"Given side info and track metadata, return the side that the\n  track belongs to.\"\"\"\n\n  orig_discnumber = int(metadata['discnumber'])\n  orig_tracknumber = int(metadata['tracknumber'])\n\n  for side_item in side_info.items():\n    (\n      side,\n      (\n        side_discnumber,\n        side_first_tracknumber,\n        side_last_tracknumber,\n        ),\n      ) = side_item\n\n    if side_discnumber == orig_discnumber \\\n        and side_first_tracknumber <= orig_tracknumber \\\n        and orig_tracknumber <= side_last_tracknumber:\n      return side\n\n  raise RuntimeError('Unable to find side for track: ' + metadata)\n\ndef analyze_release(tagger, metadata, release):\n  \"\"\"Analyze a release to determine if its sides should be\n  re-ordered\"\"\"\n\n  side_info = get_side_info(release)\n  if side_info is not None:\n    release_to_side_info[metadata['musicbrainz_albumid']] = side_info\n\nregister_album_metadata_processor(analyze_release)\n\ndef reorder_sides(tagger, metadata, *args):\n  \"\"\"Re-order sides of a release, using the information in\n  release_to_side_info.\"\"\"\n\n  if metadata['musicbrainz_albumid'] not in release_to_side_info:\n    return\n\n  side_info = release_to_side_info[metadata['musicbrainz_albumid']]\n  all_sides = list(side_info.keys())\n\n  side = find_side(side_info, metadata)\n  side_first_tracknumber = side_info[side][1]\n  side_last_tracknumber = side_info[side][2]\n\n  metadata['totaldiscs'] = str(len(all_sides))\n  metadata['discnumber'] = str(all_sides.index(side) + 1)\n\n  metadata['totaltracks'] = \\\n    str(side_last_tracknumber - side_first_tracknumber + 1)\n  metadata['tracknumber'] = \\\n    str(int(metadata['tracknumber']) - side_first_tracknumber + 1)\n\nregister_track_metadata_processor(reorder_sides)\n"
  },
  {
    "path": "plugins/replace_forbidden_symbols/replace_forbidden_symbols.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Copyright (C) 2019 Alex Rustler <alex_rustler@rambler.ru>\n#\n# This program is free software: you can redistribute it and/or modify it under\n# the terms of the GNU General Public License as published by the Free Software\n# Foundation, either version 3 of the License, or (at your option) any later\n# version.\n#\n# This program is distributed in the hope that it will be useful, but WITHOUT\n# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS\n# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more\n# details.\n#\n# You should have received a copy of the GNU General Public License along with\n# this program. If not, see <http://www.gnu.org/licenses/>.\n\nfrom picard import metadata\nfrom picard.script import register_script_function\n\nPLUGIN_NAME = \"Replace Forbidden Symbols\"\nPLUGIN_AUTHOR = \"Alex Rustler <alex_rustler@rambler.ru>\"\nPLUGIN_VERSION = \"0.3\"\nPLUGIN_API_VERSIONS = [\"0.9\", \"0.10\", \"0.11\", \"0.15\", \"2.0\", \"2.2\"]\nPLUGIN_LICENSE = \"GPL-3.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://gnu.org/licenses/gpl.html\"\nPLUGIN_DESCRIPTION = '''Replaces Windows forbidden symbols: :, /, *, ?, \", ., | etc.\n                    with a similar UNICODE version.\n                    Currently replaces characters on \"album\", \"artist\",\n                    \"title\", \"albumartist\", \"releasetype\", \"label\" tags.\n                    Also add $replace_forbidden() function for Tagger.\n                    Example: $set(composer,$script_forbidden(%composer%))\n'''\n\nCHAR_TABLE = {\n\n    # forbidden\n    \":\": \"∶\",\n    \"/\": \"⁄\",\n    \"*\": \"∗\",\n    \"?\": \"？\",\n    '\"': '″',\n    '\\\\': '⧵',\n    '.': '․',\n    '|': 'ǀ',\n    '<': '‹',\n    '>': '›'\n}\n\nFILTER_TAGS = [\n    \"album\",\n    \"artist\",\n    \"title\",\n    \"albumartist\",\n    \"releasetype\",\n    \"label\",\n]\n\n\ndef sanitize(char):\n    if char in CHAR_TABLE:\n        return CHAR_TABLE[char]\n    return char\n\n\ndef fix_forbidden(word):\n    return \"\".join(sanitize(char) for char in word)\n\n\ndef replace_forbidden(value):\n    return [fix_forbidden(x) for x in value]\n\n\ndef script_replace_forbidden(parser, value):\n    return fix_forbidden(value)\n\n\ndef main(tagger, metadata, *args):\n    for name, value in metadata.rawitems():\n        if name in FILTER_TAGS:\n            metadata[name] = replace_forbidden(value)\n\n\nmetadata.register_track_metadata_processor(main)\nmetadata.register_album_metadata_processor(main)\n\nregister_script_function(script_replace_forbidden, name=\"replace_forbidden\")\n"
  },
  {
    "path": "plugins/replaygain2/__init__.py",
    "content": "# -*- coding: utf-8 -*-\nPLUGIN_NAME = \"ReplayGain 2.0\"\nPLUGIN_AUTHOR = \"complexlogic\"\nPLUGIN_DESCRIPTION = '''\nCalculates ReplayGain information for tracks and albums according to the\n[ReplayGain 2.0 specification](https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification).\nThis plugin depends on the ReplayGain utility [rsgain](https://github.com/complexlogic/rsgain). Users\nare required to install rsgain and set its path in the plugin settings before use.\n\n#### Usage\nSelect one or more tracks, albums, or clusters then right click and select Plugin->Calculate ReplayGain.\nThe plugin will calculate ReplayGain information for the selected items and display the results in the\nmetadata window. Click the save button to write the tags to file.\n\nThe following file formats are supported:\n\n- MP3 (.mp3)\n- FLAC (.flac)\n- Ogg (.ogg, .oga, spx)\n- Opus (.opus)\n- MPEG-4 Audio (.m4a, .mp4)\n- Wavpack (.wv)\n- Monkey's Audio (.ape)\n- WMA (.wma)\n- MP2 (.mp2)\n- WAV (.wav)\n- AIFF (.aiff)\n- TAK (.tak)\n- MusePack *(Stream Version 8 only)* (.mpc)\n\nThis plugin is based on the original ReplayGain plugin by Philipp Wolfer and Sophist.\n'''\nPLUGIN_VERSION = \"1.8\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom enum import IntEnum\nfrom functools import partial\nimport subprocess  # nosec: B404\nimport shutil\nimport os\n\nfrom PyQt5.QtWidgets import QFileDialog\n\nfrom picard import log\nfrom picard.formats import (\n    AiffFile,\n    ASFFile,\n    FLACFile,\n    MonkeysAudioFile,\n    MP3File,\n    MP4File,\n    MusepackFile,\n    OggFLACFile,\n    OggOpusFile,\n    OggSpeexFile,\n    OggTheoraFile,\n    OggVorbisFile,\n    TAKFile,\n    WAVFile,\n    WavPackFile,\n)\nfrom picard.album import Album\nfrom picard.track import Track, NonAlbumTrack\nfrom picard.file import File\nfrom picard.cluster import Cluster\nfrom picard.util import thread\nfrom picard.ui.options import register_options_page, OptionsPage\nfrom picard.config import TextOption, BoolOption, IntOption, config\nfrom picard.ui.itemviews import (BaseAction, register_track_action,\n                                 register_album_action, register_cluster_action)\nfrom picard.plugins.replaygain2.ui_options_replaygain2 import Ui_ReplayGain2OptionsPage\n\n\nCLIP_MODE_DISABLED = 0\nCLIP_MODE_POSITIVE = 1\nCLIP_MODE_ALWAYS = 2\n\nCLIP_MODE_MAP = (\n    \"n\",\n    \"p\",\n    \"a\"\n)\n\nSUPPORTED_FORMATS = (\n    AiffFile,\n    ASFFile,\n    FLACFile,\n    MonkeysAudioFile,\n    MP3File,\n    MP4File,\n    MusepackFile,\n    OggFLACFile,\n    OggOpusFile,\n    OggSpeexFile,\n    OggTheoraFile,\n    OggVorbisFile,\n    TAKFile,\n    WAVFile,\n    WavPackFile,\n)\n\nTABLE_HEADER = (\n    \"filename\",\n    \"loudness\",\n    \"gain\",\n    \"peak\",\n    \"peak_db\",\n    \"peak_type\",\n    \"clipping_adjustment\"\n)\n\nTAGS = (\n    \"replaygain_album_gain\",\n    \"replaygain_album_peak\",\n    \"replaygain_album_range\",\n    \"replaygain_reference_loudness\",\n    \"replaygain_track_gain\",\n    \"replaygain_track_peak\",\n    \"replaygain_track_range\",\n    \"r128_album_gain\",\n    \"r128_track_gain\"\n)\n\n\nclass OpusMode(IntEnum):\n    STANDARD = 0\n    R128 = 1\n    BOTH = 2\n\n\n# Make sure the rsgain executable exists\ndef rsgain_found(rsgain_command, window):\n    if not os.path.exists(rsgain_command) and shutil.which(rsgain_command) is None:\n        window.set_statusbar_message(\"Failed to locate rsgain. Enter the path in the plugin settings.\")\n        return False\n    return True\n\n# Convert Picard settings dict to rsgain command line options\ndef build_options(config):\n    options = [\"custom\", \"-O\", \"-s\", \"s\"]\n    if (config.setting[\"album_tags\"]):\n        options.append(\"-a\")\n    if (config.setting[\"true_peak\"]):\n        options.append(\"-t\")\n    options += [\"-l\", str(config.setting[\"target_loudness\"])]\n    options += [\"-c\", CLIP_MODE_MAP[config.setting[\"clip_mode\"]]]\n    options += [\"-m\", str(config.setting[\"max_peak\"])]\n    return options\n\n# Convert table row to result dict\ndef parse_result(line):\n    result = dict()\n    columns = line.split(\"\\t\")\n\n    if len(columns) != len(TABLE_HEADER):\n        return None\n    for i, column in enumerate(columns):\n        result[TABLE_HEADER[i]] = column\n    return result\n\n# Format the gain as a Q7.8 fixed point number per RFC 7845\n# see: https://datatracker.ietf.org/doc/html/rfc7845\ndef format_r128(result, config):\n    gain = float(result[\"gain\"])\n    if config.setting[\"opus_m23\"]:\n        gain += float(-23 - config.setting[\"target_loudness\"])\n    return str(int(round(gain * 256.0)))\n\ndef update_metadata(metadata, track_result, album_result, is_nat, opus_mode):\n    for tag in TAGS:\n        metadata.delete(tag)\n\n    # Opus R128 tags\n    if opus_mode in (OpusMode.R128, OpusMode.BOTH):\n        metadata.set(\"r128_track_gain\", format_r128(track_result, config))\n        if album_result is not None:\n            metadata.set(\"r128_album_gain\", format_r128(album_result, config))\n\n    # Standard ReplayGain tags\n    if opus_mode in (OpusMode.STANDARD, OpusMode.BOTH):\n        metadata.set(\"replaygain_track_gain\", track_result[\"gain\"] + \" dB\")\n        metadata.set(\"replaygain_track_peak\", track_result[\"peak\"])\n        if config.setting[\"album_tags\"]:\n            if is_nat:\n                metadata.set(\"replaygain_album_gain\", track_result[\"gain\"] + \" dB\")\n                metadata.set(\"replaygain_album_peak\", track_result[\"peak\"])\n            elif album_result is not None:\n                metadata.set(\"replaygain_album_gain\", album_result[\"gain\"] + \" dB\")\n                metadata.set(\"replaygain_album_peak\", album_result[\"peak\"])\n        if config.setting[\"reference_loudness\"]:\n            metadata.set(\"replaygain_reference_loudness\", f\"{float(config.setting['target_loudness']):.2f} LUFS\")\n\ndef calculate_replaygain(input_objs, options):\n\n    # Make sure files are of supported type, build file list\n    files = list()\n    valid_list = list()\n    for obj in input_objs:\n        if isinstance(obj, Track):\n            if not obj.files:\n                continue\n            file = obj.files[0]\n        elif isinstance(obj, File):\n            file = obj\n        else:\n            raise Exception(f\"ReplayGain 2.0: Object {obj} is not a Track or File\")\n\n        if not isinstanceany(file, SUPPORTED_FORMATS):\n            raise Exception(f\"ReplayGain 2.0: File '{file.filename}' is of unsupported format\")\n        files.append(file.filename)\n        valid_list.append(obj)\n\n    call = [config.setting[\"rsgain_command\"]] + options + files\n    for item in call:\n        item.encode(\"utf-8\")\n\n    # Prevent an unwanted console spawn in Windows\n    si = None\n    if (os.name == \"nt\"):\n        si = subprocess.STARTUPINFO()\n        si.dwFlags = subprocess.STARTF_USESHOWWINDOW\n        si.wShowWindow = subprocess.SW_HIDE\n\n    # Execute the scan with rsgain\n    lines = list()\n    with subprocess.Popen(  # nosec: B603\n        call,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.STDOUT,\n        startupinfo=si,\n        encoding=\"utf-8\",\n        text=True\n    ) as process:\n        (output, _unused) = process.communicate()\n        rc = process.poll()\n        if rc:\n            raise Exception(f'ReplayGain 2.0: rsgain returned non-zero code ({rc})')\n        log.debug(output)\n        lines = output.splitlines()\n    album_tags = config.setting[\"album_tags\"]\n\n    # Make sure the number of rows in the output is what we expected\n    if (len(lines) !=\n        1                            # Table header\n        + len(valid_list)            # 1 row per track\n        + 1 if album_tags else 0):   # Album result\n        raise Exception(f\"ReplayGain 2.0: Unexpected output from rsgain: {lines}\")\n    lines.pop(0) # Don't care about the table header\n\n    # Parse album result\n    album_result = None\n    if album_tags:\n        album_result = parse_result(lines[-1])\n        lines.pop(-1)\n\n    # Parse track results\n    results = list()\n    for line in lines:\n        result = parse_result(line)\n        if result is None:\n            raise Exception(\"ReplayGain 2.0: Failed to parse result\")\n        results.append(result)\n\n    # Update track metadata with results\n    for i, item in enumerate(valid_list):\n        if isinstance(item, Track):\n            filelist = item.files\n        else: # is a file\n            filelist = [item]\n\n        for file in filelist:\n            if isinstance(file, OggOpusFile):\n                opus_mode = config.setting[\"opus_mode\"]\n            else:\n                opus_mode = OpusMode.STANDARD\n\n            update_metadata(\n                file.metadata,\n                results[i],\n                album_result,\n                isinstance(item, NonAlbumTrack),\n                opus_mode\n            )\n\n\ndef isinstanceany(obj, types):\n    return any(isinstance(obj, t) for t in types)\n\n\nclass ScanCluster(BaseAction):\n    NAME = \"Calculate Cluster Replay&Gain as Album...\"\n\n    def callback(self, objs):\n        if not rsgain_found(self.config.setting[\"rsgain_command\"], self.tagger.window):\n            return\n        clusters = list(filter(lambda o: isinstance(o, Cluster), objs))\n\n        self.options = build_options(self.config)\n        num_clusters = len(clusters)\n        if num_clusters == 1:\n            self.tagger.window.set_statusbar_message(\n                'Calculating ReplayGain for %s...', clusters[0].metadata['album']\n            )\n        else:\n            self.tagger.window.set_statusbar_message(\n                'Calculating ReplayGain for %i clusters...', num_clusters\n            )\n        for cluster in clusters:\n            thread.run_task(\n                partial(calculate_replaygain, cluster.files, self.options),\n                partial(self._replaygain_callback, cluster.files)\n            )\n\n    def _replaygain_callback(self, files, result=None, error=None):\n        if error is None:\n            for file in files:\n                file.update()\n            self.tagger.window.set_statusbar_message('ReplayGain successfully calculated.')\n        else:\n            self.tagger.window.set_statusbar_message('Could not calculate ReplayGain.')\n\nclass ScanTracks(BaseAction):\n    NAME = \"Calculate Replay&Gain...\"\n\n    def callback(self, objs):\n        if not rsgain_found(self.config.setting[\"rsgain_command\"], self.tagger.window):\n            return\n        tracks = list(filter(lambda o: isinstance(o, Track), objs))\n        self.options = build_options(self.config)\n        num_tracks = len(tracks)\n        if num_tracks == 1:\n            self.tagger.window.set_statusbar_message(\n                'Calculating ReplayGain for %s...', {tracks[0].files[0].filename}\n            )\n        else:\n            self.tagger.window.set_statusbar_message(\n                'Calculating ReplayGain for %i tracks...', num_tracks\n            )\n        thread.run_task(\n            partial(calculate_replaygain, tracks, self.options),\n            partial(self._replaygain_callback, tracks)\n        )\n\n    def _replaygain_callback(self, tracks, result=None, error=None):\n        if error is None:\n            for track in tracks:\n                for file in track.files:\n                    file.update()\n                track.update()\n            self.tagger.window.set_statusbar_message('ReplayGain successfully calculated.')\n        else:\n            self.tagger.window.set_statusbar_message('Could not calculate ReplayGain.')\n\n\nclass ScanAlbums(BaseAction):\n    NAME = \"Calculate Replay&Gain...\"\n\n    def callback(self, objs):\n        if not rsgain_found(self.config.setting[\"rsgain_command\"], self.tagger.window):\n            return\n        self.options = build_options(self.config)\n        albums = list(filter(lambda o: isinstance(o, Album), objs))\n\n        self.num_albums = len(albums)\n        self.current = 0\n        if (self.num_albums == 1):\n            self.tagger.window.set_statusbar_message(\n                'Calculating ReplayGain for \"%s\"...', albums[0].metadata['album']\n            )\n        else:\n            self.tagger.window.set_statusbar_message(\n                'Calculating ReplayGain for %i albums...', self.num_albums\n            )\n        for album in albums:\n            thread.run_task(\n                partial(calculate_replaygain, album.tracks, self.options),\n                partial(self._albumgain_callback, album)\n            )\n\n    def _format_progress(self):\n        if self.num_albums == 1:\n            return \"\"\n        else:\n            self.current += 1\n            return f\" ({self.current}/{self.num_albums})\"\n\n    def _albumgain_callback(self, album, result=None, error=None):\n        progress = self._format_progress()\n        if error is None:\n            for track in album.tracks:\n                for file in track.files:\n                    file.update()\n                track.update()\n            album.update()\n            self.tagger.window.set_statusbar_message(\n                'Successfully calculated ReplayGain for \"%(album)s\"%(progress)s.',\n                {\n                    'album': album.metadata['album'],\n                    'progress': progress,\n                }\n            )\n        else:\n            self.tagger.window.set_statusbar_message(\n                'Failed to calculate ReplayGain for \"%(album)s\"%(progress)s.',\n                {\n                    'album': album.metadata['album'],\n                    'progress': progress,\n                }\n            )\n\nclass ReplayGain2OptionsPage(OptionsPage):\n\n    NAME = \"replaygain2\"\n    TITLE = \"ReplayGain 2.0\"\n    PARENT = \"plugins\"\n\n    options = [\n        TextOption(\"setting\", \"rsgain_command\", \"rsgain\"),\n        BoolOption(\"setting\", \"album_tags\", True),\n        BoolOption(\"setting\", \"true_peak\", False),\n        BoolOption(\"setting\", \"reference_loudness\", False),\n        IntOption(\"setting\", \"target_loudness\", -18),\n        IntOption(\"setting\", \"clip_mode\", CLIP_MODE_POSITIVE),\n        IntOption(\"setting\", \"max_peak\", 0),\n        IntOption(\"setting\", \"opus_mode\", OpusMode.STANDARD),\n        BoolOption(\"setting\", \"opus_m23\", False)\n    ]\n\n    def __init__(self, parent=None):\n        super(ReplayGain2OptionsPage, self).__init__(parent)\n        self.ui = Ui_ReplayGain2OptionsPage()\n        self.ui.setupUi(self)\n        self.ui.clip_mode.addItems([\n            \"Disabled\",\n            \"Enabled for positive gain values only\",\n            \"Always enabled\"\n        ])\n        self.ui.opus_mode.addItems([\n            \"Write standard ReplayGain tags\",\n            \"Write R128_*_GAIN tags\",\n            \"Write both standard and R128 tags\"\n        ])\n        self.ui.rsgain_command_browse.clicked.connect(self.rsgain_command_browse)\n\n    def load(self):\n        self.ui.rsgain_command.setText(self.config.setting[\"rsgain_command\"])\n        self.ui.album_tags.setChecked(self.config.setting[\"album_tags\"])\n        self.ui.true_peak.setChecked(self.config.setting[\"true_peak\"])\n        self.ui.reference_loudness.setChecked(self.config.setting[\"reference_loudness\"])\n        self.ui.target_loudness.setValue(self.config.setting[\"target_loudness\"])\n        self.ui.clip_mode.setCurrentIndex(self.config.setting[\"clip_mode\"])\n        self.ui.max_peak.setValue(self.config.setting[\"max_peak\"])\n        self.ui.opus_mode.setCurrentIndex(self.config.setting[\"opus_mode\"])\n        self.ui.opus_m23.setChecked(self.config.setting[\"opus_m23\"])\n\n    def save(self):\n        self.config.setting[\"rsgain_command\"] = self.ui.rsgain_command.text()\n        self.config.setting[\"album_tags\"] = self.ui.album_tags.isChecked()\n        self.config.setting[\"true_peak\"] = self.ui.true_peak.isChecked()\n        self.config.setting[\"reference_loudness\"] = self.ui.reference_loudness.isChecked()\n        self.config.setting[\"target_loudness\"] = self.ui.target_loudness.value()\n        self.config.setting[\"clip_mode\"] = self.ui.clip_mode.currentIndex()\n        self.config.setting[\"max_peak\"] = self.ui.max_peak.value()\n        self.config.setting[\"opus_mode\"] = self.ui.opus_mode.currentIndex()\n        self.config.setting[\"opus_m23\"] = self.ui.opus_m23.isChecked()\n\n    def rsgain_command_browse(self):\n        path, _filter = QFileDialog.getOpenFileName(self, \"\", self.ui.rsgain_command.text())\n        if path:\n            path = os.path.normpath(path)\n            self.ui.rsgain_command.setText(path)\n\n\nregister_track_action(ScanTracks())\nregister_album_action(ScanAlbums())\nregister_cluster_action(ScanCluster())\nregister_options_page(ReplayGain2OptionsPage)\n"
  },
  {
    "path": "plugins/replaygain2/ui_options_replaygain2.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/replaygain2/ui_options_replaygain2.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.10\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_ReplayGain2OptionsPage(object):\n    def setupUi(self, ReplayGain2OptionsPage):\n        ReplayGain2OptionsPage.setObjectName(\"ReplayGain2OptionsPage\")\n        ReplayGain2OptionsPage.resize(477, 577)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(ReplayGain2OptionsPage.sizePolicy().hasHeightForWidth())\n        ReplayGain2OptionsPage.setSizePolicy(sizePolicy)\n        self.vboxlayout = QtWidgets.QVBoxLayout(ReplayGain2OptionsPage)\n        self.vboxlayout.setContentsMargins(9, 9, 9, 9)\n        self.vboxlayout.setSpacing(6)\n        self.vboxlayout.setObjectName(\"vboxlayout\")\n        self.replay_gain = QtWidgets.QGroupBox(ReplayGain2OptionsPage)\n        self.replay_gain.setObjectName(\"replay_gain\")\n        self.vboxlayout1 = QtWidgets.QVBoxLayout(self.replay_gain)\n        self.vboxlayout1.setContentsMargins(9, 9, 9, 9)\n        self.vboxlayout1.setSpacing(2)\n        self.vboxlayout1.setObjectName(\"vboxlayout1\")\n        self.label = QtWidgets.QLabel(self.replay_gain)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.label.setFont(font)\n        self.label.setObjectName(\"label\")\n        self.vboxlayout1.addWidget(self.label)\n        self.horizontalLayout = QtWidgets.QHBoxLayout()\n        self.horizontalLayout.setObjectName(\"horizontalLayout\")\n        self.rsgain_command = QtWidgets.QLineEdit(self.replay_gain)\n        self.rsgain_command.setObjectName(\"rsgain_command\")\n        self.horizontalLayout.addWidget(self.rsgain_command)\n        self.rsgain_command_browse = QtWidgets.QToolButton(self.replay_gain)\n        self.rsgain_command_browse.setObjectName(\"rsgain_command_browse\")\n        self.horizontalLayout.addWidget(self.rsgain_command_browse)\n        self.vboxlayout1.addLayout(self.horizontalLayout)\n        self.label_6 = QtWidgets.QLabel(self.replay_gain)\n        self.label_6.setOpenExternalLinks(True)\n        self.label_6.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)\n        self.label_6.setObjectName(\"label_6\")\n        self.vboxlayout1.addWidget(self.label_6)\n        self.album_tags = QtWidgets.QCheckBox(self.replay_gain)\n        self.album_tags.setChecked(True)\n        self.album_tags.setObjectName(\"album_tags\")\n        self.vboxlayout1.addWidget(self.album_tags)\n        self.true_peak = QtWidgets.QCheckBox(self.replay_gain)\n        self.true_peak.setObjectName(\"true_peak\")\n        self.vboxlayout1.addWidget(self.true_peak)\n        self.reference_loudness = QtWidgets.QCheckBox(self.replay_gain)\n        self.reference_loudness.setObjectName(\"reference_loudness\")\n        self.vboxlayout1.addWidget(self.reference_loudness)\n        self.label_5 = QtWidgets.QLabel(self.replay_gain)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.label_5.setFont(font)\n        self.label_5.setObjectName(\"label_5\")\n        self.vboxlayout1.addWidget(self.label_5)\n        self.target_loudness = QtWidgets.QSpinBox(self.replay_gain)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.target_loudness.sizePolicy().hasHeightForWidth())\n        self.target_loudness.setSizePolicy(sizePolicy)\n        self.target_loudness.setMinimum(-30)\n        self.target_loudness.setMaximum(-5)\n        self.target_loudness.setProperty(\"value\", -18)\n        self.target_loudness.setObjectName(\"target_loudness\")\n        self.vboxlayout1.addWidget(self.target_loudness)\n        self.label_2 = QtWidgets.QLabel(self.replay_gain)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.label_2.setFont(font)\n        self.label_2.setObjectName(\"label_2\")\n        self.vboxlayout1.addWidget(self.label_2)\n        self.clip_mode = QtWidgets.QComboBox(self.replay_gain)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.clip_mode.sizePolicy().hasHeightForWidth())\n        self.clip_mode.setSizePolicy(sizePolicy)\n        self.clip_mode.setMaxCount(3)\n        self.clip_mode.setObjectName(\"clip_mode\")\n        self.vboxlayout1.addWidget(self.clip_mode)\n        self.label_3 = QtWidgets.QLabel(self.replay_gain)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.label_3.sizePolicy().hasHeightForWidth())\n        self.label_3.setSizePolicy(sizePolicy)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.label_3.setFont(font)\n        self.label_3.setObjectName(\"label_3\")\n        self.vboxlayout1.addWidget(self.label_3)\n        self.max_peak = QtWidgets.QSpinBox(self.replay_gain)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.max_peak.sizePolicy().hasHeightForWidth())\n        self.max_peak.setSizePolicy(sizePolicy)\n        self.max_peak.setMinimum(-5)\n        self.max_peak.setMaximum(0)\n        self.max_peak.setObjectName(\"max_peak\")\n        self.vboxlayout1.addWidget(self.max_peak)\n        self.label_4 = QtWidgets.QLabel(self.replay_gain)\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        self.label_4.setFont(font)\n        self.label_4.setObjectName(\"label_4\")\n        self.vboxlayout1.addWidget(self.label_4)\n        self.opus_mode = QtWidgets.QComboBox(self.replay_gain)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.opus_mode.sizePolicy().hasHeightForWidth())\n        self.opus_mode.setSizePolicy(sizePolicy)\n        self.opus_mode.setMaxCount(3)\n        self.opus_mode.setObjectName(\"opus_mode\")\n        self.vboxlayout1.addWidget(self.opus_mode)\n        self.opus_m23 = QtWidgets.QCheckBox(self.replay_gain)\n        self.opus_m23.setObjectName(\"opus_m23\")\n        self.vboxlayout1.addWidget(self.opus_m23)\n        self.vboxlayout.addWidget(self.replay_gain)\n        spacerItem = QtWidgets.QSpacerItem(263, 21, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.vboxlayout.addItem(spacerItem)\n\n        self.retranslateUi(ReplayGain2OptionsPage)\n        self.opus_mode.setCurrentIndex(-1)\n        QtCore.QMetaObject.connectSlotsByName(ReplayGain2OptionsPage)\n\n    def retranslateUi(self, ReplayGain2OptionsPage):\n        _translate = QtCore.QCoreApplication.translate\n        self.replay_gain.setTitle(_translate(\"ReplayGain2OptionsPage\", \"ReplayGain 2.0 Settings\"))\n        self.label.setText(_translate(\"ReplayGain2OptionsPage\", \"Path to rsgain:\"))\n        self.rsgain_command.setText(_translate(\"ReplayGain2OptionsPage\", \"rsgain\"))\n        self.rsgain_command_browse.setText(_translate(\"ReplayGain2OptionsPage\", \"Browse...\"))\n        self.label_6.setText(_translate(\"ReplayGain2OptionsPage\", \"<a href=\\\"https://github.com/complexlogic/rsgain\\\">Download rsgain</a>\"))\n        self.album_tags.setText(_translate(\"ReplayGain2OptionsPage\", \"Calculate album gain/peak\"))\n        self.true_peak.setWhatsThis(_translate(\"ReplayGain2OptionsPage\", \"Use more accurate true peak calculations (trade for performance)\"))\n        self.true_peak.setText(_translate(\"ReplayGain2OptionsPage\", \"Use true peak\"))\n        self.reference_loudness.setText(_translate(\"ReplayGain2OptionsPage\", \"Write reference loudness tags\"))\n        self.label_5.setText(_translate(\"ReplayGain2OptionsPage\", \"Target Loudness (LUFS)\"))\n        self.label_2.setText(_translate(\"ReplayGain2OptionsPage\", \"Clipping Protection\"))\n        self.label_3.setText(_translate(\"ReplayGain2OptionsPage\", \"Max Peak (dB)\"))\n        self.label_4.setText(_translate(\"ReplayGain2OptionsPage\", \"Opus Files\"))\n        self.opus_m23.setText(_translate(\"ReplayGain2OptionsPage\", \"Always reference Opus R128_*_GAIN tags to -23 LUFS\"))\n"
  },
  {
    "path": "plugins/replaygain2/ui_options_replaygain2.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>ReplayGain2OptionsPage</class>\n <widget class=\"QWidget\" name=\"ReplayGain2OptionsPage\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>477</width>\n    <height>577</height>\n   </rect>\n  </property>\n  <property name=\"sizePolicy\">\n   <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Preferred\">\n    <horstretch>0</horstretch>\n    <verstretch>0</verstretch>\n   </sizepolicy>\n  </property>\n  <layout class=\"QVBoxLayout\">\n   <property name=\"spacing\">\n    <number>6</number>\n   </property>\n   <property name=\"leftMargin\">\n    <number>9</number>\n   </property>\n   <property name=\"topMargin\">\n    <number>9</number>\n   </property>\n   <property name=\"rightMargin\">\n    <number>9</number>\n   </property>\n   <property name=\"bottomMargin\">\n    <number>9</number>\n   </property>\n   <item>\n    <widget class=\"QGroupBox\" name=\"replay_gain\">\n     <property name=\"title\">\n      <string>ReplayGain 2.0 Settings</string>\n     </property>\n     <layout class=\"QVBoxLayout\">\n      <property name=\"spacing\">\n       <number>2</number>\n      </property>\n      <property name=\"leftMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"topMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"rightMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"bottomMargin\">\n       <number>9</number>\n      </property>\n      <item>\n       <widget class=\"QLabel\" name=\"label\">\n        <property name=\"font\">\n         <font>\n          <weight>75</weight>\n          <bold>true</bold>\n         </font>\n        </property>\n        <property name=\"text\">\n         <string>Path to rsgain:</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <layout class=\"QHBoxLayout\" name=\"horizontalLayout\">\n        <item>\n         <widget class=\"QLineEdit\" name=\"rsgain_command\">\n          <property name=\"text\">\n           <string>rsgain</string>\n          </property>\n         </widget>\n        </item>\n        <item>\n         <widget class=\"QToolButton\" name=\"rsgain_command_browse\">\n          <property name=\"text\">\n           <string>Browse...</string>\n          </property>\n         </widget>\n        </item>\n       </layout>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"label_6\">\n        <property name=\"text\">\n         <string>&lt;a href=&quot;https://github.com/complexlogic/rsgain&quot;&gt;Download rsgain&lt;/a&gt;</string>\n        </property>\n        <property name=\"openExternalLinks\">\n         <bool>true</bool>\n        </property>\n        <property name=\"textInteractionFlags\">\n         <set>Qt::TextBrowserInteraction</set>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QCheckBox\" name=\"album_tags\">\n        <property name=\"text\">\n         <string>Calculate album gain/peak</string>\n        </property>\n        <property name=\"checked\">\n         <bool>true</bool>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QCheckBox\" name=\"true_peak\">\n        <property name=\"whatsThis\">\n         <string>Use more accurate true peak calculations (trade for performance)</string>\n        </property>\n        <property name=\"text\">\n         <string>Use true peak</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QCheckBox\" name=\"reference_loudness\">\n        <property name=\"text\">\n         <string>Write reference loudness tags</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"label_5\">\n        <property name=\"font\">\n         <font>\n          <weight>75</weight>\n          <bold>true</bold>\n         </font>\n        </property>\n        <property name=\"text\">\n         <string>Target Loudness (LUFS)</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QSpinBox\" name=\"target_loudness\">\n        <property name=\"sizePolicy\">\n         <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Fixed\">\n          <horstretch>0</horstretch>\n          <verstretch>0</verstretch>\n         </sizepolicy>\n        </property>\n        <property name=\"minimum\">\n         <number>-30</number>\n        </property>\n        <property name=\"maximum\">\n         <number>-5</number>\n        </property>\n        <property name=\"value\">\n         <number>-18</number>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"label_2\">\n        <property name=\"font\">\n         <font>\n          <weight>75</weight>\n          <bold>true</bold>\n         </font>\n        </property>\n        <property name=\"text\">\n         <string>Clipping Protection</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QComboBox\" name=\"clip_mode\">\n        <property name=\"sizePolicy\">\n         <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Fixed\">\n          <horstretch>0</horstretch>\n          <verstretch>0</verstretch>\n         </sizepolicy>\n        </property>\n        <property name=\"maxCount\">\n         <number>3</number>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"label_3\">\n        <property name=\"sizePolicy\">\n         <sizepolicy hsizetype=\"Maximum\" vsizetype=\"Preferred\">\n          <horstretch>0</horstretch>\n          <verstretch>0</verstretch>\n         </sizepolicy>\n        </property>\n        <property name=\"font\">\n         <font>\n          <weight>75</weight>\n          <bold>true</bold>\n         </font>\n        </property>\n        <property name=\"text\">\n         <string>Max Peak (dB)</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QSpinBox\" name=\"max_peak\">\n        <property name=\"sizePolicy\">\n         <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Fixed\">\n          <horstretch>0</horstretch>\n          <verstretch>0</verstretch>\n         </sizepolicy>\n        </property>\n        <property name=\"minimum\">\n         <number>-5</number>\n        </property>\n        <property name=\"maximum\">\n         <number>0</number>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"label_4\">\n        <property name=\"font\">\n         <font>\n          <weight>75</weight>\n          <bold>true</bold>\n         </font>\n        </property>\n        <property name=\"text\">\n         <string>Opus Files</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QComboBox\" name=\"opus_mode\">\n        <property name=\"sizePolicy\">\n         <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Fixed\">\n          <horstretch>0</horstretch>\n          <verstretch>0</verstretch>\n         </sizepolicy>\n        </property>\n        <property name=\"currentIndex\">\n         <number>-1</number>\n        </property>\n        <property name=\"maxCount\">\n         <number>3</number>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QCheckBox\" name=\"opus_m23\">\n        <property name=\"text\">\n         <string>Always reference Opus R128_*_GAIN tags to -23 LUFS</string>\n        </property>\n       </widget>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <spacer>\n     <property name=\"orientation\">\n      <enum>Qt::Vertical</enum>\n     </property>\n     <property name=\"sizeHint\" stdset=\"0\">\n      <size>\n       <width>263</width>\n       <height>21</height>\n      </size>\n     </property>\n    </spacer>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/save_and_rewrite_header/save_and_rewrite_header.py",
    "content": "#!/usr/bin/python\r\n# -*- coding: utf-8 -*-\r\n#\r\n# This source code is a plugin for MusicBrainz Picard.\r\n# It adds a context menu action to save files and rewrite their header.\r\n# Copyright (C) 2015 Nicolas Cenerario\r\n#\r\n# This program is free software: you can redistribute it and/or modify\r\n# it under the terms of the GNU General Public License as published by\r\n# the Free Software Foundation, either version 3 of the License, or\r\n# (at your option) any later version.\r\n#\r\n# This program is distributed in the hope that it will be useful,\r\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r\n# GNU General Public License for more details.\r\n#\r\n# You should have received a copy of the GNU General Public License\r\n# along with this program.  If not, see <http://www.gnu.org/licenses/gpl-3.0.txt>.\r\n\r\nfrom __future__ import unicode_literals\r\n\r\nPLUGIN_NAME = \"Save and rewrite header\"\r\nPLUGIN_AUTHOR = \"Nicolas Cenerario\"\r\nPLUGIN_DESCRIPTION = \"This plugin adds a context menu action to save files and rewrite their header.\"\r\nPLUGIN_VERSION = \"0.3\"\r\nPLUGIN_API_VERSIONS = [\"0.9.0\", \"0.10\", \"0.15\", \"2.0\"]\r\nPLUGIN_LICENSE = \"GPL-3.0-or-later\"\r\nPLUGIN_LICENSE_URL = \"http://www.gnu.org/licenses/gpl-3.0.txt\"\r\n\r\nfrom _functools import partial\r\nfrom mutagen import File as MFile\r\nfrom picard.ui.itemviews import (BaseAction, register_album_action,\r\n                                 register_cluster_action,\r\n                                 register_clusterlist_action,\r\n                                 register_track_action, register_file_action)\r\nfrom picard.metadata import Metadata\r\nfrom picard.util import thread\r\n\r\n\r\nclass save_and_rewrite_header(BaseAction):\r\n\r\n    NAME = \"Save and rewrite header\"\r\n\r\n    def __init__(self):\r\n        super(save_and_rewrite_header, self).__init__()\r\n        register_file_action(self)\r\n        register_track_action(self)\r\n        register_album_action(self)\r\n        register_cluster_action(self)\r\n        register_clusterlist_action(self)\r\n\r\n    def callback(self, obj):\r\n        def save(pf):\r\n            metadata = Metadata()\r\n            metadata.copy(pf.metadata)\r\n            mf = MFile(pf.filename)\r\n            if mf is not None:\r\n                mf.delete()\r\n            return pf._save_and_rename(pf.filename, metadata)\r\n        for f in self.tagger.get_files_from_objects(obj, save=True):\r\n            f.set_pending()\r\n            thread.run_task(partial(save, f), f._saving_finished,\r\n                            priority=2, thread_pool=f.tagger.save_thread_pool)\r\n\r\nsave_and_rewrite_header()\r\n"
  },
  {
    "path": "plugins/script_logger/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2023 Bob Swift (rdswift)\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\n# pylint: disable=missing-module-docstring, wrong-import-position, line-too-long\n\nPLUGIN_NAME = 'Script Logger'\nPLUGIN_AUTHOR = 'Bob Swift (rdswift)'\nPLUGIN_DESCRIPTION = '''\nThis plugin provides a new script function `$logline()` to write entries\nto Picard's system log.  By default, the log level is set at `Info`, but\nany level can be used by providing the level as an optional second\nparameter to the function.\n\nThe function is used as:\n\n`$logline(text[,level])`\n\nwhere `text` is the text to write to the log.  The entry will be written\nat log level `Info` by default, but this can be changed by specifying a\ndifferent level as an optional second parameter.  Allowable log levels are:\n\n- E (Error)\n- W (Warning)\n- I (Info)\n- D (Debug)\n\nIf an unknown level is entered, the function will use the default `Info`\nlevel.\n'''\nPLUGIN_VERSION = '0.1'\nPLUGIN_API_VERSIONS = ['2.0', '2.1', '2.2', '2.3', '2.6', '2.9']\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.txt\"\n\nPLUGIN_USER_GUIDE_URL = \"https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/script_logger/docs/README.md\"\n\nDOCUMENTATION = \"\"\"\\\n`$logline(text[,level])`\n\nwhere `text` is the text to write to the log.  The entry will be written\nat log level `Info` by default, but this can be changed by specifying a\ndifferent level as an optional second parameter.  Allowable log levels are:\n\n- E (Error)\n- W (Warning)\n- I (Info)\n- D (Debug)\n\nIf an unknown level is entered, the function will use the default `Info`\nlevel.\n\"\"\"\n\nfrom picard import log\nfrom picard.script import register_script_function\n\nLEVELS = {\n    'E': log.error,\n    'W': log.warning,\n    'I': log.info,\n    'D': log.debug,\n}\n\ndef logline(_parser, text: str, level=None):\n    \"\"\"Logs text to the Picard log.\n\n    Args:\n        _parser (parser): Script parser\n        text (str): Text message to log\n        level (str, optional): Level to use for logging ('E', 'W', 'I' or 'D'). Defaults to 'I'.\n    \"\"\"\n    if level:\n        _level = (str(level).strip().upper() + 'I')[0]\n    else:\n        _level = 'I'\n    if _level not in LEVELS:\n        _level = 'I'\n    LEVELS[_level](text.strip())\n    return ''\n\nregister_script_function(logline, documentation=DOCUMENTATION)\n"
  },
  {
    "path": "plugins/search_engine_lookup/README.md",
    "content": "# Search Engine Lookup \\[[Download](https://github.com/rdswift/picard-plugins/raw/2.0_RDS_Plugins/plugins/search_engine_lookup/search_engine_lookup.zip)\\]\n\n## Overview\n\nThis plugin adds a right click option on a cluster, providing the ability to lookup the cluster using a search engine in a browser window.\nIt also provides a right click option on album and track items in the right-hand Album pane, allowing a search engine lookup for the cover art\nassociated with the selected album or track.\n\nWhen you right-click on the cluster, album or track, the lookup option is found under the \"Plugins\" section of the context list.\n\n## Settings\n\nA settings screen is available in Picard's options settings, under the Plugins section.  This allows the user to select their preferred\nsearch engine provider, and any additional words to provide with the cluster search.  You can also add, edit or remove search engine providers.\n\n![options image](https://github.com/rdswift/picard-plugins/raw/2.0_RDS_Plugins/plugins/search_engine_lookup/options.jpg)\n\nWhen adding or editing a provider, checkmarks to the right of each field indicate whether or not the information in the field is valid.\nThe title is valid when it contains at least two non-space characters, does not begin or end with a space, and is not the same as the\ntitle of another existing provider.  The URL is valid when it contains the search replacement parameter ``%search%`` and does not begin\nor end with, or contain any spaces.  The \"Save\" button will be disabled until both fields are valid.\n\n![options edit image](https://github.com/rdswift/picard-plugins/raw/2.0_RDS_Plugins/plugins/search_engine_lookup/options_edit.jpg)\n\n---\n"
  },
  {
    "path": "plugins/search_engine_lookup/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2020-2021, 2026 Bob Swift (rdswift)\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = 'Search Engine Lookup'\nPLUGIN_AUTHOR = 'Bob Swift'\nPLUGIN_DESCRIPTION = '''\nAdds a right click option on a cluster to look up album information using a search engine in a browser window.\n\nAdds a right click option on an album or track to look up cover art for the selected album or title.\n'''\nPLUGIN_VERSION = '2.1.1'\nPLUGIN_API_VERSIONS = ['2.0', '2.1', '2.2', '2.3']\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.txt\"\n\nimport re\nfrom urllib.parse import quote_plus\nfrom uuid import uuid4\n\nfrom PyQt5 import QtCore, QtWidgets\n\nfrom picard import config, log\nfrom picard.cluster import Cluster\nfrom picard.plugins.search_engine_lookup.ui_options_search_engine_editor import Ui_SearchEngineEditorDialog\nfrom picard.plugins.search_engine_lookup.ui_options_search_engine_lookup import Ui_SearchEngineLookupOptionsPage\nfrom picard.ui.itemviews import BaseAction, register_cluster_action, register_album_action, register_track_action\nfrom picard.ui.options import OptionsPage, register_options_page\nfrom picard.util.webbrowser2 import open as _open\n\nDEFAULT_PROVIDERS = {\n    'ea520f49-36bc-4821-a16a-38bf0340d1f3': {'name': _('Google'), 'url': r'https://www.google.com/search?q=%search%'},\n    '7b93d4b5-34d9-49a7-901c-ed0914f07aee': {'name': _('Bing'), 'url': r'https://www.bing.com/search?q=%search%'},\n    '37be75d9-5fc5-4858-87fc-b5db0896a163': {'name': _('DuckDuckGo'), 'url': r'https://duckduckgo.com/?q=%search%'},\n}\n\nDEFAULT_PROVIDER = 'ea520f49-36bc-4821-a16a-38bf0340d1f3'\nDEFAULT_EXTRA_WORDS = 'album'\n\nRE_VALIDATE_TITLE = re.compile(r'^[^\\s\"|][^\"|]*[^\\s\"|]$')\nRE_VALIDATE_URL = re.compile(r'^[^\\s\"]*%search%[^\\s\"]*$')\n\nKEY_PROVIDER = 'search_engine_lookup_provider'\nKEY_PROVIDERS = 'search_engine_lookup_providers'\nKEY_EXTRA = 'search_engine_lookup_extra_words'\n\n\ndef show_popup(title='', content='', window=None):\n    QtWidgets.QMessageBox.information(\n        window,\n        title,\n        content,\n        QtWidgets.QMessageBox.Ok,\n        QtWidgets.QMessageBox.Ok\n    )\n\n\ndef lookup_error():\n    log.error(\"{0}: No existing metadata to lookup.\".format(PLUGIN_NAME,))\n    show_popup(_('Lookup Error'), _('There is no existing data to use for a search.'))\n\n\ndef do_lookup(text):\n    provider = config.setting[KEY_PROVIDER]\n    providers = config.setting[KEY_PROVIDERS]\n    base_url = providers[provider]['url'] if provider in providers else DEFAULT_PROVIDERS[DEFAULT_PROVIDER]['url']\n    url = base_url.replace(r'%search%', quote_plus(text))\n    log.debug(\"{0}: Looking up {1}\".format(PLUGIN_NAME, url,))\n    _open(url)\n\n\ndef lookup_cover_art(title, artist):\n    text = \"{0} by {1} album cover\".format(title, artist)\n    do_lookup(text)\n\n\nclass SearchEngineLookupTest(BaseAction):\n    NAME = 'Search engine lookup'\n\n    def callback(self, cluster_list):\n        extra = config.setting[KEY_EXTRA].split()\n        for cluster in cluster_list:\n            if isinstance(cluster, Cluster):\n                parts = []\n                if 'albumartist' in cluster.metadata and cluster.metadata['albumartist']:\n                    parts.extend(cluster.metadata['albumartist'].split())\n                if 'album' in cluster.metadata and cluster.metadata['albumartist']:\n                    parts.extend(cluster.metadata['album'].split())\n                if parts:\n                    if extra:\n                        parts.extend(extra)\n                    text = ' '.join(parts)\n                    do_lookup(text)\n                else:\n                    lookup_error()\n            else:\n                log.error(\"{0}: Argument is not a cluster. {1}\".format(PLUGIN_NAME, cluster,))\n                show_popup(_('Lookup Error'), _('There was a problem with the information provided for the cluster.'))\n\n\nclass AlbumCoverArtLookup(BaseAction):\n    NAME = 'Album cover art lookup'\n\n    def callback(self, album):\n        metadata = album[0].metadata\n        if 'album' in metadata and 'albumartist' in metadata:\n            lookup_cover_art(metadata['album'], metadata['albumartist'])\n        else:\n            lookup_error()\n\n\nclass TrackCoverArtLookup(BaseAction):\n    NAME = 'Track cover art lookup'\n\n    def callback(self, track):\n        metadata = track[0].metadata\n        if 'title' in metadata and 'artist' in metadata:\n            lookup_cover_art(metadata['title'], metadata['artist'])\n        else:\n            lookup_error()\n\n\nclass SearchEngineEditDialog(QtWidgets.QDialog):\n\n    def __init__(self, parent=None, edit_provider='', edit_url='', titles=None):\n        super().__init__(parent)\n        self.parent = parent\n        self.output = None\n        self.edit_provider = edit_provider\n        self.edit_url = edit_url\n        self.providers = titles if titles else []\n\n        self.valid_no = QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.SP_DialogCancelButton).pixmap(16, 16)\n        self.valid_yes = QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.SP_DialogApplyButton).pixmap(16, 16)\n\n        self.ui = Ui_SearchEngineEditorDialog()\n        self.ui.setupUi(self)\n        self.ui.le_title.setText(self.edit_provider)\n        self.ui.le_url.setText(self.edit_url)\n\n        self.setup_actions()\n        self.check_validation()\n\n    def setup_actions(self):\n        self.ui.le_title.textChanged.connect(self.title_text_changed)\n        self.ui.le_url.textChanged.connect(self.url_text_changed)\n        self.ui.pb_save.clicked.connect(self.accept)\n        self.ui.pb_cancel.clicked.connect(self.reject)\n\n    def check_validation(self):\n        valid_title = re.match(RE_VALIDATE_TITLE, self.edit_provider) and self.edit_provider not in self.providers\n        self.ui.img_valid_title.setPixmap(self.valid_yes if valid_title else self.valid_no)\n\n        valid_url = re.match(RE_VALIDATE_URL, self.edit_url)\n        self.ui.img_valid_url.setPixmap(self.valid_yes if valid_url else self.valid_no)\n\n        # Note that this needs to be forced to a bool to avoid Qt crashing Picard\n        self.ui.pb_save.setEnabled(bool(valid_title and valid_url))\n\n    def get_output(self):\n        return self.output\n\n    def accept(self):\n        self.output = (self.edit_provider.strip(), self.edit_url.strip())\n        super().accept()\n\n    def title_text_changed(self, text):\n        self.edit_provider = text\n        self.check_validation()\n\n    def url_text_changed(self, text):\n        self.edit_url = text\n        self.check_validation()\n\n\nclass SearchEngineLookupOptionsPage(OptionsPage):\n\n    NAME = \"search_engine_lookup_options\"\n    TITLE = \"Search Engine Lookup\"\n    PARENT = \"plugins\"\n    HELP_URL = \"https://github.com/metabrainz/picard-plugins/blob/2.0/plugins/search_engine_lookup/README.md\"\n\n    options = [\n        config.Option(\"setting\", KEY_PROVIDERS, DEFAULT_PROVIDERS.copy()),\n        config.TextOption(\"setting\", KEY_PROVIDER, DEFAULT_PROVIDER),\n        config.TextOption(\"setting\", KEY_EXTRA, DEFAULT_EXTRA_WORDS),\n    ]\n\n    def __init__(self, parent=None):\n        super(SearchEngineLookupOptionsPage, self).__init__(parent)\n        self.ui = Ui_SearchEngineLookupOptionsPage()\n        self.ui.setupUi(self)\n        self.setup_actions()\n        self.provider = ''\n        self.providers = {}\n        self.additional_words = ''\n\n    def setup_actions(self):\n        self.ui.list_providers.itemChanged.connect(self.select_provider)\n        self.ui.pb_add.clicked.connect(self.add_provider)\n        self.ui.pb_edit.clicked.connect(self.edit_provider)\n        self.ui.pb_delete.clicked.connect(self.delete_provider)\n        self.ui.pb_test.clicked.connect(self.test_provider)\n        self.ui.le_additional_words.textChanged.connect(self.additional_words_changed)\n\n    def additional_words_changed(self, text):\n        self.additional_words = text\n\n    def load(self):\n        # Settings for search engine providers\n        self.providers = config.setting[KEY_PROVIDERS] or DEFAULT_PROVIDERS.copy()\n\n        # Settings for search engine provider\n        self.provider = config.setting[KEY_PROVIDER]\n        if self.provider not in self.providers:\n            # Assign an arbitrary valid value to self.provider\n            self.provider = list(self.providers)[0]\n\n        # Settings for search extra words\n        self.additional_words = config.setting[KEY_EXTRA]\n        self.ui.le_additional_words.setText(self.additional_words)\n\n        # Display list of providers\n        self.update_list()\n\n    def select_provider(self, list_item):\n        if list_item.checkState() == QtCore.Qt.Checked:\n            # New provider selected\n            self.provider = list_item.data(QtCore.Qt.UserRole)\n            self.update_list(current_item=self.provider)\n        else:\n            # Attempt to deselect the current provider leaving none selected\n            list_item.setCheckState(QtCore.Qt.Checked)\n\n    def add_provider(self):\n        provider_id = str(uuid4())\n        self.edit_provider_dialog(provider_id)\n\n    def edit_provider(self):\n        current_item = self.ui.list_providers.currentItem()\n        provider = current_item.text()\n        provider_id = current_item.data(QtCore.Qt.UserRole)\n        url = self.providers[provider_id]['url']\n        self.edit_provider_dialog(provider_id, provider, url)\n\n    def edit_provider_dialog(self, provider_id='', provider='', url=''):\n        # List of titles currently used and not allowed.  Omit current title from the list when editing.\n        titles = [x['name'] for x in self.providers.values() if x['name'] != provider]\n        dialog = SearchEngineEditDialog(self, provider, url, titles)\n        temp = dialog.exec_()\n        if temp == QtWidgets.QDialog.Accepted:\n            data = dialog.get_output()\n            if data:\n                new_provider, new_url = data\n                self.providers[provider_id] = {'name': new_provider, 'url': new_url}\n                self.update_list(provider_id)\n\n    def delete_provider(self):\n        current_item = self.ui.list_providers.currentItem()\n        provider = current_item.text()\n        provider_id = current_item.data(QtCore.Qt.UserRole)\n        if current_item.checkState() or provider_id == self.provider:\n            QtWidgets.QMessageBox.critical(\n                self,\n                _('Deletion Error'),\n                _('You cannot delete the currently selected search provider.'),\n                QtWidgets.QMessageBox.Ok,\n                QtWidgets.QMessageBox.Ok\n            )\n        else:\n            if QtWidgets.QMessageBox.warning(\n                self,\n                _('Confirm Deletion'),\n                _('You are about to permanently delete the search provider \"{provider_name}\".  Continue?').format(provider_name=provider),\n                QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,\n                QtWidgets.QMessageBox.Cancel\n            ) == QtWidgets.QMessageBox.Ok:\n                self.providers.pop(provider_id, None)\n                self.update_list()\n\n    def test_provider(self):\n        current_item = self.ui.list_providers.currentItem()\n        parts = ('The Beatles Abby Road ' + self.additional_words).strip().split()\n        url = self.providers[current_item.data(QtCore.Qt.UserRole)]['url'].replace(r'%search%', quote_plus(' '.join(parts)))\n        _open(url)\n\n    def update_list(self, current_item=None):\n        current_row = -1\n        self.ui.list_providers.clear()\n        for counter, provider_id in enumerate(self.providers):\n            item = QtWidgets.QListWidgetItem()\n            item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)\n            item.setText(self.providers[provider_id]['name'])\n            item.setCheckState(QtCore.Qt.Checked if provider_id == self.provider else QtCore.Qt.Unchecked)\n            item.setData(QtCore.Qt.UserRole, provider_id)\n            self.ui.list_providers.addItem(item)\n            if current_item and provider_id == current_item:\n                current_row = counter\n        current_row = max(current_row, 0)\n        self.ui.list_providers.setCurrentRow(current_row)\n        self.ui.list_providers.sortItems()\n\n    def save(self):\n        self._set_settings(config.setting)\n\n    def _set_settings(self, settings):\n        settings[KEY_PROVIDER] = self.provider.strip()\n        settings[KEY_EXTRA] = self.additional_words.strip()\n        settings[KEY_PROVIDERS] = self.providers or DEFAULT_PROVIDERS.copy()\n\n\nregister_cluster_action(SearchEngineLookupTest())\nregister_options_page(SearchEngineLookupOptionsPage)\nregister_album_action(AlbumCoverArtLookup())\nregister_track_action(TrackCoverArtLookup())\n"
  },
  {
    "path": "plugins/search_engine_lookup/ui_options_search_engine_editor.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/search_engine_lookup/ui_options_search_engine_editor.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.4\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_SearchEngineEditorDialog(object):\n    def setupUi(self, SearchEngineEditorDialog):\n        SearchEngineEditorDialog.setObjectName(\"SearchEngineEditorDialog\")\n        SearchEngineEditorDialog.setWindowModality(QtCore.Qt.ApplicationModal)\n        SearchEngineEditorDialog.resize(560, 205)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(SearchEngineEditorDialog.sizePolicy().hasHeightForWidth())\n        SearchEngineEditorDialog.setSizePolicy(sizePolicy)\n        SearchEngineEditorDialog.setMinimumSize(QtCore.QSize(560, 205))\n        SearchEngineEditorDialog.setMaximumSize(QtCore.QSize(560, 300))\n        self.verticalLayout = QtWidgets.QVBoxLayout(SearchEngineEditorDialog)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.scrollArea = QtWidgets.QScrollArea(SearchEngineEditorDialog)\n        self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame)\n        self.scrollArea.setWidgetResizable(True)\n        self.scrollArea.setObjectName(\"scrollArea\")\n        self.scrollAreaWidgetContents = QtWidgets.QWidget()\n        self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 542, 187))\n        self.scrollAreaWidgetContents.setObjectName(\"scrollAreaWidgetContents\")\n        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents)\n        self.verticalLayout_2.setObjectName(\"verticalLayout_2\")\n        self.label_4 = QtWidgets.QLabel(self.scrollAreaWidgetContents)\n        self.label_4.setWordWrap(True)\n        self.label_4.setObjectName(\"label_4\")\n        self.verticalLayout_2.addWidget(self.label_4)\n        spacerItem = QtWidgets.QSpacerItem(20, 5, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.verticalLayout_2.addItem(spacerItem)\n        self.gridLayout = QtWidgets.QGridLayout()\n        self.gridLayout.setContentsMargins(-1, -1, -1, 0)\n        self.gridLayout.setObjectName(\"gridLayout\")\n        self.label_2 = QtWidgets.QLabel(self.scrollAreaWidgetContents)\n        self.label_2.setObjectName(\"label_2\")\n        self.gridLayout.addWidget(self.label_2, 0, 0, 1, 1)\n        self.label_3 = QtWidgets.QLabel(self.scrollAreaWidgetContents)\n        self.label_3.setObjectName(\"label_3\")\n        self.gridLayout.addWidget(self.label_3, 1, 0, 1, 1)\n        self.le_title = QtWidgets.QLineEdit(self.scrollAreaWidgetContents)\n        font = QtGui.QFont()\n        font.setPointSize(9)\n        self.le_title.setFont(font)\n        self.le_title.setToolTipDuration(-1)\n        self.le_title.setObjectName(\"le_title\")\n        self.gridLayout.addWidget(self.le_title, 0, 1, 1, 1)\n        self.le_url = QtWidgets.QLineEdit(self.scrollAreaWidgetContents)\n        font = QtGui.QFont()\n        font.setPointSize(9)\n        self.le_url.setFont(font)\n        self.le_url.setObjectName(\"le_url\")\n        self.gridLayout.addWidget(self.le_url, 1, 1, 1, 1)\n        self.img_valid_title = QtWidgets.QLabel(self.scrollAreaWidgetContents)\n        self.img_valid_title.setMinimumSize(QtCore.QSize(16, 16))\n        self.img_valid_title.setText(\"\")\n        self.img_valid_title.setObjectName(\"img_valid_title\")\n        self.gridLayout.addWidget(self.img_valid_title, 0, 2, 1, 1)\n        self.img_valid_url = QtWidgets.QLabel(self.scrollAreaWidgetContents)\n        self.img_valid_url.setMinimumSize(QtCore.QSize(16, 16))\n        self.img_valid_url.setText(\"\")\n        self.img_valid_url.setObjectName(\"img_valid_url\")\n        self.gridLayout.addWidget(self.img_valid_url, 1, 2, 1, 1)\n        self.verticalLayout_2.addLayout(self.gridLayout)\n        self.horizontalLayout = QtWidgets.QHBoxLayout()\n        self.horizontalLayout.setContentsMargins(-1, 5, -1, 0)\n        self.horizontalLayout.setObjectName(\"horizontalLayout\")\n        spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)\n        self.horizontalLayout.addItem(spacerItem1)\n        self.pb_save = QtWidgets.QPushButton(self.scrollAreaWidgetContents)\n        self.pb_save.setObjectName(\"pb_save\")\n        self.horizontalLayout.addWidget(self.pb_save)\n        self.pb_cancel = QtWidgets.QPushButton(self.scrollAreaWidgetContents)\n        self.pb_cancel.setObjectName(\"pb_cancel\")\n        self.horizontalLayout.addWidget(self.pb_cancel)\n        self.verticalLayout_2.addLayout(self.horizontalLayout)\n        self.scrollArea.setWidget(self.scrollAreaWidgetContents)\n        self.verticalLayout.addWidget(self.scrollArea)\n\n        self.retranslateUi(SearchEngineEditorDialog)\n        QtCore.QMetaObject.connectSlotsByName(SearchEngineEditorDialog)\n\n    def retranslateUi(self, SearchEngineEditorDialog):\n        _translate = QtCore.QCoreApplication.translate\n        SearchEngineEditorDialog.setWindowTitle(_translate(\"SearchEngineEditorDialog\", \"Edit Search Engine Provider\"))\n        self.label_4.setText(_translate(\"SearchEngineEditorDialog\", \"<html><head/><body><p>Enter the title and URL for the search engine provider. Titles must be at least two non-space characters long, and must not be the same as the title of an existing provider.</p><p>When entering the URL the macro <span style=\\\"font-weight:600;\\\">%search%</span> must be included. This will be replaced by the list of search words separated by plus signs when the url is sent to your browser for display.</p></body></html>\"))\n        self.label_2.setText(_translate(\"SearchEngineEditorDialog\", \"Title:\"))\n        self.label_3.setText(_translate(\"SearchEngineEditorDialog\", \"URL:\"))\n        self.le_title.setToolTip(_translate(\"SearchEngineEditorDialog\", \"The title to show in the list for the search engine provider\"))\n        self.le_url.setToolTip(_translate(\"SearchEngineEditorDialog\", \"The URL to use for the search engine provider\"))\n        self.pb_save.setText(_translate(\"SearchEngineEditorDialog\", \"Save\"))\n        self.pb_cancel.setText(_translate(\"SearchEngineEditorDialog\", \"Cancel\"))\n"
  },
  {
    "path": "plugins/search_engine_lookup/ui_options_search_engine_editor.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>SearchEngineEditorDialog</class>\n <widget class=\"QWidget\" name=\"SearchEngineEditorDialog\">\n  <property name=\"windowModality\">\n   <enum>Qt::ApplicationModal</enum>\n  </property>\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>560</width>\n    <height>205</height>\n   </rect>\n  </property>\n  <property name=\"sizePolicy\">\n   <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Preferred\">\n    <horstretch>0</horstretch>\n    <verstretch>0</verstretch>\n   </sizepolicy>\n  </property>\n  <property name=\"minimumSize\">\n   <size>\n    <width>560</width>\n    <height>205</height>\n   </size>\n  </property>\n  <property name=\"maximumSize\">\n   <size>\n    <width>560</width>\n    <height>300</height>\n   </size>\n  </property>\n  <property name=\"windowTitle\">\n   <string>Edit Search Engine Provider</string>\n  </property>\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n   <item>\n    <widget class=\"QScrollArea\" name=\"scrollArea\">\n     <property name=\"frameShape\">\n      <enum>QFrame::NoFrame</enum>\n     </property>\n     <property name=\"widgetResizable\">\n      <bool>true</bool>\n     </property>\n     <widget class=\"QWidget\" name=\"scrollAreaWidgetContents\">\n      <property name=\"geometry\">\n       <rect>\n        <x>0</x>\n        <y>0</y>\n        <width>542</width>\n        <height>187</height>\n       </rect>\n      </property>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\n       <item>\n        <widget class=\"QLabel\" name=\"label_4\">\n         <property name=\"text\">\n          <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Enter the title and URL for the search engine provider. Titles must be at least two non-space characters long, and must not be the same as the title of an existing provider.&lt;/p&gt;&lt;p&gt;When entering the URL the macro &lt;span style=&quot;font-weight:600;&quot;&gt;%search%&lt;/span&gt; must be included. This will be replaced by the list of search words separated by plus signs when the url is sent to your browser for display.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n         </property>\n         <property name=\"wordWrap\">\n          <bool>true</bool>\n         </property>\n        </widget>\n       </item>\n       <item>\n        <spacer name=\"verticalSpacer\">\n         <property name=\"orientation\">\n          <enum>Qt::Vertical</enum>\n         </property>\n         <property name=\"sizeHint\" stdset=\"0\">\n          <size>\n           <width>20</width>\n           <height>5</height>\n          </size>\n         </property>\n        </spacer>\n       </item>\n       <item>\n        <layout class=\"QGridLayout\" name=\"gridLayout\">\n         <property name=\"bottomMargin\">\n          <number>0</number>\n         </property>\n         <item row=\"0\" column=\"0\">\n          <widget class=\"QLabel\" name=\"label_2\">\n           <property name=\"text\">\n            <string>Title:</string>\n           </property>\n          </widget>\n         </item>\n         <item row=\"1\" column=\"0\">\n          <widget class=\"QLabel\" name=\"label_3\">\n           <property name=\"text\">\n            <string>URL:</string>\n           </property>\n          </widget>\n         </item>\n         <item row=\"0\" column=\"1\">\n          <widget class=\"QLineEdit\" name=\"le_title\">\n           <property name=\"font\">\n            <font>\n             <pointsize>9</pointsize>\n            </font>\n           </property>\n           <property name=\"toolTip\">\n            <string>The title to show in the list for the search engine provider</string>\n           </property>\n           <property name=\"toolTipDuration\">\n            <number>-1</number>\n           </property>\n          </widget>\n         </item>\n         <item row=\"1\" column=\"1\">\n          <widget class=\"QLineEdit\" name=\"le_url\">\n           <property name=\"font\">\n            <font>\n             <pointsize>9</pointsize>\n            </font>\n           </property>\n           <property name=\"toolTip\">\n            <string>The URL to use for the search engine provider</string>\n           </property>\n          </widget>\n         </item>\n         <item row=\"0\" column=\"2\">\n          <widget class=\"QLabel\" name=\"img_valid_title\">\n           <property name=\"minimumSize\">\n            <size>\n             <width>16</width>\n             <height>16</height>\n            </size>\n           </property>\n           <property name=\"text\">\n            <string/>\n           </property>\n          </widget>\n         </item>\n         <item row=\"1\" column=\"2\">\n          <widget class=\"QLabel\" name=\"img_valid_url\">\n           <property name=\"minimumSize\">\n            <size>\n             <width>16</width>\n             <height>16</height>\n            </size>\n           </property>\n           <property name=\"text\">\n            <string/>\n           </property>\n          </widget>\n         </item>\n        </layout>\n       </item>\n       <item>\n        <layout class=\"QHBoxLayout\" name=\"horizontalLayout\">\n         <property name=\"topMargin\">\n          <number>5</number>\n         </property>\n         <property name=\"bottomMargin\">\n          <number>0</number>\n         </property>\n         <item>\n          <spacer name=\"horizontalSpacer\">\n           <property name=\"orientation\">\n            <enum>Qt::Horizontal</enum>\n           </property>\n           <property name=\"sizeHint\" stdset=\"0\">\n            <size>\n             <width>40</width>\n             <height>20</height>\n            </size>\n           </property>\n          </spacer>\n         </item>\n         <item>\n          <widget class=\"QPushButton\" name=\"pb_save\">\n           <property name=\"text\">\n            <string>Save</string>\n           </property>\n          </widget>\n         </item>\n         <item>\n          <widget class=\"QPushButton\" name=\"pb_cancel\">\n           <property name=\"text\">\n            <string>Cancel</string>\n           </property>\n          </widget>\n         </item>\n        </layout>\n       </item>\n      </layout>\n     </widget>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/search_engine_lookup/ui_options_search_engine_lookup.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/search_engine_lookup/ui_options_search_engine_lookup.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.4\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_SearchEngineLookupOptionsPage(object):\n    def setupUi(self, SearchEngineLookupOptionsPage):\n        SearchEngineLookupOptionsPage.setObjectName(\"SearchEngineLookupOptionsPage\")\n        SearchEngineLookupOptionsPage.resize(561, 802)\n        SearchEngineLookupOptionsPage.setMinimumSize(QtCore.QSize(360, 0))\n        self.verticalLayout = QtWidgets.QVBoxLayout(SearchEngineLookupOptionsPage)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.scrollArea = QtWidgets.QScrollArea(SearchEngineLookupOptionsPage)\n        self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame)\n        self.scrollArea.setWidgetResizable(True)\n        self.scrollArea.setObjectName(\"scrollArea\")\n        self.scrollAreaWidgetContents = QtWidgets.QWidget()\n        self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 543, 784))\n        self.scrollAreaWidgetContents.setObjectName(\"scrollAreaWidgetContents\")\n        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents)\n        self.verticalLayout_2.setObjectName(\"verticalLayout_2\")\n        self.gb_description = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)\n        self.gb_description.setObjectName(\"gb_description\")\n        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.gb_description)\n        self.verticalLayout_3.setObjectName(\"verticalLayout_3\")\n        self.format_description = QtWidgets.QLabel(self.gb_description)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.format_description.sizePolicy().hasHeightForWidth())\n        self.format_description.setSizePolicy(sizePolicy)\n        self.format_description.setWordWrap(True)\n        self.format_description.setObjectName(\"format_description\")\n        self.verticalLayout_3.addWidget(self.format_description)\n        self.verticalLayout_2.addWidget(self.gb_description)\n        self.gb_search_engine = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)\n        self.gb_search_engine.setMinimumSize(QtCore.QSize(0, 0))\n        self.gb_search_engine.setObjectName(\"gb_search_engine\")\n        self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.gb_search_engine)\n        self.verticalLayout_4.setObjectName(\"verticalLayout_4\")\n        self.list_providers = QtWidgets.QListWidget(self.gb_search_engine)\n        self.list_providers.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)\n        self.list_providers.setObjectName(\"list_providers\")\n        self.verticalLayout_4.addWidget(self.list_providers)\n        self.horizontalLayout = QtWidgets.QHBoxLayout()\n        self.horizontalLayout.setContentsMargins(-1, -1, -1, 0)\n        self.horizontalLayout.setObjectName(\"horizontalLayout\")\n        spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)\n        self.horizontalLayout.addItem(spacerItem)\n        self.pb_add = QtWidgets.QPushButton(self.gb_search_engine)\n        self.pb_add.setObjectName(\"pb_add\")\n        self.horizontalLayout.addWidget(self.pb_add)\n        self.pb_edit = QtWidgets.QPushButton(self.gb_search_engine)\n        self.pb_edit.setObjectName(\"pb_edit\")\n        self.horizontalLayout.addWidget(self.pb_edit)\n        self.pb_delete = QtWidgets.QPushButton(self.gb_search_engine)\n        self.pb_delete.setObjectName(\"pb_delete\")\n        self.horizontalLayout.addWidget(self.pb_delete)\n        self.pb_test = QtWidgets.QPushButton(self.gb_search_engine)\n        self.pb_test.setObjectName(\"pb_test\")\n        self.horizontalLayout.addWidget(self.pb_test)\n        self.verticalLayout_4.addLayout(self.horizontalLayout)\n        self.verticalLayout_2.addWidget(self.gb_search_engine)\n        self.groupBox = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)\n        self.groupBox.setMinimumSize(QtCore.QSize(0, 0))\n        self.groupBox.setObjectName(\"groupBox\")\n        self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.groupBox)\n        self.verticalLayout_5.setObjectName(\"verticalLayout_5\")\n        self.label = QtWidgets.QLabel(self.groupBox)\n        self.label.setWordWrap(True)\n        self.label.setObjectName(\"label\")\n        self.verticalLayout_5.addWidget(self.label)\n        self.le_additional_words = QtWidgets.QLineEdit(self.groupBox)\n        self.le_additional_words.setObjectName(\"le_additional_words\")\n        self.verticalLayout_5.addWidget(self.le_additional_words)\n        self.verticalLayout_2.addWidget(self.groupBox)\n        spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.verticalLayout_2.addItem(spacerItem1)\n        self.scrollArea.setWidget(self.scrollAreaWidgetContents)\n        self.verticalLayout.addWidget(self.scrollArea)\n\n        self.retranslateUi(SearchEngineLookupOptionsPage)\n        QtCore.QMetaObject.connectSlotsByName(SearchEngineLookupOptionsPage)\n\n    def retranslateUi(self, SearchEngineLookupOptionsPage):\n        _translate = QtCore.QCoreApplication.translate\n        SearchEngineLookupOptionsPage.setWindowTitle(_translate(\"SearchEngineLookupOptionsPage\", \"Form\"))\n        self.gb_description.setTitle(_translate(\"SearchEngineLookupOptionsPage\", \"Search Engine Lookup\"))\n        self.format_description.setText(_translate(\"SearchEngineLookupOptionsPage\", \"<html><head/><body><p>The Search Engine Lookup plugin allows you to initiate a search engine lookup in your browser for a cluster from the menu displayed when right-clicking the cluster.  These settings allow you select which search engine to use for the lookup, as well as any additional search terms.</p></body></html>\"))\n        self.gb_search_engine.setTitle(_translate(\"SearchEngineLookupOptionsPage\", \"Search Engine Providers\"))\n        self.pb_add.setText(_translate(\"SearchEngineLookupOptionsPage\", \"Add\"))\n        self.pb_edit.setText(_translate(\"SearchEngineLookupOptionsPage\", \"Edit\"))\n        self.pb_delete.setText(_translate(\"SearchEngineLookupOptionsPage\", \"Delete\"))\n        self.pb_test.setText(_translate(\"SearchEngineLookupOptionsPage\", \"Test\"))\n        self.groupBox.setTitle(_translate(\"SearchEngineLookupOptionsPage\", \"Additional Search Words\"))\n        self.label.setText(_translate(\"SearchEngineLookupOptionsPage\", \"<html><head/><body><p>By default, the search parameters used are the artist name and album name if they are available in the metadata. You have the option of specifying additional words to be added to the search parameters (e.g.: album).</p><p>Additional words are entered below and should be separated by spaces.</p></body></html>\"))\n"
  },
  {
    "path": "plugins/search_engine_lookup/ui_options_search_engine_lookup.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>SearchEngineLookupOptionsPage</class>\n <widget class=\"QWidget\" name=\"SearchEngineLookupOptionsPage\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>561</width>\n    <height>802</height>\n   </rect>\n  </property>\n  <property name=\"minimumSize\">\n   <size>\n    <width>360</width>\n    <height>0</height>\n   </size>\n  </property>\n  <property name=\"windowTitle\">\n   <string>Form</string>\n  </property>\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n   <item>\n    <widget class=\"QScrollArea\" name=\"scrollArea\">\n     <property name=\"frameShape\">\n      <enum>QFrame::NoFrame</enum>\n     </property>\n     <property name=\"widgetResizable\">\n      <bool>true</bool>\n     </property>\n     <widget class=\"QWidget\" name=\"scrollAreaWidgetContents\">\n      <property name=\"geometry\">\n       <rect>\n        <x>0</x>\n        <y>0</y>\n        <width>543</width>\n        <height>784</height>\n       </rect>\n      </property>\n      <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\n       <item>\n        <widget class=\"QGroupBox\" name=\"gb_description\">\n         <property name=\"title\">\n          <string>Search Engine Lookup</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_3\">\n          <item>\n           <widget class=\"QLabel\" name=\"format_description\">\n            <property name=\"sizePolicy\">\n             <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Minimum\">\n              <horstretch>0</horstretch>\n              <verstretch>0</verstretch>\n             </sizepolicy>\n            </property>\n            <property name=\"text\">\n             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The Search Engine Lookup plugin allows you to initiate a search engine lookup in your browser for a cluster from the menu displayed when right-clicking the cluster.  These settings allow you select which search engine to use for the lookup, as well as any additional search terms.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n            </property>\n            <property name=\"wordWrap\">\n             <bool>true</bool>\n            </property>\n           </widget>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QGroupBox\" name=\"gb_search_engine\">\n         <property name=\"minimumSize\">\n          <size>\n           <width>0</width>\n           <height>0</height>\n          </size>\n         </property>\n         <property name=\"title\">\n          <string>Search Engine Providers</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_4\">\n          <item>\n           <widget class=\"QListWidget\" name=\"list_providers\">\n            <property name=\"editTriggers\">\n             <set>QAbstractItemView::NoEditTriggers</set>\n            </property>\n           </widget>\n          </item>\n          <item>\n           <layout class=\"QHBoxLayout\" name=\"horizontalLayout\">\n            <property name=\"bottomMargin\">\n             <number>0</number>\n            </property>\n            <item>\n             <spacer name=\"horizontalSpacer\">\n              <property name=\"orientation\">\n               <enum>Qt::Horizontal</enum>\n              </property>\n              <property name=\"sizeHint\" stdset=\"0\">\n               <size>\n                <width>40</width>\n                <height>20</height>\n               </size>\n              </property>\n             </spacer>\n            </item>\n            <item>\n             <widget class=\"QPushButton\" name=\"pb_add\">\n              <property name=\"text\">\n               <string>Add</string>\n              </property>\n             </widget>\n            </item>\n            <item>\n             <widget class=\"QPushButton\" name=\"pb_edit\">\n              <property name=\"text\">\n               <string>Edit</string>\n              </property>\n             </widget>\n            </item>\n            <item>\n             <widget class=\"QPushButton\" name=\"pb_delete\">\n              <property name=\"text\">\n               <string>Delete</string>\n              </property>\n             </widget>\n            </item>\n            <item>\n             <widget class=\"QPushButton\" name=\"pb_test\">\n              <property name=\"text\">\n               <string>Test</string>\n              </property>\n             </widget>\n            </item>\n           </layout>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <widget class=\"QGroupBox\" name=\"groupBox\">\n         <property name=\"minimumSize\">\n          <size>\n           <width>0</width>\n           <height>0</height>\n          </size>\n         </property>\n         <property name=\"title\">\n          <string>Additional Search Words</string>\n         </property>\n         <layout class=\"QVBoxLayout\" name=\"verticalLayout_5\">\n          <item>\n           <widget class=\"QLabel\" name=\"label\">\n            <property name=\"text\">\n             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;By default, the search parameters used are the artist name and album name if they are available in the metadata. You have the option of specifying additional words to be added to the search parameters (e.g.: album).&lt;/p&gt;&lt;p&gt;Additional words are entered below and should be separated by spaces.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n            </property>\n            <property name=\"wordWrap\">\n             <bool>true</bool>\n            </property>\n           </widget>\n          </item>\n          <item>\n           <widget class=\"QLineEdit\" name=\"le_additional_words\"/>\n          </item>\n         </layout>\n        </widget>\n       </item>\n       <item>\n        <spacer name=\"verticalSpacer\">\n         <property name=\"orientation\">\n          <enum>Qt::Vertical</enum>\n         </property>\n         <property name=\"sizeHint\" stdset=\"0\">\n          <size>\n           <width>20</width>\n           <height>40</height>\n          </size>\n         </property>\n        </spacer>\n       </item>\n      </layout>\n     </widget>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/smart_title_case/smart_title_case.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n# This is the Smart Title Case plugin for MusicBrainz Picard.\n# Copyright (C) 2017 Sophist.\n#\n# Updated for use with Picard v2.0 by Bob Swift (rdswift).\n#\n# It is based on the Title Case plugin by Javier Kohen\n# Copyright 2007 Javier Kohen\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n\nPLUGIN_NAME = \"Smart Title Case\"\nPLUGIN_AUTHOR = \"Sophist based on an earlier plugin by Javier Kohen\"\nPLUGIN_DESCRIPTION = \"\"\"\nCapitalize First Character In Every Word Of Album/Track Title/Artist.<br />\nLeaves words containing embedded uppercase as-is i.e. USA or DoA.<br />\nFor Artist/AlbumArtist, title cases only artists not join phrases<br />\ne.g. The Beatles feat. The Who.\n\"\"\"\nPLUGIN_VERSION = \"0.4.2\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-3.0.html\"\n\nimport re, unicodedata\n\nfrom picard import log\nfrom picard.metadata import (\n    register_track_metadata_processor,\n    register_album_metadata_processor,\n)\n\n\ntitle_tags = ['title', 'album']\nartist_tags = [\n    ('artist', 'artists'),\n    ('artistsort', '~artists_sort'),\n    ('albumartist', '~albumartists'),\n    ('albumartistsort', '~albumartists_sort'),\n    ]\ntitle_re = re.compile(r'\\w[^-,/\\s\\u2010\\u2011]*')\n\ndef match_word(match):\n    word = match.group(0)\n    if word == word.lower():\n        word = word[0].upper() + word[1:]\n    return word\n\ndef string_title_match(match_word, string):\n    return title_re.sub(match_word, string)\n\ndef string_cleanup(string):\n    if not string:\n        return \"\"\n    return unicodedata.normalize(\"NFKC\", string)\n\ndef string_title_case(string):\n    \"\"\"Title-case a string using a less destructive method than str.title.\n    >>> string_title_case('make title case')\n    'Make Title Case'\n    >>> string_title_case('Already Title Case')\n    'Already Title Case'\n    >>> string_title_case('mIxEd cAsE')\n    'mIxEd cAsE'\n    >>> string_title_case('a')\n    'A'\n    >>> string_title_case(\"apostrophe's apostrophe's\")\n    \"Apostrophe's Apostrophe's\"\n    >>> string_title_case('(bracketed text)')\n    '(Bracketed Text)'\n    >>> string_title_case(\"'single quotes'\")\n    \"'Single Quotes'\"\n    >>> string_title_case('\"double quotes\"')\n    '\"Double Quotes\"'\n    >>> string_title_case('a,b')\n    'A,B'\n    >>> string_title_case('a-b')\n    'A-B'\n    >>> string_title_case('a/b')\n    'A/B'\n    >>> string_title_case('flügel')\n    'Flügel'\n    >>> string_title_case('HARVEST STORY by 杉山清貴')\n    'HARVEST STORY By 杉山清貴'\n    \"\"\"\n    return string_title_match(match_word, string_cleanup(string))\n\n\ndef artist_title_case(text, artists, artists_upper):\n    \"\"\"\n    Use the array of artists and the joined string\n    to identify artists to make title case\n    and the join strings to leave as-is.\n\n    >>> artist_title_case('the beatles feat. the who', ['the beatles', 'the who'], ['The Beatles', 'The Who'])\n    'The Beatles feat. The Who'\n    >>> artist_title_case('kesha feat. 3OH!3', ['kesha', '3OH!3'], ['Kesha', '3OH!3'])\n    'Kesha feat. 3OH!3'\n    \"\"\"\n    find = \"^(\" + r\")(\\s+\\S+?\\s+)(\".join((map(re.escape, map(string_cleanup,artists)))) + \")(.*$)\"\n    replace = \"\".join([r\"%s\\g<%d>\" % (a, x*2 + 2) for x, a in enumerate(artists_upper)])\n    result = re.sub(find, replace, string_cleanup(text))\n    return result\n\n\ndef title_case(tagger, metadata, *args):\n    for name in title_tags:\n        if name in metadata:\n            values = metadata.getall(name)\n            new_values = [string_title_case(v) for v in values]\n            if values != new_values:\n                log.debug(\"SmartTitleCase: %s: %r replaced with %r\", name, values, new_values)\n                metadata[name] = new_values\n    for artist_string, artists_list in artist_tags:\n        if artist_string in metadata and artists_list in metadata:\n            artist = metadata.getall(artist_string)\n            artists = metadata.getall(artists_list)\n            new_artists = list(map(string_title_case, artists))\n            new_artist = [artist_title_case(x, artists, new_artists) for x in artist]\n            if artists != new_artists and artist != new_artist:\n                log.debug(\"SmartTitleCase: %s: %s replaced with %s\", artist_string, artist, new_artist)\n                log.debug(\"SmartTitleCase: %s: %r replaced with %r\", artists_list, artists, new_artists)\n                metadata[artist_string] = new_artist\n                metadata[artists_list] = new_artists\n            elif artists != new_artists or artist != new_artist:\n                if artists != new_artists:\n                    log.warning(\"SmartTitleCase: %s changed, %s wasn't\", artists_list, artist_string)\n                    log.warning(\"SmartTitleCase: %s: %r changed to %r\", artists_list, artists, new_artists)\n                    log.warning(\"SmartTitleCase: %s: %r unchanged\", artist_string, artist)\n                else:\n                    log.warning(\"SmartTitleCase: %s changed, %s wasn't\", artist_string, artists_list)\n                    log.warning(\"SmartTitleCase: %s: %r changed to %r\", artist_string, artist, new_artist)\n                    log.warning(\"SmartTitleCase: %s: %r unchanged\", artists_list, artists)\n\nregister_track_metadata_processor(title_case)\nregister_album_metadata_processor(title_case)\n"
  },
  {
    "path": "plugins/sort_multivalue_tags/sort_multivalue_tags.py",
    "content": "# -*- coding: utf-8 -*-\n\n# This is the Sort Multivalue Tags plugin for MusicBrainz Picard.\n# Copyright (C) 2013 Sophist\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n\nPLUGIN_NAME = \"Sort Multi-Value Tags\"\nPLUGIN_AUTHOR = \"Sophist\"\nPLUGIN_DESCRIPTION = '''\nThis plugin sorts multi-value tags e.g. Performers alphabetically.<br /><br />\nNote: Some multi-value tags are excluded for the following reasons:\n<ol>\n<li>Sequence is important e.g. Artists</li>\n<li>The sequence of one tag is linked to the sequence of another e.g. Label and Catalogue number.</li>\n</ol>\n'''\nPLUGIN_VERSION = \"1.1\"\nPLUGIN_API_VERSIONS = [\"0.15\", \"2.0\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom picard.metadata import register_track_metadata_processor\nfrom picard.plugin import PluginPriority\nfrom picard import log\n\n# Define tags where sort order is important\n_sort_multivalue_tags_exclude = (\n    'artists', '~artists_sort', 'musicbrainz_artistid',\n    '~albumartists', '~albumartists_sort', 'musicbrainz_albumartistid',\n    'work', 'musicbrainz_workid',\n    'label', 'catalognumber',\n    'country', 'date',\n    'releasetype',\n    'mood',\n)\n# Possible future enhancement:\n# Sort linked tags e.g. work so that the sequence in related tags e.g. workid retains the relationship between\n# e.g. work and workid.\n\n\ndef sort_multivalue_tags(tagger, metadata, track, release):\n    for tag in list(metadata.keys()):\n        if tag in _sort_multivalue_tags_exclude:\n            continue\n        data = metadata.getall(tag)\n        if len(data) > 1:\n            sorted_data = sorted(data)\n            if data != sorted_data:\n                metadata.set(tag, sorted_data)\n                log.debug(\"%s: Tag sorted: %s = %s\", PLUGIN_NAME, tag, sorted_data)\n\n                \nregister_track_metadata_processor(sort_multivalue_tags, priority=PluginPriority.LOW)\n"
  },
  {
    "path": "plugins/soundtrack/soundtrack.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Copyright © 2015 Samir Benmendil <me@rmz.io>\n# This work is free. You can redistribute it and/or modify it under the\n# terms of the Do What The Fuck You Want To Public License, Version 2,\n# as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.\n\nPLUGIN_NAME = 'Soundtrack'\nPLUGIN_AUTHOR = 'Samir Benmendil'\nPLUGIN_LICENSE = 'WTFPL'\nPLUGIN_LICENSE_URL = 'http://www.wtfpl.net/'\nPLUGIN_DESCRIPTION = '''Sets the albumartist to \"Soundtrack\" if releasetype is a soundtrack.'''\nPLUGIN_VERSION = \"0.2\"\nPLUGIN_API_VERSIONS = [\"1.0\", \"2.0\"]\n\nfrom picard.metadata import register_album_metadata_processor\n\n\ndef soundtrack(tagger, metadata, release):\n    if \"soundtrack\" in metadata[\"releasetype\"]:\n        metadata[\"albumartist\"] = \"Soundtrack\"\n        metadata[\"albumartistsort\"] = \"Soundtrack\"\n\nregister_album_metadata_processor(soundtrack)\n"
  },
  {
    "path": "plugins/standardise_feat/standardise_feat.py",
    "content": "PLUGIN_NAME = 'Standardise Feat.'\r\nPLUGIN_AUTHOR = 'Sambhav Kothari'\r\nPLUGIN_DESCRIPTION = 'Standardises \"featuring\" join phrases for artists to \"feat.\"'\r\nPLUGIN_VERSION = \"0.3\"\r\nPLUGIN_API_VERSIONS = [\"1.4\", \"2.0\", \"2.1\", \"2.2\", \"2.3\"]\r\nPLUGIN_LICENSE = \"GPL-3.0\"\r\nPLUGIN_LICENSE_URL = \"http://www.gnu.org/licenses/gpl-3.0.txt\"\r\n\r\nimport re\r\nfrom picard import log\r\nfrom picard.metadata import register_track_metadata_processor, register_album_metadata_processor\r\nfrom picard.plugin import PluginPriority\r\n\r\n_feat_re = re.compile(r\" f(ea)?t(\\.|uring)? \", re.IGNORECASE)\r\n\r\n\r\ndef standardise_feat(artists_str, artists_list):\r\n    \"\"\"\r\n    >>> standardise_feat(\"The A\", [\"The A\"])\r\n    'The A'\r\n    >>> standardise_feat(\"The A ft. B\", [\"The A\", \"B\"])\r\n    'The A feat. B'\r\n    >>> standardise_feat(\"The A & B featuring C\", [\"The A\", \"B\", \"C\"])\r\n    'The A & B feat. C'\r\n    >>> standardise_feat(\"The A featuring B (and C)\", [\"The A\", \"B\", \"C\"])\r\n    'The A feat. B (and C)'\r\n    \"\"\"\r\n    match_exp = r\"(\\s*.*\\s*)\".join((map(re.escape, artists_list))) + r\"(\\s*.*$)\"\r\n    try:\r\n        join_phrases = re.match(match_exp, artists_str).groups()\r\n    except AttributeError:\r\n        log.debug(\"Unable to standardise artists: %r\", artists_str)\r\n        return artists_str\r\n    else:\r\n        standardised_join_phrases = [_feat_re.sub(\" feat. \", phrase)\r\n                                     for phrase in join_phrases]\r\n        # Add a blank string at the end to allow zipping of\r\n        # join phrases and artists_list since there is one less join phrase\r\n        standardised_join_phrases.append(\"\")\r\n        return \"\".join([artist + join_phrase for artist, join_phrase in\r\n                        zip(artists_list, standardised_join_phrases)])\r\n\r\n\r\ndef standardise_track_artist(tagger, metadata, track, release):\r\n    metadata[\"artist\"] = standardise_feat(metadata[\"artist\"], metadata.getall(\"artists\"))\r\n    metadata[\"artistsort\"] = standardise_feat(metadata[\"artistsort\"], metadata.getall(\"~artists_sort\"))\r\n\r\n\r\ndef standardise_album_artist(tagger, metadata, release):\r\n    metadata[\"albumartist\"] = standardise_feat(metadata[\"albumartist\"], metadata.getall(\"~albumartists\"))\r\n    metadata[\"albumartistsort\"] = standardise_feat(metadata[\"albumartistsort\"], metadata.getall(\"~albumartists_sort\"))\r\n\r\n\r\nregister_track_metadata_processor(standardise_track_artist, priority=PluginPriority.HIGH)\r\nregister_album_metadata_processor(standardise_album_artist, priority=PluginPriority.HIGH)\r\n"
  },
  {
    "path": "plugins/standardise_performers/standardise_performers.py",
    "content": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = 'Standardise Performers'\nPLUGIN_AUTHOR = 'Sophist'\nPLUGIN_DESCRIPTION = '''Splits multi-instrument performer tags into single\ninstruments and combines names so e.g. (from 10cc by 10cc track 1):\n<pre>\nPerformer [acoustic guitar, bass, dobro, electric guitar and tambourine]: Graham Gouldman\nPerformer [acoustic guitar, electric guitar, grand piano and synthesizer]: Lol Creme\nPerformer [electric guitar, moog and slide guitar]: Eric Stewart\n</pre>\nbecomes:\n<pre>\nPerformer [acoustic guitar]: Graham Gouldman; Lol Creme\nPerformer [bass]: Graham Gouldman\nPerformer [dobro]: Graham Gouldman\nPerformer [electric guitar]: Eric Stewart; Graham Gouldman; Lol Creme\nPerformer [grand piano]: Lol Creme\nPerformer [moog]: Eric Stewart\nPerformer [slide guitar]: Eric Stewart\nPerformer [synthesizer]: Lol Creme\nPerformer [tambourine]: Graham Gouldman\n</pre>\nUpdate: This version now sorts the performer tags in order to maintain a consistent value and avoid tags appearing to change even though the base data is equivalent.\n'''\nPLUGIN_VERSION = '1.0'\nPLUGIN_API_VERSIONS = [\"2.0\"]\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nimport re\nfrom picard import log\nfrom picard.metadata import register_track_metadata_processor\n\nstandardise_performers_split = re.compile(r\", | and \").split\n\n\ndef standardise_performers(album, metadata, *args):\n    for key, values in list(metadata.rawitems()):\n        if not key.startswith('performer:') \\\n                and not key.startswith('~performersort:'):\n            continue\n        mainkey, subkey = key.split(':', 1)\n        if not subkey:\n            continue\n        instruments = standardise_performers_split(subkey)\n        if len(instruments) == 1:\n            continue\n        log.debug(\"%s: Splitting Performer [%s] into separate performers\",\n                  PLUGIN_NAME,\n                  subkey,\n                  )\n        prefixes = []\n        words = instruments[0].split()\n        for word in words[:]:\n            if not word in ['guest', 'solo', 'additional', 'minor']:\n                break\n            prefixes.append(word)\n            words.remove(word)\n        instruments[0] = \" \".join(words)\n        prefix = \" \".join(prefixes) + \" \" if prefixes else \"\"\n        for instrument in instruments:\n            newkey = '%s:%s%s' % (mainkey, prefix, instrument)\n            for value in values:\n                metadata.add_unique(newkey, value)\n        del metadata[key]\n\n    # Sort performer metdata to avoid changes in processing sequence creating false changes in metadata\n    for key, values in list(metadata.rawitems()):\n        if key.startswith('performer:') or key.startswith('~performersort:'):\n            metadata[key] = sorted(values)\n\n\nfrom picard.plugin import PluginPriority\nregister_track_metadata_processor(standardise_performers,\n                                  priority=PluginPriority.HIGH)\n"
  },
  {
    "path": "plugins/submit_folksonomy_tags/README.md",
    "content": "# Submit Folksonomy Tags - Picard Plugin\n\nA plugin that lets the user submit tags from their tracks' tags - defaults to `genre` and `mood` - to their respective MusicBrainz pages' folksonomy tags via MusicBrainz Picard. Useful for music geeks who are meticulous with their genre tagging.\n\n**This plugin requires that you log into MusicBrainz via Picard.** The option to do so is in _Options > Options > General_.\n\nTo use, right click on a track or release, then go to _Plugins > Submit **x** tags to MusicBrainz_ - there are multiple options depending on if you want to submit tags to the recording, release, release group or release artist associated with the track/s or album/s you've right-clicked. The tags will be applied from the track tags you have configured.\n\nUses code from rswift's \"Submit ISRC\" plugin (specifically, the handling of the network response)\n\n## Features\nIt does what it says on the tin: submits any tags you have in the genre tags of whichever files you drop into Picard to the respective pages. Right now the following entities are supported:\n\n    - recordings\n    - releases\n    - release groups\n    - artists (by release)\n\nThe plugin can also replace certain tags if your tags don't match up with MusicBrainz's standard tags, notably with their allowed genre list (e.g. if you use \"synthpop\" and not \"synth-pop\", or you use the full name \"electronic dance music\" and not the abbreviated \"edm\").\n\n## Limitations\nRight now, this plugin only submits tags. No tags are _retrieved_ for comparison yet, meaning I've opted to implement two modes based on how the MusicBrainz API works: maintain the tags that are already saved or overwrite _all_ of your tags. For anyone using the MusicBrainz API, choosing to keep your tags is basically sending the \"upvote\" attribute with every user tag, and choosing to overwrite doesn't do that, which MusicBrainz will respond by clearing old tags. See the [tags section of the MusicBrainz API for more details.](https://musicbrainz.org/doc/MusicBrainz_API#tags)\n\nSubmitting for releases, release artists and release groups will also trigger an alert if your tags are not consistently the same across all tracks in an album. This is to prevent spamming of tags, purposeful or accidental, and is based on the standard already set for years by digital music sites and CD ripper utilities where an album would have the same genres tagged across all tracks.\n"
  },
  {
    "path": "plugins/submit_folksonomy_tags/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2023 Flaky\n# Copyright (C) 2023 Bob Swift (rdswift)\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = \"Submit Folksonomy Tags\"\nPLUGIN_AUTHOR = \"Flaky\"\nPLUGIN_DESCRIPTION = \"\"\"\nA plugin allowing submission of specific tags on tracks you own (defaults to <i>genre</i> and <i>mood</i>) as folksonomy tags on MusicBrainz. Supports submitting to recording, release, release group and release artist entities.\n\nA MusicBrainz login is required to use this plugin. Log in first by going to the General options. Then, to use, right click on a track or release then go to <i>Plugins</i> and depending on what you want to submit, choose the option you want.\n\nUses code from rdswift's \"Submit ISRC\" plugin (specifically, the handling of the network response)\n\"\"\"\nPLUGIN_VERSION = '0.3'\nPLUGIN_API_VERSIONS = ['2.2', '2.9']\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.txt\"\n\nfrom picard import config, log, PICARD_VERSION\nfrom picard.album import Album, Track\nfrom picard.ui.itemviews import (BaseAction,\n                                 register_album_action,\n                                 register_track_action\n                                 )\nfrom picard.ui.options import OptionsPage, register_options_page\nfrom picard.version import Version\nfrom picard.webservice.api_helpers import MBAPIHelper, _wrap_xml_metadata\nfrom .ui_config import TagSubmitPluginOptionsUI\nimport re\nimport functools\nfrom xml.sax.saxutils import escape\nfrom PyQt5 import QtCore\nfrom PyQt5.QtWidgets import QMessageBox\n\nNEW_MBAPIHelper = (PICARD_VERSION >= Version(2, 9, 0, 'beta', 2))\n\n# List of Qt network error codes.\n# From \"Submit ISRC\" plugin - credit to rdswift.\nq_error_codes = {\n    0: 'No error',\n    1: \"The remote server refused the connection (the server is not accepting requests).\",\n    2: \"The remote server closed the connection prematurely, before the entire reply was received and processed.\",\n    3: \"The remote host name was not found (invalid hostname).\",\n    4: \"The connection to the remote server timed out.\",\n    5: \"The operation was canceled via calls to abort() or close() before it was finished.\",\n    6: \"The SSL/TLS handshake failed and the encrypted channel could not be established. The sslErrors() signal should have been emitted.\",\n    7: \"The connection was broken due to disconnection from the network, however the system has initiated roaming to another access point. The request should be resubmitted and will be processed as soon as the connection is re-established.\",\n    8: \"The connection was broken due to disconnection from the network or failure to start the network.\",\n    9: \"The background request is not currently allowed due to platform policy.\",\n    10: \"While following redirects, the maximum limit was reached.\",\n    11: \"While following redirects, the network access API detected a redirect from a encrypted protocol (https) to an unencrypted one (http).\",\n    99: \"An unknown network-related error was detected.\",\n    101: \"The connection to the proxy server was refused (the proxy server is not accepting requests).\",\n    102: \"The proxy server closed the connection prematurely, before the entire reply was received and processed.\",\n    103: \"The proxy host name was not found (invalid proxy hostname).\",\n    104: \"The connection to the proxy timed out or the proxy did not reply in time to the request sent.\",\n    105: \"The proxy requires authentication in order to honour the request but did not accept any credentials offered (if any).\",\n    199: \"An unknown proxy-related error was detected.\",\n    201: \"The access to the remote content was denied (similar to HTTP error 403).\",\n    202: \"The operation requested on the remote content is not permitted.\",\n    203: \"The remote content was not found at the server (similar to HTTP error 404).\",\n    204: \"The remote server requires authentication to serve the content but the credentials provided were not accepted (if any).\",\n    205: \"The request needed to be sent again, but this failed for example because the upload data could not be read a second time.\",\n    206: \"The request could not be completed due to a conflict with the current state of the resource.\",\n    207: \"The requested resource is no longer available at the server.\",\n    299: \"An unknown error related to the remote content was detected.\",\n    301: \"The Network Access API cannot honor the request because the protocol is not known.\",\n    302: \"The requested operation is invalid for this protocol.\",\n    399: \"A breakdown in protocol was detected (parsing error, invalid or unexpected responses, etc.).\",\n    401: \"The server encountered an unexpected condition which prevented it from fulfilling the request.\",\n    402: \"The server does not support the functionality required to fulfill the request.\",\n    403: \"The server is unable to handle the request at this time.\",\n    499: \"An unknown error related to the server response was detected.\",\n}\n\n# Some internal settings.\n# Don't change these unless you know what you're doing.\n# You can change the tags you want to submit in the settings.\nclient_params = {\n    \"client\": f\"picard_plugin_{PLUGIN_NAME.replace(' ', '_')}-v{PLUGIN_VERSION}\"\n}\ndefault_tags_to_submit = ['genre', 'mood']\n\n# The options as saved in Picard.ini\nconfig.BoolOption(\"setting\", 'tag_submit_plugin_destructive', False)\nconfig.BoolOption(\"setting\", 'tag_submit_plugin_destructive_alert_acknowledged', False)\nconfig.BoolOption(\"setting\", 'tag_submit_plugin_aliases_enabled', False)\nconfig.ListOption(\"setting\", 'tag_submit_plugin_alias_list', [])\nconfig.ListOption(\"setting\", 'tag_submit_plugin_tags_to_submit', default_tags_to_submit)\n\ndef tag_submit_handler(document, reply, error, tagger):\n    \"\"\"\n    The function handling the network response from MusicBrainz\n    or QtNetwork, showing a message box if an error had occurred.\n\n    Uses the network response handler code from rdswift's \"Submit ISRC\"\n    plugin.\n    \"\"\"\n    if error:\n        # Error handling from rdswift's Submit ISRC plugin\n        xml_text = str(document, 'UTF-8') if isinstance(document, (bytes, bytearray, QtCore.QByteArray)) else str(document)\n\n        # Build error text message from returned xml payload\n        err_text = ''\n        matches = re.findall(r'<text>(.*?)</text>', xml_text)\n        if matches:\n            err_text = '\\n'.join(matches)\n        else:\n            err_text = ''\n\n        if not err_text:\n            err_text = q_error_codes[error] if error in q_error_codes else 'There was no error message provided.'\n\n        error = QMessageBox()\n        error.setStandardButtons(QMessageBox.Ok)\n        error.setDefaultButton(QMessageBox.Ok)\n        error.setIcon(QMessageBox.Critical)\n        error.setText(f\"<p>An error has occurred submitting the tags to MusicBrainz.</p><p>{err_text}</p>\")\n        error.exec_()\n    else:\n        tagger.window.set_statusbar_message(\n            \"Successfully submitted tags to MusicBrainz.\"\n            )\n\ndef process_tag_aliases(tag_input):\n    \"\"\"\n    Retrieves a string as input, and searches the tag alias tuple list\n    for a match.\n    \"\"\"\n    matched_tag_index = next(\n        (tag for tag,\n            tag_tuple in enumerate(config.setting['tag_submit_plugin_alias_list'])\n            if tag_tuple[0] == tag_input.lower()),\n        None\n    )\n    if matched_tag_index is not None:\n        resolved_tag = config.setting['tag_submit_plugin_alias_list'][matched_tag_index][1]\n        return resolved_tag\n    else:\n        return tag_input\n\ndef process_objs_to_track_list(objs):\n    \"\"\"\n    Creates a track list out of Album/Track objects\n    \"\"\"\n    track_list = []\n    for item in objs:\n        if isinstance(item, Track):\n            track_list.append(item)\n        elif isinstance(item, Album):\n            if len(item.tracks) > 0:\n                for track in item.tracks:\n                    track_list.append(track)\n    return track_list\n\n# TODO handle artist\ndef handle_submit_process(tagger, track_list, target_tag):\n    \"\"\"\n    Does some pre-processing before submitting tags. Handles tag deduplication\n    and halting when inconsistent tagging is detected (i.e. the user is trying\n    to submit tags to a release with the submitted track tags not being\n    consistent.)\n    \"\"\"\n\n    dict_key = \"\"\n    tags_to_search = config.setting['tag_submit_plugin_tags_to_submit']\n\n    # Variable to enable when inconsistent tagging is detected, which can be problematic for anything other than recordings.\n    alert_inconsistent = True\n    inconsistent_detected = False\n    # Variable to enable alert if multiple MBIDs are associated, must be toggled.\n    alert_multiple_mbids = False\n\n    # TODO when Windows Picard updates with Python 3.10, use case/switch.\n    if target_tag == \"musicbrainz_recordingid\":\n        dict_key = \"recording\"\n        alert_inconsistent = False\n    elif target_tag == \"musicbrainz_albumid\":\n        dict_key = \"release\"\n    elif target_tag == \"musicbrainz_releasegroupid\":\n        dict_key = \"release-group\"\n    elif target_tag == \"musicbrainz_albumartistid\" or target_tag == \"musicbrainz_artistid\":\n        dict_key = \"artist\"\n\n    data = {dict_key: {}}\n\n    last_tags = {\"mbid\": \"\"}\n    banned_mbids = {\n        # Any artist entities that can be applied to multiple artists go here.\n        # SPAs generally fit the bill here.\n        \"f731ccc4-e22a-43af-a747-64213329e088\", # artist: [anonymous]\n        \"33cf029c-63b0-41a0-9855-be2a3665fb3b\", # artist: [data]\n        \"314e1c25-dde7-4e4d-b2f4-0a7b9f7c56dc\", # artist: [dialogue]\n        \"eec63d3c-3b81-4ad4-b1e4-7c147d4d2b61\", # artist: [no artist]\n        \"9be7f096-97ec-4615-8957-8d40b5dcbc41\", # artist: [traditional]\n        \"125ec42a-7229-4250-afc5-e057484327fe\", # artist: [unknown]\n        \"89ad4ac3-39f7-470e-963a-56509c546377\", # artist: Various Artists\n        \"66ea0139-149f-4a0c-8fbf-5ea9ec4a6e49\", # artist: [Disney]\n        \"a0ef7e1d-44ff-4039-9435-7d5fefdeecc9\", # artist: [theatre]\n        \"80a8851f-444c-4539-892b-ad2a49292aa9\", # artist: [language instruction]\n        \"\",                                     # blank\n    }\n\n    for track in track_list:\n        if track.files:\n            for file in track.files:\n                mbid_list = file.metadata.getall(target_tag)\n                if len(mbid_list) > 1:\n                    alert_multiple_mbids = True\n                for mbid in mbid_list:\n                    if mbid not in banned_mbids:\n                        processed_tags = []\n                        for tag in tags_to_search:\n                            if file.metadata[tag]:\n                                if tag not in last_tags:\n                                    pass\n                                else:\n                                    # Flip the switch when current tag didn't match last tag on current mbid, on an entity that needs this checked.\n                                    if (last_tags[tag] != file.metadata[tag]) and (last_tags[\"mbid\"] == file.metadata[target_tag]) and alert_inconsistent:\n                                        inconsistent_detected = True\n                                # in any case, process the tags in case the user intends to go with it.\n                                processed_tags.extend([tag.strip().lower() for tag\n                                                    in re.split(\";|/|,\", file.metadata[tag])])\n                                last_tags[tag] = file.metadata[tag]\n                                last_tags[\"mbid\"] = file.metadata[target_tag]\n                        # If a track has multiple files associated to it, there may be duplicate tags.\n                        processed_tags = list(set(processed_tags))\n                        if processed_tags and mbid in data[dict_key]:\n                            data[dict_key][mbid].extend(processed_tags)\n                        else:\n                            data[dict_key][mbid] = processed_tags\n                    else:\n                        log.info(f\"Not submitting MBID {track.metadata[target_tag]} as it was found on 'do not submit' MBID set.\")\n\n    # Send an alert when, at the end of it all, inconsistent tagging was detected.\n    if inconsistent_detected or alert_multiple_mbids:\n        warning = QMessageBox()\n        warning.setStandardButtons(QMessageBox.Ok|QMessageBox.Cancel)\n        warning.setDefaultButton(QMessageBox.Cancel)\n        warning.setIcon(QMessageBox.Warning)\n        if inconsistent_detected and alert_multiple_mbids:\n            warning.setText(\"\"\"\n            <p><b>WARNING: INCONSISTENT TAGGING AND SUBMISSION TO MULTIPLE MBIDS DETECTED.</b></p>\n            <p>You are trying to apply different tags to multiple MusicBrainz entities.</p>\n            <p>This isn't a use case this plugin supports whatsoever due to the potential for\n            wrong tags to be unintentionally assigned, but detects and warns just in case.</p>\n            <p>If this was intentional, click OK. Otherwise, click Cancel.</p>\n            \"\"\")\n        elif inconsistent_detected:\n            warning.setText(\"\"\"\n            <p><b>WARNING: INCONSISTENT TAGGING DETECTED.</b></p>\n            <p>You are trying to apply multiple tags to one entity, which benefits more from\n            having the same tags across all tracks when submitting tags via this plugin.</p>\n            <p>If you intended to have tracks in a release to have different submitted tags,\n            it's better to cancel this attempt and choose the recording option.\n            If you didn't, you should apply the same tag across all tracks.</p>\n            <p>If this was intentional, click OK. Otherwise, click Cancel.</p>\n            \"\"\")\n        elif alert_multiple_mbids:\n            warning.setText(\"\"\"\n            <p><b>MULTIPLE MBIDS DETECTED.</b></p>\n            <p>You are trying to apply a tag to multiple MusicBrainz entities.</p>\n            <p>This isn't a use case this plugin supports whatsoever due to the potential for\n            wrong tags to be unintentionally assigned, but detects and warns just in case.</p>\n            <p>If this was intentional, click OK. Otherwise, click Cancel.</p>\n            \"\"\")\n        result = warning.exec_()\n        if result == QMessageBox.Ok:\n            upload_tags_to_mbz(data, tagger)\n        else:\n            tagger.window.set_statusbar_message(\n                \"Tag submission halted by user request.\"\n            )\n    else:\n        upload_tags_to_mbz(data, tagger)\n\ndef upload_tags_to_mbz(data, tagger):\n    \"\"\"\n    Generates the XML from the data retrieved, and then uploads it to MusicBrainz.\n    \"\"\"\n    helper = MBAPIHelper(tagger.webservice)\n\n    empty_data = {\n        \"<recording-list></recording-list>\",\n        \"<release-list></release-list>\",\n        \"<release-group-list></release-group-list>\",\n        \"<artist-list></artist-list>\"\n    }\n\n    xml_data = []\n    upvote_tag_fill = ' vote=\"upvote\"' if not config.setting['tag_submit_plugin_destructive'] else ''\n    for key in data:\n        # start the list\n        xml_data.append(f\"<{key}-list>\")\n        for mbid in data[key]:\n            # deduplicate the list of genres so no redundant tags are sent.\n            data[key][mbid] = list(set(data[key][mbid]))\n            # start the user tag list\n            xml_data.extend([f'<{key} id=\"{mbid}\">', \"<user-tag-list>\"])\n            # add the tags\n            for tag in data[key][mbid]:\n                xml_data.append(f'<user-tag{upvote_tag_fill}><name>{escape(process_tag_aliases(tag.lower()))}</name></user-tag>')\n            # close the user tag list\n            xml_data.extend([\"</user-tag-list>\", f\"</{key}>\"])\n        # close the list\n        xml_data.append(f\"</{key}-list>\")\n    # make the string that would become our submitted XML.\n    final_xml = ''.join(xml_data)\n    log.info(final_xml)\n\n    if final_xml not in empty_data:\n    # post it to MusicBrainz\n        tagger.window.set_statusbar_message(\n                \"Submitting tags to MusicBrainz...\"\n                )\n        submitted_xml = _wrap_xml_metadata(''.join(xml_data))\n        path = '/tag' if NEW_MBAPIHelper else ['tag']\n        helper.post(\n            path,\n            submitted_xml,\n            functools.partial(tag_submit_handler, tagger=tagger),\n            priority=True,\n            queryargs=client_params,\n            parse_response_type=\"xml\",\n            request_mimetype=\"application/xml; charset=utf-8\"\n        )\n    else:\n        tagger.window.set_statusbar_message(\n            \"Not submitting to MusicBrainz due to empty data.\"\n            )\n\n\nclass TagSubmitPlugin_OptionsPage(OptionsPage):\n    NAME = \"tag_submit_plugin\"\n    TITLE = \"Tag Submission Plugin\"\n    PARENT = \"plugins\"\n    HELP_URL = \"\"\n\n    def __init__(self, parent=None):\n        super().__init__()\n        self.ui = TagSubmitPluginOptionsUI(self)\n        self.destructive_acknowledgement = config.setting['tag_submit_plugin_destructive_alert_acknowledged']\n        self.ui.overwrite_radio_button.clicked.connect(self.on_destructive_selected)\n\n    def on_destructive_selected(self):\n        if not config.setting['tag_submit_plugin_destructive_alert_acknowledged']:\n            warning = QMessageBox()\n            warning.setStandardButtons(QMessageBox.Ok)\n            warning.setDefaultButton(QMessageBox.Ok)\n            warning.setIcon(QMessageBox.Warning)\n            warning.setText(\"<p><b>WARNING: BY SELECTING TO OVERWRITE ALL TAGS, THIS MEANS <i>ALL</i> TAGS.</b></p><p>By enabling this option, you acknowledge that you may lose the tags already saved online from the tracks you process via this plugin. This alert will only be displayed once before you save.</p><p>If you do not want this behaviour, select the maintain option.</p>\")\n            warning.exec_()\n            config.setting['tag_submit_plugin_destructive_alert_acknowledged'] = True\n\n    def load(self):\n        # Destructive option\n        if config.setting['tag_submit_plugin_destructive']:\n            self.ui.overwrite_radio_button.setChecked(True)\n        else:\n            self.ui.keep_radio_button.setChecked(True)\n\n        self.ui.tags_to_save_textbox.setText(\n            '; '.join(config.setting['tag_submit_plugin_tags_to_submit'])\n            )\n\n        # Aliases enabled option\n        self.ui.tag_alias_groupbox.setChecked(\n            config.setting['tag_submit_plugin_aliases_enabled']\n        )\n\n        # Alias list\n        if 'tag_submit_plugin_alias_list' in config.setting:\n            log.info(\"Alias list exists! Let's populate the table.\")\n            for alias_tuple in config.setting['tag_submit_plugin_alias_list']:\n                self.ui.add_row(alias_tuple[0], alias_tuple[1])\n            self.ui.tag_alias_table.resizeColumnsToContents()\n\n        config.setting['tag_submit_plugin_destructive_alert_acknowledged'] = self.destructive_acknowledgement\n\n    def save(self):\n        config.setting['tag_submit_plugin_destructive'] = self.ui.overwrite_radio_button.isChecked()\n        config.setting['tag_submit_plugin_aliases_enabled'] = self.ui.tag_alias_groupbox.isChecked()\n\n        tag_textbox_text = self.ui.tags_to_save_textbox.text()\n        if tag_textbox_text:\n            config.setting['tag_submit_plugin_tags_to_submit'] = [\n                tag.strip() for tag in tag_textbox_text.split(';')\n                ]\n        else:\n            config.setting['tag_submit_plugin_tags_to_submit'] = default_tags_to_submit\n\n        if config.setting['tag_submit_plugin_aliases_enabled']:\n            new_alias_list = self.ui.rows_to_tuple_list()\n            log.info(new_alias_list)\n            config.setting['tag_submit_plugin_alias_list'] = new_alias_list\n\nclass SubmitTrackTagsMenuAction(BaseAction):\n    NAME = 'Submit tags to MusicBrainz (recording)'\n\n    def callback(self, objs):\n        handle_submit_process(\n            objs[0].tagger,\n            process_objs_to_track_list(objs),\n            \"musicbrainz_recordingid\"\n            )\n\nclass SubmitReleaseTagsMenuAction(BaseAction):\n    NAME = 'Submit tags to MusicBrainz (release)'\n\n    def callback(self, objs):\n        handle_submit_process(\n            objs[0].tagger,\n            process_objs_to_track_list(objs),\n            \"musicbrainz_albumid\"\n            )\n\nclass SubmitRGTagsMenuAction(BaseAction):\n    NAME = 'Submit tags to MusicBrainz (release group)'\n\n    def callback(self, objs):\n        handle_submit_process(\n            objs[0].tagger,\n            process_objs_to_track_list(objs),\n            \"musicbrainz_releasegroupid\"\n            )\n\nclass SubmitRATagsMenuAction(BaseAction):\n    NAME = 'Submit tags to MusicBrainz (release artist)'\n\n    def callback(self, objs):\n        handle_submit_process(\n            objs[0].tagger,\n            process_objs_to_track_list(objs),\n            \"musicbrainz_albumartistid\"\n            )\n\nregister_options_page(TagSubmitPlugin_OptionsPage)\nregister_album_action(SubmitTrackTagsMenuAction())\nregister_track_action(SubmitTrackTagsMenuAction())\nregister_album_action(SubmitReleaseTagsMenuAction())\nregister_track_action(SubmitReleaseTagsMenuAction())\nregister_album_action(SubmitRGTagsMenuAction())\nregister_track_action(SubmitRGTagsMenuAction())\nregister_album_action(SubmitRATagsMenuAction())\nregister_track_action(SubmitRATagsMenuAction())\n"
  },
  {
    "path": "plugins/submit_folksonomy_tags/ui_config.py",
    "content": "from PyQt5.QtCore import (\n    QSize,\n    Qt\n    )\n\nfrom PyQt5.QtWidgets import (\n    QGridLayout,\n    QGroupBox,\n    QHBoxLayout,\n    QLabel,\n    QPushButton,\n    QRadioButton,\n    QSizePolicy,\n    QTableWidget,\n    QTableWidgetItem,\n    QVBoxLayout,\n    QAbstractItemView,\n    QLineEdit\n    )\n\nclass TagSubmitPluginOptionsUI():\n\n    def __init__(self, page):\n        self.main_container = QVBoxLayout()\n\n        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n\n        # Group box: tag saving\n        self.tag_save_groupbox = QGroupBox()\n        sizePolicy.setHeightForWidth(self.tag_save_groupbox.sizePolicy().hasHeightForWidth())\n        self.tag_save_groupbox.setSizePolicy(sizePolicy)\n        self.tag_save_groupbox_layout = QGridLayout(self.tag_save_groupbox)\n        # Label: tag saving description\n        self.tag_save_description = QLabel(self.tag_save_groupbox)\n        sizePolicy.setHeightForWidth(self.tag_save_description.sizePolicy().hasHeightForWidth())\n        self.tag_save_description.setSizePolicy(sizePolicy)\n        self.tag_save_description.setMinimumSize(QSize(0, 56))\n        self.tag_save_description.setWordWrap(True)\n        self.tag_save_groupbox_layout.addWidget(self.tag_save_description, 0, 0, 1, 1)\n        self.tag_save_groupbox.setTitle(\"Tag Saving\")\n        self.tag_save_description.setText(u\"<html><head/><body><p>There are two modes to tag saving via this plugin right now: keep all existing saved tags (only adding on tags) or overwrite them. If you are not in a position where replacing your saved tags online is a good idea, it is recommended to keep this option unchanged.</p></body></html>\")\n        self.main_container.addWidget(self.tag_save_groupbox)\n        self.tag_save_groupbox_layout.addWidget(self.tag_save_description, 0, 0, 1, 1)\n\n        self.tags_to_save_groupbox = QGroupBox()\n        sizePolicy.setHeightForWidth(self.tags_to_save_groupbox.sizePolicy().hasHeightForWidth())\n        self.tags_to_save_groupbox.setSizePolicy(sizePolicy)\n        self.tags_to_save_groupbox_layout = QGridLayout(self.tags_to_save_groupbox)\n        self.tags_to_save_groupbox.setTitle(\"Tags to Submit\")\n        self.tags_to_save_description = QLabel(self.tags_to_save_groupbox)\n        sizePolicy.setHeightForWidth(self.tags_to_save_description.sizePolicy().hasHeightForWidth())\n        self.tags_to_save_textbox = QLineEdit()\n        self.tags_to_save_description.setText(\"<html><head/><body><p>List the tags that you want to submit to MusicBrainz via the plugin in the text box below. Separate each tag with a semi-colon. (e.g. genre; mood)</p></body></html>\")\n        self.tags_to_save_groupbox_layout.addWidget(self.tags_to_save_description)\n        self.tags_to_save_textbox.setPlaceholderText(\"Tags you want to submit (separated by semicolons - e.g. genre; mood)\")\n        self.tags_to_save_groupbox_layout.addWidget(self.tags_to_save_textbox)\n        self.main_container.addWidget(self.tags_to_save_groupbox)\n\n        # Radio buttons for tag saving options (on the plugin as \"destructive\")\n        self.keep_radio_button = QRadioButton(self.tag_save_groupbox)\n        self.keep_radio_button.setText(\"Keep existing online saved tags\")\n        self.tag_save_groupbox_layout.addWidget(self.keep_radio_button, 1, 0, 1, 1)\n        self.overwrite_radio_button = QRadioButton(self.tag_save_groupbox)\n        self.overwrite_radio_button.setText(\"Overwrite all online saved tags\")\n        self.tag_save_groupbox_layout.addWidget(self.overwrite_radio_button, 2, 0, 1, 1)\n\n        # Group box: tag aliases\n        self.tag_alias_groupbox = QGroupBox()\n        sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)\n        sizePolicy1.setHorizontalStretch(0)\n        sizePolicy1.setVerticalStretch(0)\n        sizePolicy1.setHeightForWidth(self.tag_alias_groupbox.sizePolicy().hasHeightForWidth())\n        self.tag_alias_groupbox.setSizePolicy(sizePolicy1)\n        self.tag_alias_groupbox.setCheckable(True)\n        self.tag_alias_groupbox.setChecked(False)\n        self.tag_alias_groupbox_layout = QVBoxLayout(self.tag_alias_groupbox)\n        self.tag_alias_groupbox.setTitle(\"Tag Aliases\")\n        self.tag_alias_description = QLabel()\n        self.tag_alias_description.setText(\"<html><head/><body><p>There may be cases where you prefer one tag on your files to be saved as another on MusicBrainz (e.g. if your genre tags don't align with MusicBrainz's standard genre tags). In such cases, the plugin can substitute your tags with whichever tags you want when submitting.</p><p>Anything listed here is case-insensitive, as MusicBrainz will process them in lowercase anyway.</p></body></html>\")\n        self.tag_alias_description.setMinimumSize(QSize(0, 42))\n        self.tag_alias_description.setWordWrap(True)\n        self.tag_alias_groupbox_layout.addWidget(self.tag_alias_description)\n\n        # Tag alias table\n        self.tag_alias_table = QTableWidget()\n        self.tag_alias_table.setColumnCount(2)\n        __find_column = QTableWidgetItem()\n        __find_column.setText(\"Find...\")\n        self.tag_alias_table.setHorizontalHeaderItem(0, __find_column)\n        __replace_column = QTableWidgetItem()\n        __replace_column.setText(\"Replace...\")\n        self.tag_alias_table.setHorizontalHeaderItem(1, __replace_column)\n        self.tag_alias_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)\n        self.tag_alias_groupbox_layout.addWidget(self.tag_alias_table)\n\n        # Tag alias buttons\n        self.table_button_layout = QHBoxLayout()\n        self.add_row_button = QPushButton()\n        self.delete_row_button = QPushButton()\n        self.add_row_button.setText(\"Add tag alias\")\n        self.add_row_button.clicked.connect(self.add_row)\n        self.delete_row_button.setText(\"Delete selected tag aliases\")\n        self.delete_row_button.clicked.connect(self.delete_rows)\n        self.table_button_layout.addWidget(self.add_row_button)\n        self.table_button_layout.addWidget(self.delete_row_button)\n        self.tag_alias_groupbox_layout.addLayout(self.table_button_layout)\n        self.main_container.addWidget(self.tag_alias_groupbox)\n\n        page.setLayout(self.main_container)\n\n    def add_row(self, find_entry=\"\", replace_entry=\"\"):\n        \"\"\"\n        Adds a row to the table. Accepts input.\n        \"\"\"\n        row_pos = self.tag_alias_table.rowCount()\n        self.tag_alias_table.insertRow(row_pos)\n\n        find_tableitem = QTableWidgetItem(find_entry)\n        replace_tableitem = QTableWidgetItem(replace_entry)\n\n        self.tag_alias_table.setItem(row_pos, 0, find_tableitem)\n        self.tag_alias_table.setItem(row_pos, 1, replace_tableitem)\n\n    def delete_rows(self):\n        \"\"\"\n        Self-explanatory - removes the selected rows.\n        \"\"\"\n        for row in self.tag_alias_table.selectionModel().selectedRows():\n            self.tag_alias_table.removeRow(row.row())\n\n    def rows_to_tuple_list(self):\n        \"\"\"\n        Converts filled in rows to a list of tuples for the alias list setting.\n        \"\"\"\n\n        tuple_list = []\n        row_count = self.tag_alias_table.rowCount()\n        for row in range(row_count):\n            find_tableitem = self.tag_alias_table.item(row, 0).text()\n            replace_tableitem = self.tag_alias_table.item(row, 1).text()\n            if find_tableitem and replace_tableitem:\n                tuple_list.append((find_tableitem, replace_tableitem))\n        return tuple_list\n"
  },
  {
    "path": "plugins/submit_isrc/README.md",
    "content": "# Submit ISRC\n\n## Overview\n\nThis plugin adds a right click option on an album to submit the ISRCs to the MusicBrainz server specified in the Option settings.\n\nTo use this function, you must first match your files to the appropriate tracks for a release.  Once this is done, but before you save your files if you have Picard set to overwrite the `isrc` tag in your files, right-click the release and select \"Submit ISRCs\" in the \"Plugins\" section.  For each file that has a single valid ISRC in its metadata, the ISRC will be added to the recording on the release if it does not already exist.  Once all tracks for the release have been processed, the missing ISRCs will be submitted to MusicBrainz.\n\nIf a file's metadata contains multiple ISRCs, such as if the file has already been tagged, then no ISRCs will be submitted for that file.\n\nIf one of the files contains an invalid ISRC, or if the same ISRC appears in the metadata for two or more files, then a notice will be displayed and the submission process will be aborted.\n\nWhen ISRCs have been submitted, a notice will be displayed showing whether or not the submission was successful.\n\n---\n"
  },
  {
    "path": "plugins/submit_isrc/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2020-2021, 2023 Bob Swift (rdswift)\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = 'Submit ISRC'\nPLUGIN_AUTHOR = 'Bob Swift'\nPLUGIN_DESCRIPTION = '''\n<p>\nAdds a right click option on an album to submit the ISRCs to the MusicBrainz server\nspecified in the Options.\n</p><p>\nTo use this function, you must first match your files to the appropriate tracks for\na release.  Once this is done, but before you save your files if you have Picard set\nto overwrite the 'isrc' tag in your files, right-click the release and select \"Submit\nISRCs\" in the \"Plugins\" section.  For each file that has a single valid ISRC in its\nmetadata, the ISRC will be added to the recording on the release if it does not\nalready exist.  Once all tracks for the release have been processed, the missing\nISRCs will be submitted to MusicBrainz.\n</p><p>\nIf a file's metadata contains multiple ISRCs, such as if the file has already been\ntagged, then no ISRCs will be submitted for that file.\n</p><p>\nIf one of the files contains an invalid ISRC, or if the same ISRC appears in the\nmetadata for two or more files, then a notice will be displayed and the submission\nprocess will be aborted.\n</p><p>\nWhen ISRCs have been submitted, a notice will be displayed showing whether or not\nthe submission was successful.\n</p>\n'''\nPLUGIN_VERSION = '1.1'\nPLUGIN_API_VERSIONS = ['2.0', '2.1', '2.2', '2.3', '2.6', '2.9']\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.txt\"\n\nimport re\n\nfrom picard import log, PICARD_VERSION\nfrom picard.ui.itemviews import BaseAction, register_album_action\nfrom picard.version import Version\nfrom picard.webservice.api_helpers import MBAPIHelper, _wrap_xml_metadata\nfrom PyQt5 import QtCore, QtWidgets\n\nRE_VALIDATE_ISRC = re.compile(r'^[A-Z]{2}[A-Z0-9]{3}[0-9]{7}$')\n\nNEW_MBAPIHelper = (PICARD_VERSION >= Version(2, 9, 0, 'beta', 2))\n\nXML_HEADER = '<recording-list>'\nXML_TEMPLATE = '<recording id=\"{0}\"><isrc-list count=\"1\"><isrc id=\"{1}\" /></isrc-list></recording>'\nXML_FOOTER = '</recording-list>'\n\nQ_ERROR_CODES = {\n    0: 'No error',\n    1: \"The remote server refused the connection (the server is not accepting requests).\",\n    2: \"The remote server closed the connection prematurely, before the entire reply was received and processed.\",\n    3: \"The remote host name was not found (invalid hostname).\",\n    4: \"The connection to the remote server timed out.\",\n    5: \"The operation was canceled via calls to abort() or close() before it was finished.\",\n    6: \"The SSL/TLS handshake failed and the encrypted channel could not be established. The sslErrors() signal should have been emitted.\",\n    7: \"The connection was broken due to disconnection from the network, however the system has initiated roaming to another access point. The request should be resubmitted and will be processed as soon as the connection is re-established.\",\n    8: \"The connection was broken due to disconnection from the network or failure to start the network.\",\n    9: \"The background request is not currently allowed due to platform policy.\",\n    10: \"While following redirects, the maximum limit was reached.\",\n    11: \"While following redirects, the network access API detected a redirect from a encrypted protocol (https) to an unencrypted one (http).\",\n    99: \"An unknown network-related error was detected.\",\n    101: \"The connection to the proxy server was refused (the proxy server is not accepting requests).\",\n    102: \"The proxy server closed the connection prematurely, before the entire reply was received and processed.\",\n    103: \"The proxy host name was not found (invalid proxy hostname).\",\n    104: \"The connection to the proxy timed out or the proxy did not reply in time to the request sent.\",\n    105: \"The proxy requires authentication in order to honour the request but did not accept any credentials offered (if any).\",\n    199: \"An unknown proxy-related error was detected.\",\n    201: \"The access to the remote content was denied (similar to HTTP error 403).\",\n    202: \"The operation requested on the remote content is not permitted.\",\n    203: \"The remote content was not found at the server (similar to HTTP error 404).\",\n    204: \"The remote server requires authentication to serve the content but the credentials provided were not accepted (if any).\",\n    205: \"The request needed to be sent again, but this failed for example because the upload data could not be read a second time.\",\n    206: \"The request could not be completed due to a conflict with the current state of the resource.\",\n    207: \"The requested resource is no longer available at the server.\",\n    299: \"An unknown error related to the remote content was detected.\",\n    301: \"The Network Access API cannot honor the request because the protocol is not known.\",\n    302: \"The requested operation is invalid for this protocol.\",\n    399: \"A breakdown in protocol was detected (parsing error, invalid or unexpected responses, etc.).\",\n    401: \"The server encountered an unexpected condition which prevented it from fulfilling the request.\",\n    402: \"The server does not support the functionality required to fulfill the request.\",\n    403: \"The server is unable to handle the request at this time.\",\n    499: \"An unknown error related to the server response was detected.\",\n}\n\n\ndef validate_isrc(isrc):\n    \"\"\"Verify that the provided ISRC matches the standard pattern for a valid ISRC.\n\n    Args:\n        isrc (str): ISRC to validate\n\n    Returns:\n        str: Properly formatted ISRC (upper case with no spaces or hyphens) if valid, otherwise None\n    \"\"\"\n    formatted_isrc = str(isrc).upper().replace(' ', '').replace('-', '')\n    if re.match(RE_VALIDATE_ISRC, formatted_isrc):\n        return formatted_isrc\n    return None\n\n\ndef show_popup(title, content, window=None):\n    \"\"\"Display a pop-up dialog.\n\n    Args:\n        title (str): Title for the pop-up dialog.\n        content (str): Test to be displayed in the pop-up dialog..\n        window (object, optional): Parent object for the dialog. Defaults to None.\n    \"\"\"\n    QtWidgets.QMessageBox.information(\n        window,\n        title,\n        content,\n        QtWidgets.QMessageBox.Ok,\n        QtWidgets.QMessageBox.Ok\n    )\n\n\nclass SubmitAlbumISRCs(BaseAction):\n    NAME = 'Submit ISRCs'\n\n    def callback(self, album):\n        if not album:\n            log.error(\"{0}: No album specified for submitting ISRCs.\".format(PLUGIN_NAME,))\n            return\n\n        log.info(\"{0}: Submitting ISRCs for: {1}\".format(PLUGIN_NAME, album[0].metadata['album'],))\n        if not album[0].tracks:\n            log.debug(\"{0}: No tracks found in album: {1}\".format(PLUGIN_NAME, album[0].metadata['album'],))\n            show_popup('Error', 'No tracks found in the album.')\n            return\n\n        isrcs = {}\n        multi_isrcs = []\n        for track in album[0].tracks:\n            if not track.files:\n                continue\n            audio_file = track.files[0]\n            metadata = track.metadata\n            file_metadata = audio_file.orig_metadata\n\n            # No ISRC found in the file\n            if 'isrc' not in file_metadata:\n                continue\n\n            # Get string of existing ISRCs on MusicBrainz\n            if 'isrc' in metadata:\n                mb_isrc = metadata['isrc'].upper()\n            else:\n                mb_isrc = ''\n\n            # Get ISRC string from the file\n            file_isrc = file_metadata['isrc']\n\n            # Multiple ISRCs found in the file (don't process)\n            if ';' in file_isrc:\n                multi_isrcs.append('  {0} - {1}'.format(metadata['tracknumber'], metadata['title']))\n                log.info(\"{0}: Multiple ISRCs found on track {1} (not processed): {2}\".format(PLUGIN_NAME, metadata['tracknumber'], file_isrc))\n                continue\n\n            isrc = validate_isrc(file_isrc)\n\n            # ISRC does not pass validation test\n            if not isrc:\n                log.debug(\"{0}: Invalid ISRC found on track {1}: {2}\".format(PLUGIN_NAME, metadata['tracknumber'], file_isrc))\n                show_popup('Error', \"Invalid ISRC found on track {0}: '{1}'\".format(metadata['tracknumber'], file_isrc))\n                return\n\n            # ISRC already found on another track for this album\n            if isrc in isrcs:\n                log.debug(\"{0}: Duplicate ISRC found on track {1}: {2}\".format(PLUGIN_NAME, metadata['tracknumber'], file_isrc))\n                show_popup('Error', \"Duplicate ISRC found on track {0}: '{1}'\".format(metadata['tracknumber'], file_isrc))\n                return\n\n            # ISRC already associated with that track (MusicBrainz recording)\n            if isrc in mb_isrc:\n                continue\n\n            # New ISRC added for submission\n            log.debug(\"{0}: Adding ISRC '{1}' for track {2} - \\\"{3}\\\"\".format(PLUGIN_NAME, isrc, metadata['tracknumber'], metadata['title'],))\n            isrcs[isrc] = metadata['musicbrainz_recordingid']\n\n        if multi_isrcs:\n            multiple_msg = '\\n\\nThe following track audio files contained multiple ISRCs (not submitted):\\n' + '\\n'.join(multi_isrcs)\n        else:\n            multiple_msg = ''\n\n        # Save count of new ISRCs to display in success message\n        self.isrc_count = len(isrcs)\n\n        # Nothing to submit\n        if not isrcs:\n            log.debug(\"{0}: No new ISRCs found in album: {1}\".format(PLUGIN_NAME, album[0].metadata['album'],))\n            show_popup('Error', 'No new ISRCs found for the tracks in the album.{0}'.format(multiple_msg,))\n            return\n\n        if multiple_msg:\n            show_popup('Submitting', 'Submitting {0} ISRC{1}.{2}'.format(self.isrc_count, '' if self.isrc_count == 1 else 's', multiple_msg,))\n\n        # Build the xml data payload\n        xml_items = [XML_HEADER]\n        for isrc, recording in isrcs.items():\n            xml_items.append(XML_TEMPLATE.format(recording, isrc))\n        xml_items.append(XML_FOOTER)\n        data = _wrap_xml_metadata(''.join(xml_items))\n\n        # Initialize the MusicBrainz API Helper\n        webservice = album[0].tagger.webservice\n        helper = MBAPIHelper(webservice)\n\n        # Set up parameters for the helper\n        client_string = 'Picard_Plugin_{0}-v{1}'.format(PLUGIN_NAME, PLUGIN_VERSION).replace(' ', '_')\n        handler = self.submission_handler\n        path = '/recording' if NEW_MBAPIHelper else ['recording']\n        params = {\"client\": client_string}\n\n        return helper.post(path, data, handler, priority=True,\n                         queryargs=params, parse_response_type=\"xml\",\n                         request_mimetype=\"application/xml; charset=utf-8\")\n\n    def submission_handler(self, document, reply, error):\n        if not error:\n            show_popup('Success', 'Successfully submitted {0} ISRC{1}.'.format(\n                self.isrc_count,\n                '' if self.isrc_count == 1 else 's',\n            ))\n            return\n\n        # Decode response if necessary.\n        xml_text = str(document, 'UTF-8') if isinstance(document, (bytes, bytearray, QtCore.QByteArray)) else str(document)\n\n        # Build error text message from returned xml payload\n        err_text = ''\n        matches = re.findall(r'<text>(.*?)</text>', xml_text)\n        if matches:\n            err_text = '\\n'.join(matches)\n        else:\n            err_text = ''\n\n        # Use standard QNetworkReply error messages if no message was provided in the xml payload\n        if not err_text:\n            err_text = Q_ERROR_CODES[error] if error in Q_ERROR_CODES else 'There was no error message provided.'\n\n        show_popup('Error', \"There was an error processing the ISRC submission.  Please try again.\\n\\nError Code: {0}\\n\\n{1}\".format(error, err_text))\n\n\nregister_album_action(SubmitAlbumISRCs())\n"
  },
  {
    "path": "plugins/tangoinfo/README.md",
    "content": "# tango.info plugin for MusicBrainz Picard\nAutomatically get *genre, date and singers* from **tango.info**, right inside of MusicBrainz Picard.\n\n## Usage\nThen open Picard, navigate to `Options -> Options -> Plugins` and enable\nthe one called `Tango.info Adapter`, then restart and use Picard\nlike you usually would.\n\nIf(and **only if**) there is a valid `barcode` tag for your files, the plugin\nwill try to look up the corresponding album on *tango.info* and fill in the\n`genre`(capitalized), `vocal`(for Singers) and `date` tags for you.\n\n## How it works\nThe plugin looks up the barcode of an album, constructs the url for the corresponding\n*tango.info* album('product'/'TINP') page, parses the HTML, looks for the\n`tracks` table and extracts `genre`, `Perf date` and `Vocalist(s)`.\n\n## What is a TINT\nSee [tango.info wiki: TINT](https://tango.info/wiki/TINT)\n`<TINP>-<Side#>-<Track#>`, example: `TINT:00743216335725-1-5`.\n\n[tango.info wiki: TINP](https://tango.info/wiki/TINP) - *Tango Info Number\nfor a Product* is a 14-digit numeric tango.info code used by tango.info and\nothers.\n\n## Future\nProvided tango.info's [Tobias Conradi](https://tango.info/tangoinfo/eng/contact)\ndoes not change the page structure in a manner that breaks this plugin, you\nmight be able to map instrumentalists and vocalists to the id3 tags of your\nliking, or download cover art. Just construct the respective regex and change a\nfew lines.\n\n## License: GPLv2\nCopyright (C) 2016-2022  **Felix Elsner**, Sambhav Kothari, Philipp Wolfer\n\nWith help from **Sophist-UK**, whose albumartist_website plugin, to be found\n[here](https://github.com/Sophist-UK/sophist-picard-plugins),\nthis work heavily draws upon\n\nThis program is free software; you can redistribute it and/or\nmodify it under the terms of the GNU General Public License\nas published by the Free Software Foundation; either version 2\nof the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program; if not, write to the Free Software\nFoundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\n"
  },
  {
    "path": "plugins/tangoinfo/__init__.py",
    "content": "# -*- coding: utf-8 -*-\nPLUGIN_NAME = \"Tango.info Adapter\"\nPLUGIN_AUTHOR = \"Felix Elsner, Sambhav Kothari, Philipp Wolfer\"\nPLUGIN_DESCRIPTION = \"\"\"\n<p>Load genre, date and vocalist tags for latin dance music\nfrom <a href=\"https://tango.info\">tango.info</a>.</p>\n\"\"\"\nPLUGIN_VERSION = \"0.2.0\"\nPLUGIN_API_VERSIONS = [\"2.6\", \"2.7\"]\n\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nimport re\nfrom functools import partial\n\nfrom picard import log\nfrom picard.util import LockableObject\nfrom picard.metadata import register_track_metadata_processor\n\n\ntable_regex = re.compile(\n       r'<h2><a href=\"\\/tracks?\">Tracks?<\\/a><\\/h2>(?!<\\/table>)(.+?)<\\/table>'\n)  # Match the 'tracks'/'track' <table>\ntr_regex = re.compile(r\"<tr>((?!</tr>).+?)</tr>\")  # Match <tr> elements\ntd_regex = re.compile(r\"<td[^>]*>((?!</td>).+?)</td>\")  # Match <td> elements\ntint_regex = re.compile(r\"TINT:([0-9]+-[0-9]{1,2}-[0-9]{1,2})\")\n\nTANGO_INFO_HOST = \"tango.info\"\nTANGO_INFO_PORT = 443\n\n\n# FIXME Remove all this\n# This is all just a hack to get around server issues at tango.info\n# The server will reply with an empty body if the Host header is set to\n# \"tango.info:443\", but just \"tango.info\" is fine.\n# Picard 2.8+ will by default not append the port to the Host header and thus\n# not need this workaround\nfrom picard import PICARD_VERSION\nfrom picard.version import Version\nhost_workaround_needed = False\nif PICARD_VERSION < Version(2, 8, 0, 'dev', 1):\n    log.warning(\"%s: Picard version is older than 2.8, using workaround for \"\n                \"tango.info Host header issue\", PLUGIN_NAME)\n    host_workaround_needed = True\nif host_workaround_needed:\n    from picard.webservice import WSGetRequest\n    from PyQt5.QtCore import QUrl\n    class WrappedWSGetRequest(WSGetRequest):  # noqa\n        def __init__(self, *args, **kwargs):\n            super().__init__(*args, **kwargs)\n            url_without_port = QUrl(self.url().url())\n            # -1 is QUrl defaultPort\n            url_without_port.setPort(-1)\n            self.setUrl(url_without_port)\n\n\nclass TangoInfoTagger:\n\n    class TangoInfoScrapeQueue(LockableObject):\n\n        def __init__(self):\n            LockableObject.__init__(self)\n            self.queue = {}\n\n        def __contains__(self, name):\n            return name in self.queue\n\n        def __iter__(self):\n            return self.queue.__iter__()\n\n        def __getitem__(self, name):\n            self.lock_for_read()\n            value = self.queue.get(name)\n            self.unlock()\n            return value\n\n        def __setitem__(self, name, value):\n            self.lock_for_write()\n            self.queue[name] = value\n            self.unlock()\n\n        def append(self, name, value):\n            self.lock_for_write()\n            if name in self.queue:\n                self.queue[name].append(value)\n                value = False\n            else:\n                self.queue[name] = [value]\n                value = True\n            self.unlock()\n            return value\n\n        def pop(self, name):\n            self.lock_for_write()\n            value = None\n            if name in self.queue:\n                value = self.queue[name]\n                del self.queue[name]\n            self.unlock()\n            return value\n\n    def __init__(self):\n        self.albumpage_cache = {}\n        self.albumpage_queue = self.TangoInfoScrapeQueue()\n\n    def add_tangoinfo_data(self, album, track_metadata, track, release):\n\n        # Look for BARCODE or barcode tag\n        if track_metadata.get(\"barcode\"):\n            barcode = str(track_metadata[\"barcode\"])\n        elif track_metadata.get(\"BARCODE\"):\n            barcode = str(track_metadata[\"BARCODE\"])\n        else:\n            # Abort if no barcode in track_metadata\n            return\n\n        # https://tango.info/wiki/TINT = <TINP>-<Side#>-<Track#>\n        # https://tango.info/wiki/TINP - \"Tango Info Number for a Product\" is a\n        # 14-digit numeric tango.info code used by tango.info and others.\n        # Example: TINT:00743216335725-1-5\n        # This plugin normalizes TINTs by stripping all zeros from the left\n        tint = \"%s-%s-%s\" % (\n            barcode.lstrip(\"0\"),\n            str(track_metadata.get(\"discnumber\", \"1\")),\n            str(track_metadata.get(\"tracknumber\")),\n        )\n\n        if barcode in self.albumpage_cache:\n            if self.albumpage_cache[barcode].get(tint):\n                for field in (\"genre\", \"date\", \"vocal\"):\n                    # Do no overwrite with empty data\n                    if not self.albumpage_cache[barcode][tint].get(field):\n                        continue\n                    track_metadata[field] = \\\n                        self.albumpage_cache[barcode][tint][field]\n            else:\n                log.debug(\n                    \"%s: No information on tango.info for barcode %s\",\n                    PLUGIN_NAME, barcode)\n        else:\n            self.website_add_track(album, album._new_tracks[-1], barcode, tint)\n\n    def website_add_track(self, album, track, barcode, tint, zeros=0):\n        \"\"\"\n        :param zeros: Number of zeros that have been prepended to the barcode\n        \"\"\"\n        self.album_add_request(album)\n\n        if self.albumpage_queue.append(barcode, (track, album, tint)):\n\n            # Barcode, zero-padded as needed\n            path = \"/%s\" % (\"0\" * zeros) + barcode\n\n            log.debug(\"%s: Downloading from tango.info: track %s, album %s \"\n                      \"with TINT %s from %s\",\n                      PLUGIN_NAME, str(track), str(album), tint,\n                      \"https://%s%s\" % (TANGO_INFO_HOST, path))\n            # FIXME: Remove this\n            if host_workaround_needed:\n                ws = album.tagger.webservice\n                def get_patched(self, *args, **kwargs):  # noqa\n                    request = WrappedWSGetRequest(self, *args, **kwargs)\n                    return ws.add_request(request)\n                ws.get_patched = get_patched\n                return ws.get_patched(\n                    TANGO_INFO_HOST,\n                    TANGO_INFO_PORT,\n                    path,\n                    partial(self.website_process, barcode, zeros),\n                    priority=False,\n                    important=False,\n                    parse_response_type=None,\n                    queryargs=None,\n                )\n            # Call website_process() as a partial func\n            return album.tagger.webservice.get(\n                TANGO_INFO_HOST,\n                TANGO_INFO_PORT,\n                path,\n                partial(self.website_process, barcode, zeros),\n                priority=False,\n                important=False,\n                parse_response_type=None,\n                queryargs=None,\n            )\n\n    def website_process(self, barcode, zeros, response_bytes, reply, error):\n        \"\"\"\n        response_bytes: PyQt5.QtCore.QByteArray, equals reply.readAll()\n        reply:          PyQt5.QtNetwork.QNetworkReply\n        error:          PyQt5.QtNetwork.QNetworkReply.NetworkError (optional)\n        \"\"\"\n\n        if error:\n            log.warning(\"%s: Network error retrieving info for barcode %s\",\n                        PLUGIN_NAME, barcode)\n            track_triple = self.albumpage_queue.pop(barcode)\n            for track, album, tint in track_triple:\n                self.album_remove_request(album)\n            return\n\n        # Decode QByteArray into unicode\n        response_decoded = response_bytes.data().decode('utf-8')\n\n        tangoinfo_albumdata = self.extract_data(barcode, response_decoded)\n\n        self.albumpage_cache[barcode] = tangoinfo_albumdata\n        track_triple = self.albumpage_queue.pop(barcode)\n\n        if tangoinfo_albumdata:\n            if zeros:\n                log.debug(\"%s: tango.info does not seem to have data for \"\n                          \"barcode %s. \"\n                          \"However, retrying with barcode %s (i.e. the same \"\n                          \"with %s prepended) was successful. \"\n                          \"This most likely means either MusicBrainz or \"\n                          \"tango.info has stored a wrong barcode for this \"\n                          \"release. You might want to investigate this \"\n                          \"discrepancy and report it.\",\n                          PLUGIN_NAME, barcode, (\"0\" * zeros) + barcode,\n                          (\"a zero\" if zeros == 1 else \"%d zeros\" % zeros))\n\n            for track, album, tint in track_triple:\n                tm = track.metadata\n                if not self.albumpage_cache[barcode].get(tint):\n                    self.album_remove_request(album)\n                    continue\n\n                for field in (\"genre\", \"date\", \"vocal\"):\n                    # Write track metadata\n                    if self.albumpage_cache[barcode][tint].get(field):\n                        tm[field] = self.albumpage_cache[barcode][tint][field]\n\n                for file in track.iterfiles():\n                    fm = file.metadata\n                    for field in (\"genre\", \"date\", \"vocal\"):\n                        if not self.albumpage_cache[barcode][tint].get(field):\n                            continue\n                        # Write file metadata\n                        fm[field] = self.albumpage_cache[barcode][tint][field]\n                self.album_remove_request(album)\n        else:\n            if zeros >= 2:\n                log.debug(\"%s: Could not load album with barcode %s even with \"\n                          \"zero prepended(%s). This most likely means \"\n                          \"tango.info does not have a release for this \"\n                          \"barcode (or MusicBrainz has a wrong barcode)\",\n                          PLUGIN_NAME, barcode, (\"0\" * zeros) + barcode)\n                for track, album, tint in track_triple:\n                    self.album_remove_request(album)\n                return\n\n            log.debug(\"%s: Retrying with 0-padded barcode for barcode %s\",\n                      PLUGIN_NAME, barcode)\n\n            for track, album, tint in track_triple:\n                # Try again with zero-prepended barcode, but at most two times\n                self.website_add_track(\n                    album, track, barcode, tint, zeros=(zeros + 1)\n                )\n                self.album_remove_request(album)\n\n    def album_add_request(self, album):\n        album._requests += 1\n\n    def album_remove_request(self, album):\n        album._requests -= 1\n        album._finalize_loading(None)\n\n    def extract_data(self, barcode, response):\n\n        # Check whether we have a concealed 404 and get the homepage\n        if \"<title>Contents - tango.info</title>\" in response:\n            log.debug(\"%s: No album with barcode %s on tango.info\",\n                      PLUGIN_NAME, barcode)\n            return\n\n        table = re.findall(table_regex, response)\n        if not table:\n            log.warning(\"%s: Could not extract table from album webpage - \"\n                        \"regex failed or page structure changed\", PLUGIN_NAME)\n            return\n        table = table[0]  # re.findall() returns a list\n\n        # Content inside of <tr> elements\n        trcontent = (match.groups()[0] for match in tr_regex.finditer(table))\n\n        page_structure_warned = False  # Ratelimit warnings\n\n        albuminfo = {}\n\n        for tr in trcontent:\n            # Content inside of <td> elements\n            trackinfo = [m.groups()[0] for m in td_regex.finditer(tr)]\n\n            # Example of expected structure:\n            # <tr>\n            # <td class=\"side_num\">1</td>\n            # <td class=\"track_num\">6</td>\n            # <td><a href=\"/T0370182390\">Ese sos vos</a></td>\n            # <td><a href=\"/genre.tango\">tango</a></td>\n            # <td><a href=\"/RicarTantu\">Ricardo Tanturi</a></td>\n            # <td><a href=\"/AlberDeluc\">Alberto Castillo</a></td>\n            # <td><a class=\"date\" href=\"/1941-12-23\">1941-12-23</a></td>\n            # <td>02:38</td>\n            # <td><a href=\"/00743216335725-1-6\"\n            #        title=\"TINT:00743216335725-1-6\">info</a><br /></td>\n            # </tr>\n\n            # Sanity checks\n            if not trackinfo:\n                # Check if list is empty, e.g. contains a <th> for table header\n                continue\n            if len(trackinfo) < 9:\n                if not page_structure_warned:  # Only warn once per <tr>\n                    log.warning(\"%s: Table '<tr>' structure on webpage \"\n                                \"unexpected for barcode %s\",\n                                PLUGIN_NAME, barcode)\n                    page_structure_warned = True\n                # Bail out early\n                continue\n\n            # Get tango.info TINT, e.g.\n            # <a href=\"/00743216335725-1-4\" title=\"TINT:00743216335725-1-4\">\n            tint = re.findall(tint_regex, trackinfo[8])\n            if tint:\n                # Normalize TINT by stripping all leading slashes and zeros\n                tint = tint[0].lstrip(\"/\").lstrip(\"0\")\n                albuminfo[tint] = {}\n            else:\n                # This really shouldn't happen\n                log.warning(\"%s: No TINT found on webpage for barcode %s\",\n                            PLUGIN_NAME, barcode)\n                continue\n\n            # Get genre, e.g.\n            # <a href=\"/genre.tango\">tango</a>\n            if trackinfo[3] != \"-\":\n                genre = re.split(\"<|>\", trackinfo[3])[2].title()\n                albuminfo[tint]['genre'] = genre\n\n            # Get date, e.g.\n            # <a class=\"date\" href=\"/1941-08-14\">1941-08-14</a>\n            if trackinfo[6] != \"-\":\n                date = re.split(\"<|>\", trackinfo[6])[2]\n                albuminfo[tint]['date'] = date\n\n            # Get singers, e.g.\n            # <a href=\"/AlberDeluc\">Alberto Castillo</a>\n            if trackinfo[5] != \"-\":\n                # Catch and strip <a> tags\n                vocal = re.sub(\"<[^>]*>\", \"\", trackinfo[5])\n                albuminfo[tint]['vocal'] = vocal\n\n        return albuminfo or None\n\n\ntagger = TangoInfoTagger()\nregister_track_metadata_processor(tagger.add_tangoinfo_data)\n"
  },
  {
    "path": "plugins/theaudiodb/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (c) 2015-2020 Philipp Wolfer\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = 'TheAudioDB cover art'\nPLUGIN_AUTHOR = 'Philipp Wolfer'\nPLUGIN_DESCRIPTION = 'Use cover art from TheAudioDB.'\nPLUGIN_VERSION = \"1.3.1\"\nPLUGIN_API_VERSIONS = [\"2.0\", \"2.1\", \"2.2\", \"2.3\", \"2.4\", \"2.5\", \"2.6\"]\nPLUGIN_LICENSE = \"GPL-2.0-or-later\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom base64 import b64decode\nfrom PyQt5.QtCore import QUrl\nfrom PyQt5.QtNetwork import QNetworkReply\nfrom picard import config, log\nfrom picard.coverart.providers import (\n    CoverArtProvider,\n    ProviderOptions,\n    register_cover_art_provider,\n)\nfrom picard.coverart.image import CoverArtImage\nfrom picard.config import (\n    BoolOption,\n    TextOption,\n)\nfrom picard.webservice import ratecontrol\nfrom .ui_options_theaudiodb import Ui_TheAudioDbOptionsPage\n\nTHEAUDIODB_HOST = \"www.theaudiodb.com\"\nTHEAUDIODB_PORT = 443\nTHEAUDIODB_APIKEY = 'MWQ2NTY1NjQ2OTRmMTM0ZDY1NjU2NA=='\n\nOPTION_CDART_ALWAYS = \"always\"\nOPTION_CDART_NEVER = \"never\"\nOPTION_CDART_NOALBUMART = \"noalbumart\"\n\n\n# No rate limit for TheAudioDB.\nratecontrol.set_minimum_delay((THEAUDIODB_HOST, THEAUDIODB_PORT), 0)\n\n\nclass TheAudioDbOptionsPage(ProviderOptions):\n\n    _options_ui = Ui_TheAudioDbOptionsPage\n\n    options = [\n        TextOption(\"setting\", \"theaudiodb_use_cdart\", OPTION_CDART_NOALBUMART),\n        BoolOption(\"setting\", \"theaudiodb_use_high_quality\", False),\n    ]\n\n    def load(self):\n        if config.setting[\"theaudiodb_use_cdart\"] == OPTION_CDART_ALWAYS:\n            self.ui.theaudiodb_cdart_use_always.setChecked(True)\n        elif config.setting[\"theaudiodb_use_cdart\"] == OPTION_CDART_NEVER:\n            self.ui.theaudiodb_cdart_use_never.setChecked(True)\n        elif config.setting[\"theaudiodb_use_cdart\"] == OPTION_CDART_NOALBUMART:\n            self.ui.theaudiodb_cdart_use_if_no_albumcover.setChecked(True)\n        self.ui.theaudiodb_use_high_quality.setChecked(\n            config.setting['theaudiodb_use_high_quality'])\n\n    def save(self):\n        if self.ui.theaudiodb_cdart_use_always.isChecked():\n            config.setting[\"theaudiodb_use_cdart\"] = OPTION_CDART_ALWAYS\n        elif self.ui.theaudiodb_cdart_use_never.isChecked():\n            config.setting[\"theaudiodb_use_cdart\"] = OPTION_CDART_NEVER\n        elif self.ui.theaudiodb_cdart_use_if_no_albumcover.isChecked():\n            config.setting[\"theaudiodb_use_cdart\"] = OPTION_CDART_NOALBUMART\n        config.setting['theaudiodb_use_high_quality'] = self.ui.theaudiodb_use_high_quality.isChecked()\n\n\nclass TheAudioDbCoverArtImage(CoverArtImage):\n\n    \"\"\"Image from The Audio DB\"\"\"\n\n    support_types = True\n    sourceprefix = \"AUDIODB\"\n\n    def parse_url(self, url):\n        super().parse_url(url)\n        # Workaround for Picard always returning port 80 regardless of the\n        # scheme. No longer necessary for Picard >= 2.1.3\n        self.port = self.url.port(443 if self.url.scheme() == 'https' else 80)\n\n\nclass CoverArtProviderTheAudioDb(CoverArtProvider):\n\n    \"\"\"Use TheAudioDB to get cover art\"\"\"\n\n    NAME = \"TheAudioDB\"\n    TITLE = \"TheAudioDB\"\n    OPTIONS = TheAudioDbOptionsPage\n\n    def __init__(self, coverart):\n        super().__init__(coverart)\n        self.__api_key = b64decode(THEAUDIODB_APIKEY).decode()\n\n    def enabled(self):\n        return super().enabled() and not self.coverart.front_image_found\n\n    def queue_images(self):\n        release_group_id = self.metadata[\"musicbrainz_releasegroupid\"]\n        path = \"/api/v1/json/%s/album-mb.php\" % self.__api_key\n        queryargs = {\n            \"i\": bytes(QUrl.toPercentEncoding(release_group_id)).decode()\n        }\n        log.debug(\"TheAudioDB: Queued download: %s?i=%s\", path, queryargs[\"i\"])\n        self.album.tagger.webservice.get(\n            THEAUDIODB_HOST,\n            THEAUDIODB_PORT,\n            path,\n            self._json_downloaded,\n            priority=True,\n            important=False,\n            parse_response_type='json',\n            queryargs=queryargs)\n        self.album._requests += 1\n        return CoverArtProvider.WAIT\n\n    def _json_downloaded(self, data, reply, error):\n        self.album._requests -= 1\n\n        if error:\n            if error != QNetworkReply.ContentNotFoundError:\n                error_level = log.error\n            else:\n                error_level = log.debug\n            error_level(\"TheAudioDB: Problem requesting metadata: %s\", error)\n        else:\n            try:\n                releases = data.get(\"album\")\n                if not releases:\n                    log.debug(\"TheAudioDB: No cover art found for %s\",\n                              reply.url().url())\n                    return\n                release = releases[0]\n                albumart_url = None\n                if config.setting['theaudiodb_use_high_quality']:\n                    albumart_url = release.get(\"strAlbumThumbHQ\")\n                if not albumart_url:\n                    albumart_url = release.get(\"strAlbumThumb\")\n                cdart_url = release.get(\"strAlbumCDart\")\n                use_cdart = config.setting[\"theaudiodb_use_cdart\"]\n\n                if albumart_url:\n                    self._select_and_add_cover_art(albumart_url, [\"front\"])\n\n                if cdart_url and (use_cdart == OPTION_CDART_ALWAYS\n                                  or (use_cdart == OPTION_CDART_NOALBUMART\n                                      and not albumart_url)):\n                    types = [\"medium\"]\n                    if not albumart_url:\n                        types.append(\"front\")\n                    self._select_and_add_cover_art(cdart_url, types)\n            except (TypeError):\n                log.error(\"TheAudioDB: Problem processing downloaded metadata\", exc_info=True)\n            finally:\n                self.next_in_queue()\n\n    def _select_and_add_cover_art(self, url, types):\n        log.debug(\"TheAudioDB: Found artwork %s\" % url)\n        self.queue_put(TheAudioDbCoverArtImage(url, types=types))\n\n\nregister_cover_art_provider(CoverArtProviderTheAudioDb)\n"
  },
  {
    "path": "plugins/theaudiodb/ui_options_theaudiodb.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/theaudiodb/ui_options_theaudiodb.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.4\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_TheAudioDbOptionsPage(object):\n    def setupUi(self, TheAudioDbOptionsPage):\n        TheAudioDbOptionsPage.setObjectName(\"TheAudioDbOptionsPage\")\n        TheAudioDbOptionsPage.resize(442, 364)\n        self.vboxlayout = QtWidgets.QVBoxLayout(TheAudioDbOptionsPage)\n        self.vboxlayout.setContentsMargins(9, 9, 9, 9)\n        self.vboxlayout.setSpacing(6)\n        self.vboxlayout.setObjectName(\"vboxlayout\")\n        self.groupBox = QtWidgets.QGroupBox(TheAudioDbOptionsPage)\n        self.groupBox.setObjectName(\"groupBox\")\n        self.vboxlayout1 = QtWidgets.QVBoxLayout(self.groupBox)\n        self.vboxlayout1.setContentsMargins(9, 9, 9, 9)\n        self.vboxlayout1.setSpacing(2)\n        self.vboxlayout1.setObjectName(\"vboxlayout1\")\n        self.label = QtWidgets.QLabel(self.groupBox)\n        self.label.setTextFormat(QtCore.Qt.RichText)\n        self.label.setWordWrap(True)\n        self.label.setOpenExternalLinks(True)\n        self.label.setObjectName(\"label\")\n        self.vboxlayout1.addWidget(self.label)\n        self.vboxlayout.addWidget(self.groupBox)\n        self.verticalGroupBox = QtWidgets.QGroupBox(TheAudioDbOptionsPage)\n        self.verticalGroupBox.setObjectName(\"verticalGroupBox\")\n        self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalGroupBox)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.theaudiodb_cdart_use_always = QtWidgets.QRadioButton(self.verticalGroupBox)\n        self.theaudiodb_cdart_use_always.setObjectName(\"theaudiodb_cdart_use_always\")\n        self.verticalLayout.addWidget(self.theaudiodb_cdart_use_always)\n        self.theaudiodb_cdart_use_if_no_albumcover = QtWidgets.QRadioButton(self.verticalGroupBox)\n        self.theaudiodb_cdart_use_if_no_albumcover.setObjectName(\"theaudiodb_cdart_use_if_no_albumcover\")\n        self.verticalLayout.addWidget(self.theaudiodb_cdart_use_if_no_albumcover)\n        self.theaudiodb_cdart_use_never = QtWidgets.QRadioButton(self.verticalGroupBox)\n        self.theaudiodb_cdart_use_never.setObjectName(\"theaudiodb_cdart_use_never\")\n        self.verticalLayout.addWidget(self.theaudiodb_cdart_use_never)\n        self.vboxlayout.addWidget(self.verticalGroupBox)\n        self.theaudiodb_use_high_quality = QtWidgets.QCheckBox(TheAudioDbOptionsPage)\n        self.theaudiodb_use_high_quality.setObjectName(\"theaudiodb_use_high_quality\")\n        self.vboxlayout.addWidget(self.theaudiodb_use_high_quality)\n        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.vboxlayout.addItem(spacerItem)\n\n        self.retranslateUi(TheAudioDbOptionsPage)\n        QtCore.QMetaObject.connectSlotsByName(TheAudioDbOptionsPage)\n\n    def retranslateUi(self, TheAudioDbOptionsPage):\n        _translate = QtCore.QCoreApplication.translate\n        self.groupBox.setTitle(_translate(\"TheAudioDbOptionsPage\", \"TheAudioDB cover art\"))\n        self.label.setText(_translate(\"TheAudioDbOptionsPage\", \"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.0//EN\\\" \\\"http://www.w3.org/TR/REC-html40/strict.dtd\\\">\\n\"\n\"<html><head><meta name=\\\"qrichtext\\\" content=\\\"1\\\" /></head><body>\\n\"\n\"<p>This plugin loads cover art from <a href=\\\"https://www.theaudiodb.com\\\">TheAudioDB</a>. TheAudioDB is a community Database of audio artwork and data. Their content is only possible thanks to the hard work of volunteer editors. If you like this plugin, please consider <a href=\\\"https://www.theaudiodb.com/user_register_now.php\\\">registering</a> as an editor or supporting the TheAudioDB <a href=\\\"https://www.patreon.com/thedatadb\\\">patreon campaign</a>.</p></body></html>\"))\n        self.verticalGroupBox.setTitle(_translate(\"TheAudioDbOptionsPage\", \"Medium images\"))\n        self.theaudiodb_cdart_use_always.setText(_translate(\"TheAudioDbOptionsPage\", \"Always load medium images\"))\n        self.theaudiodb_cdart_use_if_no_albumcover.setText(_translate(\"TheAudioDbOptionsPage\", \"Load only if no front cover is available\"))\n        self.theaudiodb_cdart_use_never.setText(_translate(\"TheAudioDbOptionsPage\", \"Never load medium images\"))\n        self.theaudiodb_use_high_quality.setText(_translate(\"TheAudioDbOptionsPage\", \"Use high resolution images, if available\"))\n"
  },
  {
    "path": "plugins/theaudiodb/ui_options_theaudiodb.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>TheAudioDbOptionsPage</class>\n <widget class=\"QWidget\" name=\"TheAudioDbOptionsPage\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>442</width>\n    <height>364</height>\n   </rect>\n  </property>\n  <layout class=\"QVBoxLayout\">\n   <property name=\"spacing\">\n    <number>6</number>\n   </property>\n   <property name=\"leftMargin\">\n    <number>9</number>\n   </property>\n   <property name=\"topMargin\">\n    <number>9</number>\n   </property>\n   <property name=\"rightMargin\">\n    <number>9</number>\n   </property>\n   <property name=\"bottomMargin\">\n    <number>9</number>\n   </property>\n   <item>\n    <widget class=\"QGroupBox\" name=\"groupBox\">\n     <property name=\"title\">\n      <string>TheAudioDB cover art</string>\n     </property>\n     <layout class=\"QVBoxLayout\">\n      <property name=\"spacing\">\n       <number>2</number>\n      </property>\n      <property name=\"leftMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"topMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"rightMargin\">\n       <number>9</number>\n      </property>\n      <property name=\"bottomMargin\">\n       <number>9</number>\n      </property>\n      <item>\n       <widget class=\"QLabel\" name=\"label\">\n        <property name=\"text\">\n         <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;\n&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;/head&gt;&lt;body&gt;\n&lt;p&gt;This plugin loads cover art from &lt;a href=&quot;https://www.theaudiodb.com&quot;&gt;TheAudioDB&lt;/a&gt;. TheAudioDB is a community Database of audio artwork and data. Their content is only possible thanks to the hard work of volunteer editors. If you like this plugin, please consider &lt;a href=&quot;https://www.theaudiodb.com/user_register_now.php&quot;&gt;registering&lt;/a&gt; as an editor or supporting the TheAudioDB &lt;a href=&quot;https://www.patreon.com/thedatadb&quot;&gt;patreon campaign&lt;/a&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>\n        </property>\n        <property name=\"textFormat\">\n         <enum>Qt::RichText</enum>\n        </property>\n        <property name=\"wordWrap\">\n         <bool>true</bool>\n        </property>\n        <property name=\"openExternalLinks\">\n         <bool>true</bool>\n        </property>\n       </widget>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <widget class=\"QGroupBox\" name=\"verticalGroupBox\">\n     <property name=\"title\">\n      <string>Medium images</string>\n     </property>\n     <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n      <item>\n       <widget class=\"QRadioButton\" name=\"theaudiodb_cdart_use_always\">\n        <property name=\"text\">\n         <string>Always load medium images</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QRadioButton\" name=\"theaudiodb_cdart_use_if_no_albumcover\">\n        <property name=\"text\">\n         <string>Load only if no front cover is available</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <widget class=\"QRadioButton\" name=\"theaudiodb_cdart_use_never\">\n        <property name=\"text\">\n         <string>Never load medium images</string>\n        </property>\n       </widget>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <widget class=\"QCheckBox\" name=\"theaudiodb_use_high_quality\">\n     <property name=\"text\">\n      <string>Use high resolution images, if available</string>\n     </property>\n    </widget>\n   </item>\n   <item>\n    <spacer name=\"verticalSpacer\">\n     <property name=\"orientation\">\n      <enum>Qt::Vertical</enum>\n     </property>\n     <property name=\"sizeHint\" stdset=\"0\">\n      <size>\n       <width>20</width>\n       <height>40</height>\n      </size>\n     </property>\n    </spacer>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/titlecase/titlecase.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright 2007 Javier Kohen\n#\n# This program is free software; you can redistribute it and/or modify\n# it under the terms of the GNU General Public License version 2 as\n# published by the Free Software Foundation\n\nPLUGIN_NAME = \"Title Case\"\nPLUGIN_AUTHOR = \"Javier Kohen, Sambhav Kothari\"\nPLUGIN_DESCRIPTION = \"Capitalize First Character In Every Word Of A Title\"\nPLUGIN_VERSION = \"1.0.2\"\nPLUGIN_API_VERSIONS = ['2.0']\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nimport unicodedata\nfrom picard.plugin import PluginPriority\n\n\ndef iswbound(char):\n    \"\"\"Returns whether the given character is a word boundary.\"\"\"\n    category = unicodedata.category(char)\n    # If it's a space separator or punctuation\n    return 'Zs' == category or 'Sk' == category or 'P' == category[0]\n\n\ndef utitle(string):\n    \"\"\"Title-case a string using a less destructive method than str.title.\"\"\"\n    new_string = string[0].capitalize()\n    cap = False\n    for i in range(1, len(string)):\n        s = string[i]\n        # Special case apostrophe in the middle of a word.\n        if s in \"’'\" and string[i - 1].isalpha():\n            cap = False\n        elif iswbound(s):\n            cap = True\n        elif cap and s.isalpha():\n            cap = False\n            s = s.capitalize()\n        else:\n            cap = False\n        new_string += s\n    return new_string\n\n\ndef title(string):\n    \"\"\"Title-case a string using a less destructive method than str.title.\"\"\"\n    if not string:\n        return \"\"\n    # if the string is all uppercase, lowercase it - Erich/Javier\n    #   Lots of Japanese songs use entirely upper-case English titles,\n    #   so I don't like this change... - JoeW\n    #if string == string.upper(): string = string.lower()\n    return utitle(str(string))\n\nfrom picard.metadata import (\n    register_track_metadata_processor,\n    register_album_metadata_processor,\n)\n\n\ndef title_case(tagger, metadata, *args):\n    for name, value in metadata.rawitems():\n        if name in [\"title\", \"album\", \"artist\"]:\n            metadata[name] = [title(x) for x in value]\n\nregister_track_metadata_processor(title_case, priority=PluginPriority.LOW)\nregister_album_metadata_processor(title_case, priority=PluginPriority.LOW)\n"
  },
  {
    "path": "plugins/tracks2clipboard/tracks2clipboard.py",
    "content": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = \"Copy Cluster to Clipboard\"\nPLUGIN_AUTHOR = \"Michael Elsdörfer, Sambhav Kothari\"\nPLUGIN_DESCRIPTION = \"Exports a cluster's tracks to the clipboard, so it can be copied into the tracklist field on MusicBrainz\"\nPLUGIN_VERSION = \"1.0\"\nPLUGIN_API_VERSIONS = [\"2.0\"]\n\n\nfrom PyQt5 import QtWidgets\nfrom picard.cluster import Cluster\nfrom picard.util import format_time\nfrom picard.ui.itemviews import BaseAction, register_cluster_action\n\n\nclass CopyClusterToClipboard(BaseAction):\n    NAME = \"Copy Cluster to Clipboard...\"\n\n    def callback(self, objs):\n        if len(objs) != 1 or not isinstance(objs[0], Cluster):\n            return\n        cluster = objs[0]\n\n        artists = set()\n        for i, file in enumerate(cluster.files):\n            artists.add(file.metadata[\"artist\"])\n\n        tracks = []\n        for i, file in enumerate(cluster.files):\n            try:\n                i = int(file.metadata[\"tracknumber\"])\n            except:\n                i += 1\n\n            if len(artists) > 1:\n                tracks.append((i, \"%s. %s - %s (%s)\" % (\n                    i,\n                    file.metadata[\"title\"],\n                    file.metadata[\"artist\"],\n                    format_time(file.metadata.length))))\n            else:\n                tracks.append((i, \"%s. %s (%s)\" % (\n                    i,\n                    file.metadata[\"title\"],\n                    format_time(file.metadata.length))))\n\n        clipboard = QtWidgets.QApplication.clipboard()\n        clipboard.setText(\"\\n\".join([x[1] for x in sorted(tracks)]))\n\n\nregister_cluster_action(CopyClusterToClipboard())\n"
  },
  {
    "path": "plugins/viewvariables/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = 'View script variables'\nPLUGIN_AUTHOR = 'Sophist'\nPLUGIN_DESCRIPTION = '''Display a dialog box listing the metadata variables for the track / file.<br /><br />\nThis allows you to see metadata variables beginning with \"~\" which are not normally visible in the metadata\npane of the main Picard window, which can be useful when you are writing tagging or file naming scripts.\n'''\nPLUGIN_VERSION = '0.7.2'\nPLUGIN_API_VERSIONS = ['2.0']\nPLUGIN_LICENSE = \"GPL-2.0\"\nPLUGIN_LICENSE_URL = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n\nfrom PyQt5 import QtWidgets, QtCore\ntry:\n    from picard.util.tags import PRESERVED_TAGS\nexcept ImportError:\n    from picard.file import File\n    PRESERVED_TAGS = File._default_preserved_tags\n\nfrom picard.file import File\nfrom picard.track import Track\nfrom picard.ui.itemviews import BaseAction, register_file_action, register_track_action\nfrom picard.plugins.viewvariables.ui_variables_dialog import Ui_VariablesDialog\n\n\nclass ViewVariables(BaseAction):\n    NAME = 'View script variables'\n\n    def callback(self, objs):\n        obj = objs[0]\n        files = self.tagger.get_files_from_objects(objs)\n        if files:\n            obj = files[0]\n        dialog = ViewVariablesDialog(obj)\n        dialog.exec_()\n\n\nclass ViewVariablesDialog(QtWidgets.QDialog):\n\n    def __init__(self, obj, parent=None):\n        QtWidgets.QDialog.__init__(self, parent)\n        self.ui = Ui_VariablesDialog()\n        self.ui.setupUi(self)\n        self.ui.buttonBox.accepted.connect(self.accept)\n        self.ui.buttonBox.rejected.connect(self.reject)\n        metadata = obj.metadata\n        if isinstance(obj, File):\n            self.setWindowTitle(_(\"File: %s\") % obj.base_filename)\n        elif isinstance(obj, Track):\n            tn = metadata['tracknumber']\n            if len(tn) == 1:\n                tn = \"0\" + tn\n            self.setWindowTitle(_(\"Track: %s %s \") % (tn, metadata['title']))\n        else:\n            self.setWindowTitle(_(\"Variables\"))\n        self._display_metadata(metadata)\n\n    def _display_metadata(self, metadata):\n        keys = metadata.keys()\n        keys = sorted(keys, key=lambda key:\n                      '0' + key if key in PRESERVED_TAGS and key.startswith('~') else\n                      '1' + key if key.startswith('~') else\n                      '2' + key)\n        media = hidden = album = False\n        table = self.ui.metadata_table\n        key_example, value_example = self.get_table_items(table, 0)\n        self.key_flags = key_example.flags()\n        self.value_flags = value_example.flags()\n        table.setRowCount(len(keys) + 3)\n        i = 0\n        for key in keys:\n            if key in PRESERVED_TAGS and key.startswith('~'):\n                if not media:\n                    self.add_separator_row(table, i, _(\"File variables\"))\n                    i += 1\n                    media = True\n            elif key.startswith('~'):\n                if not hidden:\n                    self.add_separator_row(table, i, _(\"Hidden variables\"))\n                    i += 1\n                    hidden = True\n            else:\n                if not album:\n                    self.add_separator_row(table, i, _(\"Tag variables\"))\n                    i += 1\n                    album = True\n\n            key_item, value_item = self.get_table_items(table, i)\n            i += 1\n            key_item.setText(\"_\" + key[1:] if key.startswith('~') else key)\n            if key in metadata:\n                value = metadata.getall(key)\n                if len(value) == 1 and value[0] != '':\n                    value = value[0]\n                else:\n                    value = repr(value)\n                value_item.setText(value)\n\n    def add_separator_row(self, table, i, title):\n        key_item, value_item = self.get_table_items(table, i)\n        font = key_item.font()\n        font.setBold(True)\n        key_item.setFont(font)\n        key_item.setText(title)\n\n    def get_table_items(self, table, i):\n        key_item = table.item(i, 0)\n        value_item = table.item(i, 1)\n        if not key_item:\n            key_item = QtWidgets.QTableWidgetItem()\n            key_item.setFlags(self.key_flags)\n            table.setItem(i, 0, key_item)\n        if not value_item:\n            value_item = QtWidgets.QTableWidgetItem()\n            value_item.setFlags(self.value_flags)\n            table.setItem(i, 1, value_item)\n        return key_item, value_item\n\nvv = ViewVariables()\nregister_file_action(vv)\nregister_track_action(vv)\n"
  },
  {
    "path": "plugins/viewvariables/ui_variables_dialog.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/viewvariables/ui_variables_dialog.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.4\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_VariablesDialog(object):\n    def setupUi(self, VariablesDialog):\n        VariablesDialog.setObjectName(\"VariablesDialog\")\n        VariablesDialog.resize(600, 450)\n        self.verticalLayout = QtWidgets.QVBoxLayout(VariablesDialog)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.metadata_table = QtWidgets.QTableWidget(VariablesDialog)\n        self.metadata_table.setAutoFillBackground(False)\n        self.metadata_table.setSelectionMode(QtWidgets.QAbstractItemView.ContiguousSelection)\n        self.metadata_table.setRowCount(1)\n        self.metadata_table.setColumnCount(2)\n        self.metadata_table.setObjectName(\"metadata_table\")\n        item = QtWidgets.QTableWidgetItem()\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        item.setFont(font)\n        self.metadata_table.setHorizontalHeaderItem(0, item)\n        item = QtWidgets.QTableWidgetItem()\n        font = QtGui.QFont()\n        font.setBold(True)\n        font.setWeight(75)\n        item.setFont(font)\n        self.metadata_table.setHorizontalHeaderItem(1, item)\n        item = QtWidgets.QTableWidgetItem()\n        item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled)\n        self.metadata_table.setItem(0, 0, item)\n        item = QtWidgets.QTableWidgetItem()\n        item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEnabled)\n        self.metadata_table.setItem(0, 1, item)\n        self.metadata_table.horizontalHeader().setDefaultSectionSize(150)\n        self.metadata_table.horizontalHeader().setSortIndicatorShown(False)\n        self.metadata_table.horizontalHeader().setStretchLastSection(True)\n        self.metadata_table.verticalHeader().setVisible(False)\n        self.metadata_table.verticalHeader().setDefaultSectionSize(20)\n        self.metadata_table.verticalHeader().setMinimumSectionSize(20)\n        self.verticalLayout.addWidget(self.metadata_table)\n        self.buttonBox = QtWidgets.QDialogButtonBox(VariablesDialog)\n        self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)\n        self.buttonBox.setObjectName(\"buttonBox\")\n        self.verticalLayout.addWidget(self.buttonBox)\n\n        self.retranslateUi(VariablesDialog)\n        QtCore.QMetaObject.connectSlotsByName(VariablesDialog)\n\n    def retranslateUi(self, VariablesDialog):\n        _translate = QtCore.QCoreApplication.translate\n        item = self.metadata_table.horizontalHeaderItem(0)\n        item.setText(_translate(\"VariablesDialog\", \"Variable\"))\n        item = self.metadata_table.horizontalHeaderItem(1)\n        item.setText(_translate(\"VariablesDialog\", \"Value\"))\n        __sortingEnabled = self.metadata_table.isSortingEnabled()\n        self.metadata_table.setSortingEnabled(False)\n        self.metadata_table.setSortingEnabled(__sortingEnabled)\n"
  },
  {
    "path": "plugins/viewvariables/ui_variables_dialog.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>VariablesDialog</class>\n <widget class=\"QDialog\" name=\"VariablesDialog\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>600</width>\n    <height>450</height>\n   </rect>\n  </property>\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n   <item>\n    <widget class=\"QTableWidget\" name=\"metadata_table\">\n     <property name=\"autoFillBackground\">\n      <bool>false</bool>\n     </property>\n     <property name=\"selectionMode\">\n      <enum>QAbstractItemView::ContiguousSelection</enum>\n     </property>\n     <property name=\"rowCount\">\n      <number>1</number>\n     </property>\n     <property name=\"columnCount\">\n      <number>2</number>\n     </property>\n     <attribute name=\"horizontalHeaderDefaultSectionSize\">\n      <number>150</number>\n     </attribute>\n     <attribute name=\"horizontalHeaderShowSortIndicator\" stdset=\"0\">\n      <bool>false</bool>\n     </attribute>\n     <attribute name=\"horizontalHeaderStretchLastSection\">\n      <bool>true</bool>\n     </attribute>\n     <attribute name=\"verticalHeaderVisible\">\n      <bool>false</bool>\n     </attribute>\n     <attribute name=\"verticalHeaderDefaultSectionSize\">\n      <number>20</number>\n     </attribute>\n     <attribute name=\"verticalHeaderMinimumSectionSize\">\n      <number>20</number>\n     </attribute>\n     <row/>\n     <column>\n      <property name=\"text\">\n       <string>Variable</string>\n      </property>\n      <property name=\"font\">\n       <font>\n        <weight>75</weight>\n        <bold>true</bold>\n       </font>\n      </property>\n     </column>\n     <column>\n      <property name=\"text\">\n       <string>Value</string>\n      </property>\n      <property name=\"font\">\n       <font>\n        <weight>75</weight>\n        <bold>true</bold>\n       </font>\n      </property>\n     </column>\n     <item row=\"0\" column=\"0\">\n      <property name=\"text\">\n       <string/>\n      </property>\n      <property name=\"flags\">\n       <set>ItemIsSelectable|ItemIsEnabled</set>\n      </property>\n     </item>\n     <item row=\"0\" column=\"1\">\n      <property name=\"flags\">\n       <set>ItemIsSelectable|ItemIsEnabled</set>\n      </property>\n     </item>\n    </widget>\n   </item>\n   <item>\n    <widget class=\"QDialogButtonBox\" name=\"buttonBox\">\n     <property name=\"standardButtons\">\n      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>\n     </property>\n    </widget>\n   </item>\n  </layout>\n </widget>\n <tabstops>\n  <tabstop>buttonBox</tabstop>\n </tabstops>\n <resources/>\n <connections/>\n</ui>\n"
  },
  {
    "path": "plugins/wikidata/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright © 2016 Daniel sobey <dns@dns.id.au >\n\n# This work is free. You can redistribute it and/or modify it under the\n# terms of the Do What The Fuck You Want To Public License, Version 2,\n# as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.\n\nPLUGIN_NAME = 'Wikidata Genre'\nPLUGIN_AUTHOR = 'Daniel Sobey, Sambhav Kothari'\nPLUGIN_DESCRIPTION = 'Query wikidata to get genre tags'\nPLUGIN_VERSION = '1.4.5'\nPLUGIN_API_VERSIONS = [\"2.0\", \"2.1\", \"2.2\"]\nPLUGIN_LICENSE = 'WTFPL'\nPLUGIN_LICENSE_URL = 'http://www.wtfpl.net/'\n\nimport re\nfrom functools import partial\nfrom picard import config, log\nfrom picard.metadata import register_track_metadata_processor\nfrom picard.plugins.wikidata.ui_options_wikidata import Ui_WikidataOptionsPage\nfrom picard.ui.options import register_options_page, OptionsPage\nfrom picard.webservice import ratecontrol\n\n\nWIKIDATA_HOST = 'www.wikidata.org'\nWIKIDATA_PORT = 443\n\nratecontrol.set_minimum_delay((WIKIDATA_HOST, WIKIDATA_PORT), 0)\n\n\ndef parse_ignored_tags(ignore_tags_setting):\n    ignore_tags = []\n    for tag in ignore_tags_setting.lower().split(','):\n        if not tag:\n            break\n        tag = tag.strip()\n        if tag.startswith('/') and tag.endswith('/'):\n            try:\n                tag = re.compile(tag[1:-1])\n            except re.error:\n                log.error(\n                    'Error parsing ignored tag \"%s\"', tag, exc_info=True)\n        ignore_tags.append(tag)\n    return ignore_tags\n\n\ndef matches_ignored(ignore_tags, tag):\n    if ignore_tags:\n        tag = tag.lower().strip()\n        for pattern in ignore_tags:\n            if hasattr(pattern, 'match'):\n                match = pattern.match(tag)\n            else:\n                match = pattern == tag\n            if match:\n                return True\n    return False\n\n\nclass Wikidata:\n\n    RELEASE_GROUP = 1\n    ARTIST = 2\n    WORK = 3\n\n    def __init__(self):\n        # Key: mbid, value: List of metadata entries to be updated when we have parsed everything\n        self.requests = {}\n\n        # Key: mbid, value: List of items to track the number of outstanding requests\n        self.itemAlbums = {}\n\n        # cache, items that have been found\n        # key: mbid, value: list of strings containing the genre's\n        self.cache = {}\n\n        # metabrainz url\n        self.mb_host = ''\n        self.mb_port = ''\n\n        # web service & logger\n        self.ws = None\n        self.log = None\n\n        # settings from options, options\n        self.use_release_group_genres = False\n        self.use_artist_genres = False\n        self.use_artist_only_if_no_release = False\n        self.ignore_genres_from_these_artists = ''\n        self.ignore_genres_from_these_artists_list = []\n        self.use_work_genres = True\n        self.ignore_these_genres = ''\n        self.ignore_these_genres_list = []\n        self.genre_delimiter = ''\n\n    # not used\n    def process_release(self, album, metadata, release):\n        self.ws = album.tagger.webservice\n        self.log = album.log\n        item_id = metadata.getall('musicbrainz_releasegroupid')[0]\n\n        log.info('WIKIDATA: Processing release group %s ' % item_id)\n        self.process_request(metadata, album, item_id, item_type='release-group')\n        for artist in metadata.getall('musicbrainz_albumartistid'):\n            item_id = artist\n            log.info('WIKIDATA: Processing release artist %s' % item_id)\n            self.process_request(metadata, album, item_id, item_type='artist')\n\n    # Main processing function\n    # First see if we have already found what we need in the cache, finalize loading\n    # Next see if we are already looking for the item\n    #   If we are, add this item to the list of items to be updated once we find what we are looking for.\n    #   Otherwise we are the first one to look up this item, start a new request\n    # metadata, map containing the new metadata\n    #\n    def process_request(self, metadata, album, item_id, item_type):\n        log.debug('WIKIDATA: Looking up cache for item: %s' % item_id)\n        log.debug('WIKIDATA: Album request count: %s' % album._requests)\n        log.debug('WIKIDATA: Item type %s' % item_type)\n        if item_id in self.cache:\n            log.debug('WIKIDATA: Found item in cache')\n            genre_list = self.cache[item_id]\n            new_genre = set(metadata.getall(\"genre\"))\n            new_genre.update(genre_list)\n            #sort the new genre list so that they don't appear as new entries (not a change) next time\n            metadata[\"genre\"] = self.genre_delimiter.join(sorted(new_genre))\n            return\n        else:\n            # pending requests are handled by adding the metadata object to a\n            # list of things to be updated when the genre is found\n            if item_id in self.itemAlbums:\n                log.debug(\n                    'WIKIDATA: Request already pending, add it to the list of items to update once this has been'\n                    'found')\n                self.requests[item_id].append(metadata)\n            else:\n                self.requests[item_id] = [metadata]\n                self.itemAlbums[item_id] = album\n                album._requests += 1\n\n                log.debug('WIKIDATA: First request for this item')\n                log.debug('WIKIDATA: About to call Musicbrainz to look up %s ' % item_id)\n\n                path = '/ws/2/%s/%s' % (item_type, item_id)\n                queryargs = {\"inc\": \"url-rels\"}\n\n                self.ws.get(self.mb_host, self.mb_port, path, partial(self.musicbrainz_release_lookup, item_id,\n                                                                      metadata),\n                            parse_response_type=\"xml\", priority=False, important=False, queryargs=queryargs)\n\n    def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error):\n        found = False\n        if error:\n            log.error('WIKIDATA: Error retrieving release group info')\n        else:\n            if 'metadata' in response.children:\n                if 'release_group' in response.metadata[0].children and self.use_release_group_genres:\n                    if 'relation_list' in response.metadata[0].release_group[0].children:\n                        for relation in response.metadata[0].release_group[0].relation_list[0].relation:\n                            if relation.type == 'wikidata' and 'target' in relation.children:\n                                found = True\n                                wikidata_url = relation.target[0].text\n                                log.debug('WIKIDATA: wikidata url found for RELEASE_GROUP: %s ', wikidata_url)\n                                self.process_wikidata(Wikidata.RELEASE_GROUP, wikidata_url, item_id)\n                if 'artist' in response.metadata[0].children and self.use_artist_genres:\n                    if 'relation_list' in response.metadata[0].artist[0].children:\n                        for relation in response.metadata[0].artist[0].relation_list[0].relation:\n                            if relation.type == 'wikidata' and 'target' in relation.children:\n                                found = True\n                                wikidata_url = relation.target[0].text\n                                self.process_wikidata(Wikidata.ARTIST, wikidata_url, item_id)\n                                log.debug('WIKIDATA: wikidata url found for ARTIST: %s ', wikidata_url)\n                if 'work' in response.metadata[0].children and self.use_work_genres:\n                    if 'relation_list' in response.metadata[0].work[0].children:\n                        for relation in response.metadata[0].work[0].relation_list[0].relation:\n                            if relation.type == 'wikidata' and 'target' in relation.children:\n                                found = True\n                                wikidata_url = relation.target[0].text\n                                log.debug('WIKIDATA: wikidata url found for WORK: %s ', wikidata_url)\n                                self.process_wikidata(Wikidata.WORK, wikidata_url, item_id)\n        if not found:\n            log.debug('WIKIDATA: No wikidata url found for item_id: %s ', item_id)\n\n        album = self.itemAlbums[item_id]\n        album._requests -= 1\n        if not album._requests:\n            self.itemAlbums = {k: v for k, v in self.itemAlbums.items() if v != album}\n            album._finalize_loading(None)\n        log.info('WIKIDATA: Total remaining requests: %s' % album._requests)\n        if not self.itemAlbums:\n            self.requests.clear()\n            log.info('WIKIDATA: Finished (A)')\n\n    def process_wikidata(self, genre_source_type, wikidata_url, item_id):\n        album = self.itemAlbums[item_id]\n        album._requests += 1\n        item = wikidata_url.split('/')[4]\n        path = \"/wiki/Special:EntityData/\" + item + \".rdf\"\n        log.debug('WIKIDATA: Fetching from wikidata.org%s' % path)\n        self.ws.get(WIKIDATA_HOST, WIKIDATA_PORT, path,\n                    partial(self.parse_wikidata_response, item, item_id, genre_source_type),\n                    parse_response_type=\"xml\", priority=False, important=False)\n\n    def parse_wikidata_response(self, item, item_id, genre_source_type, response, reply, error):\n        genre_entries = []\n        genre_list = []\n        if error:\n            log.error('WIKIDATA: error getting data from wikidata.org')\n        else:\n            if 'RDF' in response.children:\n                node = response.RDF[0]\n                for node1 in node.Description:\n                    if 'about' in node1.attribs:\n                        if node1.attribs.get('about') == 'http://www.wikidata.org/entity/%s' % item:\n                            for key, val in list(node1.children.items()):\n                                if key == 'P136':\n                                    for i in val:\n                                        if 'resource' in i.attribs:\n                                            tmp = i.attribs.get('resource')\n                                            if 'entity' == tmp.split('/')[3] and len(tmp.split('/')) == 5:\n                                                genre_id = tmp.split('/')[4]\n                                                log.debug(\n                                                    'WIKIDATA: Found the wikidata id for the genre: %s' % genre_id)\n                                                genre_entries.append(tmp)\n                        else:\n                            for tmp in genre_entries:\n                                if tmp == node1.attribs.get('about'):\n                                    list1 = node1.children.get('name')\n                                    if not list1:\n                                    \tlog.warning('WIKIDATA: Response does not contain a name field')\n                                    else:\n                                        for node2 in list1:\n                                            if node2.attribs.get('lang') == 'en':\n                                                genre = node2.text.title()\n                                                if not matches_ignored(self.ignore_these_genres_list, genre):\n                                                    genre_list.append(genre)\n                                                    log.debug('New genre has been found and ALLOWED: %s' % genre)\n                                                else:\n                                                    log.debug('New genre has been found, but IGNORED: %s' % genre)\n\n        if len(genre_list) > 0:\n            log.debug('WIKIDATA: item_id: %s' % item_id)\n            log.debug('WIKIDATA: Final list of wikidata id found: %s' % genre_entries)\n            log.debug('WIKIDATA: Final list of genre: %s' % genre_list)\n            log.info('WIKIDATA: Total items to update: %d ' % len(self.requests[item_id]))\n\n            for metadata in self.requests[item_id]:\n                if genre_source_type == Wikidata.RELEASE_GROUP:\n                    metadata['~release_group_genre_sourced'] = True\n                elif genre_source_type == Wikidata.ARTIST:\n                    if self.use_artist_only_if_no_release and metadata['~release_group_genre_sourced'] or \\\n                            matches_ignored(self.ignore_genres_from_these_artists_list, metadata.get(\"artist\")):\n                        if item_id not in self.cache:\n                            self.cache[item_id] = []\n                        log.debug('WIKIDATA: NOT setting Artist-sourced genre: %s ' % genre_list)\n                        continue\n                    else:\n                        log.debug('WIKIDATA: Setting Artist-sourced genre: %s ' % genre_list)\n\n                # getall doesn't handle delimiters so we need to check-n-parse here\n                old_genre_metadata = metadata.getall(\"genre\")\n                old_genre_list = []\n                for genre in old_genre_metadata:\n                    if self.genre_delimiter and self.genre_delimiter in genre:\n                        old_genre_list.extend(genre.split(self.genre_delimiter))\n                    else:\n                        old_genre_list.append(genre)\n\n                new_genre = set(old_genre_list)\n                new_genre.update(genre_list)\n                # Sort the new genre list so that they don't appear as new entries (not a change) next time\n                log.debug('WIKIDATA: setting metadata genre to : %s ' % new_genre)\n                if self.genre_delimiter:\n                    metadata[\"genre\"] = self.genre_delimiter.join(sorted(new_genre))\n                else:\n                    metadata[\"genre\"] = sorted(new_genre)\n\n                log.debug('WIKIDATA: setting cache genre to : %s ' % genre_list)\n                self.cache[item_id] = genre_list\n        else:\n            log.debug('WIKIDATA: genre not found in wikidata')\n\n        log.debug('WIKIDATA: seeing if we can finalize tags...')\n\n        album = self.itemAlbums[item_id]\n        album._requests -= 1\n        if not album._requests:\n            self.itemAlbums = {k: v for k, v in self.itemAlbums.items() if v != album}\n            album._finalize_loading(None)\n        log.info('WIKIDATA: total remaining requests: %s' % album._requests)\n        if not self.itemAlbums:\n            self.requests.clear()\n            log.info('WIKIDATA: Finished (B)')\n\n    def process_track(self, album, metadata, track, release):\n        self.update_settings()\n        self.ws = album.tagger.webservice\n        self.log = album.log\n\n        log.info('WIKIDATA: Processing Track...')\n        if self.use_release_group_genres:\n            for release_group in metadata.getall('musicbrainz_releasegroupid'):\n                log.debug('WIKIDATA: Looking up release group metadata for %s ' % release_group)\n                self.process_request(metadata, album, release_group, item_type='release-group')\n\n        if self.use_artist_genres:\n            for artist in metadata.getall('musicbrainz_albumartistid'):\n                log.debug('WIKIDATA: Processing release artist %s' % artist)\n                self.process_request(metadata, album, artist, item_type='artist')\n\n        if self.use_artist_genres:\n            for artist in metadata.getall('musicbrainz_artistid'):\n                log.debug('WIKIDATA: Processing track artist %s' % artist)\n                self.process_request(metadata, album, artist, item_type='artist')\n\n        if self.use_work_genres:\n            for workid in metadata.getall('musicbrainz_workid'):\n                log.debug('WIKIDATA: Processing track artist %s' % workid)\n                self.process_request(metadata, album, workid, item_type='work')\n\n    def update_settings(self):\n        self.mb_host = config.setting[\"server_host\"]\n        self.mb_port = config.setting[\"server_port\"]\n        self.use_release_group_genres = config.setting[\"\"]\n        self.use_work_genres = config.setting[\"wikidata_use_work_genres\"]\n        # Some changed settings could invalidate the cache, so clear it to be safe\n        if self.use_release_group_genres != config.setting[\"wikidata_use_release_group_genres\"]:\n            self.use_release_group_genres = config.setting[\"wikidata_use_release_group_genres\"]\n            self.cache.clear()\n        if self.use_artist_genres != config.setting[\"wikidata_use_artist_genres\"]:\n            self.use_artist_genres = config.setting[\"wikidata_use_artist_genres\"]\n            self.cache.clear()\n        if self.use_artist_only_if_no_release != config.setting[\"wikidata_use_artist_only_if_no_release\"]:\n            self.use_artist_only_if_no_release = config.setting[\"wikidata_use_artist_only_if_no_release\"]\n            self.cache.clear()\n        if self.ignore_genres_from_these_artists != parse_ignored_tags(\n                config.setting[\"wikidata_ignore_genres_from_these_artists\"]):\n            self.ignore_genres_from_these_artists_list = parse_ignored_tags(\n                config.setting[\"wikidata_ignore_genres_from_these_artists\"])\n            self.cache.clear()\n        if self.use_work_genres != config.setting[\"wikidata_use_work_genres\"]:\n            self.use_work_genres = config.setting[\"wikidata_use_work_genres\"]\n            self.cache.clear()\n        if self.ignore_these_genres != parse_ignored_tags(config.setting[\"wikidata_ignore_these_genres\"]):\n            self.ignore_these_genres = parse_ignored_tags(config.setting[\"wikidata_ignore_these_genres\"])\n            self.ignore_these_genres_list = parse_ignored_tags(\n                config.setting[\"wikidata_ignore_these_genres\"])\n            self.cache.clear()\n        if config.setting[\"write_id3v23\"]:\n            self.genre_delimiter = config.setting[\"wikidata_genre_delimiter\"]\n\n\nclass WikidataOptionsPage(OptionsPage):\n    NAME = \"wikidata\"\n    TITLE = \"Wikidata Genre\"\n    PARENT = \"plugins\"\n\n    options = [\n        config.BoolOption(\"setting\", \"wikidata_use_release_group_genres\", True),\n        config.BoolOption(\"setting\", \"wikidata_use_artist_genres\", True),\n        config.BoolOption(\"setting\", \"wikidata_use_artist_only_if_no_release\", True),\n        config.TextOption(\"setting\", \"wikidata_ignore_genres_from_these_artists\", \"\"),\n        config.BoolOption(\"setting\", \"wikidata_use_work_genres\", True),\n        config.TextOption(\"setting\", \"wikidata_ignore_these_genres\", \"seen live, favorites, /\\\\d+ of \\\\d+ stars/\"),\n        config.TextOption(\"setting\", \"wikidata_genre_delimiter\", \"; \"),\n    ]\n\n    def __init__(self, parent=None):\n        super(WikidataOptionsPage, self).__init__(parent)\n        self.ui = Ui_WikidataOptionsPage()\n        self.ui.setupUi(self)\n        if not config.setting[\"write_id3v23\"]:\n            self.ui.genre_delimiter.setEnabled(False);\n            self.ui.genre_delimiter_label.setEnabled(False);\n        else:\n            self.ui.genre_delimiter.setEnabled(True);\n            self.ui.genre_delimiter_label.setEnabled(True);\n\n    def load(self):\n        setting = config.setting\n        self.ui.use_release_group_genres.setChecked(setting[\"wikidata_use_release_group_genres\"])\n        self.ui.use_artist_genres.setChecked(setting[\"wikidata_use_artist_genres\"])\n        self.ui.use_artist_only_if_no_release.setChecked(setting[\"wikidata_use_artist_only_if_no_release\"])\n        self.ui.ignore_genres_from_these_artists.setText(setting[\"wikidata_ignore_genres_from_these_artists\"])\n        self.ui.use_work_genres.setChecked(setting[\"wikidata_use_work_genres\"])\n        self.ui.ignore_these_genres.setText(setting[\"wikidata_ignore_these_genres\"])\n        if config.setting[\"write_id3v23\"]:\n            self.ui.genre_delimiter.setEditText(setting[\"wikidata_genre_delimiter\"])\n\n    def save(self):\n        setting = config.setting\n        setting[\"wikidata_use_release_group_genres\"] = self.ui.use_release_group_genres.isChecked()\n        setting[\"wikidata_use_artist_genres\"] = self.ui.use_artist_genres.isChecked()\n        setting[\"wikidata_use_artist_only_if_no_release\"] = self.ui.use_artist_only_if_no_release.isChecked()\n        setting[\"wikidata_ignore_genres_from_these_artists\"] = str(self.ui.ignore_genres_from_these_artists.text())\n        setting[\"wikidata_use_work_genres\"] = self.ui.use_work_genres.isChecked()\n        setting[\"wikidata_ignore_these_genres\"] = str(self.ui.ignore_these_genres.text())\n        if config.setting[\"write_id3v23\"]:\n            setting[\"wikidata_genre_delimiter\"] = str(self.ui.genre_delimiter.currentText())\n\n\nwikidata = Wikidata()\nregister_track_metadata_processor(wikidata.process_track)\nregister_options_page(WikidataOptionsPage)\n"
  },
  {
    "path": "plugins/wikidata/ui_options_wikidata.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/wikidata/ui_options_wikidata.ui'\n#\n# Created by: PyQt5 UI code generator 5.15.4\n#\n# WARNING: Any manual changes made to this file will be lost when pyuic5 is\n# run again.  Do not edit this file unless you know what you are doing.\n\n\nfrom PyQt5 import QtCore, QtGui, QtWidgets\n\n\nclass Ui_WikidataOptionsPage(object):\n    def setupUi(self, WikidataOptionsPage):\n        WikidataOptionsPage.setObjectName(\"WikidataOptionsPage\")\n        WikidataOptionsPage.resize(602, 512)\n        self.verticalLayout_2 = QtWidgets.QVBoxLayout(WikidataOptionsPage)\n        self.verticalLayout_2.setObjectName(\"verticalLayout_2\")\n        self.releaseGroup_groupBox = QtWidgets.QGroupBox(WikidataOptionsPage)\n        self.releaseGroup_groupBox.setEnabled(True)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.releaseGroup_groupBox.sizePolicy().hasHeightForWidth())\n        self.releaseGroup_groupBox.setSizePolicy(sizePolicy)\n        self.releaseGroup_groupBox.setMinimumSize(QtCore.QSize(0, 0))\n        self.releaseGroup_groupBox.setObjectName(\"releaseGroup_groupBox\")\n        self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.releaseGroup_groupBox)\n        self.verticalLayout_4.setObjectName(\"verticalLayout_4\")\n        self.use_release_group_genres = QtWidgets.QCheckBox(self.releaseGroup_groupBox)\n        self.use_release_group_genres.setEnabled(True)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.use_release_group_genres.sizePolicy().hasHeightForWidth())\n        self.use_release_group_genres.setSizePolicy(sizePolicy)\n        self.use_release_group_genres.setChecked(True)\n        self.use_release_group_genres.setObjectName(\"use_release_group_genres\")\n        self.verticalLayout_4.addWidget(self.use_release_group_genres)\n        self.verticalLayout_2.addWidget(self.releaseGroup_groupBox)\n        self.artist_groupBox = QtWidgets.QGroupBox(WikidataOptionsPage)\n        self.artist_groupBox.setEnabled(True)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.artist_groupBox.sizePolicy().hasHeightForWidth())\n        self.artist_groupBox.setSizePolicy(sizePolicy)\n        self.artist_groupBox.setMinimumSize(QtCore.QSize(0, 0))\n        self.artist_groupBox.setObjectName(\"artist_groupBox\")\n        self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.artist_groupBox)\n        self.verticalLayout_8.setObjectName(\"verticalLayout_8\")\n        self.use_artist_genres = QtWidgets.QCheckBox(self.artist_groupBox)\n        self.use_artist_genres.setObjectName(\"use_artist_genres\")\n        self.verticalLayout_8.addWidget(self.use_artist_genres)\n        self.hLayout_use_artist_no_release = QtWidgets.QHBoxLayout()\n        self.hLayout_use_artist_no_release.setObjectName(\"hLayout_use_artist_no_release\")\n        spacerItem = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)\n        self.hLayout_use_artist_no_release.addItem(spacerItem)\n        self.use_artist_only_if_no_release = QtWidgets.QCheckBox(self.artist_groupBox)\n        self.use_artist_only_if_no_release.setEnabled(False)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.use_artist_only_if_no_release.sizePolicy().hasHeightForWidth())\n        self.use_artist_only_if_no_release.setSizePolicy(sizePolicy)\n        self.use_artist_only_if_no_release.setCheckable(True)\n        self.use_artist_only_if_no_release.setChecked(False)\n        self.use_artist_only_if_no_release.setObjectName(\"use_artist_only_if_no_release\")\n        self.hLayout_use_artist_no_release.addWidget(self.use_artist_only_if_no_release)\n        self.verticalLayout_8.addLayout(self.hLayout_use_artist_no_release)\n        self.hLayout_ignore_genres_from_artists_label = QtWidgets.QHBoxLayout()\n        self.hLayout_ignore_genres_from_artists_label.setObjectName(\"hLayout_ignore_genres_from_artists_label\")\n        spacerItem1 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)\n        self.hLayout_ignore_genres_from_artists_label.addItem(spacerItem1)\n        self.ignore_genres_from_these_artists_label = QtWidgets.QLabel(self.artist_groupBox)\n        self.ignore_genres_from_these_artists_label.setEnabled(False)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.ignore_genres_from_these_artists_label.sizePolicy().hasHeightForWidth())\n        self.ignore_genres_from_these_artists_label.setSizePolicy(sizePolicy)\n        self.ignore_genres_from_these_artists_label.setObjectName(\"ignore_genres_from_these_artists_label\")\n        self.hLayout_ignore_genres_from_artists_label.addWidget(self.ignore_genres_from_these_artists_label)\n        self.verticalLayout_8.addLayout(self.hLayout_ignore_genres_from_artists_label)\n        self.hLayout_ignore_genres_from_artists = QtWidgets.QHBoxLayout()\n        self.hLayout_ignore_genres_from_artists.setObjectName(\"hLayout_ignore_genres_from_artists\")\n        spacerItem2 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)\n        self.hLayout_ignore_genres_from_artists.addItem(spacerItem2)\n        self.ignore_genres_from_these_artists = QtWidgets.QLineEdit(self.artist_groupBox)\n        self.ignore_genres_from_these_artists.setEnabled(False)\n        self.ignore_genres_from_these_artists.setObjectName(\"ignore_genres_from_these_artists\")\n        self.hLayout_ignore_genres_from_artists.addWidget(self.ignore_genres_from_these_artists)\n        self.verticalLayout_8.addLayout(self.hLayout_ignore_genres_from_artists)\n        self.verticalLayout_2.addWidget(self.artist_groupBox)\n        self.work_groupBox = QtWidgets.QGroupBox(WikidataOptionsPage)\n        self.work_groupBox.setEnabled(True)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.work_groupBox.sizePolicy().hasHeightForWidth())\n        self.work_groupBox.setSizePolicy(sizePolicy)\n        self.work_groupBox.setObjectName(\"work_groupBox\")\n        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.work_groupBox)\n        self.verticalLayout_3.setObjectName(\"verticalLayout_3\")\n        self.use_work_genres = QtWidgets.QCheckBox(self.work_groupBox)\n        self.use_work_genres.setChecked(True)\n        self.use_work_genres.setObjectName(\"use_work_genres\")\n        self.verticalLayout_3.addWidget(self.use_work_genres)\n        self.verticalLayout_2.addWidget(self.work_groupBox)\n        self.generalSettings_groupBox = QtWidgets.QGroupBox(WikidataOptionsPage)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.generalSettings_groupBox.sizePolicy().hasHeightForWidth())\n        self.generalSettings_groupBox.setSizePolicy(sizePolicy)\n        self.generalSettings_groupBox.setMinimumSize(QtCore.QSize(0, 120))\n        self.generalSettings_groupBox.setObjectName(\"generalSettings_groupBox\")\n        self.verticalLayout = QtWidgets.QVBoxLayout(self.generalSettings_groupBox)\n        self.verticalLayout.setObjectName(\"verticalLayout\")\n        self.ignore_genres_label = QtWidgets.QLabel(self.generalSettings_groupBox)\n        self.ignore_genres_label.setObjectName(\"ignore_genres_label\")\n        self.verticalLayout.addWidget(self.ignore_genres_label)\n        self.hLayout_ignore_these_genres = QtWidgets.QHBoxLayout()\n        self.hLayout_ignore_these_genres.setObjectName(\"hLayout_ignore_these_genres\")\n        spacerItem3 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)\n        self.hLayout_ignore_these_genres.addItem(spacerItem3)\n        self.ignore_these_genres = QtWidgets.QLineEdit(self.generalSettings_groupBox)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.ignore_these_genres.sizePolicy().hasHeightForWidth())\n        self.ignore_these_genres.setSizePolicy(sizePolicy)\n        self.ignore_these_genres.setObjectName(\"ignore_these_genres\")\n        self.hLayout_ignore_these_genres.addWidget(self.ignore_these_genres)\n        self.verticalLayout.addLayout(self.hLayout_ignore_these_genres)\n        self.genre_delimiter_label = QtWidgets.QLabel(self.generalSettings_groupBox)\n        self.genre_delimiter_label.setEnabled(False)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.genre_delimiter_label.sizePolicy().hasHeightForWidth())\n        self.genre_delimiter_label.setSizePolicy(sizePolicy)\n        self.genre_delimiter_label.setWordWrap(False)\n        self.genre_delimiter_label.setObjectName(\"genre_delimiter_label\")\n        self.verticalLayout.addWidget(self.genre_delimiter_label)\n        self.hLayout_genre_delimiter = QtWidgets.QHBoxLayout()\n        self.hLayout_genre_delimiter.setObjectName(\"hLayout_genre_delimiter\")\n        spacerItem4 = QtWidgets.QSpacerItem(20, 17, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)\n        self.hLayout_genre_delimiter.addItem(spacerItem4)\n        self.genre_delimiter = QtWidgets.QComboBox(self.generalSettings_groupBox)\n        self.genre_delimiter.setEnabled(False)\n        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)\n        sizePolicy.setHorizontalStretch(0)\n        sizePolicy.setVerticalStretch(0)\n        sizePolicy.setHeightForWidth(self.genre_delimiter.sizePolicy().hasHeightForWidth())\n        self.genre_delimiter.setSizePolicy(sizePolicy)\n        self.genre_delimiter.setEditable(True)\n        self.genre_delimiter.setObjectName(\"genre_delimiter\")\n        self.genre_delimiter.addItem(\"\")\n        self.genre_delimiter.setItemText(0, \" / \")\n        self.genre_delimiter.addItem(\"\")\n        self.genre_delimiter.addItem(\"\")\n        self.genre_delimiter.setItemText(2, \";\")\n        self.genre_delimiter.addItem(\"\")\n        self.genre_delimiter.addItem(\"\")\n        self.genre_delimiter.setItemText(4, \", \")\n        self.hLayout_genre_delimiter.addWidget(self.genre_delimiter)\n        spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)\n        self.hLayout_genre_delimiter.addItem(spacerItem5)\n        self.verticalLayout.addLayout(self.hLayout_genre_delimiter)\n        self.verticalLayout_2.addWidget(self.generalSettings_groupBox)\n        spacerItem6 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)\n        self.verticalLayout_2.addItem(spacerItem6)\n\n        self.retranslateUi(WikidataOptionsPage)\n        self.use_artist_genres.toggled['bool'].connect(self.ignore_genres_from_these_artists_label.setEnabled)\n        self.use_artist_genres.toggled['bool'].connect(self.use_artist_only_if_no_release.setEnabled)\n        self.use_artist_genres.toggled['bool'].connect(self.ignore_genres_from_these_artists.setEnabled)\n        QtCore.QMetaObject.connectSlotsByName(WikidataOptionsPage)\n\n    def retranslateUi(self, WikidataOptionsPage):\n        _translate = QtCore.QCoreApplication.translate\n        self.releaseGroup_groupBox.setTitle(_translate(\"WikidataOptionsPage\", \"Release Group Genre Settings\"))\n        self.use_release_group_genres.setText(_translate(\"WikidataOptionsPage\", \"Use Release Group genres\"))\n        self.artist_groupBox.setTitle(_translate(\"WikidataOptionsPage\", \"Artist Genre Settings\"))\n        self.use_artist_genres.setText(_translate(\"WikidataOptionsPage\", \"Use Artist genres\"))\n        self.use_artist_only_if_no_release.setText(_translate(\"WikidataOptionsPage\", \"Use Artist genres only if no Release Group genres exist\"))\n        self.ignore_genres_from_these_artists_label.setText(_translate(\"WikidataOptionsPage\", \"Ignore Artist genres from these Artist(s):  (comma separated regular expressions)\"))\n        self.work_groupBox.setTitle(_translate(\"WikidataOptionsPage\", \"Work Genre Settings\"))\n        self.use_work_genres.setText(_translate(\"WikidataOptionsPage\", \"Use Work genres, when applicable\"))\n        self.generalSettings_groupBox.setTitle(_translate(\"WikidataOptionsPage\", \"General Settings\"))\n        self.ignore_genres_label.setText(_translate(\"WikidataOptionsPage\", \"Ignore these genres:   (comma separated regular expressions)\"))\n        self.genre_delimiter_label.setText(_translate(\"WikidataOptionsPage\", \"Genre Delimiter: (only applicable to ID3v2.3 tags)\"))\n        self.genre_delimiter.setItemText(1, _translate(\"WikidataOptionsPage\", \"; \"))\n        self.genre_delimiter.setItemText(3, _translate(\"WikidataOptionsPage\", \" ; \"))\n"
  },
  {
    "path": "plugins/wikidata/ui_options_wikidata.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>WikidataOptionsPage</class>\n <widget class=\"QWidget\" name=\"WikidataOptionsPage\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>602</width>\n    <height>512</height>\n   </rect>\n  </property>\n  <layout class=\"QVBoxLayout\" name=\"verticalLayout_2\">\n   <item>\n    <widget class=\"QGroupBox\" name=\"releaseGroup_groupBox\">\n     <property name=\"enabled\">\n      <bool>true</bool>\n     </property>\n     <property name=\"sizePolicy\">\n      <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n       <horstretch>0</horstretch>\n       <verstretch>0</verstretch>\n      </sizepolicy>\n     </property>\n     <property name=\"minimumSize\">\n      <size>\n       <width>0</width>\n       <height>0</height>\n      </size>\n     </property>\n     <property name=\"title\">\n      <string>Release Group Genre Settings</string>\n     </property>\n     <layout class=\"QVBoxLayout\" name=\"verticalLayout_4\">\n      <item>\n       <widget class=\"QCheckBox\" name=\"use_release_group_genres\">\n        <property name=\"enabled\">\n         <bool>true</bool>\n        </property>\n        <property name=\"sizePolicy\">\n         <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Fixed\">\n          <horstretch>0</horstretch>\n          <verstretch>0</verstretch>\n         </sizepolicy>\n        </property>\n        <property name=\"text\">\n         <string>Use Release Group genres</string>\n        </property>\n        <property name=\"checked\">\n         <bool>true</bool>\n        </property>\n       </widget>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <widget class=\"QGroupBox\" name=\"artist_groupBox\">\n     <property name=\"enabled\">\n      <bool>true</bool>\n     </property>\n     <property name=\"sizePolicy\">\n      <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n       <horstretch>0</horstretch>\n       <verstretch>0</verstretch>\n      </sizepolicy>\n     </property>\n     <property name=\"minimumSize\">\n      <size>\n       <width>0</width>\n       <height>0</height>\n      </size>\n     </property>\n     <property name=\"title\">\n      <string>Artist Genre Settings</string>\n     </property>\n     <layout class=\"QVBoxLayout\" name=\"verticalLayout_8\">\n      <item>\n       <widget class=\"QCheckBox\" name=\"use_artist_genres\">\n        <property name=\"text\">\n         <string>Use Artist genres</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <layout class=\"QHBoxLayout\" name=\"hLayout_use_artist_no_release\">\n        <item>\n         <spacer name=\"horizontalSpacer_3\">\n          <property name=\"orientation\">\n           <enum>Qt::Horizontal</enum>\n          </property>\n          <property name=\"sizeType\">\n           <enum>QSizePolicy::Fixed</enum>\n          </property>\n          <property name=\"sizeHint\" stdset=\"0\">\n           <size>\n            <width>20</width>\n            <height>20</height>\n           </size>\n          </property>\n         </spacer>\n        </item>\n        <item>\n         <widget class=\"QCheckBox\" name=\"use_artist_only_if_no_release\">\n          <property name=\"enabled\">\n           <bool>false</bool>\n          </property>\n          <property name=\"sizePolicy\">\n           <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Fixed\">\n            <horstretch>0</horstretch>\n            <verstretch>0</verstretch>\n           </sizepolicy>\n          </property>\n          <property name=\"text\">\n           <string>Use Artist genres only if no Release Group genres exist</string>\n          </property>\n          <property name=\"checkable\">\n           <bool>true</bool>\n          </property>\n          <property name=\"checked\">\n           <bool>false</bool>\n          </property>\n         </widget>\n        </item>\n       </layout>\n      </item>\n      <item>\n       <layout class=\"QHBoxLayout\" name=\"hLayout_ignore_genres_from_artists_label\">\n        <item>\n         <spacer name=\"horizontalSpacer_2\">\n          <property name=\"orientation\">\n           <enum>Qt::Horizontal</enum>\n          </property>\n          <property name=\"sizeType\">\n           <enum>QSizePolicy::Fixed</enum>\n          </property>\n          <property name=\"sizeHint\" stdset=\"0\">\n           <size>\n            <width>20</width>\n            <height>20</height>\n           </size>\n          </property>\n         </spacer>\n        </item>\n        <item>\n         <widget class=\"QLabel\" name=\"ignore_genres_from_these_artists_label\">\n          <property name=\"enabled\">\n           <bool>false</bool>\n          </property>\n          <property name=\"sizePolicy\">\n           <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Fixed\">\n            <horstretch>0</horstretch>\n            <verstretch>0</verstretch>\n           </sizepolicy>\n          </property>\n          <property name=\"text\">\n           <string>Ignore Artist genres from these Artist(s):  (comma separated regular expressions)</string>\n          </property>\n         </widget>\n        </item>\n       </layout>\n      </item>\n      <item>\n       <layout class=\"QHBoxLayout\" name=\"hLayout_ignore_genres_from_artists\">\n        <item>\n         <spacer name=\"horizontalSpacer_4\">\n          <property name=\"orientation\">\n           <enum>Qt::Horizontal</enum>\n          </property>\n          <property name=\"sizeType\">\n           <enum>QSizePolicy::Fixed</enum>\n          </property>\n          <property name=\"sizeHint\" stdset=\"0\">\n           <size>\n            <width>20</width>\n            <height>20</height>\n           </size>\n          </property>\n         </spacer>\n        </item>\n        <item>\n         <widget class=\"QLineEdit\" name=\"ignore_genres_from_these_artists\">\n          <property name=\"enabled\">\n           <bool>false</bool>\n          </property>\n         </widget>\n        </item>\n       </layout>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <widget class=\"QGroupBox\" name=\"work_groupBox\">\n     <property name=\"enabled\">\n      <bool>true</bool>\n     </property>\n     <property name=\"sizePolicy\">\n      <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n       <horstretch>0</horstretch>\n       <verstretch>0</verstretch>\n      </sizepolicy>\n     </property>\n     <property name=\"title\">\n      <string>Work Genre Settings</string>\n     </property>\n     <layout class=\"QVBoxLayout\" name=\"verticalLayout_3\">\n      <item>\n       <widget class=\"QCheckBox\" name=\"use_work_genres\">\n        <property name=\"text\">\n         <string>Use Work genres, when applicable</string>\n        </property>\n        <property name=\"checked\">\n         <bool>true</bool>\n        </property>\n       </widget>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <widget class=\"QGroupBox\" name=\"generalSettings_groupBox\">\n     <property name=\"sizePolicy\">\n      <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Fixed\">\n       <horstretch>0</horstretch>\n       <verstretch>0</verstretch>\n      </sizepolicy>\n     </property>\n     <property name=\"minimumSize\">\n      <size>\n       <width>0</width>\n       <height>120</height>\n      </size>\n     </property>\n     <property name=\"title\">\n      <string>General Settings</string>\n     </property>\n     <layout class=\"QVBoxLayout\" name=\"verticalLayout\">\n      <item>\n       <widget class=\"QLabel\" name=\"ignore_genres_label\">\n        <property name=\"text\">\n         <string>Ignore these genres:   (comma separated regular expressions)</string>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <layout class=\"QHBoxLayout\" name=\"hLayout_ignore_these_genres\">\n        <item>\n         <spacer name=\"horizontalSpacer_6\">\n          <property name=\"orientation\">\n           <enum>Qt::Horizontal</enum>\n          </property>\n          <property name=\"sizeType\">\n           <enum>QSizePolicy::Fixed</enum>\n          </property>\n          <property name=\"sizeHint\" stdset=\"0\">\n           <size>\n            <width>20</width>\n            <height>20</height>\n           </size>\n          </property>\n         </spacer>\n        </item>\n        <item>\n         <widget class=\"QLineEdit\" name=\"ignore_these_genres\">\n          <property name=\"sizePolicy\">\n           <sizepolicy hsizetype=\"Expanding\" vsizetype=\"Fixed\">\n            <horstretch>0</horstretch>\n            <verstretch>0</verstretch>\n           </sizepolicy>\n          </property>\n         </widget>\n        </item>\n       </layout>\n      </item>\n      <item>\n       <widget class=\"QLabel\" name=\"genre_delimiter_label\">\n        <property name=\"enabled\">\n         <bool>false</bool>\n        </property>\n        <property name=\"sizePolicy\">\n         <sizepolicy hsizetype=\"Preferred\" vsizetype=\"Preferred\">\n          <horstretch>0</horstretch>\n          <verstretch>0</verstretch>\n         </sizepolicy>\n        </property>\n        <property name=\"text\">\n         <string>Genre Delimiter: (only applicable to ID3v2.3 tags)</string>\n        </property>\n        <property name=\"wordWrap\">\n         <bool>false</bool>\n        </property>\n       </widget>\n      </item>\n      <item>\n       <layout class=\"QHBoxLayout\" name=\"hLayout_genre_delimiter\">\n        <item>\n         <spacer name=\"horizontalSpacer_7\">\n          <property name=\"orientation\">\n           <enum>Qt::Horizontal</enum>\n          </property>\n          <property name=\"sizeType\">\n           <enum>QSizePolicy::Fixed</enum>\n          </property>\n          <property name=\"sizeHint\" stdset=\"0\">\n           <size>\n            <width>20</width>\n            <height>17</height>\n           </size>\n          </property>\n         </spacer>\n        </item>\n        <item>\n         <widget class=\"QComboBox\" name=\"genre_delimiter\">\n          <property name=\"enabled\">\n           <bool>false</bool>\n          </property>\n          <property name=\"sizePolicy\">\n           <sizepolicy hsizetype=\"Fixed\" vsizetype=\"Fixed\">\n            <horstretch>0</horstretch>\n            <verstretch>0</verstretch>\n           </sizepolicy>\n          </property>\n          <property name=\"editable\">\n           <bool>true</bool>\n          </property>\n          <item>\n           <property name=\"text\">\n            <string notr=\"true\" extracomment=\"default used by Kodi\"> / </string>\n           </property>\n          </item>\n          <item>\n           <property name=\"text\">\n            <string>; </string>\n           </property>\n          </item>\n          <item>\n           <property name=\"text\">\n            <string notr=\"true\">;</string>\n           </property>\n          </item>\n          <item>\n           <property name=\"text\">\n            <string> ; </string>\n           </property>\n          </item>\n          <item>\n           <property name=\"text\">\n            <string notr=\"true\">, </string>\n           </property>\n          </item>\n         </widget>\n        </item>\n        <item>\n         <spacer name=\"horizontalSpacer_8\">\n          <property name=\"orientation\">\n           <enum>Qt::Horizontal</enum>\n          </property>\n          <property name=\"sizeHint\" stdset=\"0\">\n           <size>\n            <width>40</width>\n            <height>20</height>\n           </size>\n          </property>\n         </spacer>\n        </item>\n       </layout>\n      </item>\n     </layout>\n    </widget>\n   </item>\n   <item>\n    <spacer name=\"verticalSpacer\">\n     <property name=\"orientation\">\n      <enum>Qt::Vertical</enum>\n     </property>\n     <property name=\"sizeHint\" stdset=\"0\">\n      <size>\n       <width>20</width>\n       <height>40</height>\n      </size>\n     </property>\n    </spacer>\n   </item>\n  </layout>\n </widget>\n <resources/>\n <connections>\n  <connection>\n   <sender>use_artist_genres</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>ignore_genres_from_these_artists_label</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>300</x>\n     <y>58</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>313</x>\n     <y>111</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_artist_genres</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>use_artist_only_if_no_release</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>300</x>\n     <y>58</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>313</x>\n     <y>83</y>\n    </hint>\n   </hints>\n  </connection>\n  <connection>\n   <sender>use_artist_genres</sender>\n   <signal>toggled(bool)</signal>\n   <receiver>ignore_genres_from_these_artists</receiver>\n   <slot>setEnabled(bool)</slot>\n   <hints>\n    <hint type=\"sourcelabel\">\n     <x>300</x>\n     <y>58</y>\n    </hint>\n    <hint type=\"destinationlabel\">\n     <x>313</x>\n     <y>139</y>\n    </hint>\n   </hints>\n  </connection>\n </connections>\n</ui>\n"
  },
  {
    "path": "plugins/workandmovement/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2018-2019, 2021-2022 Philipp Wolfer\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n# 02110-1301, USA.\n\nPLUGIN_NAME = 'Work & Movement'\nPLUGIN_AUTHOR = 'Philipp Wolfer'\nPLUGIN_DESCRIPTION = '''Set work and movement based on work relationships.\n\nThis plugin attempts to set the `movement` and `movementnumber` tags, but only\nif the work linked to the recording is part of a larger work. The `work` tag\nthen gets set to the work of which the movement is a part of.\n\nIf the recording is only linked to a simple work without separate parts then it\nis not considered a proper work and the work and movement related tags will be\ncleared.\n\nThe plugin will always set the original values, as loaded from MusicBrainz, of\nthe `work` and `musicbrainz_workid` tags into the variables `%%recording_work%`\nand `%%recording_workid%` to be used in scripting.\n'''\nPLUGIN_VERSION = '1.1'\nPLUGIN_API_VERSIONS = ['2.1', '2.2', '2.3', '2.4', '2.5', '2.6', '2.7', '2.8']\nPLUGIN_LICENSE = 'GPL-2.0-or-later'\nPLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl-2.0.html'\n\n\nimport re\n\nfrom .roman import (\n    fromRoman,\n    RomanError,\n)\n\nfrom picard import log\nfrom picard.metadata import register_track_metadata_processor\n\n\n_re_work_title = re.compile(r'(?P<work>.*):\\s+(?P<movementnumber>[IVXLCDM]+)\\.\\s[\\s,:;./]*(?P<movement>.*)')\n_re_part_number = re.compile(r'(?P<number>[0-9IVXLCDM]+)\\.?\\s[\\s,:;./]*')\n_re_separators = re.compile(r'^[\\s,:;./-]+')\n\n\nclass Work:\n    def __init__(self, title, mbid=None):\n        self.mbid = mbid\n        self.title = title\n        self.is_movement = False\n        self.is_work = False\n        self.part_number = 0\n        self.parent = None\n\n    def __str__(self):\n        s = []\n        if self.parent:\n            s.append(str(self.parent))\n        if self.is_movement:\n            work_type = 'Movement'\n        elif self.is_work:\n            work_type = 'Work'\n        else:\n            work_type = 'Unknown'\n        s.append('%s %i: %s' % (work_type, self.part_number, self.title))\n        return '\\n'.join(s)\n\n\ndef is_performance_work(rel):\n    return (rel['target-type'] == 'work'\n            and rel['direction'] == 'forward'\n            and rel['type'] == 'performance')\n\n\ndef is_parent_work(rel):\n    return (rel['target-type'] == 'work'\n            and rel['direction'] == 'backward'\n            and rel['type'] == 'parts')\n\n\ndef is_movement_like(rel):\n    return ('movement' in rel['attributes']\n            or 'act' in rel['attributes']\n            or 'ordering-key' in rel)\n\n\ndef is_child_work(rel):\n    return (rel['target-type'] == 'work'\n            and rel['direction'] == 'forward'\n            and rel['type'] == 'parts')\n\n\ndef number_to_int(s):\n    \"\"\"\n    Converts a numeric string to int. `s` can also be a Roman numeral.\n    \"\"\"\n    try:\n        return int(s)\n    except ValueError:\n        try:\n            return fromRoman(s)\n        except RomanError as e:\n            raise ValueError(e)\n\n\ndef parse_work_name(title):\n    return _re_work_title.search(title)\n\n\ndef create_work_and_movement_from_title(work):\n    \"\"\"\n    Attempts to parse work.title in the form \"<Work>: <Number>. <Movement>\",\n    where <Number> is in Roman numerals.\n    Sets the `is_movement` and `part_number` properties on `work` and creates\n    a `parent` work if not already present.\n    \"\"\"\n    title = work.title\n    match = parse_work_name(title)\n    if match:\n        work.title = match.group('movement')\n        work.is_movement = True\n        try:\n            number = number_to_int(match.group('movementnumber'))\n        except ValueError as e:\n            log.error(e)\n            number = 0\n        if not work.part_number:\n            work.part_number = number\n        elif work.part_number != number:\n            log.warning('Movement number mismatch for \"%s\": %s != %i',\n                        title, match.group('movementnumber'), work.part_number)\n        if not work.parent:\n            work.parent = Work(match.group('work'))\n            work.parent.is_work = True\n        elif work.parent.title != match.group('work'):\n            log.warning('Movement work name mismatch for \"%s\": \"%s\" != \"%s\"',\n                        title, match.group('work'), work.parent.title)\n    return work\n\n\ndef normalize_movement_title(work):\n    \"\"\"\n    Removes the parent work title and part number from the beginning of\n    `work.title`. This ensures movement names don't contain duplicated\n    information even if they do not follow the strict naming format used by\n    `create_work_and_movement_from_title`.\n    \"\"\"\n    movement_title = work.title\n    if work.parent:\n        work_title = work.parent.title\n        if movement_title.startswith(work_title):\n            movement_title = movement_title[len(work_title):]\n            movement_title = _re_separators.sub('', movement_title)\n    match = _re_part_number.match(movement_title)\n    if match:\n        # Only remove the number if it matches the part_number\n        try:\n            number = number_to_int(match.group('number'))\n            if number == work.part_number:\n                movement_title = _re_part_number.sub('', movement_title)\n        except ValueError as e:\n            log.warning(e)\n    return movement_title\n\n\ndef parse_work(work_rel):\n    work = Work(work_rel['title'], work_rel['id'])\n    if 'relations' in work_rel:\n        for rel in work_rel['relations']:\n            # If this work has parents and is linked to those as 'movement' or\n            # 'act' we consider it a part of a larger work and store it\n            # in the movement tag. The parent will be set as the work.\n            if is_parent_work(rel):\n                if is_movement_like(rel):\n                    work.is_movement = True\n                    work.part_number = rel.get('ordering-key')\n                    if 'work' in rel:\n                        work.parent = parse_work(rel['work'])\n                        work.parent.is_work = True\n                    work.title = normalize_movement_title(work)\n                else:\n                    # Not a movement, but still part of a larger work.\n                    # Mark it as a work.\n                    work.is_work = True\n            # If this work has any parts, we consider it a proper work.\n            # This is a recording directly linked to a larger work.\n            if is_child_work(rel):\n                work.is_work = True\n    return work\n\n\ndef unset_work(metadata):\n    metadata.delete('work')\n    metadata.delete('musicbrainz_workid')\n    metadata.delete('movement')\n    metadata.delete('movementnumber')\n    metadata.delete('movementtotal')\n    metadata.delete('showmovement')\n\n\ndef set_work(metadata, work):\n    metadata['work'] = work.title\n    metadata['musicbrainz_workid'] = work.mbid\n    metadata['showmovement'] = 1\n\n\ndef process_track(album, metadata, track, release):\n    if 'recording' in track:\n        recording = track['recording']\n    else:\n        recording = track\n\n    if 'relations' not in recording:\n        return\n\n    # Remember original work and work ID\n    metadata['~recording_work'] = metadata.getall('work')\n    metadata['~recording_workid'] = metadata.getall('musicbrainz_workid')\n\n    # Try to find a suitable work linked to the recording.\n    # As a fallback try to get work + movement information by parsing\n    # the recording title.\n    work = Work(recording['title'])\n    for rel in recording['relations']:\n        if is_performance_work(rel):\n            work = parse_work(rel['work'])\n            # Only use the first work that qualifies as a work or movement\n            log.debug('Found work:\\n%s', work)\n            if work.is_movement or work.is_work:\n                break\n\n    unset_work(metadata)\n    if not work.is_movement:\n        work = create_work_and_movement_from_title(work)\n\n    if work.is_movement and work.parent and work.parent.is_work:\n        metadata['movement'] = work.title\n        if work.part_number:\n            metadata['movementnumber'] = work.part_number\n        set_work(metadata, work.parent)\n    elif work.is_work:\n        set_work(metadata, work)\n\n\nregister_track_metadata_processor(process_track)\n"
  },
  {
    "path": "plugins/workandmovement/roman.py",
    "content": "\"\"\"Convert to and from Roman numerals\"\"\"\n\n__author__ = \"Mark Pilgrim (f8dy@diveintopython.org)\"\n__version__ = \"1.4\"\n__date__ = \"8 August 2001\"\n__copyright__ = \"\"\"Copyright (c) 2001 Mark Pilgrim\n\nThis program is part of \"Dive Into Python\", a free Python tutorial for\nexperienced programmers.  Visit http://diveintopython.org/ for the\nlatest version.\n\nThis program is free software; you can redistribute it and/or modify\nit under the terms of the Python 2.1.1 license, available at\nhttp://www.python.org/2.1.1/license.html\n\"\"\"\n\nimport re\n\n#Define exceptions\nclass RomanError(Exception): pass\nclass OutOfRangeError(RomanError): pass\nclass NotIntegerError(RomanError): pass\nclass InvalidRomanNumeralError(RomanError): pass\n\n#Define digit mapping\nromanNumeralMap = (('M',  1000),\n                   ('CM', 900),\n                   ('D',  500),\n                   ('CD', 400),\n                   ('C',  100),\n                   ('XC', 90),\n                   ('L',  50),\n                   ('XL', 40),\n                   ('X',  10),\n                   ('IX', 9),\n                   ('V',  5),\n                   ('IV', 4),\n                   ('I',  1))\n\ndef toRoman(n):\n    \"\"\"convert integer to Roman numeral\"\"\"\n    if not isinstance(n, int):\n        raise NotIntegerError(\"decimals can not be converted\")\n    if not (0 < n < 5000):\n        raise OutOfRangeError(\"number out of range (must be 1..4999)\")\n\n    result = \"\"\n    for numeral, integer in romanNumeralMap:\n        while n >= integer:\n            result += numeral\n            n -= integer\n    return result\n\n#Define pattern to detect valid Roman numerals\nromanNumeralPattern = re.compile(\"\"\"\n    ^                   # beginning of string\n    M{0,4}              # thousands - 0 to 4 M's\n    (CM|CD|D?C{0,3})    # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's),\n                        #            or 500-800 (D, followed by 0 to 3 C's)\n    (XC|XL|L?X{0,3})    # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's),\n                        #        or 50-80 (L, followed by 0 to 3 X's)\n    (IX|IV|V?I{0,3})    # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's),\n                        #        or 5-8 (V, followed by 0 to 3 I's)\n    $                   # end of string\n    \"\"\" ,re.VERBOSE)\n\ndef fromRoman(s):\n    \"\"\"convert Roman numeral to integer\"\"\"\n    if not s:\n        raise InvalidRomanNumeralError('Input can not be blank')\n    if not romanNumeralPattern.search(s):\n        raise InvalidRomanNumeralError('Invalid Roman numeral: %s' % s)\n\n    result = 0\n    index = 0\n    for numeral, integer in romanNumeralMap:\n        while s[index:index+len(numeral)] == numeral:\n            result += integer\n            index += len(numeral)\n    return result\n"
  },
  {
    "path": "setup.cfg",
    "content": "[flake8]\n# E127: continuation line over-indented for visual indent\n# E128: continuation line under-indented for visual indent\n# E129: visually indented line with same indent as next logical line\n# E226: missing whitespace around arithmetic operator\n# E241: multiple spaces after ','\n# E402: module level import not at top of file\n# E501: line too long (xx > 79 characters)\n# W503: line break occurred before a binary operator\nignore = E127,E128,E129,E226,E241,E402,E501,W503\nbuiltins = _,N_,ngettext,gettext_attributes,pgettext_attributes,gettext_countries\nexclude = ui_*.py\n"
  },
  {
    "path": "test/__init__.py",
    "content": ""
  },
  {
    "path": "test/plugin_test_case.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Picard, the next-generation MusicBrainz tagger\n#\n# Copyright (C) 2018 Wieland Hoffmann\n# Copyright (C) 2019-2023 Philipp Wolfer\n# Copyright (C) 2020-2021 Laurent Monin\n# Copyright (C) 2021 Bob Swift\n#\n# This program is free software; you can redistribute it and/or\n# modify it under the terms of the GNU General Public License\n# as published by the Free Software Foundation; either version 2\n# of the License, or (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program; if not, write to the Free Software\n# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n\n\nimport importlib\nimport logging\nimport os\nimport shutil\nimport sys\nimport unittest\nfrom tempfile import mkdtemp, mkstemp\nfrom types import ModuleType\nfrom typing import Any, Callable, Optional\nfrom unittest.mock import Mock, patch\n\nimport picard\nfrom picard import config, log\nfrom picard.i18n import setup_gettext\nfrom picard.plugin import _PLUGIN_MODULE_PREFIX, _unregister_module_extensions\nfrom picard.pluginmanager import PluginManager\nfrom picard.releasegroup import ReleaseGroup\nfrom PyQt5 import QtCore\n\n\nclass FakeThreadPool(QtCore.QObject):\n\n    def start(self, runnable, priority):\n        runnable.run()\n\n\nclass FakeTagger(QtCore.QObject):\n\n    tagger_stats_changed = QtCore.pyqtSignal()\n\n    def __init__(self):\n        QtCore.QObject.__init__(self)\n        QtCore.QObject.config = config\n        QtCore.QObject.log = log\n        self.tagger_stats_changed.connect(self.emit)\n        self.exit_cleanup = []\n        self.files = {}\n        self.stopping = False\n        self.thread_pool = FakeThreadPool()\n        self.priority_thread_pool = FakeThreadPool()\n        self.save_thread_pool = FakeThreadPool()\n        self.mb_api = Mock()\n\n    def register_cleanup(self, func: Callable[[], Any]) -> None:\n        self.exit_cleanup.append(func)\n\n    def run_cleanup(self) -> None:\n        for f in self.exit_cleanup:\n            f()\n\n    def emit(self, *args) -> None:\n        pass\n\n    def get_release_group_by_id(self, rg_id: str) -> ReleaseGroup:\n        return ReleaseGroup(rg_id)\n\n\nclass PluginTestCase(unittest.TestCase):\n    def setUp(self) -> None:\n        log.set_level(logging.DEBUG)\n        setup_gettext(None, \"C\")\n        self.tagger = FakeTagger()\n        QtCore.QObject.tagger = self.tagger\n        QtCore.QCoreApplication.instance = lambda: self.tagger\n        self.addCleanup(self.tagger.run_cleanup)\n        self.init_config()\n\n        self.tmp_directory = self.mktmpdir()\n        # return tmp_directory from pluginmanager.plugin_dirs\n        self.patchers = [\n            patch(\n                \"picard.pluginmanager.plugin_dirs\", return_value=[self.tmp_directory]\n            ).start(),\n            patch(\n                \"picard.util.thread.to_main\",\n                side_effect=lambda func, *args, **kwargs: func(*args, **kwargs),\n            ).start(),\n        ]\n\n    def tearDown(self) -> None:\n        for patcher in self.patchers:\n            patcher.stop()\n\n    @staticmethod\n    def init_config() -> None:\n        fake_config = Mock()\n        fake_config.setting = {}\n        fake_config.persist = {}\n        fake_config.profiles = {}\n        # Make config object available for legacy use\n        config.config = fake_config\n        config.setting = fake_config.setting\n        config.persist = fake_config.persist\n        config.profiles = fake_config.profiles\n\n    @staticmethod\n    def set_config_values(\n        setting: Optional[dict] = None,\n        persist: Optional[dict] = None,\n        profiles: Optional[dict] = None,\n    ) -> None:\n        if setting:\n            for key, value in setting.items():\n                config.config.setting[key] = value\n        if persist:\n            for key, value in persist.items():\n                config.config.persist[key] = value\n        if profiles:\n            for key, value in profiles.items():\n                config.config.profiles[key] = value\n\n    def mktmpdir(self, ignore_errors: bool = False) -> None:\n        tmpdir = mkdtemp(suffix=self.__class__.__name__)\n        self.addCleanup(shutil.rmtree, tmpdir, ignore_errors=ignore_errors)\n        return tmpdir\n\n    def copy_file_tmp(self, filepath: str, ext: Optional[str] = None) -> str:\n        fd, copy = mkstemp(suffix=ext)\n        os.close(fd)\n        self.addCleanup(self.remove_file_tmp, copy)\n        shutil.copy(filepath, copy)\n        return copy\n\n    @staticmethod\n    def remove_file_tmp(filepath: str) -> None:\n        if os.path.isfile(filepath):\n            os.unlink(filepath)\n\n    def _test_plugin_install(self, name: str, module: str) -> ModuleType:\n        self.unload_plugin(module)\n        with self.assertRaises(ImportError):\n            importlib.import_module(f\"picard.plugins.{module}\")\n\n        pm = PluginManager(plugins_directory=self.tmp_directory)\n\n        msg = f\"install_plugin: {name} from {module}\"\n        pm.install_plugin(os.path.join(\"plugins\", module))\n        self.assertEqual(len(pm.plugins), 1, msg)\n        self.assertEqual(pm.plugins[0].name, name, msg)\n\n        self.set_config_values(setting={\"enabled_plugins\": [module]})\n\n        # if module is properly loaded, this should work\n        return importlib.import_module(f\"picard.plugins.{module}\")\n\n    def unload_plugin(self, plugin_name: str) -> None:\n        \"\"\"for testing purposes\"\"\"\n        _unregister_module_extensions(plugin_name)\n        if hasattr(picard.plugins, plugin_name):\n            delattr(picard.plugins, plugin_name)\n        key = _PLUGIN_MODULE_PREFIX + plugin_name\n        if key in sys.modules:\n            del sys.modules[key]\n"
  },
  {
    "path": "test/test_add_to_collection.py",
    "content": "import os\nfrom itertools import chain\nfrom test.plugin_test_case import PluginTestCase\nfrom typing import Union\nfrom unittest.mock import ANY, patch\n\nfrom picard.collection import Collection, user_collections\nfrom picard.file import File\nfrom picard.file import _file_post_save_processors as post_save_processors\nfrom picard.ui.options import _pages as option_pages\n\n\nclass TestAddToCollection(PluginTestCase):\n    SAVE_FILE_SETTINGS = {\n        \"dont_write_tags\": True,\n        \"rename_files\": False,\n        \"move_files\": False,\n        \"delete_empty_dirs\": False,\n        \"save_images_to_files\": False,\n        \"clear_existing_tags\": False,\n        \"compare_ignore_tags\": [],\n    }\n\n    def install_plugin(self) -> None:\n        self.plugin = self._test_plugin_install(\n            \"Add to Collection\", \"add_to_collection\"\n        )\n\n    def create_file(self, file_name: str, album_id: Union[str, None] = None) -> File:\n        file_path = os.path.join(self.tmp_directory, file_name)\n        file = File(file_path)\n        if album_id:\n            file.metadata[\"musicbrainz_albumid\"] = album_id\n        self.tagger.files[file_path] = file\n        return file\n\n    def tearDown(self) -> None:\n        user_collections.clear()\n        return super().tearDown()\n\n    def test_hooks_installed(self) -> None:\n        self.install_plugin()\n\n        self.assertIn(\n            self.plugin.post_save_processor.post_save_processor,\n            list(chain.from_iterable(post_save_processors.functions.values())),\n        )\n\n        self.assertIn(\n            self.plugin.options.AddToCollectionOptionsPage, list(option_pages)\n        )\n\n    def test_file_save(self) -> None:\n        self.install_plugin()\n\n        self.set_config_values(\n            setting={\n                **self.SAVE_FILE_SETTINGS,\n                \"add_to_collection_id\": \"test_collection_id\",\n            }\n        )\n\n        file = self.create_file(file_name=\"test.mp3\", album_id=\"test_album_id\")\n        user_collections[\"test_collection_id\"] = Collection(\n            collection_id=\"test_collection_id\", name=\"Test Collection\", size=0\n        )\n        with patch(\"picard.collection.Collection.add_releases\") as add_releases:\n            file.save()\n            add_releases.assert_called_with(set([\"test_album_id\"]), callback=ANY)\n\n    def test_two_files_save(self) -> None:\n        self.install_plugin()\n\n        self.set_config_values(\n            setting={\n                **self.SAVE_FILE_SETTINGS,\n                \"add_to_collection_id\": \"test_collection_id\",\n            }\n        )\n\n        collection = Collection(\n            collection_id=\"test_collection_id\", name=\"Test Collection\", size=0\n        )\n        user_collections[\"test_collection_id\"] = collection\n        with patch(\n            \"picard.collection.Collection.add_releases\",\n            side_effect=lambda ids, callback: collection.releases.update(ids),\n        ) as add_releases:\n            for i in range(2):\n                file = self.create_file(\n                    file_name=f\"test{i}.mp3\", album_id=\"test_album_id\"\n                )\n                file.save()\n            # only added once\n            add_releases.assert_called_once_with(set([\"test_album_id\"]), callback=ANY)\n\n    def test_no_collection_id_setting(self) -> None:\n        self.install_plugin()\n\n        self.set_config_values(setting=self.SAVE_FILE_SETTINGS)\n\n        file = self.create_file(file_name=\"test.mp3\", album_id=\"test_album_id\")\n        user_collections[\"test_collection_id\"] = Collection(\n            collection_id=\"test_collection_id\", name=\"Test Collection\", size=0\n        )\n        with patch(\"picard.collection.Collection.add_releases\") as add_releases:\n            file.save()\n            add_releases.assert_not_called()\n\n    def test_no_user_collection(self) -> None:\n        self.install_plugin()\n\n        self.set_config_values(\n            setting={\n                **self.SAVE_FILE_SETTINGS,\n                \"add_to_collection_id\": \"test_collection_id\",\n            }\n        )\n\n        file = self.create_file(file_name=\"test.mp3\", album_id=\"test_album_id\")\n        with patch(\"picard.collection.Collection.add_releases\") as add_releases:\n            file.save()\n            add_releases.assert_not_called()\n\n    def test_no_release(self) -> None:\n        self.install_plugin()\n\n        self.set_config_values(\n            setting={\n                **self.SAVE_FILE_SETTINGS,\n                \"add_to_collection_id\": \"test_collection_id\",\n            }\n        )\n\n        file = self.create_file(file_name=\"test.mp3\")\n        with patch(\"picard.collection.Collection.add_releases\") as add_releases:\n            file.save()\n            add_releases.assert_not_called()\n"
  },
  {
    "path": "test/test_doctest.py",
    "content": "import doctest\n\n\ndef load_tests(loader, tests, ignore):\n    from plugins.addrelease import addrelease\n    tests.addTests(doctest.DocTestSuite(addrelease))\n\n    from plugins import decade\n    tests.addTests(doctest.DocTestSuite(decade))\n\n    from plugins.smart_title_case import smart_title_case\n    tests.addTests(doctest.DocTestSuite(smart_title_case))\n\n    from plugins.standardise_feat import standardise_feat\n    tests.addTests(doctest.DocTestSuite(standardise_feat))\n\n    from plugins.key_wheel_converter import key_wheel_converter\n    tests.addTests(doctest.DocTestSuite(key_wheel_converter))\n\n    from plugins import enhanced_titles\n    tests.addTests(doctest.DocTestSuite(enhanced_titles))\n    return tests\n"
  },
  {
    "path": "test/test_generate.py",
    "content": "import doctest\nimport os\nimport glob\nimport json\nimport shutil\nimport tempfile\nimport unittest\nfrom contextlib import redirect_stdout\nfrom io import StringIO\nfrom generate import build_json, zip_files\n\n\nclass GenerateTestCase(unittest.TestCase):\n\n    \"\"\"Run tests\"\"\"\n\n    # The file that contains json data\n    PLUGIN_FILE = \"plugins.json\"\n\n    # The directory which contains plugin files\n    PLUGIN_DIR = \"plugins\"\n\n    def setUp(self):\n\n        # Destination directory\n        self.dest_dir = tempfile.mkdtemp()\n        self.addCleanup(shutil.rmtree, self.dest_dir)\n        self.plugin_file = os.path.join(self.dest_dir, self.PLUGIN_FILE)\n\n    @staticmethod\n    def with_suppressed_stdout(func, *args, **kwargs):\n        out = StringIO()\n        try:\n            with redirect_stdout(out):\n                func(*args, **kwargs)\n        except Exception:\n            print(out.getvalue())\n            raise\n\n    def test_generate_json(self):\n        \"\"\"\n        Generates the json data from all the plugins\n        and asserts that all plugins are accounted for.\n        \"\"\"\n\n        self.with_suppressed_stdout(build_json, self.dest_dir)\n\n        # Load the json file\n        with open(self.plugin_file, \"r\") as in_file:\n            plugin_json = json.load(in_file)[\"plugins\"]\n\n        # All top level directories in plugin_dir\n        plugin_folders = next(os.walk(self.PLUGIN_DIR))[1]\n\n        # Number of entries in the json should be equal to the\n        # number of folders in plugin_dir\n        self.assertEqual(len(plugin_json), len(plugin_folders))\n\n    def test_generate_zip(self):\n        \"\"\"\n        Generates zip files for all folders and asserts\n        that all folders are accounted for.\n        \"\"\"\n\n        self.with_suppressed_stdout(zip_files, self.dest_dir)\n\n        # All zip files in plugin_dir\n        plugin_zips = glob.glob(os.path.join(self.dest_dir, \"*.zip\"))\n\n        # All top level directories in plugin_dir\n        plugin_folders = next(os.walk(self.PLUGIN_DIR))[1]\n\n        # Number of folders should be equal to number of zips\n        self.assertEqual(len(plugin_zips), len(plugin_folders))\n\n    def test_valid_json(self):\n        \"\"\"\n        Asserts that the json data contains all the fields\n        for all the plugins.\n        \"\"\"\n\n        self.with_suppressed_stdout(build_json, self.dest_dir)\n\n        # Load the json file\n        with open(self.plugin_file, \"r\") as in_file:\n            plugin_json = json.load(in_file)[\"plugins\"]\n\n        # All plugins should contain all required fields\n        for module_name, data in plugin_json.items():\n            self.assertIsInstance(data['name'], str)\n            self.assertIsInstance(data['api_versions'], list)\n            self.assertIsInstance(data['author'], str)\n            self.assertIsInstance(data['description'], str)\n            self.assertIsInstance(data['version'], str)\n"
  },
  {
    "path": "test/test_keep.py",
    "content": "#!/usr/bin/env python\n# coding: utf-8\nimport unittest\n\nfrom picard.metadata import Metadata\nfrom picard.script import ScriptParser\nfrom plugins.keep.keep import keep  # noqa: F401 pylint: disable=unused-import\n\n\nclass TestKeep(unittest.TestCase):\n    def setUp(self):\n        self.parser = ScriptParser()\n\n    def test_keep_simple(self):\n        meta = Metadata(\n            {\n                \"foo\": \"foo\",\n                \"bar\": \"bar\"\n            })\n\n        sc = \"\"\"$keep(bar)\"\"\"\n        self.parser.eval(sc, meta)\n        self.assertEqual(meta[\"bar\"], \"bar\")\n        self.assertEqual(len(meta.keys()), 1)\n        self.assertNotIn(\"foo\", meta)\n\n    def test_keep_mbid(self):\n        meta = Metadata(\n            {\n                \"foo\": \"foo\",\n                \"bar\": \"bar\",\n                \"musicbrainz_albumid\": \"albumid\",\n            })\n\n        sc = \"\"\"$keep(bar)\"\"\"\n        self.parser.eval(sc, meta)\n        self.assertEqual(meta[\"musicbrainz_albumid\"], \"albumid\")\n        self.assertEqual(len(meta.keys()), 2)\n\n    def test_keep_nonfiletags(self):\n        meta = Metadata(\n            {\n                \"foo\": \"foo\",\n                \"bar\": \"bar\",\n                \"~baz\": \"baz\",\n            })\n\n        sc = \"\"\"$keep(bar)\"\"\"\n        self.parser.eval(sc, meta)\n        self.assertEqual(meta[\"~baz\"], \"baz\")\n        self.assertEqual(len(meta.keys()), 2)\n\n    def _description_test(self, tagname):\n        meta = Metadata(\n            {\n                \"foo\": \"foo\",\n                \"bar\": \"bar\",\n                tagname: tagname\n            })\n\n        tag_without_description = tagname.split(\":\")[0]\n        sc = \"\"\"$keep({tag})\"\"\".format(tag=tag_without_description)\n        self.parser.eval(sc, meta)\n        self.assertEqual(meta[tagname], tagname)\n        self.assertEqual(len(meta.keys()), 1)\n\n    def test_keep_performer(self):\n        self._description_test(\"performer:vocal\")\n        self._description_test(\"performer:\")\n\n    def test_keep_lyrics(self):\n        self._description_test(\"lyrics:foo\")\n        self._description_test(\"lyrics:\")\n\n    def test_keep_comment(self):\n        self._description_test(\"comment:foo\")\n        self._description_test(\"comment:\")\n\n    def test_keep_with_description(self):\n        meta = Metadata(\n            {\n                \"foo\": \"foo\",\n                \"bar\": \"bar\",\n                \"performer:vocal\": \"performer:vocal\",\n                \"performer:guitar\": \"performer:guitar\"\n            })\n\n        sc = \"\"\"$keep(performer:vocal)\"\"\"\n        self.parser.eval(sc, meta)\n        self.assertEqual(meta[\"performer:vocal\"], \"performer:vocal\")\n        self.assertEqual(len(meta.keys()), 1)\n"
  }
]