Full Code of metabrainz/picard-plugins for AI

2.0 1ad24cca7804 cached
157 files
2.3 MB
598.7k tokens
987 symbols
1 requests
Download .txt
Showing preview only (2,393K chars total). Download the full file or copy to clipboard to get everything.
Repository: metabrainz/picard-plugins
Branch: 2.0
Commit: 1ad24cca7804
Files: 157
Total size: 2.3 MB

Directory structure:
gitextract_xqnp2_8g/

├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .prospector.yml
├── .pylintrc
├── README.md
├── build_ui.py
├── generate.py
├── get_plugin_data.py
├── plugins/
│   ├── abbreviate_artistsort/
│   │   └── abbreviate_artistsort.py
│   ├── acousticbrainz/
│   │   ├── __init__.py
│   │   ├── ui_options_acousticbrainz_tags.py
│   │   └── ui_options_acousticbrainz_tags.ui
│   ├── acousticbrainz_tonal-rhythm/
│   │   └── acousticbrainz_tonal-rhythm.py
│   ├── add_to_collection/
│   │   ├── README.md
│   │   ├── __init__.py
│   │   ├── manifest.py
│   │   ├── options.py
│   │   ├── override_module.py
│   │   ├── post_save_processor.py
│   │   ├── settings.py
│   │   └── ui_add_to_collection_options.py
│   ├── additional_artists_details/
│   │   ├── __init__.py
│   │   ├── docs/
│   │   │   └── README.md
│   │   ├── options_additional_artists_details.ui
│   │   └── ui_options_additional_artists_details.py
│   ├── additional_artists_variables/
│   │   └── additional_artists_variables.py
│   ├── addrelease/
│   │   └── addrelease.py
│   ├── albumartist_website/
│   │   └── albumartist_website.py
│   ├── albumartistextension/
│   │   └── albumartistextension.py
│   ├── amazon/
│   │   └── amazon.py
│   ├── bpm/
│   │   ├── __init__.py
│   │   ├── ui_options_bpm.py
│   │   └── ui_options_bpm.ui
│   ├── classical_extras/
│   │   ├── Readme.md
│   │   ├── __init__.py
│   │   ├── const.py
│   │   ├── options_classical_extras.ui
│   │   ├── suffixtree.py
│   │   └── ui_options_classical_extras.py
│   ├── classicdiscnumber/
│   │   └── classicdiscnumber.py
│   ├── collect_artists/
│   │   └── collect_artists.py
│   ├── compatible_TXXX/
│   │   └── compatible_TXXX.py
│   ├── critiquebrainz/
│   │   └── critiquebrainz.py
│   ├── cuesheet/
│   │   └── cuesheet.py
│   ├── decade/
│   │   └── __init__.py
│   ├── decode_cyrillic/
│   │   └── decode_cyrillic.py
│   ├── decode_greek_cyrillic/
│   │   └── decode_greek1253.py
│   ├── deezerart/
│   │   ├── __init__.py
│   │   ├── deezer/
│   │   │   ├── __init__.py
│   │   │   ├── client.py
│   │   │   └── obj.py
│   │   ├── options.py
│   │   └── options.ui
│   ├── discnumber/
│   │   └── discnumber.py
│   ├── enhanced_titles/
│   │   ├── __init__.py
│   │   ├── options_enhanced_titles.ui
│   │   └── ui_options_enhanced_titles.py
│   ├── fanarttv/
│   │   ├── __init__.py
│   │   ├── ui_options_fanarttv.py
│   │   └── ui_options_fanarttv.ui
│   ├── featartist/
│   │   └── featartist.py
│   ├── featartistsintitles/
│   │   └── featartistsintitles.py
│   ├── fix_tracknums/
│   │   └── fix_tracknums.py
│   ├── format_performer_tags/
│   │   ├── __init__.py
│   │   ├── docs/
│   │   │   ├── HISTORY.md
│   │   │   └── README.md
│   │   ├── ui_options_format_performer_tags.py
│   │   └── ui_options_format_performer_tags.ui
│   ├── genre_mapper/
│   │   ├── __init__.py
│   │   ├── options_genre_mapper.ui
│   │   └── ui_options_genre_mapper.py
│   ├── haikuattrs/
│   │   └── haikuattrs.py
│   ├── happidev_lyrics/
│   │   └── happidev_lyrics.py
│   ├── hyphen_unicode/
│   │   └── hyphen_unicode.py
│   ├── instruments/
│   │   └── instruments.py
│   ├── keep/
│   │   └── keep.py
│   ├── key_wheel_converter/
│   │   └── key_wheel_converter.py
│   ├── lastfm/
│   │   ├── __init__.py
│   │   ├── ui_options_lastfm.py
│   │   └── ui_options_lastfm.ui
│   ├── loadasnat/
│   │   └── loadasnat.py
│   ├── losslessfuncs/
│   │   └── __init__.py
│   ├── lrclib_lyrics/
│   │   ├── __init__.py
│   │   ├── option_lrclib_lyrics.py
│   │   └── option_lrclib_lyrics.ui
│   ├── matroska_tagger/
│   │   └── matroska_tagger.py
│   ├── mod/
│   │   └── __init__.py
│   ├── moodbars/
│   │   ├── __init__.py
│   │   ├── ui_options_moodbar.py
│   │   └── ui_options_moodbar.ui
│   ├── musixmatch/
│   │   ├── README
│   │   ├── __init__.py
│   │   └── ui_options_musixmatch.py
│   ├── no_release/
│   │   └── no_release.py
│   ├── non_ascii_equivalents/
│   │   └── non_ascii_equivalents.py
│   ├── padded/
│   │   └── padded.py
│   ├── papercdcase/
│   │   └── papercdcase.py
│   ├── performer_tag_replace/
│   │   ├── __init__.py
│   │   ├── options_performer_tag_replace.ui
│   │   └── ui_options_performer_tag_replace.py
│   ├── persistent_variables/
│   │   ├── __init__.py
│   │   └── ui_variables_dialog.py
│   ├── playlist/
│   │   └── playlist.py
│   ├── post_tagging_actions/
│   │   ├── __init__.py
│   │   ├── actions_status.py
│   │   ├── actions_status.ui
│   │   ├── docs/
│   │   │   └── guide.md
│   │   ├── options_post_tagging_actions.py
│   │   └── options_post_tagging_actions.ui
│   ├── release_type/
│   │   └── release_type.py
│   ├── releasetag_aggregations/
│   │   └── releasetag_aggregations.py
│   ├── remove_perfect_albums/
│   │   └── remove_perfect_albums.py
│   ├── reorder_sides/
│   │   └── reorder_sides.py
│   ├── replace_forbidden_symbols/
│   │   └── replace_forbidden_symbols.py
│   ├── replaygain2/
│   │   ├── __init__.py
│   │   ├── ui_options_replaygain2.py
│   │   └── ui_options_replaygain2.ui
│   ├── save_and_rewrite_header/
│   │   └── save_and_rewrite_header.py
│   ├── script_logger/
│   │   └── __init__.py
│   ├── search_engine_lookup/
│   │   ├── README.md
│   │   ├── __init__.py
│   │   ├── ui_options_search_engine_editor.py
│   │   ├── ui_options_search_engine_editor.ui
│   │   ├── ui_options_search_engine_lookup.py
│   │   └── ui_options_search_engine_lookup.ui
│   ├── smart_title_case/
│   │   └── smart_title_case.py
│   ├── sort_multivalue_tags/
│   │   └── sort_multivalue_tags.py
│   ├── soundtrack/
│   │   └── soundtrack.py
│   ├── standardise_feat/
│   │   └── standardise_feat.py
│   ├── standardise_performers/
│   │   └── standardise_performers.py
│   ├── submit_folksonomy_tags/
│   │   ├── README.md
│   │   ├── __init__.py
│   │   └── ui_config.py
│   ├── submit_isrc/
│   │   ├── README.md
│   │   └── __init__.py
│   ├── tangoinfo/
│   │   ├── README.md
│   │   └── __init__.py
│   ├── theaudiodb/
│   │   ├── __init__.py
│   │   ├── ui_options_theaudiodb.py
│   │   └── ui_options_theaudiodb.ui
│   ├── titlecase/
│   │   └── titlecase.py
│   ├── tracks2clipboard/
│   │   └── tracks2clipboard.py
│   ├── viewvariables/
│   │   ├── __init__.py
│   │   ├── ui_variables_dialog.py
│   │   └── ui_variables_dialog.ui
│   ├── wikidata/
│   │   ├── __init__.py
│   │   ├── ui_options_wikidata.py
│   │   └── ui_options_wikidata.ui
│   └── workandmovement/
│       ├── __init__.py
│       └── roman.py
├── setup.cfg
└── test/
    ├── __init__.py
    ├── plugin_test_case.py
    ├── test_add_to_collection.py
    ├── test_doctest.py
    ├── test_generate.py
    └── test_keep.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/test.yml
================================================
name: Test

on: [push, pull_request]

jobs:
  unittest:

    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']

    steps:
    - uses: actions/checkout@v1
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v1
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install gettext
      run: sudo apt-get install gettext
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install picard
    - name: Test plugins
      run: python -m unittest discover -v


================================================
FILE: .gitignore
================================================
# Generated by the script
plugins.json
plugins/*.zip

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

# Distribution / packaging / development
.venv/
.vscode/
.Python
env/
build/


================================================
FILE: .prospector.yml
================================================
# Configuration for prospector, mainly used by Codacy.

pep8:
  # Please see comments in setup.cfg as to why we disable the below checks.
  disable:
    - E127
    - E128
    - E129
    - E226
    - E241
    - E501
    - W503


pyflakes:
  disable:
    # Undefined name. Ignore this since it otherwise detects the gettext
    # helpers (_, ngettext, N_) as undefined. There seems to be no clean way
    # to specify additional builtins for pyflakes with prospector.
    # We also test for this with flake8 in a saner way.
    - F821
    # F401: Module imported but unused
    # F841: Unused variables
    # We have some valid cases for both, but PyFlakes does not allow to ignore
    # them case by case. flake8 also checks this, so this is redundant.
    - F401
    - F841
    # Ignore syntax errors as reported by PyFlakes since on Codacy this does
    # not support Python 3 syntax.
    # - F999


================================================
FILE: .pylintrc
================================================
[MASTER]

# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-whitelist=

# Add files or directories to the ignore list. They should be base names, not
# paths.
ignore=CVS

# Add files or directories matching the regex patterns to the ignore list. The
# regex matches against base names, not paths.
ignore-patterns=ui_.*\.py

# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=

# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use.
jobs=0

# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100

# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=

# Pickle collected data for later comparisons.
persistent=yes

# Specify a configuration file.
#rcfile=

# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes

# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no


[MESSAGES CONTROL]

# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
confidence=

# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=all

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=consider-using-enumerate,
       format-combined-specification,
       return-in-init,
       catching-non-exception,
       bad-except-order,
       unexpected-special-method-signature,
       # Enforce list comprehensions
       # Newline at EOF
       raising-bad-type,
       raising-non-exception,
       format-needs-mapping,
       invalid-all-object,
       bad-super-call,
       nonexistent-operator,
       missing-kwoa,
       missing-format-argument-key,
       init-is-generator,
       access-member-before-definition,
       used-before-assignment,
       redundant-keyword-arg,
       assert-on-tuple,
       assignment-from-no-return,
       expression-not-assigned,
       misplaced-bare-raise,
       redefined-argument-from-local,
       not-in-loop,
       bad-exception-context,
       unidiomatic-typecheck,
       no-staticmethod-decorator,
       nonlocal-and-global,
       confusing-with-statement,
       global-variable-undefined,
       global-variable-not-assigned,
       inconsistent-mro,
       no-classmethod-decorator,
       nonlocal-without-binding,
       duplicate-bases,
       duplicate-argument-name,
       duplicate-key,
       useless-else-on-loop,
       arguments-differ,
       logging-too-many-args,
       too-few-format-args,
       bad-format-string-key,
       invalid-sequence-index,
       inherit-non-class,
       bad-format-string,
       invalid-format-index,
       invalid-star-assignment-target,
       no-method-argument,
       no-value-for-parameter,
       missing-format-attribute,
       logging-too-few-args,
       too-few-format-args,
       mixed-format-string,
       # Old style class
       logging-format-truncated,
       truncated-format-string,
       notimplemented-raised,
       # Builtin redefined
       function-redefined,
       reimported,
       repeated-keyword,
       lost-exception,
       return-outside-function,
       return-arg-in-generator,
       non-iterator-returned,
       method-hidden,
       too-many-star-expressions,
       trailing-whitespace,
       unexpected-keyword-arg,
       missing-format-string-key,
       unnecessary-lambda,
       unnecessary-pass,
       unreachable,
       logging-unsupported-format,
       bad-format-character,
       unused-import,
       exec-used,
       pointless-statement,
       pointless-string-statement,
       undefined-all-variable,
       misplaced-future,
       continue-in-finally,
       invalid-slots,
       invalid-slice-index,
       invalid-slots-object,
       star-needs-assignment-target,
       global-at-module-level,
       yield-outside-function,
       mixed-indentation,
       non-parent-init-called,
       bare-except,
       #no-self-use,
       dangerous-default-value,
       arguments-differ,
       signature-differs,
       duplicate-except,
       abstract-class-instantiated,
       binary-op-exception,
       undefined-variable



[REPORTS]

# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)

# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
#msg-template=

# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
output-format=text

# Tells whether to display a full report or only the messages.
reports=no

# Activate the evaluation score.
score=yes


[REFACTORING]

# Maximum number of nested blocks for function / method body
max-nested-blocks=5

# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit


[FORMAT]

# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=

# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$

# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4

# String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1
# tab).
indent-string='    '

# Maximum number of characters on a single line.
max-line-length=100

# Maximum number of lines in a module.
max-module-lines=1000

# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,
               dict-separator

# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no

# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no


[SPELLING]

# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4

# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package..
spelling-dict=

# List of comma separated words that should not be checked.
spelling-ignore-words=

# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=

# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no


[SIMILARITIES]

# Ignore comments when computing similarities.
ignore-comments=yes

# Ignore docstrings when computing similarities.
ignore-docstrings=yes

# Ignore imports when computing similarities.
ignore-imports=no

# Minimum lines number of a similarity.
min-similarity-lines=4


[VARIABLES]

# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=_, N_, ngettext, gettext_countries,
                    gettext_attributes, pgettext_attributes

# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes

# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
          _cb

# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_

# Argument names that match this expression will be ignored. Default to name
# with leading underscore.
ignored-argument-names=_.*|^ignored_|^unused_

# Tells whether we should check for unused import in __init__ files.
init-import=no

# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io


[MISCELLANEOUS]

# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
      XXX,
      TODO


[LOGGING]

# Format style used to check logging format string. `old` means using %
# formatting, while `new` is for `{}` formatting.
logging-format-style=old

# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging


[BASIC]

# Naming style matching correct argument names.
argument-naming-style=snake_case

# Regular expression matching correct argument names. Overrides argument-
# naming-style.
#argument-rgx=

# Naming style matching correct attribute names.
attr-naming-style=snake_case

# Regular expression matching correct attribute names. Overrides attr-naming-
# style.
#attr-rgx=

# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
          bar,
          baz,
          toto,
          tutu,
          tata

# Naming style matching correct class attribute names.
class-attribute-naming-style=any

# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style.
#class-attribute-rgx=

# Naming style matching correct class names.
class-naming-style=PascalCase

# Regular expression matching correct class names. Overrides class-naming-
# style.
#class-rgx=

# Naming style matching correct constant names.
const-naming-style=UPPER_CASE

# Regular expression matching correct constant names. Overrides const-naming-
# style.
#const-rgx=

# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1

# Naming style matching correct function names.
function-naming-style=snake_case

# Regular expression matching correct function names. Overrides function-
# naming-style.
#function-rgx=

# Good variable names which should always be accepted, separated by a comma.
good-names=i,
           j,
           k,
           ex,
           Run,
           _

# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no

# Naming style matching correct inline iteration names.
inlinevar-naming-style=any

# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style.
#inlinevar-rgx=

# Naming style matching correct method names.
method-naming-style=snake_case

# Regular expression matching correct method names. Overrides method-naming-
# style.
#method-rgx=

# Naming style matching correct module names.
module-naming-style=snake_case

# Regular expression matching correct module names. Overrides module-naming-
# style.
#module-rgx=

# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=

# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_

# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty

# Naming style matching correct variable names.
variable-naming-style=snake_case

# Regular expression matching correct variable names. Overrides variable-
# naming-style.
#variable-rgx=


[TYPECHECK]

# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager

# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=

# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes

# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes

# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes

# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local

# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=

# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes

# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1

# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1


[CLASSES]

# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
                      __new__,
                      setUp

# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
                  _fields,
                  _replace,
                  _source,
                  _make

# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls

# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=cls


[IMPORTS]

# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no

# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no

# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=optparse,tkinter.tix

# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled).
ext-import-graph=

# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled).
import-graph=

# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled).
int-import-graph=

# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=

# Force import order to recognize a module as part of a third party library.
known-third-party=enchant


[DESIGN]

# Maximum number of arguments for function / method.
max-args=5

# Maximum number of attributes for a class (see R0902).
max-attributes=7

# Maximum number of boolean expressions in an if statement.
max-bool-expr=5

# Maximum number of branch for function / method body.
max-branches=12

# Maximum number of locals for function / method body.
max-locals=15

# Maximum number of parents for a class (see R0901).
max-parents=7

# Maximum number of public methods for a class (see R0904).
max-public-methods=20

# Maximum number of return / yield for function / method body.
max-returns=6

# Maximum number of statements in function / method body.
max-statements=50

# Minimum number of public methods for a class (see R0903).
min-public-methods=2


[EXCEPTIONS]

# Exceptions that will emit a warning when being caught. Defaults to
# "Exception".
overgeneral-exceptions=Exception


================================================
FILE: README.md
================================================
# MusicBrainz Picard Plugins

This 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.

> [!NOTE]
> 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).

Note 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.

## Development Notes

The 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.


================================================
FILE: build_ui.py
================================================
#!/usr/bin/env python3

import glob
import os

from PyQt5 import uic


os.chdir(os.path.dirname(__file__))
plugin_dir = os.path.relpath('plugins')


def compile_ui(uifile, pyfile):
    if newer(uifile, pyfile):
        print("compiling %s -> %s" % (uifile, pyfile))
        with open(pyfile, "w") as out:
            uic.compileUi(uifile, out)
    else:
        print("skipping %s -> %s: up to date" % (uifile, pyfile))


def newer(file1, file2):
    """Returns True, if file1 has been modified after file2
    """
    if not os.path.exists(file2):
        return True
    return os.path.getmtime(file1) > os.path.getmtime(file2)


if __name__ == '__main__':
    for uifile in glob.glob(os.path.join(plugin_dir, '*/*.ui')):
        pyfile = os.path.splitext(os.path.basename(uifile))[0] + '.py'
        pyfile = os.path.join(os.path.dirname(uifile), pyfile)
        compile_ui(uifile, pyfile)


================================================
FILE: generate.py
================================================
#!/usr/bin/env python3

from __future__ import print_function
import argparse
import os
import json
import zipfile

from hashlib import md5
from subprocess import check_call

from get_plugin_data import get_plugin_data

VERSION_TO_BRANCH = {
    None: '2.0',
    '1.0': '1.0',
    '2.0': '2.0',
}


def build_json(dest_dir):
    """
    Traverse the plugins directory to generate json data.
    """

    plugins = {}

    # All top level directories in plugin_dir are plugins
    for dirname in next(os.walk(plugin_dir))[1]:

        files = {}
        data = {}

        if dirname in [".git"]:
            continue

        dirpath = os.path.join(plugin_dir, dirname)
        for root, dirs, filenames in os.walk(dirpath):
            for filename in filenames:
                ext = os.path.splitext(filename)[1]

                if ext not in [".pyc"]:
                    file_path = os.path.join(root, filename)
                    with open(file_path, "rb") as md5file:
                        md5Hash = md5(md5file.read()).hexdigest()
                    files[file_path.split(os.path.join(dirpath, ''))[1]] = md5Hash

                    if ext in ['.py'] and not data:
                        data = get_plugin_data(os.path.join(plugin_dir, dirname, filename))

        if files and data:
            print("Added: " + dirname)
            data['files'] = files
            plugins[dirname] = data
    out_path = os.path.join(dest_dir, plugin_file)
    with open(out_path, "w") as out_file:
        json.dump({"plugins": plugins}, out_file, sort_keys=True, indent=2)


def zip_files(dest_dir):
    """
    Zip up plugin folders
    """

    for dirname in next(os.walk(plugin_dir))[1]:
        archive_path = os.path.join(dest_dir, dirname)
        archive = zipfile.ZipFile(archive_path + ".zip", "w")

        dirpath = os.path.join(plugin_dir, dirname)
        plugin_files = []

        for root, dirs, filenames in os.walk(dirpath):
            for filename in filenames:
                file_path = os.path.join(root, filename)
                plugin_files.append(file_path)

        if (len(plugin_files) == 1
            and os.path.basename(plugin_files[0]) != '__init__.py'):
            # There's only one file, put it directly into the zipfile
            archive.write(plugin_files[0],
                          os.path.basename(plugin_files[0]),
                          compress_type=zipfile.ZIP_DEFLATED)
        else:
            for filename in plugin_files:
                # Preserve the folder structure relative to plugin_dir
                # in the zip file
                name_in_zip = os.path.join(os.path.relpath(filename,
                                                           plugin_dir))
                archive.write(filename,
                              name_in_zip,
                              compress_type=zipfile.ZIP_DEFLATED)

        print("Created: " + dirname + ".zip")


# The file that contains json data
plugin_file = "plugins.json"

# The directory which contains plugin files
plugin_dir = "plugins"

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Generate plugin files for Picard website.')
    parser.add_argument('version', nargs='?', help="Build output files for the specified version")
    parser.add_argument('--build_dir', default="build", help="Path for the build output. DEFAULT = %(default)s")
    parser.add_argument('--pull', action='store_true', dest='pull', help="Pulls the remote origin and updates the files before building")
    parser.add_argument('--no-zip', action='store_false', dest='zip', help="Do not generate the zip files in the build output")
    parser.add_argument('--no-json', action='store_false', dest='json', help="Do not generate the json file in the build output")
    args = parser.parse_args()
    check_call(["git", "checkout", "-q", VERSION_TO_BRANCH[args.version], '--', 'plugins'])
    dest_dir = os.path.abspath(os.path.join(args.build_dir, args.version or ''))
    if not os.path.exists(dest_dir):
        os.makedirs(dest_dir)
    if args.pull:
        check_call(["git", "pull", "-q"])
    if args.json:
        build_json(dest_dir)
    if args.zip:
        zip_files(dest_dir)


================================================
FILE: get_plugin_data.py
================================================
# -*- coding: utf-8 -*-

from __future__ import print_function
import ast

KNOWN_DATA = [
    'PLUGIN_NAME',
    'PLUGIN_AUTHOR',
    'PLUGIN_VERSION',
    'PLUGIN_API_VERSIONS',
    'PLUGIN_LICENSE',
    'PLUGIN_LICENSE_URL',
    'PLUGIN_DESCRIPTION',
]


def get_plugin_data(filepath):
    """Parse a python file and return a dict with plugin metadata"""
    data = {}
    with open(filepath, 'r', encoding='utf-8') as plugin_file:
        source = plugin_file.read()
        try:
            root = ast.parse(source, filepath)
        except:
            print("Cannot parse " + filepath)
            raise
        for node in ast.iter_child_nodes(root):
            if isinstance(node, ast.Assign) and len(node.targets) == 1:
                target = node.targets[0]
                if (isinstance(target, ast.Name)
                    and isinstance(target.ctx, ast.Store)
                        and target.id in KNOWN_DATA):
                    name = target.id.replace('PLUGIN_', '', 1).lower()
                    if name not in data:
                        try:
                            data[name] = ast.literal_eval(node.value)
                        except ValueError:
                            print('Cannot evaluate value in '
                                  + filepath + ':' +
                                  ast.dump(node))
        return data


================================================
FILE: plugins/abbreviate_artistsort/abbreviate_artistsort.py
================================================
# -*- coding: utf-8 -*-

# This is the Sort Multivalue Tags plugin for MusicBrainz Picard.
# Copyright (C) 2013 Sophist
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

PLUGIN_NAME = "Abbreviate artist-sort"
PLUGIN_AUTHOR = "Sophist"
PLUGIN_DESCRIPTION = '''Abbreviate Artist-Sort and Album-Artist-Sort Tags.
e.g. "Vivaldi, Antonio" becomes "Vivaldi, A."
This is particularly useful for classical albums that can have a long list of artists.
%artistsort% is abbreviated into %_artistsort_abbrev% and
%albumartistsort% is abbreviated into %_albumartistsort_abbrev%.'''
PLUGIN_VERSION = "0.4.1"
PLUGIN_API_VERSIONS = ["1.0", "2.0"]
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"


from picard import log
from picard.metadata import register_track_metadata_processor

# NOTE: This plugin will not work consistently if you have not enabled the 'Standardize Artist Names' option!
# The algorithm for this is complicated because the tags can contain multiple names separated by various characters
# As an example from http://musicbrainz.org/release/6c0cfb20-2606-46c1-9306-ee5e7cb5bfdf
#   Sorted:   Vivaldi, Antonio, Caldara, Antonio; Queyras, Jean-Guihen, Kallweit, Georg, Akademie für Alte Musik Berlin
#   Unsorted: Antonio Vivaldi, Antonio Caldara; Jean-Guihen Queyras, Georg Kallweit, Akademie für Alte Musik Berlin
# As you can see, in unsorted, names are separated by ',' as well as ';' but could be e.g. 'feat:'
# The only known is that in sorted, surname is separated from forename(s) by a ','
# It is further complicated by non-latin (e.g. japanese) script artist names
#   where unsorted is in local script, but sorted can be in latin, and names can be in local locale or general.

# If the names are just reversed, and we shift Unsorted to the right by one character (for the ',' in sorted),
# then the punctuation should start to match up:
#   Sorted:   Vivaldi, Antonio, Caldara, Antonio; Queyras, Jean-Guihen, Kallweit, Georg, Akademie für Alte Musik Berlin
#   Unsorted:  Antonio Vivaldi, Antonio Caldara; Jean-Guihen Queyras, Georg Kallweit, Akademie für Alte Musik Berlin
#                             ^
# Of course if we have non-latin or locale names, alignment could be way off:
#   Sorted: Verdi, Giuseppe, Vivaldi, Antonio
#   Unsorted: Joe Green, Antonio Vivaldi

# In the absence of an array version of the tags (PR pending)
# we need to process the tag and sorted tag together in a special way as follows:
#
# 1. Look for the first ',' in sorted and tentatively set surname to the string up to that point.
#   Case a. Sorted:   Stuff, ...
#           Unsorted: Stuff, ...
#   Case b. Sorted:   Surname, Forename(s)...
#           Unsorted: Forename(s) Surname...
#     Special case: Sorted:   Major, Major...
#                   Unsorted: Major Major...
#   Case c. Sorted:   Stuff; Surname, Forename(s)...
#           Unsorted: Stuff; Forename(s) Surname...
#   Case d. Sorted:   Latin Surname, Latin Forename(s)...
#           Unsorted: Foreign...
#   Case e. Sorted:   Beatles, The...
#           Unsorted: The Beatles...
# 2. Locate surname in unsorted:
#   Case a. unsorted starts with surname - move both to new strings
#   Case b. surname can be found in unsorted - forename(s) are what is before and match beginning of rest
#   Case c. If first word is same in sorted and unsorted, move words that match to new strings, then treat as b.
#   Case d. Try to handle without abbreviating and get to next name which might not be foreign

_abbreviate_tags = [
    ('albumartistsort', 'albumartist', '~albumartistsort_abbrev'),
    ('artistsort', 'artist', '~artistsort_abbrev'),
]
_prefixes = ["A", "The"]
_split = ", "
_abbreviate_cache = {}


def abbreviate_artistsort(tagger, metadata, track, release):

    for sortTag, unsortTag, sortTagNew in _abbreviate_tags:
        if not (sortTag in metadata and unsortTag in metadata):
            continue

        sorts = list(metadata.getall(sortTag))
        unsorts = list(metadata.getall(unsortTag))
        for i in range(0, min(len(sorts), len(unsorts))):
            sort = sorts[i]
            log.debug("%s: Trying to abbreviate '%s'." % (PLUGIN_NAME, sort))
            if sort in _abbreviate_cache:
                log.debug("  Using abbreviation found in cache: '%s'." % (_abbreviate_cache[sort]))
                sorts[i] = _abbreviate_cache[sort]
                continue
            unsort = unsorts[i]
            new_sort = ""
            new_unsort = ""

            while len(sort) > 0 and len(unsort) > 0:

                if not _split in sort:
                    log.debug("  Ending without separator '%s' - moving '%s'." % (_split, sort))
                    new_sort += sort
                    new_unsort += unsort
                    sort = unsort = ""
                    continue

                surname, rest = sort.split(_split, 1)
                if rest == "":
                    log.debug("  Ending with separator '%s' - moving '%s'." % (_split, surname))
                    new_sort += sort
                    new_unsort += unsort
                    sort = unsort = ""
                    continue

                # Move leading whitespace
                new_unsort += unsort[0:len(unsort) - len(unsort.lstrip())]
                unsort = unsort.lstrip()

                # Sorted:   Stuff, ...
                # Unsorted: Stuff, ...
                temp = surname + _split
                l = len(temp)
                if unsort[:l] == temp:
                    log.debug("  No forename - moving '%s'." % (surname))
                    new_sort += temp
                    new_unsort += temp
                    sort = sort[l:]
                    unsort = unsort[l:]
                    continue

                # Sorted:   Stuff; Surname, Forename(s)...
                # Unsorted: Stuff; Forename(s) Surname...
                # Move matching words plus white-space one by one
                if unsort.find(' ' + surname) == -1:
                    while surname.split(None, 1)[0] == unsort.split(None, 1)[0]:
                        x = unsort.split(None, 1)[0]
                        log.debug("  Moving matching word '%s'." % (x))
                        new_sort += x
                        new_unsort += x
                        surname = surname[len(x):]
                        unsort = unsort[len(x):]
                        new_sort += surname[0:len(surname) - len(surname.lstrip())]
                        surname = surname.lstrip()
                        new_unsort += unsort[0:len(unsort) - len(unsort.lstrip())]
                        unsort = unsort.lstrip()

                # If we still can't find surname then we are up a creek...
                pos = unsort.find(' ' + surname)
                if pos == -1:
                    log.debug(
                        _("%s: Track %s: Unable to abbreviate surname '%s' - not matched in unsorted %s: '%s'."),
                        PLUGIN_NAME,
                        metadata['tracknumber'],
                        surname,
                        unsortTag,
                        unsort[i],
                    )
                    log.warning("  Could not match surname '%s' in remaining unsorted: %s" % (surname, unsort))
                    break

                # Sorted:   Surname, Forename(s)...
                # Unsorted: Forename(s) Surname...
                forename = unsort[:pos]
                if rest[:len(forename)] != forename:
                    log.debug(
                        _("%s: Track %s: Unable to abbreviate surname (%s) - forename (%s) not matched in unsorted %s: '%s'."),
                        PLUGIN_NAME,
                        metadata['tracknumber'],
                        surname,
                        forename,
                        unsortTag,
                        unsort[i],
                    )
                    log.warning("  Could not match forename (%s) for surname (%s) in remaining unsorted (%s):" % (forename, surname, unsort))
                    break

                inits = ' '.join([x[0] + '.' for x in forename.split()])

                # Sorted:   Beatles, The...
                # Unsorted: The Beatles...
                if forename in _prefixes:
                    inits = forename

                new_sort += surname + _split + inits
                sort = rest[len(forename):]
                new_sort += sort[0:len(sort) - len(sort[1:].lstrip())]
                sort = sort[1:].lstrip()
                new_unsort += forename
                unsort = unsort[len(forename):]
                new_unsort += unsort[0:len(unsort) - len(unsort.lstrip())]
                unsort = unsort.lstrip()
                new_unsort += surname
                unsort = unsort[len(surname):]
                new_unsort += unsort[0:len(unsort) - len(unsort[1:].lstrip())]
                unsort = unsort[1:].lstrip()

                if forename != inits:
                    log.debug(
                        _("%s: Abbreviated surname (%s, %s) to (%s, %s) in '%s'."),
                        PLUGIN_NAME,
                        surname,
                        forename,
                        surname,
                        inits,
                        sortTag,
                    )
                    log.debug("Abbreviated (%s, %s) to (%s, %s)." % (surname, forename, surname, inits))
            else:  # while loop ended without a break i.e. no errors
                if unsorts[i] != new_unsort:
                    log.error(
                        _("%s: Track %s: Logic error - mangled %s from '%s' to '%s'."),
                        PLUGIN_NAME,
                        metadata['tracknumber'],
                        unsortTag,
                        unsorts[i],
                        new_unsort,
                    )
                    log.warning("Error: Unsorted text for %s has changed from '%s' to '%s'!" % (unsortTag, unsorts[i], new_unsort))
                _abbreviate_cache[sorts[i]] = new_sort
                log.debug("  Abbreviated and cached (%s) as (%s)." % (sorts[i], new_sort))
                if sorts[i] != new_sort:
                    log.debug(_("%s: Abbreviated tag '%s' to '%s'."),
                              PLUGIN_NAME,
                              sorts[i],
                              new_sort,
                              )
                    sorts[i] = new_sort
        metadata[sortTagNew] = sorts

register_track_metadata_processor(abbreviate_artistsort)


================================================
FILE: plugins/acousticbrainz/__init__.py
================================================
# -*- coding: utf-8 -*-
# AcousticBrainz plugin for Picard
#
# Copyright (C) 2021 Wargreen <wargreen@lebib.org>
# Copyright (C) 2021 Philipp Wolfer <ph.wolfer@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

# Plugin metadata
# =============================================================================

PLUGIN_NAME = 'AcousticBrainz Tags'
PLUGIN_AUTHOR = ('Wargreen <wargreen@lebib.org>, '
                 'Hugo Geoffroy "pistache" <pistache@lebib.org>, '
                 'Philipp Wolfer <ph.wolfer@gmail.com>, '
                 'Regorxxx <regorxxx@protonmail.com>')
PLUGIN_DESCRIPTION = '''
Tag files with tags from the AcousticBrainz database, all highlevel classifiers
and tonal/rhythm data.
<br/><br/>
By default, only simple mood and genre information is saved, but the plugin can
be configured to include all highlevel data.
<br/><br/>
Based on code from Andrew Cook, Sambhav Kothari
<br/><br/>
<b>WARNING:</b> Experimental plugin. All guarantees voided by use.'''

PLUGIN_LICENSE = "GPL-2.0"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.txt"
PLUGIN_VERSION = "2.2.3"
PLUGIN_API_VERSIONS = ["2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7"]

# Plugin configuration
# =============================================================================

ACOUSTICBRAINZ_HOST = "acousticbrainz.org"
ACOUSTICBRAINZ_PORT = 80


# Subset of the low level data to add as tags.
# Represent the data as nested dicts.

SUBLOWLEVEL_SUBSET = {"rhythm": {"bpm": None},
                      "tonal": {"chords_changes_rate": None,
                                "chords_key": None,
                                "chords_scale": None,
                                "key_key": None,
                                "key_scale": None}}

# Imports
# =============================================================================

from functools import partial
from json import dumps as dump_json

from picard import (
    config,
    log,
)
from picard.metadata import (
    register_album_metadata_processor,
    register_track_metadata_processor,
)
from picard.ui.options import (
    OptionsPage,
    register_options_page,
)
from picard.webservice import ratecontrol
from picard.plugins.acousticbrainz.ui_options_acousticbrainz_tags import Ui_AcousticBrainzOptionsPage

ratecontrol.set_minimum_delay((ACOUSTICBRAINZ_HOST, ACOUSTICBRAINZ_PORT), 1000)

# Constants
# =============================================================================

LOWLEVEL = "lowlevel"
HIGHLEVEL = "highlevel"


# Logging utilities
# -------------------------------------------------------------------------

def log_msg(logger, text, *args):
    logger("%s: " + text, PLUGIN_NAME, *args)

def debug(*args):
    log_msg(log.debug, *args)

def warning(*args):
    log_msg(log.warning, *args)

def error(*args):
    log_msg(log.error, *args)


# TrackDataProcessor class
# =============================================================================
# (used to apply AcousticBrainz data to Track metadata)

class TrackDataProcessor:
    def __init__(self, recording_id, metadata, level, data, files=None):
        self.recording_id = recording_id
        self.metadata = metadata
        self.level = level
        self.data = self._extract_data(data)
        self.files = files
        self.do_simplemood = config.setting["acousticbrainz_add_simplemood"]
        self.simplemood_tagname = config.setting["acousticbrainz_simplemood_tagname"]
        self.do_simplegenre = config.setting["acousticbrainz_add_simplegenre"]
        self.simplegenre_tagname = config.setting["acousticbrainz_simplegenre_tagname"]
        self.do_keybpm = config.setting["acousticbrainz_add_keybpm"]
        self.do_fullhighlevel = config.setting["acousticbrainz_add_fullhighlevel"]
        self.do_sublowlevel = config.setting["acousticbrainz_add_sublowlevel"]

    # Logging utilities
    # -------------------------------------------------------------------------

    def log(self, logger, text, *args):
        log_msg(logger, "[%s: %s] " + text, self.shortid, self.title, *args)

    def debug(self, *args):
        self.log(log.debug, *args)

    def warning(self, *args):
        self.log(log.warning, *args)

    def error(self, *args):
        self.log(log.error, *args)

    # Read-only properties
    # -------------------------------------------------------------------------

    @property
    def shortid(self):
        return self.recording_id[:8]

    @property
    def title(self):
        return self.metadata["title"]

    # Callback
    # -------------------------------------------------------------------------

    def process(self):
        if not self.data:
            self.warning('No %s data for track %s', self.level, self.recording_id)

        if self.level == HIGHLEVEL:
            if self.do_simplemood:
                self.process_simplemood()
            if self.do_simplegenre:
                self.process_simplegenre()
            if self.do_fullhighlevel:
                self.process_fullhighlevel()

        if self.level == LOWLEVEL:
            if self.do_keybpm:
                self.process_keybpm()
            if self.do_sublowlevel:
                self.process_sublowlevel()

    # Processing helper methods
    # -------------------------------------------------------------------------

    def _extract_data(self, data):
        if not self.recording_id in data:
            return {}
        if self.level == LOWLEVEL:
            return data[self.recording_id]["0"]
        elif self.level == HIGHLEVEL:
            return data[self.recording_id]["0"]["highlevel"]

    def filter_data(self, data, subset):
        result = {}
        for key, value in subset.items():
            if key in data:
                if isinstance(value, dict):
                    self.debug("filter_data : traversing %s", key)
                    result[key] = self.filter_data(data[key], value)
                else:
                    self.debug("filter_data : adding result %s", key)
                    result[key] = data[key]
            else:
                self.debug("filter_data : Subset key %s not found in data", key)
                continue
        self.debug("filter_data : result : %s", result)
        return result

    def update_metadata(self, name, values):
        self.metadata[name] = values
        if self.files:
            for file in self.files:
                file.metadata[name] = values

    # Processing methods
    # -------------------------------------------------------------------------
    # (fill metadata with fetched data)

    def process_simplemood(self):
        self.debug("processing simplemood data")

        mood_tagname = self.simplemood_tagname;
        moods = []

        for classifier, data in self.data.items():
            if "value" in data:
                value = data["value"]
                inverted = value.startswith("not_")
                if classifier.startswith("mood_") and not inverted:
                    moods.append(value)

        self.update_metadata(mood_tagname, moods)

    def process_simplegenre(self):
        self.debug("processing simplegenre data")

        genre_tagname = self.simplegenre_tagname;
        genres = []

        for classifier, data in self.data.items():
            if "value" in data:
                value = data["value"]
                inverted = value.startswith("not_")
                if classifier.startswith("genre_") and not inverted:
                    genres.append(value)

        self.update_metadata(genre_tagname, genres)

    def process_fullhighlevel(self):
        self.debug("processing fullhighlevel data")

        f_count, c_count = 0, 0
        for classifier, data in self.data.items():
            classifier = classifier.lower()
            if "all" in data:
                c_count += 1
                for feature, proba in data["all"].items():
                    feature = feature.lower()
                    f_count += 1
                    self.update_metadata("ab:hi:{}:{}".format(classifier, feature), dump_json(proba))
            else:
                self.warning("fullhighlevel : ignored invalid classifier data (%s)", classifier)

        self.debug("fullhighlevel : parsed %d features from %d classifiers", f_count, c_count)

    def process_keybpm(self):
        self.debug("processing keybpm data")
        if "tonal" in self.data:
            tonal_data = self.data["tonal"]
            if "key_key" in tonal_data:
                key = tonal_data["key_key"]
                if "key_scale" in tonal_data:
                    if tonal_data["key_scale"] == "minor":
                        key += "m"
                self.update_metadata('key', key)
                self.debug("track '%s' is in key %s", self.title, key)

        if "rhythm" in self.data:
            rhythm_data = self.data["rhythm"]
            if "bpm" in rhythm_data:
                bpm = int(rhythm_data["bpm"] + 0.5)
                self.update_metadata('bpm', bpm)
                self.debug("keybpm : Track '%s' has %s bpm", self.title, bpm)

    def process_sublowlevel(self):
        self.debug("processing sublowlevel data")
        subset = SUBLOWLEVEL_SUBSET

        filtered_data = self.filter_data(self.data, subset)

        f_count, c_count = 0, 0
        for classifier, data in filtered_data.items():
            classifier = classifier.lower()
            c_count += 1
            for feature, proba in data.items():
                feature = feature.lower()
                f_count += 1
                self.update_metadata("ab:lo:{}:{}".format(classifier, feature), proba)

        self.debug("sublowlevel : parsed %d features from %d classifiers", f_count, c_count)


class AcousticBrainzRequest:

    MAX_BATCH_SIZE = 25

    def __init__(self, webservice, recording_ids):
        self.webservice = webservice
        self.recording_ids = recording_ids

    def request_highlevel(self, callback):
        self._batch('high-level', self.recording_ids, callback, {})

    def request_lowlevel(self, callback):
        self._batch('low-level', self.recording_ids, callback, {})

    def _batch(self, action, recording_ids, callback, result, response=None, reply=None, error=None):
        if response and not error:
            self._merge_results(result, response)

        if not recording_ids or error:
            callback(result, error)
            return

        batch = recording_ids[:self.MAX_BATCH_SIZE]
        recording_ids = recording_ids[self.MAX_BATCH_SIZE:]
        self._do_request(action, batch,
            callback=partial(self._batch, action, recording_ids, callback, result))

    def _do_request(self, action, recording_ids, callback):
        self.webservice.get(
            ACOUSTICBRAINZ_HOST,
            ACOUSTICBRAINZ_PORT,
            '/api/v1/%s' % action,
            callback,
            priority=True,
            parse_response_type='json',
            queryargs=self._get_query_args(action, recording_ids)
        )

    def _get_query_args(self, action, recording_ids):
        queryargs = {
            'recording_ids': ';'.join(recording_ids),
        }
        if action == 'high-level':
            queryargs['map_classes'] = 'true'
        return queryargs

    def _merge_results(self, full, new):
        mapping = new.get('mbid_mapping', {})
        new = {mapping.get(k, k): v for (k, v) in new.items() if k != 'mbid_mapping'}
        full.update(new)


# Plugin class
# =============================================================================
# (provides track processing callback)

class AcousticBrainzPlugin:

    result_cache = {
        LOWLEVEL: {},
        HIGHLEVEL: {},
    }

    def process_album(self, album, metadata, release):
        debug('Processing album %s', album.id)
        recording_ids = self.get_recording_ids(release)
        self.run_requests(album, recording_ids, self.album_callback)

    def process_track(self, album, metadata, track_node, release_node):
        # Run requests for standalone recordings
        if not release_node and 'id' in track_node:
            recording_id = track_node['id']
            debug('Processing recording %s', recording_id)
            self.run_requests(album, [recording_id], partial(self.nat_callback, recording_id))
        # Apply metadata changes for already loaded results for album tracks
        elif 'recording' in track_node:
            recording_id = track_node['recording']['id']
            for level in (LOWLEVEL, HIGHLEVEL):
                result = self.result_cache[level].get(album.id)
                if result:
                    self.apply_result(recording_id, metadata, level, result)

    def run_requests(self, album, recording_ids, callback):
        request = AcousticBrainzRequest(album.tagger.webservice, recording_ids)
        if self.do_highlevel:
            album._requests += 1
            request.request_highlevel(partial(callback, HIGHLEVEL, album))
        if self.do_lowlevel:
            album._requests += 1
            request.request_lowlevel(partial(callback, LOWLEVEL, album))

    @property
    def do_highlevel(self):
        return (config.setting["acousticbrainz_add_simplemood"]
            or config.setting["acousticbrainz_add_simplegenre"]
            or config.setting["acousticbrainz_add_fullhighlevel"])

    @property
    def do_lowlevel(self):
        return (config.setting["acousticbrainz_add_keybpm"]
            or config.setting["acousticbrainz_add_sublowlevel"])

    def get_recording_ids(self, release):
        return [track['recording']['id'] for track in self.iter_tracks(release)]

    @staticmethod
    def iter_tracks(release):
        for media in release['media']:
            if 'pregap' in media:
                yield media['pregap']

            if 'tracks' in media:
                yield from media['tracks']

            if 'data-tracks' in media:
                yield from media['data-tracks']

    def album_callback(self, level, album, result=None, error=None):
        if not error:
            # Store the result, the actual processing will be done by the
            # track metadata processor.
            self.result_cache[level][album.id] = result
            album.run_when_loaded(partial(self.clear_cache, level, album))
        album._requests -= 1
        album._finalize_loading(error)

    def nat_callback(self, recording_id, level, album, result=None, error=None):
        for track in album.tracks:
            if track.id == recording_id:
                self.apply_result(track.id, track.metadata, level, result, files=track.files)

    def apply_result(self, recording_id, metadata, level, result, files=None):
        debug('Updating recording %s with %s results', recording_id, level)
        processor = TrackDataProcessor(recording_id, metadata, level, result, files=files)
        processor.process()

    def clear_cache(self, level, album):
        try:
            del self.result_cache[level][album.id]
        except KeyError:
            pass


# Plugin options page
# =============================================================================
# (define plugin options and link with user interface)

class AcousticBrainzOptionsPage(OptionsPage):
    NAME = "acousticbrainz_tags"
    TITLE = "AcousticBrainz tags"
    PARENT = "plugins"

    options = [
        config.BoolOption("setting", "acousticbrainz_add_simplemood", True),
        config.TextOption("setting", "acousticbrainz_simplemood_tagname", "ab:mood"),
        config.BoolOption("setting", "acousticbrainz_add_simplegenre", True),
        config.TextOption("setting", "acousticbrainz_simplegenre_tagname", "ab:genre"),
        config.BoolOption("setting", "acousticbrainz_add_keybpm", False),
        config.BoolOption("setting", "acousticbrainz_add_fullhighlevel", False),
        config.BoolOption("setting", "acousticbrainz_add_sublowlevel", False)
    ]

    def __init__(self, parent=None):
        super(AcousticBrainzOptionsPage, self).__init__(parent)
        self.ui = Ui_AcousticBrainzOptionsPage()
        self.ui.setupUi(self)

    def load(self):
        setting = config.setting
        self.ui.add_simplemood.setChecked(setting["acousticbrainz_add_simplemood"])
        self.ui.simplemood_tagname.setText(setting["acousticbrainz_simplemood_tagname"])
        self.ui.add_simplegenre.setChecked(setting["acousticbrainz_add_simplegenre"])
        self.ui.simplegenre_tagname.setText(setting["acousticbrainz_simplegenre_tagname"])
        self.ui.add_fullhighlevel.setChecked(setting["acousticbrainz_add_fullhighlevel"])
        self.ui.add_keybpm.setChecked(setting["acousticbrainz_add_keybpm"])
        self.ui.add_sublowlevel.setChecked(setting["acousticbrainz_add_sublowlevel"])

    def save(self):
        setting = config.setting
        setting["acousticbrainz_add_simplemood"] = self.ui.add_simplemood.isChecked()
        setting["acousticbrainz_simplemood_tagname"] = str(self.ui.simplemood_tagname.text())
        setting["acousticbrainz_add_simplegenre"] = self.ui.add_simplegenre.isChecked()
        setting["acousticbrainz_simplegenre_tagname"] = str(self.ui.simplegenre_tagname.text())
        setting["acousticbrainz_add_keybpm"] = self.ui.add_keybpm.isChecked()
        setting["acousticbrainz_add_fullhighlevel"] = self.ui.add_fullhighlevel.isChecked()
        setting["acousticbrainz_add_sublowlevel"] = self.ui.add_sublowlevel.isChecked()


plugin = AcousticBrainzPlugin()
register_album_metadata_processor(plugin.process_album)
register_track_metadata_processor(plugin.process_track)
register_options_page(AcousticBrainzOptionsPage)


================================================
FILE: plugins/acousticbrainz/ui_options_acousticbrainz_tags.py
================================================
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'plugins/acousticbrainz/ui_options_acousticbrainz_tags.ui'
#
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_AcousticBrainzOptionsPage(object):
    def setupUi(self, AcousticBrainzOptionsPage):
        AcousticBrainzOptionsPage.setObjectName("AcousticBrainzOptionsPage")
        AcousticBrainzOptionsPage.setEnabled(True)
        AcousticBrainzOptionsPage.resize(527, 443)
        AcousticBrainzOptionsPage.setWindowTitle("")
        self.verticalLayout = QtWidgets.QVBoxLayout(AcousticBrainzOptionsPage)
        self.verticalLayout.setObjectName("verticalLayout")
        self.acousticbrainzTags_groupBox = QtWidgets.QGroupBox(AcousticBrainzOptionsPage)
        self.acousticbrainzTags_groupBox.setObjectName("acousticbrainzTags_groupBox")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.acousticbrainzTags_groupBox)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.add_simplemood = QtWidgets.QCheckBox(self.acousticbrainzTags_groupBox)
        self.add_simplemood.setObjectName("add_simplemood")
        self.verticalLayout_2.addWidget(self.add_simplemood)
        self.simplemood_tagname_label = QtWidgets.QLabel(self.acousticbrainzTags_groupBox)
        self.simplemood_tagname_label.setObjectName("simplemood_tagname_label")
        self.verticalLayout_2.addWidget(self.simplemood_tagname_label)
        self.simplemood_tagname = QtWidgets.QLineEdit(self.acousticbrainzTags_groupBox)
        self.simplemood_tagname.setObjectName("simplemood_tagname")
        self.verticalLayout_2.addWidget(self.simplemood_tagname)
        self.add_simplegenre = QtWidgets.QCheckBox(self.acousticbrainzTags_groupBox)
        self.add_simplegenre.setObjectName("add_simplegenre")
        self.verticalLayout_2.addWidget(self.add_simplegenre)
        self.simplegenre_tagname_label = QtWidgets.QLabel(self.acousticbrainzTags_groupBox)
        self.simplegenre_tagname_label.setObjectName("simplegenre_tagname_label")
        self.verticalLayout_2.addWidget(self.simplegenre_tagname_label)
        self.simplegenre_tagname = QtWidgets.QLineEdit(self.acousticbrainzTags_groupBox)
        self.simplegenre_tagname.setObjectName("simplegenre_tagname")
        self.verticalLayout_2.addWidget(self.simplegenre_tagname)
        self.add_keybpm = QtWidgets.QCheckBox(self.acousticbrainzTags_groupBox)
        self.add_keybpm.setObjectName("add_keybpm")
        self.verticalLayout_2.addWidget(self.add_keybpm)
        self.add_fullhighlevel = QtWidgets.QCheckBox(self.acousticbrainzTags_groupBox)
        self.add_fullhighlevel.setObjectName("add_fullhighlevel")
        self.verticalLayout_2.addWidget(self.add_fullhighlevel)
        self.add_sublowlevel = QtWidgets.QCheckBox(self.acousticbrainzTags_groupBox)
        self.add_sublowlevel.setObjectName("add_sublowlevel")
        self.verticalLayout_2.addWidget(self.add_sublowlevel)
        spacerItem = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)
        self.verticalLayout_2.addItem(spacerItem)
        self.sublowlevel_descr = QtWidgets.QLabel(self.acousticbrainzTags_groupBox)
        self.sublowlevel_descr.setTextFormat(QtCore.Qt.RichText)
        self.sublowlevel_descr.setObjectName("sublowlevel_descr")
        self.verticalLayout_2.addWidget(self.sublowlevel_descr)
        spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        self.verticalLayout_2.addItem(spacerItem1)
        self.verticalLayout.addWidget(self.acousticbrainzTags_groupBox)

        self.retranslateUi(AcousticBrainzOptionsPage)
        QtCore.QMetaObject.connectSlotsByName(AcousticBrainzOptionsPage)

    def retranslateUi(self, AcousticBrainzOptionsPage):
        _translate = QtCore.QCoreApplication.translate
        self.acousticbrainzTags_groupBox.setTitle(_translate("AcousticBrainzOptionsPage", "AcousticBrainz Tags"))
        self.add_simplemood.setText(_translate("AcousticBrainzOptionsPage", "Add simple Mood tags"))
        self.simplemood_tagname_label.setText(_translate("AcousticBrainzOptionsPage", "Mood tag name:"))
        self.add_simplegenre.setText(_translate("AcousticBrainzOptionsPage", "Add simple Genre tags"))
        self.simplegenre_tagname_label.setText(_translate("AcousticBrainzOptionsPage", "Genre tag name:"))
        self.add_keybpm.setText(_translate("AcousticBrainzOptionsPage", "Add simple BPM and Key tags"))
        self.add_fullhighlevel.setText(_translate("AcousticBrainzOptionsPage", "Add all highlevel AcousticBrainz tags"))
        self.add_sublowlevel.setText(_translate("AcousticBrainzOptionsPage", "Add a subset of the lowlevel AcousticBrainz tags"))
        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>"))


================================================
FILE: plugins/acousticbrainz/ui_options_acousticbrainz_tags.ui
================================================
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>AcousticBrainzOptionsPage</class>
 <widget class="QWidget" name="AcousticBrainzOptionsPage">
  <property name="enabled">
   <bool>true</bool>
  </property>
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>527</width>
    <height>443</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string/>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <item>
    <widget class="QGroupBox" name="acousticbrainzTags_groupBox">
     <property name="title">
      <string>AcousticBrainz Tags</string>
     </property>
     <layout class="QVBoxLayout" name="verticalLayout_2">
      <item>
       <widget class="QCheckBox" name="add_simplemood">
        <property name="text">
         <string>Add simple Mood tags</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QLabel" name="simplemood_tagname_label">
        <property name="text">
         <string>Mood tag name:</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QLineEdit" name="simplemood_tagname"/>
      </item>
      <item>
       <widget class="QCheckBox" name="add_simplegenre">
        <property name="text">
         <string>Add simple Genre tags</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QLabel" name="simplegenre_tagname_label">
        <property name="text">
         <string>Genre tag name:</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QLineEdit" name="simplegenre_tagname"/>
      </item>
      <item>
       <widget class="QCheckBox" name="add_keybpm">
        <property name="text">
         <string>Add simple BPM and Key tags</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QCheckBox" name="add_fullhighlevel">
        <property name="text">
         <string>Add all highlevel AcousticBrainz tags</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QCheckBox" name="add_sublowlevel">
        <property name="text">
         <string>Add a subset of the lowlevel AcousticBrainz tags</string>
        </property>
       </widget>
      </item>
      <item>
       <spacer name="verticalSpacer_2">
        <property name="orientation">
         <enum>Qt::Vertical</enum>
        </property>
        <property name="sizeType">
         <enum>QSizePolicy::Preferred</enum>
        </property>
        <property name="sizeHint" stdset="0">
         <size>
          <width>20</width>
          <height>20</height>
         </size>
        </property>
       </spacer>
      </item>
      <item>
       <widget class="QLabel" name="sublowlevel_descr">
        <property name="text">
         <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>
        </property>
        <property name="textFormat">
         <enum>Qt::RichText</enum>
        </property>
       </widget>
      </item>
      <item>
       <spacer name="verticalSpacer">
        <property name="orientation">
         <enum>Qt::Vertical</enum>
        </property>
        <property name="sizeHint" stdset="0">
         <size>
          <width>20</width>
          <height>40</height>
         </size>
        </property>
       </spacer>
      </item>
     </layout>
    </widget>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>


================================================
FILE: plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py
================================================
# -*- coding: utf-8 -*-
# Acousticbrainz Tonal/Rhythm plugin for Picard
# Copyright (C) 2015  Sophist
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

PLUGIN_NAME = 'AcousticBrainz Tonal-Rhythm'
PLUGIN_AUTHOR = 'Sophist, Sambhav Kothari'
PLUGIN_DESCRIPTION = '''Add's the following tags:
<ul>
<li>Key (in ID3v2.3 format)</li>
<li>Beats Per Minute (BPM)</li>
</ul>
from the AcousticBrainz database.<br/><br/>
<em>This plugin is deprecated, please consider using the AcousticBrainz Tags
plugin instead.</em>
'''
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.txt"
PLUGIN_VERSION = '1.1.6'
PLUGIN_API_VERSIONS = ["2.0"]  # Requires support for TKEY which is in 1.4

from json import JSONDecodeError

from picard import log
from picard.metadata import register_track_metadata_processor
from functools import partial
from picard.webservice import ratecontrol
from picard.util import load_json

ACOUSTICBRAINZ_HOST = "acousticbrainz.org"
ACOUSTICBRAINZ_PORT = 80

ratecontrol.set_minimum_delay((ACOUSTICBRAINZ_HOST, ACOUSTICBRAINZ_PORT), 1000)


class AcousticBrainz_Key:

    def get_data(self, album, track_metadata, track_node, release_node):
        if "musicbrainz_recordingid" not in track_metadata:
            log.error("%s: Error parsing response. No MusicBrainz recording id found.",
                      PLUGIN_NAME)
            return
        recordingId = track_metadata['musicbrainz_recordingid']
        if recordingId:
            log.debug("%s: Add AcousticBrainz request for %s (%s)",
                      PLUGIN_NAME, track_metadata['title'], recordingId)
            self.album_add_request(album)
            path = "/%s/low-level" % recordingId
            return album.tagger.webservice.get(
                        ACOUSTICBRAINZ_HOST,
                        ACOUSTICBRAINZ_PORT,
                        path,
                        partial(self.process_data, album, track_metadata),
                        priority=True,
                        important=False,
                        parse_response_type=None)

    def process_data(self, album, track_metadata, response, reply, error):
        if error:
            log.error("%s: Network error retrieving acousticBrainz data for recordingId %s",
                      PLUGIN_NAME, track_metadata['musicbrainz_recordingid'])
            self.album_remove_request(album)
            return
        try:
            data = load_json(response)
        except JSONDecodeError:
            log.error("%s: Network error retrieving AcousticBrainz data for recordingId %s",
                      PLUGIN_NAME, track_metadata['musicbrainz_recordingid'])
            self.album_remove_request(album)
            return
        if "tonal" in data:
            if "key_key" in data["tonal"]:
                key = data["tonal"]["key_key"]
                if "key_scale" in data["tonal"]:
                    scale = data["tonal"]["key_scale"]
                    if scale == "minor":
                        key += "m"
                track_metadata["key"] = key
                log.debug("%s: Track '%s' is in key %s", PLUGIN_NAME, track_metadata["title"], key)
        if "rhythm" in data:
            if "bpm" in data["rhythm"]:
                bpm = int(data["rhythm"]["bpm"] + 0.5)
                track_metadata["bpm"] = bpm
                log.debug("%s: Track '%s' has %s bpm", PLUGIN_NAME, track_metadata["title"], bpm)
        self.album_remove_request(album)

    def album_add_request(self, album):
        album._requests += 1

    def album_remove_request(self, album):
        album._requests -= 1
        if album._requests == 0:
            album._finalize_loading(None)


register_track_metadata_processor(AcousticBrainz_Key().get_data)


================================================
FILE: plugins/add_to_collection/README.md
================================================
# Add to Collection

This plugin allows you to add any saved release to one of your user release [collections](https://musicbrainz.org/doc/Collections).

Download [here](https://picard.musicbrainz.org/api/v2/download?id=add_to_collection).

---

The plugin adds a settings page under the "Plugins" section under "Options..." from Picard's main menu that lets you choose
the collection you want to add the releases to

![settings](assets/settings.png)


================================================
FILE: plugins/add_to_collection/__init__.py
================================================
from picard.plugins.add_to_collection.manifest import *
from picard.plugins.add_to_collection import options, post_save_processor

options.register_options()
post_save_processor.register_processor()


================================================
FILE: plugins/add_to_collection/manifest.py
================================================
PLUGIN_NAME = "Add to Collection"
PLUGIN_AUTHOR = "Dvir Yitzchaki (dvirtz@gmail.com)"
PLUGIN_DESCRIPTION = "Adds any saved release to one of your user collections"
PLUGIN_VERSION = "0.1.2"
PLUGIN_API_VERSIONS = ["2.0"]
PLUGIN_LICENSE = ("MIT",)
PLUGIN_LICENSE_URL = "https://spdx.org/licenses/MIT.html"
PLUGIN_USER_GUIDE_URL = (
    "https://github.com/metabrainz/picard-plugins/blob/2.0/plugins/add_to_collection/README.md"
)


================================================
FILE: plugins/add_to_collection/options.py
================================================
from picard.collection import Collection, user_collections
from picard.ui.options import OptionsPage, register_options_page

from picard.plugins.add_to_collection import settings
from picard.plugins.add_to_collection.manifest import PLUGIN_NAME
from picard.plugins.add_to_collection.ui_add_to_collection_options import (
    Ui_AddToCollectionOptions,
)
from picard.plugins.add_to_collection.override_module import override_module


class AddToCollectionOptionsPage(OptionsPage):
    NAME = "add-to-collection"
    TITLE = PLUGIN_NAME
    PARENT = "plugins"

    options = [settings.collection_id_option()]

    def __init__(self, parent=None) -> None:
        super().__init__(parent)
        self.ui = Ui_AddToCollectionOptions()
        self.ui.setupUi(self)

    def load(self) -> None:
        self.set_collection_name(settings.collection_id())

    def save(self) -> None:
        settings.set_collection_id(self.ui.collection_name.currentData())

    def set_collection_name(self, value: str) -> None:
        self.ui.collection_name.clear()
        collection: Collection
        for collection in sorted(user_collections.values(), key=lambda c: c.name.lower()):
            self.ui.collection_name.addItem(collection.name, collection.id)
        idx = self.ui.collection_name.findData(value)
        if idx != -1:
            self.ui.collection_name.setCurrentIndex(idx)


def register_options() -> None:
    with override_module(AddToCollectionOptionsPage):
        register_options_page(AddToCollectionOptionsPage)


================================================
FILE: plugins/add_to_collection/override_module.py
================================================
from contextlib import contextmanager
from typing import Generator


@contextmanager
def override_module(obj: object) -> Generator[None, None, None]:
    # picard expects hooks to be defined at module level
    module = obj.__module__
    obj.__module__ = ".".join(module.split(".")[:-1])
    yield
    obj.__module__ = module


================================================
FILE: plugins/add_to_collection/post_save_processor.py
================================================
from picard import log
from picard.collection import Collection, user_collections
from picard.file import File, register_file_post_save_processor

from picard.plugins.add_to_collection import settings
from picard.plugins.add_to_collection.override_module import override_module


def post_save_processor(file: File) -> None:
    collection_id = settings.collection_id()
    if not collection_id:
        log.error("cannot find collection ID setting")
        return
    collection: Collection = user_collections.get(collection_id)
    if not collection:
        log.error(f"cannot find collection with id {collection_id}")
        return
    release_id = file.metadata["musicbrainz_albumid"]
    if release_id and release_id not in collection.releases:
        log.debug("Adding release %r to %r", release_id, collection.name)
        collection.add_releases(set([release_id]), callback=lambda: None)


def register_processor() -> None:
    with override_module(post_save_processor):
        register_file_post_save_processor(post_save_processor)


================================================
FILE: plugins/add_to_collection/settings.py
================================================
from picard.config import TextOption, get_config
from typing import Optional

COLLECTION_ID = "add_to_collection_id"


def collection_id_option() -> TextOption:
    return TextOption(section="setting", name=COLLECTION_ID, default=None)


def collection_id() -> Optional[str]:
    config = get_config()
    if COLLECTION_ID in config.setting:
        return config.setting[COLLECTION_ID]
    return None


def set_collection_id(value: str) -> None:
    config = get_config()
    config.setting[COLLECTION_ID] = value


================================================
FILE: plugins/add_to_collection/ui_add_to_collection_options.py
================================================
from PyQt5 import QtCore, QtWidgets


class Ui_AddToCollectionOptions(object):
    def setupUi(self, AddToCollectionOptions):
        AddToCollectionOptions.setObjectName("AddToCollectionOptions")
        AddToCollectionOptions.resize(472, 215)
        self.verticalLayout = QtWidgets.QVBoxLayout(AddToCollectionOptions)
        self.verticalLayout.setObjectName("verticalLayout")
        self.collection_label = QtWidgets.QLabel(AddToCollectionOptions)
        self.collection_label.setObjectName("collection_label")
        self.verticalLayout.addWidget(self.collection_label)
        sizePolicy = QtWidgets.QSizePolicy(
            QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed
        )
        self.collection_name = QtWidgets.QComboBox(AddToCollectionOptions)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.collection_name.sizePolicy().hasHeightForWidth()
        )
        self.collection_name.setSizePolicy(sizePolicy)
        self.collection_name.setEditable(False)
        self.collection_name.setObjectName("collection_name")
        self.verticalLayout.addWidget(self.collection_name)
        spacerItem = QtWidgets.QSpacerItem(
            20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding
        )
        self.verticalLayout.addItem(spacerItem)

        self.retranslateUi(AddToCollectionOptions)
        QtCore.QMetaObject.connectSlotsByName(AddToCollectionOptions)

    def retranslateUi(self, AddToCollectionOptions):
        _translate = QtCore.QCoreApplication.translate
        AddToCollectionOptions.setWindowTitle(_("Form"))
        self.collection_label.setText(_("Collection to add releases to:"))


================================================
FILE: plugins/additional_artists_details/__init__.py
================================================
# -*- coding: utf-8 -*-
"""Additional Artists Details
"""
# Copyright (C) 2023-2024 Bob Swift (rdswift)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.

# pylint: disable=line-too-long
# pylint: disable=import-error
# pylint: disable=too-many-arguments
# pylint: disable=too-many-locals

from collections import namedtuple
from functools import partial

from picard import (
    config,
    log,
)
from picard.album import register_album_post_removal_processor
from picard.metadata import (
    register_album_metadata_processor,
    register_track_metadata_processor,
)
from picard.plugin import PluginPriority
from picard.plugins.additional_artists_details.ui_options_additional_artists_details import (
    Ui_AdditionalArtistsDetailsOptionsPage,
)
from picard.webservice.api_helpers import MBAPIHelper

from picard.ui.options import (
    OptionsPage,
    register_options_page,
)


PLUGIN_NAME = 'Additional Artists Details'
PLUGIN_AUTHOR = 'Bob Swift (rdswift)'
PLUGIN_DESCRIPTION = '''
This plugin provides specialized album and track variables with artist details for use in tagging and naming scripts.  Note that this creates
additional calls to the MusicBrainz API for the artist and area information, and this will slow down processing.  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.
<br /><br />
Please see the <a href="https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/additional_artists_details/docs/README.md">user
guide</a> on GitHub for more information.
'''
PLUGIN_VERSION = '0.4'
PLUGIN_API_VERSIONS = ['2.0', '2.1', '2.2', '2.7', '2.8', '2.11']
PLUGIN_LICENSE = 'GPL-2.0-or-later'
PLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl-2.0.html'

PLUGIN_USER_GUIDE_URL = 'https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/additional_artists_details/docs/README.md'

# Named tuples for code clarity
Area = namedtuple('Area', ['parent', 'name', 'country', 'type', 'type_text'])
MetadataPair = namedtuple('MetadataPair', ['artists', 'target'])

# MusicBrainz ID codes for relationship types
RELATIONSHIP_TYPE_PART_OF = 'de7cc874-8b1b-3a05-8272-f3834c968fb7'

# MusicBrainz ID codes for area types
AREA_TYPE_COUNTRY = '06dd0ae4-8c74-30bb-b43d-95dcedf961de'
AREA_TYPE_COUNTY = 'bcecec27-8bdb-3e00-8254-d948dda502fa'
AREA_TYPE_MUNICIPALITY = '17246454-5ac4-36a1-b81a-4753eb2dab20'

# Area types to exclude from the location string
EXCLUDE_AREA_TYPES = {AREA_TYPE_MUNICIPALITY, AREA_TYPE_COUNTY}

# Standard text for arguments
ALBUM_ARTISTS = 'album_artists'
ARTIST = 'artist'
ARTIST_REQUESTS = 'artist_requests'
AREA = 'area'
AREA_REQUESTS = 'area_requests'
ISO_CODES_1 = 'iso-3166-1-codes'
ISO_CODES_2 = 'iso-3166-2-codes'
OPT_AREA_DETAILS = 'aad_area_details'
OPT_PROCESS_TRACKS = 'aad_process_tracks'
TRACKS = 'tracks'


def log_helper(text, *args):
    """Logging helper to prepend the plugin name to the text.

    Args:
        text (str): Text to log.
        args (list): List of text replacement arguments.

    Returns:
        list: updated text and replacement arguments.
    """
    retval = ["%s: " + text, PLUGIN_NAME]
    retval.extend(args)
    return retval


class CustomHelper(MBAPIHelper):
    """Custom MusicBrainz API helper to retrieve artist and area information.
    """

    def get_artist_by_id(self, _id, handler, inc=None, priority=False, important=False,
                         mblogin=False, refresh=False):
        """Get information for the specified artist MBID.

        Args:
            _id (str): Artist MBID to retrieve.
            handler (object): Callback used to process the returned information.
            inc (list, optional): List of includes to add to the API call. Defaults to None.
            priority (bool, optional): Process the request at a high priority. Defaults to False.
            important (bool, optional): Identify the request as important. Defaults to False.
            mblogin (bool, optional): Request requires logging into MusicBrainz. Defaults to False.
            refresh (bool, optional): Request triggers a refresh. Defaults to False.

        Returns:
            RequestTask: Requested task object.
        """
        return self._get_by_id(ARTIST, _id, handler, inc, priority=priority, important=important, mblogin=mblogin, refresh=refresh)

    def get_area_by_id(self, _id, handler, inc=None, priority=False, important=False, mblogin=False, refresh=False):
        """Get information for the specified area MBID.

        Args:
            _id (str): Area MBID to retrieve.
            handler (object): Callback used to process the returned information.
            inc (list, optional): List of includes to add to the API call. Defaults to None.
            priority (bool, optional): Process the request at a high priority. Defaults to False.
            important (bool, optional): Identify the request as important. Defaults to False.
            mblogin (bool, optional): Request requires logging into MusicBrainz. Defaults to False.
            refresh (bool, optional): Request triggers a refresh. Defaults to False.

        Returns:
            RequestTask: Requested task object.
        """
        if inc is None:
            inc = ['area-rels']
        return self._get_by_id(AREA, _id, handler, inc, priority=priority, important=important, mblogin=mblogin, refresh=refresh)


class ArtistDetailsPlugin:
    """Plugin to retrieve artist details, including area and country information.
    """
    result_cache = {
        ARTIST: {},
        ARTIST_REQUESTS: set(),
        AREA: {},
        AREA_REQUESTS: set(),
    }
    album_processing_count = {}
    albums = {}

    def _make_empty_target(self, album_id):
        """Create an empty album target node if it doesn't exist.

        Args:
            album_id (str): MBID of the album.
        """
        if album_id not in self.albums:
            self.albums[album_id] = {ALBUM_ARTISTS: set(), TRACKS: []}

    def _add_target(self, album_id, artists, target_metadata):
        """Add a metadata target to update for an album.

        Args:
            album_id (str): MBID of the album.
            artists (set): Set of artists to include.
            target_metadata (Metadata): Target metadata to update.
        """
        self._make_empty_target(album_id)
        self.albums[album_id][TRACKS].append(MetadataPair(artists, target_metadata))

    def _remove_album(self, album_id):
        """Removes an album from the metadata processing dictionary.

        Args:
            album_id (str): MBID of the album to remove.
        """
        log.debug(*log_helper("Removing album '%s'", album_id))
        self.albums.pop(album_id, None)
        self.album_processing_count.pop(album_id, None)

    def _album_add_request(self, album):
        """Increment the number of pending requests for an album.

        Args:
            album (Album): The Album object to use for the processing.
        """
        if album.id not in self.album_processing_count:
            self.album_processing_count[album.id] = 0
        self.album_processing_count[album.id] += 1
        album._requests += 1

    def _album_remove_request(self, album):
        """Decrement the number of pending requests for an album.

        Args:
            album (Album): The Album object to use for the processing.
        """
        if album.id not in self.album_processing_count:
            self.album_processing_count[album.id] = 1
        self.album_processing_count[album.id] -= 1
        album._requests -= 1
        album._finalize_loading(None)   # pylint: disable=protected-access

    def remove_album(self, album):
        """Remove the album from the albums processing dictionary.

        Args:
            album (Album): The album object to remove.
        """
        self._remove_album(album.id)

    def make_album_vars(self, album, album_metadata, _release_metadata):
        """Process album artists.

        Args:
            album (Album): The Album object to use for the processing.
            album_metadata (Metadata): Metadata object for the album.
            _release_metadata (dict): Dictionary of release data from MusicBrainz api.
        """
        artists = set(artist.id for artist in album.get_album_artists())
        self._make_empty_target(album.id)
        self.albums[album.id][ALBUM_ARTISTS] = artists
        if not config.setting[OPT_PROCESS_TRACKS]:
            log.info(*log_helper("Track artist processing is disabled."))
        self._artist_processing(artists, album, album_metadata, 'Album')

    def make_track_vars(self, album, album_metadata, track_metadata, _release_metadata):
        """Process track artists.

        Args:
            album (Album): The Album object to use for the processing.
            album_metadata (Metadata): Metadata object for the album.
            track_metadata (dict): Dictionary of track data from MusicBrainz api.
            _release_metadata (dict): Dictionary of release data from MusicBrainz api.
        """
        artists = set()
        source_type = 'track'
        # Test for valid metadata node.
        # The 'artist-credit' key should always be there.
        # This check is to avoid a runtime error if it doesn't exist for some reason.
        if config.setting[OPT_PROCESS_TRACKS]:
            if 'artist-credit' in track_metadata:
                for artist_credit in track_metadata['artist-credit']:
                    if 'artist' in artist_credit:
                        if 'id' in artist_credit['artist']:
                            artists.add(artist_credit['artist']['id'])
                    else:
                        # No 'artist' specified.  Log as an error.
                        self._metadata_error(album.id, 'artist-credit.artist', source_type)
            else:
                # No valid metadata found.  Log as error.
                self._metadata_error(album.id, 'artist-credit', source_type)
        self._artist_processing(artists, album, album_metadata, 'Track')

    def _artist_processing(self, artists, album, destination_metadata, source_type):
        """Retrieves the information for each artist not already processed.

        Args:
            artists (set): Set of artist MBIDs to process.
            album (Album): Album object to use for the processing.
            destination_metadata (Metadata): Metadata object to update with the new variables.
            source_type (str): Source type (album or track) for logging messages.
        """
        for temp_id in artists:
            if temp_id not in self.result_cache[ARTIST_REQUESTS]:
                self.result_cache[ARTIST_REQUESTS].add(temp_id)
                log.debug(*log_helper('Retrieving artist ID %s information from MusicBrainz.', temp_id))
                self._get_artist_info(temp_id, album)
            else:
                log.debug(*log_helper('%s artist ID %s information available from cache.', source_type, temp_id))
        self._add_target(album.id, artists, destination_metadata)
        self._save_artist_metadata(album.id)

    def _save_artist_metadata(self, album_id):
        """Saves the new artist details variables to the metadata targets for the specified album.

        Args:
            album_id (str): MBID of the album to process.
        """
        if album_id in self.album_processing_count and self.album_processing_count[album_id]:
            return
        if album_id not in self.albums or not self.albums[album_id][TRACKS]:
            log.error(*log_helper("No metadata targets found for album '%s'", album_id))
            return
        for item in self.albums[album_id][TRACKS]:
            # Add album artists to track so they are available in the metadata
            artists = self.albums[album_id][ALBUM_ARTISTS].copy().union(item.artists)
            destination_metadata = item.target
            for artist in artists:
                if artist in self.result_cache[ARTIST]:
                    self._set_artist_metadata(destination_metadata, artist, self.result_cache[ARTIST][artist])

    def _set_artist_metadata(self, destination_metadata, artist_id, artist_info):
        """Adds the artist information to the destination metadata.

        Args:
            destination_metadata (Metadata): Metadata object to update with new variables.
            artist_id (str): MBID of the artist to update.
            artist_info (dict): Dictionary of information for the artist.
        """
        def _set_item(key, value):
            destination_metadata[f"~artist_{artist_id}_{key.replace('-', '_')}"] = value

        for item in artist_info.keys():
            if item in {'area', 'begin-area', 'end-area'}:
                country, location = self._drill_area(artist_info[item])
                if country:
                    _set_item(item.replace('area', 'country'), country)
                if location:
                    _set_item(item.replace('area', 'location'), location)
            else:
                _set_item(item, artist_info[item])

    def _get_artist_info(self, artist_id, album):
        """Gets the artist information from the MusicBrainz website.

        Args:
            artist_id (str): MBID of the artist to retrieve.
            album (Album): The Album object to use for the processing.
        """
        self._album_add_request(album)
        helper = CustomHelper(album.tagger.webservice)
        handler = partial(
            self._artist_submission_handler,
            artist=artist_id,
            album=album,
        )
        return helper.get_artist_by_id(artist_id, handler)

    def _artist_submission_handler(self, document, _reply, error, artist=None, album=None):
        """Handles the response from the webservice requests for artist information.
        """
        try:
            if error:
                log.error(*log_helper("Artist '%s' information retrieval error.", artist))
                return
            artist_info = {}
            for item in ['type', 'gender', 'name', 'sort-name', 'disambiguation']:
                if item in document and document[item]:
                    artist_info[item] = document[item]
            if 'life-span' in document:
                for item in ['begin', 'end']:
                    if item in document['life-span'] and document['life-span'][item]:
                        artist_info[item] = document['life-span'][item]
            for item in ['area', 'begin-area', 'end-area']:
                if item in document and document[item] and 'id' in document[item] and document[item]['id']:
                    area_id = document[item]['id']
                    artist_info[item] = area_id
                    if area_id not in self.result_cache[AREA_REQUESTS]:
                        self._get_area_info(area_id, album)
            self.result_cache[ARTIST][artist] = artist_info
        finally:
            self._album_remove_request(album)
            self._save_artist_metadata(album.id)

    def _get_area_info(self, area_id, album):
        """Gets the area information from the MusicBrainz website.

        Args:
            area_id (str): MBID of the area to retrieve.
            album (Album): The Album object to use for the processing.
        """
        self.result_cache[AREA_REQUESTS].add(area_id)
        self._album_add_request(album)
        log.debug(*log_helper('Retrieving area ID %s from MusicBrainz.', area_id))
        helper = CustomHelper(album.tagger.webservice)
        handler = partial(
            self._area_submission_handler,
            area=area_id,
            album=album,
        )
        return helper.get_area_by_id(area_id, handler)

    def _area_submission_handler(self, document, _reply, error, area=None, album=None):
        """Handles the response from the webservice requests for area information.
        """
        try:
            if error:
                log.error(*log_helper("Area '%s' information retrieval error.", area))
                return
            (_id, name, country, _type, type_text) = self._parse_area(document)
            if _type == AREA_TYPE_COUNTRY and _id not in self.result_cache[AREA]:
                self._area_logger(_id, f"{name} ({country})", type_text)
                self.result_cache[AREA][_id] = Area('', name, country, _type, type_text)
            if 'relations' in document:
                for rel in document['relations']:
                    self._parse_area_relation(_id, rel, album, name, _type, type_text)
        finally:
            self._album_remove_request(album)
            self._save_artist_metadata(album.id)

    @staticmethod
    def _area_logger(area_id, area_name, area_type):
        """Adds a log entry for the area retrieved.

        Args:
            area_id (str): MBID of the area added.
            area_name (str): Name of the area added.
            area_type (str): Type of area added.
        """
        log.debug(*log_helper("Adding area: %s => %s of type '%s'", area_id, area_name, area_type))

    def _parse_area_relation(self, area_id, area_relation, album, area_name, area_type, area_type_text):
        """Parse an area relation to extract the area information.

        Args:
            area_id (str): MBID of the area providing the relationship.
            area_relation (dict): Dictionary of the area relationship.
            album (Album): The Album object to use for the processing.
            area_name (str): Name of the area providing the relationship.
            area_type (str): MBID of the type of area providing the relationship.
            area_type_text (str): Text description of the area providing the relationship.
        """
        if 'type-id' not in area_relation or 'area' not in area_relation or area_relation['type-id'] != RELATIONSHIP_TYPE_PART_OF:
            return
        (_id, name, country, _type, type_text) = self._parse_area(area_relation['area'])
        if not _id:
            return

        if 'direction' in area_relation and area_relation['direction'] == 'backward':
            if area_id not in self.result_cache[AREA]:
                self._area_logger(area_id, area_name, area_type_text)
                self.result_cache[AREA][area_id] = Area(_id, area_name, '', area_type, type_text)
                self.result_cache[AREA_REQUESTS].add(area_id)
            if _type == AREA_TYPE_COUNTRY:
                if _id not in self.result_cache[AREA]:
                    self._area_logger(_id, f"{name} ({country})", type_text)
                    self.result_cache[AREA][_id] = Area('', name, country, _type, type_text)
                    self.result_cache[AREA_REQUESTS].add(_id)
            else:
                if _id not in self.result_cache[AREA] and _id not in self.result_cache[AREA_REQUESTS]:
                    self._get_area_info(_id, album)
        else:
            self._area_logger(_id, name, type_text)
            self.result_cache[AREA_REQUESTS].add(_id)
            self.result_cache[AREA][_id] = Area(area_id, name, '', _type, type_text)

    @staticmethod
    def _parse_area(area_info):
        """Parse a dictionary of area information to return selected elements.

        Args:
            area_info (dict): Area information to parse.

        Returns:
            tuple: Selected information for the area (id, name, country code, type code, type text).
        """
        if 'id' not in area_info:
            return ('', '', '', '', '')
        area_id = area_info['id']
        area_name = area_info['name'] if 'name' in area_info else 'Unknown Name'
        area_type = area_info['type-id'] if 'type-id' in area_info else ''
        area_type_text = area_info['type'] if 'type' in area_info else 'Unknown Area Type'
        country = ''
        if area_type == AREA_TYPE_COUNTRY:
            if ISO_CODES_1 in area_info and area_info[ISO_CODES_1]:
                country = area_info[ISO_CODES_1][0]
            elif ISO_CODES_2 in area_info and area_info[ISO_CODES_2]:
                country = area_info[ISO_CODES_2][0][:2]
        return (area_id, area_name, country, area_type, area_type_text)

    @staticmethod
    def _metadata_error(album_id, metadata_element, metadata_group):
        """Logs metadata-related errors.

        Args:
            album_id (str): MBID of the album being processed.
            metadata_element (str): Metadata element initiating the error.
            metadata_group (str): Metadata group initiating the error.
        """
        log.error(*log_helper("Album '%s' missing '%s' in %s metadata.", album_id, metadata_element, metadata_group))

    def _drill_area(self, area_id):
        """Drills up from the specified area to determine the two-character
        country code and the full location description for the area.

        Args:
            area_id (str): MBID of the area to process.

        Returns:
            tuple: The two-character country code and full location description for the area.
        """
        country = ''
        location = []
        i = 7   # Counter to avoid potential runaway processing
        while i and area_id and not country:
            i -= 1
            area = self.result_cache[AREA][area_id] if area_id in self.result_cache[AREA] else Area('', '', '', '', '')
            country = area.country
            area_id = area.parent
            if not location or config.setting[OPT_AREA_DETAILS] or area.type not in EXCLUDE_AREA_TYPES:
                location.append(area.name)
        return country, ', '.join(location)


class AdditionalArtistsDetailsOptionsPage(OptionsPage):
    """Options page for the Additional Artists Details plugin.
    """

    NAME = "additional_artists_details"
    TITLE = "Additional Artists Details"
    PARENT = "plugins"

    options = [
        config.BoolOption('setting', OPT_PROCESS_TRACKS, False),
        config.BoolOption('setting', OPT_AREA_DETAILS, False),
    ]

    def __init__(self, parent=None):
        super(AdditionalArtistsDetailsOptionsPage, self).__init__(parent)
        self.ui = Ui_AdditionalArtistsDetailsOptionsPage()
        self.ui.setupUi(self)

        # Enable external link
        self.ui.format_description.setOpenExternalLinks(True)

    def load(self):
        """Load the option settings.
        """
        self.ui.cb_process_tracks.setChecked(config.setting[OPT_PROCESS_TRACKS])
        self.ui.cb_area_details.setChecked(config.setting[OPT_AREA_DETAILS])

    def save(self):
        """Save the option settings.
        """
        # self._set_settings(config.setting)
        config.setting[OPT_PROCESS_TRACKS] = self.ui.cb_process_tracks.isChecked()
        config.setting[OPT_AREA_DETAILS] = self.ui.cb_area_details.isChecked()


plugin = ArtistDetailsPlugin()

# Register the plugin to run at a LOW priority so that other plugins that
# modify the artist information can complete their processing and this plugin
# is working with the latest updated data.
register_album_metadata_processor(plugin.make_album_vars, priority=PluginPriority.LOW)
register_track_metadata_processor(plugin.make_track_vars, priority=PluginPriority.LOW)

register_album_post_removal_processor(plugin.remove_album)
register_options_page(AdditionalArtistsDetailsOptionsPage)


================================================
FILE: plugins/additional_artists_details/docs/README.md
================================================
# Additional Artists Details

## Overview

This 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.

***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.

---

## Option Settings

The 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.

![Additional Artists Details Option Settings](option_settings.png "Additional Artists Details Option Settings")

---

## What it Does

This 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.

### Variables Created

* **\_artist_\{artist_id\}_begin** - The begin date (birth date) of the artist
* **\_artist_\{artist_id\}_begin_country** - The begin two-character country code of the artist
* **\_artist_\{artist_id\}_begin_location** - The begin location of the artist
* **\_artist_\{artist_id\}_country** - The two-character country code for the artist
* **\_artist_\{artist_id\}_disambiguation** - The disambiguation information for the artist
* **\_artist_\{artist_id\}_end** - The end date (death date) of the artist
* **\_artist_\{artist_id\}_end_country** - The end two-character country code of the artist
* **\_artist_\{artist_id\}_end_location** - The end location of the artist
* **\_artist_\{artist_id\}_gender** - The gender of the artist
* **\_artist_\{artist_id\}_name** - The name of the artist
* **\_artist_\{artist_id\}_sort_name** - The sort name of the artist
* **\_artist_\{artist_id\}_type** - The type of artist (person, group, etc.)

A 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.

---

## Example

If 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:

Sarah Blackwood (primary artist):

* **\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_begin** = "1980-10-18"
* **\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_begin_country** = "CA"
* **\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_begin_location** = "Burlington, Ontario, Canada"
* **\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_country** = "CA"
* **\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_disambiguation** = "rockabilly, + "Somebody That I Used to Know""
* **\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_gender** = "Female"
* **\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_location** = "Canada"
* **\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_name** = "Sarah Blackwood"
* **\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_sort_name** = "Blackwood, Sarah"
* **\_artist_af7e5ea9_bd58_4346_8f78_d672e9f297f7_type** = "Person"

Jenny Pleau (additional artist)

* **\_artist_07fa21a9_c253_4ed0_b711_d63f7965b723_country** = "CA"
* **\_artist_07fa21a9_c253_4ed0_b711_d63f7965b723_gender** = "Female"
* **\_artist_07fa21a9_c253_4ed0_b711_d63f7965b723_location** = "Kitchener, Ontario, Canada"
* **\_artist_07fa21a9_c253_4ed0_b711_d63f7965b723_name** = "Jenni Pleau"
* **\_artist_07fa21a9_c253_4ed0_b711_d63f7965b723_sort_name** = "Pleau, Jenni"
* **\_artist_07fa21a9_c253_4ed0_b711_d63f7965b723_type** = "Person"

Emily Bones (additional artist)

* **\_artist_541d331c_f041_4895_b8f2_7db9e27dc5ab_gender** = "Female"
* **\_artist_541d331c_f041_4895_b8f2_7db9e27dc5ab_name** = "Emily Bones"
* **\_artist_541d331c_f041_4895_b8f2_7db9e27dc5ab_sort_name** = "Bones, Emily"
* **\_artist_541d331c_f041_4895_b8f2_7db9e27dc5ab_type** = "Person"

Note that variables will only be created for information that exists for the artist's record.

This could be used to set a **%country%** tag to the country code of the (primary) album artist with a tagging script like:

```taggerscript
$set(_tmp,_artist_$getmulti(%musicbrainz_albumartistid%,0)_)
$set(country,$if2($get(%_tmp%country),$get(%_tmp%begin_country),$get(%_tmp%end_country),xx))
```


================================================
FILE: plugins/additional_artists_details/options_additional_artists_details.ui
================================================
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>AdditionalArtistsDetailsOptionsPage</class>
 <widget class="QWidget" name="AdditionalArtistsDetailsOptionsPage">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>561</width>
    <height>802</height>
   </rect>
  </property>
  <property name="minimumSize">
   <size>
    <width>100</width>
    <height>0</height>
   </size>
  </property>
  <property name="windowTitle">
   <string>Form</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <item>
    <widget class="QScrollArea" name="scrollArea">
     <property name="frameShape">
      <enum>QFrame::NoFrame</enum>
     </property>
     <property name="widgetResizable">
      <bool>true</bool>
     </property>
     <widget class="QWidget" name="scrollAreaWidgetContents">
      <property name="geometry">
       <rect>
        <x>0</x>
        <y>0</y>
        <width>543</width>
        <height>784</height>
       </rect>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_2">
       <item>
        <widget class="QGroupBox" name="gb_description">
         <property name="title">
          <string>Additional Artists Details</string>
         </property>
         <layout class="QVBoxLayout" name="verticalLayout_3">
          <item>
           <widget class="QLabel" name="format_description">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
              <horstretch>0</horstretch>
              <verstretch>0</verstretch>
             </sizepolicy>
            </property>
            <property name="text">
             <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>
            </property>
            <property name="wordWrap">
             <bool>true</bool>
            </property>
           </widget>
          </item>
         </layout>
        </widget>
       </item>
       <item>
        <widget class="QGroupBox" name="gb_process_track_artists">
         <property name="title">
          <string>Process Track Artists</string>
         </property>
         <layout class="QVBoxLayout" name="verticalLayout_4">
          <item>
           <widget class="QLabel" name="label">
            <property name="text">
             <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>
            </property>
            <property name="wordWrap">
             <bool>true</bool>
            </property>
           </widget>
          </item>
          <item>
           <widget class="QCheckBox" name="cb_process_tracks">
            <property name="text">
             <string>Process track artists</string>
            </property>
           </widget>
          </item>
         </layout>
        </widget>
       </item>
       <item>
        <widget class="QGroupBox" name="gb_area_details">
         <property name="title">
          <string>Include Area Details</string>
         </property>
         <layout class="QVBoxLayout" name="verticalLayout_5">
          <item>
           <widget class="QLabel" name="label_2">
            <property name="text">
             <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>
            </property>
            <property name="wordWrap">
             <bool>true</bool>
            </property>
           </widget>
          </item>
          <item>
           <widget class="QCheckBox" name="cb_area_details">
            <property name="text">
             <string>Include area details</string>
            </property>
           </widget>
          </item>
         </layout>
        </widget>
       </item>
       <item>
        <spacer name="verticalSpacer">
         <property name="orientation">
          <enum>Qt::Vertical</enum>
         </property>
         <property name="sizeHint" stdset="0">
          <size>
           <width>20</width>
           <height>40</height>
          </size>
         </property>
        </spacer>
       </item>
      </layout>
     </widget>
    </widget>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>


================================================
FILE: plugins/additional_artists_details/ui_options_additional_artists_details.py
================================================
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file './plugins/additional_artists_details/options_additional_artists_details.ui'
#
# Created by: PyQt5 UI code generator 5.15.6
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_AdditionalArtistsDetailsOptionsPage(object):
    def setupUi(self, AdditionalArtistsDetailsOptionsPage):
        AdditionalArtistsDetailsOptionsPage.setObjectName("AdditionalArtistsDetailsOptionsPage")
        AdditionalArtistsDetailsOptionsPage.resize(561, 802)
        AdditionalArtistsDetailsOptionsPage.setMinimumSize(QtCore.QSize(100, 0))
        self.verticalLayout = QtWidgets.QVBoxLayout(AdditionalArtistsDetailsOptionsPage)
        self.verticalLayout.setObjectName("verticalLayout")
        self.scrollArea = QtWidgets.QScrollArea(AdditionalArtistsDetailsOptionsPage)
        self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame)
        self.scrollArea.setWidgetResizable(True)
        self.scrollArea.setObjectName("scrollArea")
        self.scrollAreaWidgetContents = QtWidgets.QWidget()
        self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 543, 784))
        self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.gb_description = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)
        self.gb_description.setObjectName("gb_description")
        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.gb_description)
        self.verticalLayout_3.setObjectName("verticalLayout_3")
        self.format_description = QtWidgets.QLabel(self.gb_description)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(self.format_description.sizePolicy().hasHeightForWidth())
        self.format_description.setSizePolicy(sizePolicy)
        self.format_description.setWordWrap(True)
        self.format_description.setObjectName("format_description")
        self.verticalLayout_3.addWidget(self.format_description)
        self.verticalLayout_2.addWidget(self.gb_description)
        self.gb_process_track_artists = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)
        self.gb_process_track_artists.setObjectName("gb_process_track_artists")
        self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.gb_process_track_artists)
        self.verticalLayout_4.setObjectName("verticalLayout_4")
        self.label = QtWidgets.QLabel(self.gb_process_track_artists)
        self.label.setWordWrap(True)
        self.label.setObjectName("label")
        self.verticalLayout_4.addWidget(self.label)
        self.cb_process_tracks = QtWidgets.QCheckBox(self.gb_process_track_artists)
        self.cb_process_tracks.setObjectName("cb_process_tracks")
        self.verticalLayout_4.addWidget(self.cb_process_tracks)
        self.verticalLayout_2.addWidget(self.gb_process_track_artists)
        self.gb_area_details = QtWidgets.QGroupBox(self.scrollAreaWidgetContents)
        self.gb_area_details.setObjectName("gb_area_details")
        self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.gb_area_details)
        self.verticalLayout_5.setObjectName("verticalLayout_5")
        self.label_2 = QtWidgets.QLabel(self.gb_area_details)
        self.label_2.setWordWrap(True)
        self.label_2.setObjectName("label_2")
        self.verticalLayout_5.addWidget(self.label_2)
        self.cb_area_details = QtWidgets.QCheckBox(self.gb_area_details)
        self.cb_area_details.setObjectName("cb_area_details")
        self.verticalLayout_5.addWidget(self.cb_area_details)
        self.verticalLayout_2.addWidget(self.gb_area_details)
        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        self.verticalLayout_2.addItem(spacerItem)
        self.scrollArea.setWidget(self.scrollAreaWidgetContents)
        self.verticalLayout.addWidget(self.scrollArea)

        self.retranslateUi(AdditionalArtistsDetailsOptionsPage)
        QtCore.QMetaObject.connectSlotsByName(AdditionalArtistsDetailsOptionsPage)

    def retranslateUi(self, AdditionalArtistsDetailsOptionsPage):
        _translate = QtCore.QCoreApplication.translate
        AdditionalArtistsDetailsOptionsPage.setWindowTitle(_translate("AdditionalArtistsDetailsOptionsPage", "Form"))
        self.gb_description.setTitle(_translate("AdditionalArtistsDetailsOptionsPage", "Additional Artists Details"))
        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>"))
        self.gb_process_track_artists.setTitle(_translate("AdditionalArtistsDetailsOptionsPage", "Process Track Artists"))
        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>"))
        self.cb_process_tracks.setText(_translate("AdditionalArtistsDetailsOptionsPage", "Process track artists"))
        self.gb_area_details.setTitle(_translate("AdditionalArtistsDetailsOptionsPage", "Include Area Details"))
        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>"))
        self.cb_area_details.setText(_translate("AdditionalArtistsDetailsOptionsPage", "Include area details"))


================================================
FILE: plugins/additional_artists_variables/additional_artists_variables.py
================================================
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018-2023 Bob Swift (rdswift)
# Copyright (C) 2023 Ruud van Asseldonk (ruuda)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.

PLUGIN_NAME = 'Additional Artists Variables'
PLUGIN_AUTHOR = 'Bob Swift (rdswift)'
PLUGIN_DESCRIPTION = '''
This plugin provides specialized album and track variables for use in
naming scripts. It is based on the "Album Artist Extension" plugin, but
expands the functionality to also include track artists. Note that it
cannot be used as a direct drop-in replacement for the "Album Artist
Extension" plugin because the variables are provided with different
names.  This will require changes to existing scripts if switching to
this plugin.
<br /><br />
Please 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.
'''
PLUGIN_VERSION = '1.0'
PLUGIN_API_VERSIONS = ['2.0', '2.1', '2.2', '2.7', '2.9', '2.10', '2.11']
PLUGIN_LICENSE = 'GPL-2.0-or-later'
PLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl-2.0.html'

PLUGIN_USER_GUIDE_URL = 'https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/additional_artists_variables/docs/README.md'

from operator import itemgetter

from picard import config, log
from picard.metadata import (register_album_metadata_processor,
                             register_track_metadata_processor)
from picard.plugin import PluginPriority


ID_ALIASES_ARTIST_NAME = '894afba6-2816-3c24-8072-eadb66bd04bc'
ID_ALIASES_LEGAL_NAME = 'd4dcd0c0-b341-3612-a332-c0ce797b25cf'


def process_artists(album_id, source_metadata, destination_metadata, source_type):
    # Test for valid metadata node.
    # The 'artist-credit' key should always be there.
    # This check is to avoid a runtime error if it doesn't exist for some reason.
    if 'artist-credit' in source_metadata:
        # Initialize variables to default values
        sort_pri_artist = ''
        sort_pri_artist_cred = ''
        std_artist = ''
        cred_artist = ''
        sort_artist = ''
        cred_sort_artist = ''
        legal_artist = ''
        additional_std_artist = ''
        additional_cred_artist = ''
        additional_sort_artist = ''
        additional_cred_sort_artist = ''
        additional_legal_artist = ''
        std_artist_list = []
        cred_artist_list = []
        sort_artist_list = []
        cred_sort_artist_list = []
        legal_artist_list = []
        artist_count = 0
        artist_ids = []
        artist_types = []
        artist_join_phrases = []
        for artist_credit in source_metadata['artist-credit']:
            # Initialize temporary variables for each loop.
            temp_std_name = ''
            temp_sort_name = ''
            temp_cred_name = ''
            temp_cred_sort_name = ''
            temp_legal_name = ''
            temp_legal_sort_name = ''
            temp_phrase = ''
            temp_id = ''
            temp_type = ''
            # Check if there is a 'joinphrase' specified.
            if 'joinphrase' in artist_credit:
                temp_phrase = artist_credit['joinphrase']
            else:
                metadata_error(album_id, 'artist-credit.joinphrase', source_type)
            # Check if there is a 'name' specified.
            if 'name' in artist_credit:
                temp_cred_name = artist_credit['name']
            else:
                metadata_error(album_id, 'artist-credit.name', source_type)
            # Check if there is an 'artist' specified.
            if 'artist' in artist_credit:
                if 'id' in artist_credit['artist']:
                    temp_id = artist_credit['artist']['id']
                else:
                    metadata_error(album_id, 'artist-credit.artist.id', source_type)
                if 'name' in artist_credit['artist']:
                    temp_std_name = artist_credit['artist']['name']
                else:
                    metadata_error(album_id, 'artist-credit.artist.name', source_type)
                if 'sort-name' in artist_credit['artist']:
                    temp_sort_name = artist_credit['artist']['sort-name']
                    temp_cred_sort_name = temp_sort_name
                else:
                    metadata_error(album_id, 'artist-credit.artist.sort-name', source_type)
                if 'type' in artist_credit['artist']:
                    temp_type = artist_credit['artist']['type']
                else:
                    metadata_error(album_id, 'artist-credit.artist.type', source_type)
                if 'aliases' in artist_credit['artist']:
                    for item in artist_credit['artist']['aliases']:
                        if 'type-id' in item and item['type-id'] == ID_ALIASES_LEGAL_NAME:
                            if 'ended' in item and not item['ended']:
                                if 'name' in item:
                                    temp_legal_name = item['name']
                                if 'sort-name' in item:
                                    temp_legal_sort_name = item['sort-name']
                        if 'type-id' in item and item['type-id'] == ID_ALIASES_ARTIST_NAME:
                            if 'name' in item and 'sort-name' in item and str(item['name']).lower() == temp_cred_name.lower():
                                temp_cred_sort_name = item['sort-name']
                tag_list = []
                if config.setting['max_genres']:
                    for tag_type in ['user-genres', 'genres', 'user-tags', 'tags']:
                        if tag_type in artist_credit['artist']:
                            for item in sorted(sorted(artist_credit['artist'][tag_type], key=itemgetter('name')), key=itemgetter('count'), reverse=True):
                                if item['count'] > 0:
                                    tag_list.append(item['name'])
                    tag_list = tag_list[:config.setting['max_genres']]
            else:
                # No 'artist' specified.  Log as an error.
                metadata_error(album_id, 'artist-credit.artist', source_type)
            std_artist += temp_std_name + temp_phrase
            cred_artist += temp_cred_name + temp_phrase
            sort_artist += temp_sort_name + temp_phrase
            cred_sort_artist += temp_cred_sort_name + temp_phrase
            artist_types.append(temp_type if temp_type else 'unknown',)
            artist_join_phrases.append(temp_phrase if temp_phrase else '\u200B',)
            if temp_legal_name:
                legal_artist += temp_legal_name + temp_phrase
                legal_artist_list.append(temp_legal_name,)
            else:
                # Use standardized name for combined string if legal name not available
                legal_artist += temp_std_name + temp_phrase
                # Use 'n/a' for list if legal name not available
                legal_artist_list.append('n/a',)
            if temp_std_name:
                std_artist_list.append(temp_std_name,)
            if temp_sort_name:
                sort_artist_list.append(temp_sort_name,)
            if temp_cred_name:
                cred_artist_list.append(temp_cred_name,)
            if temp_cred_sort_name:
                cred_sort_artist_list.append(temp_cred_sort_name,)
            if temp_id:
                artist_ids.append(temp_id,)
            if artist_count < 1:
                if temp_id:
                    destination_metadata['~artists_{0}_primary_id'.format(source_type,)] = temp_id
                destination_metadata['~artists_{0}_primary_std'.format(source_type,)] = temp_std_name
                destination_metadata['~artists_{0}_primary_cred'.format(source_type,)] = temp_cred_name
                destination_metadata['~artists_{0}_primary_sort'.format(source_type,)] = temp_sort_name
                destination_metadata['~artists_{0}_primary_cred_sort'.format(source_type,)] = temp_cred_sort_name
                destination_metadata['~artists_{0}_primary_legal'.format(source_type,)] = temp_legal_name
                destination_metadata['~artists_{0}_primary_sort_legal'.format(source_type,)] = temp_legal_sort_name
                sort_pri_artist += temp_sort_name + temp_phrase
                sort_pri_artist_cred += temp_cred_sort_name + temp_phrase
                if tag_list and source_type == 'album':
                    destination_metadata['~artists_{0}_primary_tags'.format(source_type,)] = tag_list
            else:
                sort_pri_artist += temp_std_name + temp_phrase
                additional_std_artist += temp_std_name + temp_phrase
                additional_cred_artist += temp_cred_name + temp_phrase
                additional_sort_artist += temp_sort_name + temp_phrase
                if temp_legal_name:
                    additional_legal_artist += temp_legal_name + temp_phrase
                else:
                    additional_legal_artist += temp_std_name + temp_phrase
                if temp_cred_sort_name:
                    additional_cred_sort_artist += temp_cred_sort_name + temp_phrase
                    sort_pri_artist_cred += temp_cred_sort_name + temp_phrase
                else:
                    additional_cred_sort_artist += temp_sort_name + temp_phrase
                    sort_pri_artist_cred += temp_sort_name + temp_phrase
            artist_count += 1
    else:
        # No valid metadata found.  Log as error.
        metadata_error(album_id, 'artist-credit', source_type)
    additional_std_artist_list = std_artist_list[1:]
    additional_cred_artist_list = cred_artist_list[1:]
    additional_sort_artist_list = sort_artist_list[1:]
    additional_cred_sort_artist_list = cred_sort_artist_list[1:]
    additional_legal_artist_list = legal_artist_list[1:]
    additional_artist_ids = artist_ids[1:]
    if additional_artist_ids:
        destination_metadata['~artists_{0}_additional_id'.format(source_type,)] = additional_artist_ids
    if additional_std_artist:
        destination_metadata['~artists_{0}_additional_std'.format(source_type,)] = additional_std_artist
    if additional_cred_artist:
        destination_metadata['~artists_{0}_additional_cred'.format(source_type,)] = additional_cred_artist
    if additional_sort_artist:
        destination_metadata['~artists_{0}_additional_sort'.format(source_type,)] = additional_sort_artist
    if additional_cred_sort_artist:
        destination_metadata['~artists_{0}_additional_cred_sort'.format(source_type,)] = additional_cred_sort_artist
    if additional_legal_artist:
        destination_metadata['~artists_{0}_additional_legal'.format(source_type,)] = additional_legal_artist
    if additional_std_artist_list:
        destination_metadata['~artists_{0}_additional_std_multi'.format(source_type,)] = additional_std_artist_list
    if additional_cred_artist_list:
        destination_metadata['~artists_{0}_additional_cred_multi'.format(source_type,)] = additional_cred_artist_list
    if additional_sort_artist_list:
        destination_metadata['~artists_{0}_additional_sort_multi'.format(source_type,)] = additional_sort_artist_list
    if additional_cred_sort_artist_list:
        destination_metadata['~artists_{0}_additional_cred_sort_multi'.format(source_type,)] = additional_cred_sort_artist_list
    if additional_legal_artist_list:
        destination_metadata['~artists_{0}_additional_legal_multi'.format(source_type,)] = additional_legal_artist_list
    if std_artist:
        destination_metadata['~artists_{0}_all_std'.format(source_type,)] = std_artist
    if cred_artist:
        destination_metadata['~artists_{0}_all_cred'.format(source_type,)] = cred_artist
    if cred_sort_artist:
        destination_metadata['~artists_{0}_all_cred_sort'.format(source_type,)] = cred_sort_artist
    if sort_artist:
        destination_metadata['~artists_{0}_all_sort'.format(source_type,)] = sort_artist
    if legal_artist:
        destination_metadata['~artists_{0}_all_legal'.format(source_type,)] = legal_artist
    if std_artist_list:
        destination_metadata['~artists_{0}_all_std_multi'.format(source_type,)] = std_artist_list
    if cred_artist_list:
        destination_metadata['~artists_{0}_all_cred_multi'.format(source_type,)] = cred_artist_list
    if sort_artist_list:
        destination_metadata['~artists_{0}_all_sort_multi'.format(source_type,)] = sort_artist_list
    if cred_sort_artist_list:
        destination_metadata['~artists_{0}_all_cred_sort_multi'.format(source_type,)] = cred_sort_artist_list
    if legal_artist_list:
        destination_metadata['~artists_{0}_all_legal_multi'.format(source_type,)] = legal_artist_list
    if sort_pri_artist:
        destination_metadata['~artists_{0}_all_sort_primary'.format(source_type,)] = sort_pri_artist
    if artist_types:
        destination_metadata['~artists_{0}_all_types'.format(source_type,)] = artist_types
    if artist_join_phrases:
        destination_metadata['~artists_{0}_all_join_phrases'.format(source_type,)] = artist_join_phrases
    if artist_count:
        destination_metadata['~artists_{0}_all_count'.format(source_type,)] = artist_count


def make_album_vars(album, album_metadata, release_metadata):
    album_id = release_metadata['id'] if release_metadata else 'No Album ID'
    process_artists(album_id, release_metadata, album_metadata, 'album')


def make_track_vars(album, album_metadata, track_metadata, release_metadata):
    album_id = release_metadata['id'] if release_metadata else 'No Album ID'
    process_artists(album_id, track_metadata, album_metadata, 'track')


def metadata_error(album_id, metadata_element, metadata_group):
    log.error("{0}: {1!r}: Missing '{2}' in {3} metadata.".format(
        PLUGIN_NAME, album_id, metadata_element, metadata_group,))


# Register the plugin to run at a LOW priority so that other plugins that
# modify the artist information can complete their processing and this plugin
# is working with the latest updated data.
register_album_metadata_processor(make_album_vars, priority=PluginPriority.LOW)
register_track_metadata_processor(make_track_vars, priority=PluginPriority.LOW)


================================================
FILE: plugins/addrelease/addrelease.py
================================================
# -*- coding: utf-8 -*-

PLUGIN_NAME = "Add Cluster As Release"
PLUGIN_AUTHOR = 'Frederik "Freso" S. Olesen, Lukáš Lalinský, Philip Jägenstedt'
PLUGIN_DESCRIPTION = "Adds a plugin context menu option to clusters and single\
 files to help you quickly add them as releases or standalone recordings to\
 the MusicBrainz database via the website by pre-populating artists,\
 track names and times."
PLUGIN_VERSION = "0.7.3"
PLUGIN_API_VERSIONS = ["2.0"]

from picard import config, log
from picard.cluster import Cluster
from picard.const import MUSICBRAINZ_SERVERS
from picard.file import File
from picard.util import webbrowser2
from picard.ui.itemviews import BaseAction, register_cluster_action, register_file_action

import os
import tempfile

HTML_HEAD = """<!doctype html>
<meta charset="UTF-8">
<title>%s</title>
<form action="%s" method="post">
"""
HTML_INPUT = """<input type="hidden" name="%s" value="%s">
"""
HTML_TAIL = """<input type="submit" value="%s">
</form>
<script>document.forms[0].submit()</script>
"""
HTML_ATTR_ESCAPE = {
    "&": "&amp;",
    '"': "&quot;"
}


def mbserver_url(path):
    host = config.setting["server_host"]
    port = config.setting["server_port"]
    if host in MUSICBRAINZ_SERVERS or port == 443:
        urlstring = "https://%s%s" % (host, path)
    elif port is None or port == 80:
        urlstring = "http://%s%s" % (host, path)
    else:
        urlstring = "http://%s:%d%s" % (host, port, path)
    return urlstring


class AddObjectAsEntity(BaseAction):
    NAME = "Add Object As Entity..."
    objtype = None
    submit_path = '/'

    def __init__(self):
        super(AddObjectAsEntity, self).__init__()
        self.form_values = {}

    def check_object(self, objs, objtype):
        """
        Checks if a given object array is valid (ie., has one item) and that
        its item is an object of the given type.

        Returns either False (if conditions are not met), or the object in the
        array.
        """
        if not isinstance(objs[0], objtype) or len(objs) != 1:
            return False
        else:
            return objs[0]

    def add_form_value(self, key, value):
        "Add global (e.g., release level) name-value pair."
        self.form_values[key] = value

    def set_form_values(self, objdata):
        return

    def generate_html_file(self, form_values):
        (fd, fp) = tempfile.mkstemp(suffix=".html")

        with os.fdopen(fd, "w", encoding="utf-8") as f:
            def esc(s):
                return "".join(HTML_ATTR_ESCAPE.get(c, c) for c in s)
            # add a global (release-level) name-value

            def nv(n, v):
                f.write(HTML_INPUT % (esc(n), esc(v)))

            f.write(HTML_HEAD % (self.NAME, mbserver_url(self.submit_path)))

            for key in form_values:
                nv(key, form_values[key])

            f.write(HTML_TAIL % (self.NAME))

        return fp

    def open_html_file(self, fp):
        webbrowser2.open("file://" + fp)

    def callback(self, objs):
        objdata = self.check_object(objs, self.objtype)
        try:
            if objdata:
                self.set_form_values(objdata)
                html_file = self.generate_html_file(self.form_values)
                self.open_html_file(html_file)
        finally:
            self.form_values.clear()


class AddClusterAsRelease(AddObjectAsEntity):
    NAME = "Add Cluster As Release..."
    objtype = Cluster
    submit_path = '/release/add'

    def __init__(self):
        super().__init__()
        self.discnumber_shift = -1

    def extract_discnumber(self, metadata):
        """
        >>> from picard.metadata import Metadata
        >>> m = Metadata()
        >>> AddClusterAsRelease().extract_discnumber(m)
        0
        >>> m["discnumber"] = "boop"
        >>> AddClusterAsRelease().extract_discnumber(m)
        0
        >>> m["discnumber"] = "1"
        >>> AddClusterAsRelease().extract_discnumber(m)
        0
        >>> m["discnumber"] = 1
        >>> AddClusterAsRelease().extract_discnumber(m)
        0
        >>> m["discnumber"] = -1
        >>> AddClusterAsRelease().extract_discnumber(m)
        0
        >>> m["discnumber"] = "1/1"
        >>> AddClusterAsRelease().extract_discnumber(m)
        0
        >>> m["discnumber"] = "2/2"
        >>> AddClusterAsRelease().extract_discnumber(m)
        1
        >>> a = AddClusterAsRelease()
        >>> m["discnumber"] = "-2/2"
        >>> a.extract_discnumber(m)
        0
        >>> m["discnumber"] = "-1/4"
        >>> a.extract_discnumber(m)
        1
        >>> m["discnumber"] = "1/4"
        >>> a.extract_discnumber(m)
        3

        """
        # As per https://musicbrainz.org/doc/Development/Release_Editor_Seeding#Tracklists_data
        # the medium numbers ("m") must be starting with 0.
        # Maybe the existing tags don't have disc numbers in them or
        # they're starting with something smaller than or equal to 0, so try
        # to produce a sane disc number.
        try:
            discnumber = metadata.get("discnumber", "1")
            # Split off any totaldiscs information
            discnumber = discnumber.split("/", 1)[0]
            m = int(discnumber)
            if m <= 0:
                # A disc number was smaller than or equal to 0 - all other
                # disc numbers need to be changed to accommodate that.
                self.discnumber_shift = max(self.discnumber_shift, 0 - m)
            m = m + self.discnumber_shift
        except ValueError as e:
            # The most likely reason for an exception at this point is because
            # the disc number in the tags was not a number. Just log the
            # exception and assume the medium number is 0.
            log.info("Trying to get the disc number of %s caused the following error: %s; assuming 0",
                     metadata["~filename"], e)
            m = 0
        return m

    def set_form_values(self, cluster):
        nv = self.add_form_value

        nv("artist_credit.names.0.artist.name", cluster.metadata["albumartist"])
        nv("name", cluster.metadata["album"])

        for i, file in enumerate(cluster.files):
            try:
                i = int(file.metadata["tracknumber"]) - 1
            except:
                pass

            m = self.extract_discnumber(file.metadata)

            # add a track-level name-value
            def tnv(n, v):
                nv("mediums.%d.track.%d.%s" % (m, i, n), v)

            tnv("name", file.metadata["title"])
            if file.metadata["artist"] != cluster.metadata["albumartist"]:
                tnv("artist_credit.names.0.name", file.metadata["artist"])
            tnv("length", str(file.metadata.length))


class AddFileAsRecording(AddObjectAsEntity):
    NAME = "Add File As Standalone Recording..."
    objtype = File
    submit_path = '/recording/create'

    def set_form_values(self, track):
        nv = self.add_form_value
        nv("edit-recording.name", track.metadata["title"])
        nv("edit-recording.artist_credit.names.0.artist.name", track.metadata["artist"])
        nv("edit-recording.length", track.metadata["~length"])


class AddFileAsRelease(AddObjectAsEntity):
    NAME = "Add File As Release..."
    objtype = File
    submit_path = '/release/add'

    def set_form_values(self, track):
        nv = self.add_form_value

        # Main album attributes
        if track.metadata["albumartist"]:
            nv("artist_credit.names.0.artist.name", track.metadata["albumartist"])
        else:
            nv("artist_credit.names.0.artist.name", track.metadata["artist"])
        if track.metadata["album"]:
            nv("name", track.metadata["album"])
        else:
            nv("name", track.metadata["title"])

        # Tracklist
        nv("mediums.0.track.0.name", track.metadata["title"])
        nv("mediums.0.track.0.artist_credit.names.0.name", track.metadata["artist"])
        nv("mediums.0.track.0.length", str(track.metadata.length))


register_cluster_action(AddClusterAsRelease())
register_file_action(AddFileAsRecording())
register_file_action(AddFileAsRelease())

if __name__ == "__main__":
    import doctest
    doctest.testmod()


================================================
FILE: plugins/albumartist_website/albumartist_website.py
================================================
# -*- coding: utf-8 -*-

PLUGIN_NAME = 'Album Artist Website'
PLUGIN_AUTHOR = 'Sophist, Sambhav Kothari, Philipp Wolfer'
PLUGIN_DESCRIPTION = '''Add's the album artist(s) Official Homepage(s)
(if they are defined in the MusicBrainz database).'''
PLUGIN_VERSION = '1.2'
PLUGIN_API_VERSIONS = ["2.0", "2.1", "2.2"]
PLUGIN_LICENSE = "GPL-2.0"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

from picard import config, log
from picard.util import LockableObject
from picard.metadata import register_track_metadata_processor
from functools import partial

class AlbumArtistWebsite:

    class ArtistWebsiteQueue(LockableObject):

        def __init__(self):
            LockableObject.__init__(self)
            self.queue = {}

        def __contains__(self, name):
            return name in self.queue

        def __iter__(self):
            return self.queue.__iter__()

        def __getitem__(self, name):
            self.lock_for_read()
            value = self.queue[name] if name in self.queue else None
            self.unlock()
            return value

        def __setitem__(self, name, value):
            self.lock_for_write()
            self.queue[name] = value
            self.unlock()

        def append(self, name, value):
            self.lock_for_write()
            if name in self.queue:
                self.queue[name].append(value)
                value = False
            else:
                self.queue[name] = [value]
                value = True
            self.unlock()
            return value

        def remove(self, name):
            self.lock_for_write()
            value = None
            if name in self.queue:
                value = self.queue[name]
                del self.queue[name]
            self.unlock()
            return value

    def __init__(self):
        self.website_cache = {}
        self.website_queue = self.ArtistWebsiteQueue()

    def add_artist_website(self, album, track_metadata, track_node, release_node):
        albumArtistIds = track_metadata.getall('musicbrainz_albumartistid')
        for artistId in albumArtistIds:
            # Jump through hoops to get track object!!
            track = album._new_tracks[-1]
            if artistId in self.website_cache:
                if self.website_cache[artistId]:
                    self.add_websites_to_track(track, self.website_cache[artistId])
            else:
                self.website_add_track(album, track, artistId)

    def website_add_track(self, album, track, artistId):
        self.album_add_request(album)
        if self.website_queue.append(artistId, (track, album)):
            host = config.setting["server_host"]
            port = config.setting["server_port"]
            path = "/ws/2/%s/%s" % ('artist', artistId)
            queryargs = {"inc": "url-rels"}
            return album.tagger.webservice.get(host, port, path,
                        partial(self.website_process, artistId),
                                parse_response_type="xml", priority=True, important=False,
                                queryargs=queryargs)

    def website_process(self, artistId, response, reply, error):
        if error:
            log.error("%s: %r: Network error retrieving artist record", PLUGIN_NAME, artistId)
            tuples = self.website_queue.remove(artistId)
            for track, album in tuples:
                self.album_remove_request(album)
            return
        urls = self.artist_process_metadata(artistId, response)
        self.website_cache[artistId] = urls
        tuples = self.website_queue.remove(artistId)
        for track, album in tuples:
            self.add_websites_to_track(track, urls)
            self.album_remove_request(album)

    def add_websites_to_track(self, track, urls):
        tm = track.metadata
        websites = tm.getall('website')
        websites += urls
        websites.sort()
        tm['website'] = websites
        for file in track.iterfiles(True):
            file.metadata['website'] = websites

    def album_add_request(self, album):
        album._requests += 1

    def album_remove_request(self, album):
        album._requests -= 1
        album._finalize_loading(None)

    def artist_process_metadata(self, artistId, response):
        log.debug("%s: %r: Processing Artist record for official website urls: %r", PLUGIN_NAME, artistId, response)
        relations = self.artist_get_relations(response)
        if not relations:
            log.info("%s: %r: Artist does have any associated urls.", PLUGIN_NAME, artistId)
            return []

        urls = []
        for relation in relations:
            log.debug("%s: %r: Examining: %r", PLUGIN_NAME, artistId, relation)
            if 'type' in relation.attribs and relation.type == 'official homepage':
                if 'target' in relation.children and len(relation.target) > 0:
                    if not 'ended' in relation.children or relation.ended[0].text != 'true':
                        log.debug("%s: Adding artist url: %s", PLUGIN_NAME, relation.target[0].text)
                        urls.append(relation.target[0].text)
                    else:
                        log.debug("%s: Artist url has ended: %s", PLUGIN_NAME, relation.target[0].text)
                else:
                    log.debug("%s: No url in relation: %r", PLUGIN_NAME, relation)

        if urls:
            log.info("%s: %r: Artist Official Homepages: %r", PLUGIN_NAME, artistId, urls)
        else:
            log.info("%s: %r: Artist does not have any official website urls.", PLUGIN_NAME, artistId)
        return sorted(urls)

    def artist_get_relations(self, response):
        log.debug("%s: artist_get_relations called", PLUGIN_NAME)
        if 'metadata' in response.children and len(response.metadata) > 0:
            if 'artist' in response.metadata[0].children and len(response.metadata[0].artist) > 0:
                if 'relation_list' in response.metadata[0].artist[0].children and len(response.metadata[0].artist[0].relation_list) > 0:
                    if 'relation' in response.metadata[0].artist[0].relation_list[0].children:
                        log.debug("%s: artist_get_relations returning: %r", PLUGIN_NAME, response.metadata[0].artist[0].relation_list[0].relation)
                        return response.metadata[0].artist[0].relation_list[0].relation
                    else:
                        log.debug("%s: artist_get_relations - no relation in relation_list", PLUGIN_NAME)
                else:
                    log.debug("%s: artist_get_relations - no relation_list in artist", PLUGIN_NAME)
            else:
                log.debug("%s: artist_get_relations - no artist in metadata", PLUGIN_NAME)
        else:
            log.debug("%s: artist_get_relations - no metadata in response", PLUGIN_NAME)
        return None


register_track_metadata_processor(AlbumArtistWebsite().add_artist_website)


================================================
FILE: plugins/albumartistextension/albumartistextension.py
================================================
PLUGIN_NAME = 'AlbumArtist Extension'
PLUGIN_AUTHOR = 'Bob Swift (rdswift)'
PLUGIN_DESCRIPTION = '''
This plugin provides standardized, credited and sorted artist information
for the album artist.  This is useful when your tagging or renaming scripts
require both the standardized artist name and the credited artist name, or
more detailed information about the album artists.
<br /><br />
The information is provided in the following variables:
<ul>
<li>_aaeStdAlbumArtists = The standardized version of the album artists.
<li>_aaeCredAlbumArtists = The credited version of the album artists.
<li>_aaeSortAlbumArtists = The sorted version of the album artists.
<li>_aaeStdPrimaryAlbumArtist = The standardized version of the first
    (primary) album artist.
<li>_aaeCredPrimaryAlbumArtist = The credited version of the first (primary)
    album artist.
<li>_aaeSortPrimaryAlbumArtist = The sorted version of the first (primary)
    album artist.
<li>_aaeAlbumArtistCount = The number of artists comprising the album artist.
</ul>
PLEASE NOTE: Once the plugin is installed, it automatically makes these 
variables available to File Naming Scripts and other scripts in Picard. 
Like other variables, you must mention them in a script for them to affect 
the file name or other data.
<br /><br />
This plugin is no longer being maintained. 
Consider switching to the 
[Additional Artists Variables plugin](https://github.com/rdswift/picard-plugins/tree/2.0/plugins/additional_artists_variables), 
which fills this 
role, and also includes additional variables. That other plugin uses different 
names for the album artist names provided here, so you if you switch plugins, you
will need to update your scripts with the different names.
<br /><br />
Version 0.6.1 of this plugin functions identically to Version 0.6. Only this 
description (and the version number) has changed.
'''

PLUGIN_VERSION = "0.6.1"
PLUGIN_API_VERSIONS = ["2.0"]
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

from picard import config, log
from picard.metadata import register_album_metadata_processor
from picard.plugin import PluginPriority

def add_artist_std_name(album, album_metadata, release_metadata):
    album_id = release_metadata['id']
    # Test for valid metadata node for the release
    if 'artist-credit' in release_metadata:
        # Initialize variables to default values
        cred_artist = ""
        std_artist = ""
        sort_artist = ""
        artist_count = 0
        # The 'artist-credit' key should always be there.
        # This check is to avoid a runtime error if it doesn't exist for some reason.
        for artist_credit in release_metadata['artist-credit']:
            # Initialize temporary variables for each loop.
            temp_std_name = ""
            temp_cred_name = ""
            temp_sort_name = ""
            temp_phrase = ""
            # Check if there is a 'joinphrase' specified.
            if 'joinphrase' in artist_credit:
                temp_phrase = artist_credit['joinphrase']
            else:
                metadata_error(album_id, 'artist-credit.joinphrase')
            # Check if there is a 'name' specified.
            if 'name' in artist_credit:
                temp_cred_name = artist_credit['name']
            else:
                metadata_error(album_id, 'artist-credit.name')
            # Check if there is an 'artist' specified.
            if 'artist' in artist_credit:
                # Check if there is a 'name' specified.
                if 'name' in artist_credit['artist']:
                    temp_std_name = artist_credit['artist']['name']
                else:
                    metadata_error(album_id, 'artist-credit.artist.name')
                if 'sort-name' in artist_credit['artist']:
                    temp_sort_name = artist_credit['artist']['sort-name']
                else:
                    metadata_error(album_id, 'artist-credit.artist.sort-name')
            else:
                metadata_error(album_id, 'artist-credit.artist')
            std_artist += temp_std_name + temp_phrase
            cred_artist += temp_cred_name + temp_phrase
            sort_artist += temp_sort_name + temp_phrase
            if artist_count < 1:
                album_metadata["~aaeStdPrimaryAlbumArtist"] = temp_std_name
                album_metadata["~aaeCredPrimaryAlbumArtist"] = temp_cred_name
                album_metadata["~aaeSortPrimaryAlbumArtist"] = temp_sort_name
            artist_count += 1
    else:
        metadata_error(album_id, 'artist-credit')
    if std_artist:
        album_metadata["~aaeStdAlbumArtists"] = std_artist
    if cred_artist:
        album_metadata["~aaeCredAlbumArtists"] = cred_artist
    if sort_artist:
        album_metadata["~aaeSortAlbumArtists"] = sort_artist
    if artist_count:
        album_metadata["~aaeAlbumArtistCount"] = artist_count
    return None


def metadata_error(album_id, metadata_element):
    log.error("%s: %r: Missing '%s' in release metadata.",
            PLUGIN_NAME, album_id, metadata_element)

# Register the plugin to run at a LOW priority so that other plugins that
# modify the artist information can complete their processing and this plugin
# is working with the latest updated data.
register_album_metadata_processor(add_artist_std_name, priority=PluginPriority.LOW)


================================================
FILE: plugins/amazon/amazon.py
================================================
# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
# Copyright (C) 2007 Oliver Charles
# Copyright (C) 2007-2011, 2019, 2021 Philipp Wolfer
# Copyright (C) 2007, 2010, 2011 Lukáš Lalinský
# Copyright (C) 2011 Michael Wiencek
# Copyright (C) 2011-2012 Wieland Hoffmann
# Copyright (C) 2013-2016 Laurent Monin
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

PLUGIN_NAME = 'Amazon cover art'
PLUGIN_AUTHOR = 'MusicBrainz Picard developers'
PLUGIN_DESCRIPTION = 'Use cover art from Amazon.'
PLUGIN_VERSION = "1.1"
PLUGIN_API_VERSIONS = ["2.2", "2.3", "2.4", "2.5", "2.6", "2.7"]
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

from picard import log
from picard.coverart.image import CoverArtImage
from picard.coverart.providers import (
    CoverArtProvider,
    register_cover_art_provider,
)
from picard.util import parse_amazon_url

# amazon image file names are unique on all servers and constructed like
# <ASIN>.<ServerNumber>.[SML]ZZZZZZZ.jpg
# A release sold on amazon.de has always <ServerNumber> = 03, for example.
# Releases not sold on amazon.com, don't have a "01"-version of the image,
# so we need to make sure we grab an existing image.
AMAZON_SERVER = {
    "amazon.com": {
        "server": "ec1.images-amazon.com",
        "id": "01",
    },
    "amazon.jp": {
        "server": "ec1.images-amazon.com",
        "id": "09",
    },
    "amazon.co.jp": {
        "server": "ec1.images-amazon.com",
        "id": "09",
    },
    "amazon.co.uk": {
        "server": "ec1.images-amazon.com",
        "id": "02",
    },
    "amazon.de": {
        "server": "ec2.images-amazon.com",
        "id": "03",
    },
    "amazon.ca": {
        "server": "ec1.images-amazon.com",
        "id": "01",  # .com and .ca are identical
    },
    "amazon.fr": {
        "server": "ec1.images-amazon.com",
        "id": "08"
    },
}

AMAZON_IMAGE_PATH = '/images/P/%(asin)s.%(serverid)s.%(size)s.jpg'

# First item in the list will be tried first
AMAZON_SIZES = (
    # huge size option is only available for items
    # that have a ZOOMing picture on its amazon web page
    # and it doesn't work for all of the domain names
    # '_SCRM_',        # huge size
    'LZZZZZZZ',      # large size, option format 1
    # '_SCLZZZZZZZ_',  # large size, option format 3
    'MZZZZZZZ',      # default image size, format 1
    # '_SCMZZZZZZZ_',  # medium size, option format 3
    # 'TZZZZZZZ',      # medium image size, option format 1
    # '_SCTZZZZZZZ_',  # small size, option format 3
    # 'THUMBZZZ',      # small size, option format 1
)


class CoverArtProviderAmazon(CoverArtProvider):

    """Use Amazon ASIN Musicbrainz relationships to get cover art"""

    NAME = "Amazon"
    TITLE = N_('Amazon')

    def __init__(self, coverart):
        super().__init__(coverart)
        self._has_url_relation = False

    def enabled(self):
        return (super().enabled()
                and not self.coverart.front_image_found)

    def queue_images(self):
        self.match_url_relations(('amazon asin', 'has_Amazon_ASIN'),
                                 self._queue_from_asin_relation)
        # No URL relationships loaded, try by ASIN
        if not self._has_url_relation:
            self._queue_from_asin()
        return CoverArtProvider.FINISHED

    def _queue_from_asin_relation(self, url):
        """Queue cover art images from Amazon"""
        amz = parse_amazon_url(url)
        if amz is None:
            return
        log.debug("Found ASIN relation : %s %s", amz['host'], amz['asin'])
        self._has_url_relation = True
        if amz['host'] in AMAZON_SERVER:
            server_info = AMAZON_SERVER[amz['host']]
        else:
            server_info = AMAZON_SERVER['amazon.com']
        self._queue_asin(server_info, amz['asin'])

    def _queue_from_asin(self):
        asin = self.release.get('asin')
        if asin:
            log.debug("Found ASIN : %s", asin)
            for server_info in AMAZON_SERVER.values():
               self._queue_asin(server_info, asin)

    def _queue_asin(self, server_info, asin):
        host = server_info['server']
        for size in AMAZON_SIZES:
            path = AMAZON_IMAGE_PATH % {
                'asin': asin,
                'serverid': server_info['id'],
                'size': size
            }
            self.queue_put(CoverArtImage("http://%s%s" % (host, path)))


register_cover_art_provider(CoverArtProviderAmazon)


================================================
FILE: plugins/bpm/__init__.py
================================================
# -*- coding: utf-8 -*-

# Changelog:
# [2015-09-15] Initial version
# [2017-11-24] Qt5, Python3 for Picard-plugins branch 2
# [2020-12-25] Move access to config.settings outside of thread
# Dependancies:
# aubio, numpy
#

PLUGIN_NAME = "BPM Analyzer"
PLUGIN_AUTHOR = "Len Joubert, Sambhav Kothari, Philipp Wolfer"
PLUGIN_DESCRIPTION = """Calculate BPM for selected files and albums. Linux only version with dependancy on Aubio and Numpy"""
PLUGIN_LICENSE = "GPL-2.0"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"
PLUGIN_VERSION = "1.5.2"
PLUGIN_API_VERSIONS = ["2.0"]
# PLUGIN_INCOMPATIBLE_PLATFORMS = [
#    'win32', 'cygwyn', 'darwin', 'os2', 'os2emx', 'riscos', 'atheos']

from aubio import source, tempo
from functools import partial
from numpy import median, diff

from picard.config import config, IntOption
from picard.file import File
from picard.plugins.bpm.ui_options_bpm import Ui_BPMOptionsPage
from picard.track import Track
from picard.ui.itemviews import BaseAction, register_file_action
from picard.ui.options import register_options_page, OptionsPage
from picard.util import thread


bpm_slider_settings = {
    1: (44100, 1024, 512),
    2: (8000, 512, 128),
    3: (4000, 128, 64),
}


class FileBPM(BaseAction):
    NAME = N_("Calculate BPM...")

    def __init__(self):
        super().__init__()
        self._close = False
        self.tagger.aboutToQuit.connect(self._cleanup)

    def _cleanup(self):
        self._close = True

    def _add_file_to_queue(self, file):
        settings = bpm_slider_settings[config.setting["bpm_slider_parameter"]]
        thread.run_task(
            partial(self._calculate_bpm, file, settings),
            partial(self._calculate_bpm_callback, file))

    def callback(self, objs):
        for obj in objs:
            if isinstance(obj, Track):
                for file_ in obj.linked_files:
                    self._add_file_to_queue(file_)
            elif isinstance(obj, File):
                self._add_file_to_queue(obj)

    def _calculate_bpm(self, file, settings):
        self.tagger.window.set_statusbar_message(
            N_('Calculating BPM for "%(filename)s"...'),
            {'filename': file.filename}
        )
        calculated_bpm = self._get_file_bpm(file.filename, settings)
        # self.tagger.log.debug('%s' % (calculated_bpm))
        if self._close:
            return
        file.metadata["bpm"] = str(round(calculated_bpm, 1))

    def _calculate_bpm_callback(self, file, result=None, error=None):
        if not error:
            file.update()
            self.tagger.window.set_statusbar_message(
                N_('BPM for "%(filename)s" successfully calculated.'),
                {'filename': file.filename}
            )
        else:
            self.tagger.window.set_statusbar_message(
                N_('Could not calculate BPM for "%(filename)s".'),
                {'filename': file.filename}
            )

    def _get_file_bpm(self, path, settings):
        """ Calculate the beats per minute (bpm) of a given file.
            path: path to the file
            buf_size    length of FFT
            hop_size    number of frames between two consecutive runs
            samplerate  sampling rate of the signal to analyze
        """

        samplerate, buf_size, hop_size = settings
        mediasource = source(path, samplerate, hop_size)
        samplerate = mediasource.samplerate
        beattracking = tempo("specdiff", buf_size, hop_size, samplerate)
        # List of beats, in samples
        beats = []
        # Total number of frames read
        total_frames = 0

        while True:
            if self._close:
                return
            samples, read = mediasource()
            is_beat = beattracking(samples)
            if is_beat:
                this_beat = beattracking.get_last_s()
                beats.append(this_beat)
            total_frames += read
            if read < hop_size:
                break

        # Convert to periods and to bpm
        bpms = 60. / diff(beats)
        return median(bpms)


class BPMOptionsPage(OptionsPage):

    NAME = "bpm"
    TITLE = "BPM"
    PARENT = "plugins"
    ACTIVE = True

    options = [
        IntOption("setting", "bpm_slider_parameter", 1)
    ]

    def __init__(self, parent=None):
        super(BPMOptionsPage, self).__init__(parent)
        self.ui = Ui_BPMOptionsPage()
        self.ui.setupUi(self)
        self.ui.slider_parameter.valueChanged.connect(self.update_parameters)
        self.update_parameters()

    def load(self):
        cfg = self.config.setting
        self.ui.slider_parameter.setValue(cfg["bpm_slider_parameter"])

    def save(self):
        cfg = self.config.setting
        cfg["bpm_slider_parameter"] = self.ui.slider_parameter.value()

    def update_parameters(self):
        val = self.ui.slider_parameter.value()
        samplerate, buf_size, hop_size = [str(v) for v in
                                          bpm_slider_settings[val]]
        self.ui.samplerate_value.setText(samplerate)
        self.ui.win_s_value.setText(buf_size)
        self.ui.hop_s_value.setText(hop_size)


register_file_action(FileBPM())
register_options_page(BPMOptionsPage)


================================================
FILE: plugins/bpm/ui_options_bpm.py
================================================
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'plugins/bpm/ui_options_bpm.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_BPMOptionsPage(object):
    def setupUi(self, BPMOptionsPage):
        BPMOptionsPage.setObjectName("BPMOptionsPage")
        BPMOptionsPage.resize(611, 273)
        self.verticalLayout = QtWidgets.QVBoxLayout(BPMOptionsPage)
        self.verticalLayout.setObjectName("verticalLayout")
        self.bpm_options = QtWidgets.QGroupBox(BPMOptionsPage)
        self.bpm_options.setObjectName("bpm_options")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.bpm_options)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.verticalWidget = QtWidgets.QWidget(self.bpm_options)
        self.verticalWidget.setMaximumSize(QtCore.QSize(500, 16777215))
        self.verticalWidget.setObjectName("verticalWidget")
        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.verticalWidget)
        self.verticalLayout_3.setObjectName("verticalLayout_3")
        self.slider_parameter = QtWidgets.QSlider(self.verticalWidget)
        self.slider_parameter.setMinimum(1)
        self.slider_parameter.setMaximum(3)
        self.slider_parameter.setPageStep(1)
        self.slider_parameter.setOrientation(QtCore.Qt.Horizontal)
        self.slider_parameter.setTickPosition(QtWidgets.QSlider.TicksBelow)
        self.slider_parameter.setTickInterval(1)
        self.slider_parameter.setObjectName("slider_parameter")
        self.verticalLayout_3.addWidget(self.slider_parameter)
        self.slider_labels = QtWidgets.QHBoxLayout()
        self.slider_labels.setSpacing(6)
        self.slider_labels.setObjectName("slider_labels")
        self.slider_super_fast = QtWidgets.QLabel(self.verticalWidget)
        self.slider_super_fast.setLayoutDirection(QtCore.Qt.RightToLeft)
        self.slider_super_fast.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
        self.slider_super_fast.setObjectName("slider_super_fast")
        self.slider_labels.addWidget(self.slider_super_fast)
        self.slider_default = QtWidgets.QLabel(self.verticalWidget)
        self.slider_default.setToolTip("")
        self.slider_default.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTop|QtCore.Qt.AlignTrailing)
        self.slider_default.setObjectName("slider_default")
        self.slider_labels.addWidget(self.slider_default)
        self.verticalLayout_3.addLayout(self.slider_labels)
        self.line = QtWidgets.QFrame(self.verticalWidget)
        self.line.setFrameShape(QtWidgets.QFrame.HLine)
        self.line.setFrameShadow(QtWidgets.QFrame.Sunken)
        self.line.setObjectName("line")
        self.verticalLayout_3.addWidget(self.line)
        self.gridLayout = QtWidgets.QGridLayout()
        self.gridLayout.setObjectName("gridLayout")
        self.samplerate_label = QtWidgets.QLabel(self.verticalWidget)
        self.samplerate_label.setObjectName("samplerate_label")
        self.gridLayout.addWidget(self.samplerate_label, 2, 0, 1, 1)
        self.samplerate_value = QtWidgets.QLabel(self.verticalWidget)
        self.samplerate_value.setText("")
        self.samplerate_value.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
        self.samplerate_value.setObjectName("samplerate_value")
        self.gridLayout.addWidget(self.samplerate_value, 2, 1, 1, 1)
        self.hop_s_value = QtWidgets.QLabel(self.verticalWidget)
        self.hop_s_value.setText("")
        self.hop_s_value.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
        self.hop_s_value.setObjectName("hop_s_value")
        self.gridLayout.addWidget(self.hop_s_value, 1, 1, 1, 1)
        self.hop_s_label = QtWidgets.QLabel(self.verticalWidget)
        self.hop_s_label.setObjectName("hop_s_label")
        self.gridLayout.addWidget(self.hop_s_label, 1, 0, 1, 1)
        self.win_s_value = QtWidgets.QLabel(self.verticalWidget)
        self.win_s_value.setText("")
        self.win_s_value.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter)
        self.win_s_value.setObjectName("win_s_value")
        self.gridLayout.addWidget(self.win_s_value, 0, 1, 1, 1)
        self.win_s_label = QtWidgets.QLabel(self.verticalWidget)
        self.win_s_label.setObjectName("win_s_label")
        self.gridLayout.addWidget(self.win_s_label, 0, 0, 1, 1)
        self.gridLayout.setColumnStretch(0, 4)
        self.verticalLayout_3.addLayout(self.gridLayout)
        self.verticalLayout_2.addWidget(self.verticalWidget)
        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        self.verticalLayout_2.addItem(spacerItem)
        self.verticalLayout.addWidget(self.bpm_options)

        self.retranslateUi(BPMOptionsPage)
        QtCore.QMetaObject.connectSlotsByName(BPMOptionsPage)

    def retranslateUi(self, BPMOptionsPage):
        _translate = QtCore.QCoreApplication.translate
        self.bpm_options.setTitle(_translate("BPMOptionsPage", "BPM Analyze Parameters:"))
        self.slider_super_fast.setText(_translate("BPMOptionsPage", "Super Fast"))
        self.slider_default.setText(_translate("BPMOptionsPage", "Default"))
        self.samplerate_label.setText(_translate("BPMOptionsPage", "Samplerate:"))
        self.hop_s_label.setText(_translate("BPMOptionsPage", "Number of frames between two consecutive runs:"))
        self.win_s_label.setText(_translate("BPMOptionsPage", "Length of FFT:"))


================================================
FILE: plugins/bpm/ui_options_bpm.ui
================================================
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>BPMOptionsPage</class>
 <widget class="QWidget" name="BPMOptionsPage">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>611</width>
    <height>273</height>
   </rect>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <item>
    <widget class="QGroupBox" name="bpm_options">
     <property name="title">
      <string>BPM Analyze Parameters:</string>
     </property>
     <layout class="QVBoxLayout" name="verticalLayout_2">
      <item>
       <widget class="QWidget" name="verticalWidget" native="true">
        <property name="maximumSize">
         <size>
          <width>500</width>
          <height>16777215</height>
         </size>
        </property>
        <layout class="QVBoxLayout" name="verticalLayout_3">
         <item>
          <widget class="QSlider" name="slider_parameter">
           <property name="minimum">
            <number>1</number>
           </property>
           <property name="maximum">
            <number>3</number>
           </property>
           <property name="pageStep">
            <number>1</number>
           </property>
           <property name="orientation">
            <enum>Qt::Horizontal</enum>
           </property>
           <property name="tickPosition">
            <enum>QSlider::TicksBelow</enum>
           </property>
           <property name="tickInterval">
            <number>1</number>
           </property>
          </widget>
         </item>
         <item>
          <layout class="QHBoxLayout" name="slider_labels">
           <property name="spacing">
            <number>6</number>
           </property>
           <item>
            <widget class="QLabel" name="slider_super_fast">
             <property name="layoutDirection">
              <enum>Qt::RightToLeft</enum>
             </property>
             <property name="text">
              <string>Super Fast</string>
             </property>
             <property name="alignment">
              <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
             </property>
            </widget>
           </item>
           <item>
            <widget class="QLabel" name="slider_default">
             <property name="toolTip">
              <string extracomment="Most accurate"/>
             </property>
             <property name="text">
              <string>Default</string>
             </property>
             <property name="alignment">
              <set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
             </property>
            </widget>
           </item>
          </layout>
         </item>
         <item>
          <widget class="Line" name="line">
           <property name="orientation">
            <enum>Qt::Horizontal</enum>
           </property>
          </widget>
         </item>
         <item>
          <layout class="QGridLayout" name="gridLayout" columnstretch="4,0">
           <item row="2" column="0">
            <widget class="QLabel" name="samplerate_label">
             <property name="text">
              <string>Samplerate:</string>
             </property>
            </widget>
           </item>
           <item row="2" column="1">
            <widget class="QLabel" name="samplerate_value">
             <property name="text">
              <string/>
             </property>
             <property name="alignment">
              <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
             </property>
            </widget>
           </item>
           <item row="1" column="1">
            <widget class="QLabel" name="hop_s_value">
             <property name="text">
              <string/>
             </property>
             <property name="alignment">
              <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
             </property>
            </widget>
           </item>
           <item row="1" column="0">
            <widget class="QLabel" name="hop_s_label">
             <property name="text">
              <string>Number of frames between two consecutive runs:</string>
             </property>
            </widget>
           </item>
           <item row="0" column="1">
            <widget class="QLabel" name="win_s_value">
             <property name="text">
              <string/>
             </property>
             <property name="alignment">
              <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
             </property>
            </widget>
           </item>
           <item row="0" column="0">
            <widget class="QLabel" name="win_s_label">
             <property name="text">
              <string>Length of FFT:</string>
             </property>
            </widget>
           </item>
          </layout>
         </item>
        </layout>
       </widget>
      </item>
      <item>
       <spacer name="verticalSpacer">
        <property name="orientation">
         <enum>Qt::Vertical</enum>
        </property>
        <property name="sizeHint" stdset="0">
         <size>
          <width>20</width>
          <height>40</height>
         </size>
        </property>
       </spacer>
      </item>
     </layout>
    </widget>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>


================================================
FILE: plugins/classical_extras/Readme.md
================================================
# General Information
This 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).

This 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.

This 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.  
It 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.

Hidden 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.
Tags 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. 
If the Options Page does not provide sufficient flexibility, users familiar with scripting can write Tagger Scripts to access the hidden variables directly.

## Updates
Version 2.0.11: Fix error when colons used to infer work names.

Version 2.0.10: Add hidden variable \_cwp_worktype_genres for types obtained from work (or any of its parents)

Version 2.0.9: Bug fix

Version 2.0.8: Add error trapping for certain MB database inconsistencies.

Version 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.

Version 2.0.6: Fixed crash on Picard 2.2.

Version 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.

Version 2.0.4: Fix occasional regex backtracking crash. Make naming of movement tags consistent with Picard docs. 
Added an option to attempt to get works and movement info from title if there are no work relationships (requires title in form "work: movement"). 
If 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) 
then 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.

Version 2.0.3: Fix exception when references XML file does not exist

Version 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.  
Added 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).   
A 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).   
A 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.

Version 2.0.1: Minor update to add _composer_lastnames variable.

Version 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.

For a list of previous version changes, see the end of this document.

# Installation
Install the zip file in your plugins folder in the usual fashion.

# Usage
After 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.

**Important**: 
1.  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.  
2.  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.  
3.  **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.

    **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. 
4.  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.  
5.  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.

## Artists tab
There are five coloured sections as shown in the screen image below:

![Artist options](https://music.highmossergate.co.uk/classical-extras-screenshots/artists/)

1. "Create extra artist metadata" should be selected otherwise this section will not run. This is the default.

2. "Work-artist/performer naming options". 
  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. 

   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.

    >Note regarding aliases and "credited as" names:  
    In a MB release, an artist can appear in one of seven contexts. Each of these is accessible in releaseXmlNode
    and the track and recording contexts are also accessible in trackXmlNode.  
    (They are applied in sequence - e.g. track artist credit will over-ride release artist credit)  
    The seven contexts are:  
    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)  
    Release-group: credited-as and alias  
    Release: credited-as and alias  
    Release relationship: credited-as only (see note)  
    Recording relationship (direct): credited-as only (see note)  
    Recording relationship (via work): credited-as only (see note)  
    Track: credited-as and alias  
    Note: Aliases **may** be retrieved for "relationship" artists, but the retrieval is not reliable (MusicBrainz webservice issue)

    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).

    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).

     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).

     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).

3. "Recording artist options".
  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).  
  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.

    Classical Extras puts the recording artists into 'hidden variables' (as a minimum) using the chosen naming convention.
  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.

     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.

     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.

4. "Other artist options":

    "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.
        <table>
        <tr><td>Artist type</td><td>Host tag</td><td>Hidden variable</td></tr>
        <tr><td>writer</td><td>composer</td><td>writers</td></tr>
        <tr><td>lyricist</td><td>lyricist</td><td>lyricists</td></tr>
        <tr><td>revised by</td><td>arranger</td><td>revisors</td></tr>
        <tr><td>translator</td><td>lyricist</td><td>translators</td></tr>
        <tr><td>arranger</td><td>arranger</td><td>arrangers</td></tr>
        <tr><td>reconstructed by</td><td>arranger</td><td>reconstructors</td></tr>
        <tr><td>orchestrator</td><td>arranger</td><td>orchestrators</td></tr>
        <tr><td>instrument arranger</td><td>arranger</td><td>arrangers (with instrument type in brackets)</td></tr>
        <tr><td>vocal arranger</td><td>arranger</td><td>arrangers (with voice type in brackets)</td></tr>
        <tr><td>chorus master</td><td>conductor</td><td>chorusmasters</td></tr>
        <tr><td>concertmaster</td><td>performer (with annotation as a sub-key)</td><td>leaders</td></tr>
        </table>

    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. 

     (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.)

     "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
Download .txt
gitextract_xqnp2_8g/

├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .prospector.yml
├── .pylintrc
├── README.md
├── build_ui.py
├── generate.py
├── get_plugin_data.py
├── plugins/
│   ├── abbreviate_artistsort/
│   │   └── abbreviate_artistsort.py
│   ├── acousticbrainz/
│   │   ├── __init__.py
│   │   ├── ui_options_acousticbrainz_tags.py
│   │   └── ui_options_acousticbrainz_tags.ui
│   ├── acousticbrainz_tonal-rhythm/
│   │   └── acousticbrainz_tonal-rhythm.py
│   ├── add_to_collection/
│   │   ├── README.md
│   │   ├── __init__.py
│   │   ├── manifest.py
│   │   ├── options.py
│   │   ├── override_module.py
│   │   ├── post_save_processor.py
│   │   ├── settings.py
│   │   └── ui_add_to_collection_options.py
│   ├── additional_artists_details/
│   │   ├── __init__.py
│   │   ├── docs/
│   │   │   └── README.md
│   │   ├── options_additional_artists_details.ui
│   │   └── ui_options_additional_artists_details.py
│   ├── additional_artists_variables/
│   │   └── additional_artists_variables.py
│   ├── addrelease/
│   │   └── addrelease.py
│   ├── albumartist_website/
│   │   └── albumartist_website.py
│   ├── albumartistextension/
│   │   └── albumartistextension.py
│   ├── amazon/
│   │   └── amazon.py
│   ├── bpm/
│   │   ├── __init__.py
│   │   ├── ui_options_bpm.py
│   │   └── ui_options_bpm.ui
│   ├── classical_extras/
│   │   ├── Readme.md
│   │   ├── __init__.py
│   │   ├── const.py
│   │   ├── options_classical_extras.ui
│   │   ├── suffixtree.py
│   │   └── ui_options_classical_extras.py
│   ├── classicdiscnumber/
│   │   └── classicdiscnumber.py
│   ├── collect_artists/
│   │   └── collect_artists.py
│   ├── compatible_TXXX/
│   │   └── compatible_TXXX.py
│   ├── critiquebrainz/
│   │   └── critiquebrainz.py
│   ├── cuesheet/
│   │   └── cuesheet.py
│   ├── decade/
│   │   └── __init__.py
│   ├── decode_cyrillic/
│   │   └── decode_cyrillic.py
│   ├── decode_greek_cyrillic/
│   │   └── decode_greek1253.py
│   ├── deezerart/
│   │   ├── __init__.py
│   │   ├── deezer/
│   │   │   ├── __init__.py
│   │   │   ├── client.py
│   │   │   └── obj.py
│   │   ├── options.py
│   │   └── options.ui
│   ├── discnumber/
│   │   └── discnumber.py
│   ├── enhanced_titles/
│   │   ├── __init__.py
│   │   ├── options_enhanced_titles.ui
│   │   └── ui_options_enhanced_titles.py
│   ├── fanarttv/
│   │   ├── __init__.py
│   │   ├── ui_options_fanarttv.py
│   │   └── ui_options_fanarttv.ui
│   ├── featartist/
│   │   └── featartist.py
│   ├── featartistsintitles/
│   │   └── featartistsintitles.py
│   ├── fix_tracknums/
│   │   └── fix_tracknums.py
│   ├── format_performer_tags/
│   │   ├── __init__.py
│   │   ├── docs/
│   │   │   ├── HISTORY.md
│   │   │   └── README.md
│   │   ├── ui_options_format_performer_tags.py
│   │   └── ui_options_format_performer_tags.ui
│   ├── genre_mapper/
│   │   ├── __init__.py
│   │   ├── options_genre_mapper.ui
│   │   └── ui_options_genre_mapper.py
│   ├── haikuattrs/
│   │   └── haikuattrs.py
│   ├── happidev_lyrics/
│   │   └── happidev_lyrics.py
│   ├── hyphen_unicode/
│   │   └── hyphen_unicode.py
│   ├── instruments/
│   │   └── instruments.py
│   ├── keep/
│   │   └── keep.py
│   ├── key_wheel_converter/
│   │   └── key_wheel_converter.py
│   ├── lastfm/
│   │   ├── __init__.py
│   │   ├── ui_options_lastfm.py
│   │   └── ui_options_lastfm.ui
│   ├── loadasnat/
│   │   └── loadasnat.py
│   ├── losslessfuncs/
│   │   └── __init__.py
│   ├── lrclib_lyrics/
│   │   ├── __init__.py
│   │   ├── option_lrclib_lyrics.py
│   │   └── option_lrclib_lyrics.ui
│   ├── matroska_tagger/
│   │   └── matroska_tagger.py
│   ├── mod/
│   │   └── __init__.py
│   ├── moodbars/
│   │   ├── __init__.py
│   │   ├── ui_options_moodbar.py
│   │   └── ui_options_moodbar.ui
│   ├── musixmatch/
│   │   ├── README
│   │   ├── __init__.py
│   │   └── ui_options_musixmatch.py
│   ├── no_release/
│   │   └── no_release.py
│   ├── non_ascii_equivalents/
│   │   └── non_ascii_equivalents.py
│   ├── padded/
│   │   └── padded.py
│   ├── papercdcase/
│   │   └── papercdcase.py
│   ├── performer_tag_replace/
│   │   ├── __init__.py
│   │   ├── options_performer_tag_replace.ui
│   │   └── ui_options_performer_tag_replace.py
│   ├── persistent_variables/
│   │   ├── __init__.py
│   │   └── ui_variables_dialog.py
│   ├── playlist/
│   │   └── playlist.py
│   ├── post_tagging_actions/
│   │   ├── __init__.py
│   │   ├── actions_status.py
│   │   ├── actions_status.ui
│   │   ├── docs/
│   │   │   └── guide.md
│   │   ├── options_post_tagging_actions.py
│   │   └── options_post_tagging_actions.ui
│   ├── release_type/
│   │   └── release_type.py
│   ├── releasetag_aggregations/
│   │   └── releasetag_aggregations.py
│   ├── remove_perfect_albums/
│   │   └── remove_perfect_albums.py
│   ├── reorder_sides/
│   │   └── reorder_sides.py
│   ├── replace_forbidden_symbols/
│   │   └── replace_forbidden_symbols.py
│   ├── replaygain2/
│   │   ├── __init__.py
│   │   ├── ui_options_replaygain2.py
│   │   └── ui_options_replaygain2.ui
│   ├── save_and_rewrite_header/
│   │   └── save_and_rewrite_header.py
│   ├── script_logger/
│   │   └── __init__.py
│   ├── search_engine_lookup/
│   │   ├── README.md
│   │   ├── __init__.py
│   │   ├── ui_options_search_engine_editor.py
│   │   ├── ui_options_search_engine_editor.ui
│   │   ├── ui_options_search_engine_lookup.py
│   │   └── ui_options_search_engine_lookup.ui
│   ├── smart_title_case/
│   │   └── smart_title_case.py
│   ├── sort_multivalue_tags/
│   │   └── sort_multivalue_tags.py
│   ├── soundtrack/
│   │   └── soundtrack.py
│   ├── standardise_feat/
│   │   └── standardise_feat.py
│   ├── standardise_performers/
│   │   └── standardise_performers.py
│   ├── submit_folksonomy_tags/
│   │   ├── README.md
│   │   ├── __init__.py
│   │   └── ui_config.py
│   ├── submit_isrc/
│   │   ├── README.md
│   │   └── __init__.py
│   ├── tangoinfo/
│   │   ├── README.md
│   │   └── __init__.py
│   ├── theaudiodb/
│   │   ├── __init__.py
│   │   ├── ui_options_theaudiodb.py
│   │   └── ui_options_theaudiodb.ui
│   ├── titlecase/
│   │   └── titlecase.py
│   ├── tracks2clipboard/
│   │   └── tracks2clipboard.py
│   ├── viewvariables/
│   │   ├── __init__.py
│   │   ├── ui_variables_dialog.py
│   │   └── ui_variables_dialog.ui
│   ├── wikidata/
│   │   ├── __init__.py
│   │   ├── ui_options_wikidata.py
│   │   └── ui_options_wikidata.ui
│   └── workandmovement/
│       ├── __init__.py
│       └── roman.py
├── setup.cfg
└── test/
    ├── __init__.py
    ├── plugin_test_case.py
    ├── test_add_to_collection.py
    ├── test_doctest.py
    ├── test_generate.py
    └── test_keep.py
Download .txt
SYMBOL INDEX (987 symbols across 115 files)

FILE: build_ui.py
  function compile_ui (line 13) | def compile_ui(uifile, pyfile):
  function newer (line 22) | def newer(file1, file2):

FILE: generate.py
  function build_json (line 21) | def build_json(dest_dir):
  function zip_files (line 60) | def zip_files(dest_dir):

FILE: get_plugin_data.py
  function get_plugin_data (line 17) | def get_plugin_data(filepath):

FILE: plugins/abbreviate_artistsort/abbreviate_artistsort.py
  function abbreviate_artistsort (line 82) | def abbreviate_artistsort(tagger, metadata, track, release):

FILE: plugins/acousticbrainz/__init__.py
  function log_msg (line 95) | def log_msg(logger, text, *args):
  function debug (line 98) | def debug(*args):
  function warning (line 101) | def warning(*args):
  function error (line 104) | def error(*args):
  class TrackDataProcessor (line 112) | class TrackDataProcessor:
    method __init__ (line 113) | def __init__(self, recording_id, metadata, level, data, files=None):
    method log (line 130) | def log(self, logger, text, *args):
    method debug (line 133) | def debug(self, *args):
    method warning (line 136) | def warning(self, *args):
    method error (line 139) | def error(self, *args):
    method shortid (line 146) | def shortid(self):
    method title (line 150) | def title(self):
    method process (line 156) | def process(self):
    method _extract_data (line 177) | def _extract_data(self, data):
    method filter_data (line 185) | def filter_data(self, data, subset):
    method update_metadata (line 201) | def update_metadata(self, name, values):
    method process_simplemood (line 211) | def process_simplemood(self):
    method process_simplegenre (line 226) | def process_simplegenre(self):
    method process_fullhighlevel (line 241) | def process_fullhighlevel(self):
    method process_keybpm (line 258) | def process_keybpm(self):
    method process_sublowlevel (line 277) | def process_sublowlevel(self):
  class AcousticBrainzRequest (line 295) | class AcousticBrainzRequest:
    method __init__ (line 299) | def __init__(self, webservice, recording_ids):
    method request_highlevel (line 303) | def request_highlevel(self, callback):
    method request_lowlevel (line 306) | def request_lowlevel(self, callback):
    method _batch (line 309) | def _batch(self, action, recording_ids, callback, result, response=Non...
    method _do_request (line 322) | def _do_request(self, action, recording_ids, callback):
    method _get_query_args (line 333) | def _get_query_args(self, action, recording_ids):
    method _merge_results (line 341) | def _merge_results(self, full, new):
  class AcousticBrainzPlugin (line 351) | class AcousticBrainzPlugin:
    method process_album (line 358) | def process_album(self, album, metadata, release):
    method process_track (line 363) | def process_track(self, album, metadata, track_node, release_node):
    method run_requests (line 377) | def run_requests(self, album, recording_ids, callback):
    method do_highlevel (line 387) | def do_highlevel(self):
    method do_lowlevel (line 393) | def do_lowlevel(self):
    method get_recording_ids (line 397) | def get_recording_ids(self, release):
    method iter_tracks (line 401) | def iter_tracks(release):
    method album_callback (line 412) | def album_callback(self, level, album, result=None, error=None):
    method nat_callback (line 421) | def nat_callback(self, recording_id, level, album, result=None, error=...
    method apply_result (line 426) | def apply_result(self, recording_id, metadata, level, result, files=No...
    method clear_cache (line 431) | def clear_cache(self, level, album):
  class AcousticBrainzOptionsPage (line 442) | class AcousticBrainzOptionsPage(OptionsPage):
    method __init__ (line 457) | def __init__(self, parent=None):
    method load (line 462) | def load(self):
    method save (line 472) | def save(self):

FILE: plugins/acousticbrainz/ui_options_acousticbrainz_tags.py
  class Ui_AcousticBrainzOptionsPage (line 14) | class Ui_AcousticBrainzOptionsPage(object):
    method setupUi (line 15) | def setupUi(self, AcousticBrainzOptionsPage):
    method retranslateUi (line 66) | def retranslateUi(self, AcousticBrainzOptionsPage):

FILE: plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py
  class AcousticBrainz_Key (line 49) | class AcousticBrainz_Key:
    method get_data (line 51) | def get_data(self, album, track_metadata, track_node, release_node):
    method process_data (line 71) | def process_data(self, album, track_metadata, response, reply, error):
    method album_add_request (line 100) | def album_add_request(self, album):
    method album_remove_request (line 103) | def album_remove_request(self, album):

FILE: plugins/add_to_collection/options.py
  class AddToCollectionOptionsPage (line 12) | class AddToCollectionOptionsPage(OptionsPage):
    method __init__ (line 19) | def __init__(self, parent=None) -> None:
    method load (line 24) | def load(self) -> None:
    method save (line 27) | def save(self) -> None:
    method set_collection_name (line 30) | def set_collection_name(self, value: str) -> None:
  function register_options (line 40) | def register_options() -> None:

FILE: plugins/add_to_collection/override_module.py
  function override_module (line 6) | def override_module(obj: object) -> Generator[None, None, None]:

FILE: plugins/add_to_collection/post_save_processor.py
  function post_save_processor (line 9) | def post_save_processor(file: File) -> None:
  function register_processor (line 24) | def register_processor() -> None:

FILE: plugins/add_to_collection/settings.py
  function collection_id_option (line 7) | def collection_id_option() -> TextOption:
  function collection_id (line 11) | def collection_id() -> Optional[str]:
  function set_collection_id (line 18) | def set_collection_id(value: str) -> None:

FILE: plugins/add_to_collection/ui_add_to_collection_options.py
  class Ui_AddToCollectionOptions (line 4) | class Ui_AddToCollectionOptions(object):
    method setupUi (line 5) | def setupUi(self, AddToCollectionOptions):
    method retranslateUi (line 34) | def retranslateUi(self, AddToCollectionOptions):

FILE: plugins/additional_artists_details/__init__.py
  function log_helper (line 96) | def log_helper(text, *args):
  class CustomHelper (line 111) | class CustomHelper(MBAPIHelper):
    method get_artist_by_id (line 115) | def get_artist_by_id(self, _id, handler, inc=None, priority=False, imp...
    method get_area_by_id (line 133) | def get_area_by_id(self, _id, handler, inc=None, priority=False, impor...
  class ArtistDetailsPlugin (line 153) | class ArtistDetailsPlugin:
    method _make_empty_target (line 165) | def _make_empty_target(self, album_id):
    method _add_target (line 174) | def _add_target(self, album_id, artists, target_metadata):
    method _remove_album (line 185) | def _remove_album(self, album_id):
    method _album_add_request (line 195) | def _album_add_request(self, album):
    method _album_remove_request (line 206) | def _album_remove_request(self, album):
    method remove_album (line 218) | def remove_album(self, album):
    method make_album_vars (line 226) | def make_album_vars(self, album, album_metadata, _release_metadata):
    method make_track_vars (line 241) | def make_track_vars(self, album, album_metadata, track_metadata, _rele...
    method _artist_processing (line 269) | def _artist_processing(self, artists, album, destination_metadata, sou...
    method _save_artist_metadata (line 288) | def _save_artist_metadata(self, album_id):
    method _set_artist_metadata (line 307) | def _set_artist_metadata(self, destination_metadata, artist_id, artist...
    method _get_artist_info (line 328) | def _get_artist_info(self, artist_id, album):
    method _artist_submission_handler (line 344) | def _artist_submission_handler(self, document, _reply, error, artist=N...
    method _get_area_info (line 370) | def _get_area_info(self, area_id, album):
    method _area_submission_handler (line 388) | def _area_submission_handler(self, document, _reply, error, area=None,...
    method _area_logger (line 407) | def _area_logger(area_id, area_name, area_type):
    method _parse_area_relation (line 417) | def _parse_area_relation(self, area_id, area_relation, album, area_nam...
    method _parse_area (line 453) | def _parse_area(area_info):
    method _metadata_error (line 477) | def _metadata_error(album_id, metadata_element, metadata_group):
    method _drill_area (line 487) | def _drill_area(self, area_id):
  class AdditionalArtistsDetailsOptionsPage (line 510) | class AdditionalArtistsDetailsOptionsPage(OptionsPage):
    method __init__ (line 523) | def __init__(self, parent=None):
    method load (line 531) | def load(self):
    method save (line 537) | def save(self):

FILE: plugins/additional_artists_details/ui_options_additional_artists_details.py
  class Ui_AdditionalArtistsDetailsOptionsPage (line 14) | class Ui_AdditionalArtistsDetailsOptionsPage(object):
    method setupUi (line 15) | def setupUi(self, AdditionalArtistsDetailsOptionsPage):
    method retranslateUi (line 76) | def retranslateUi(self, AdditionalArtistsDetailsOptionsPage):

FILE: plugins/additional_artists_variables/additional_artists_variables.py
  function process_artists (line 53) | def process_artists(album_id, source_metadata, destination_metadata, sou...
  function make_album_vars (line 256) | def make_album_vars(album, album_metadata, release_metadata):
  function make_track_vars (line 261) | def make_track_vars(album, album_metadata, track_metadata, release_metad...
  function metadata_error (line 266) | def metadata_error(album_id, metadata_element, metadata_group):

FILE: plugins/addrelease/addrelease.py
  function mbserver_url (line 39) | def mbserver_url(path):
  class AddObjectAsEntity (line 51) | class AddObjectAsEntity(BaseAction):
    method __init__ (line 56) | def __init__(self):
    method check_object (line 60) | def check_object(self, objs, objtype):
    method add_form_value (line 73) | def add_form_value(self, key, value):
    method set_form_values (line 77) | def set_form_values(self, objdata):
    method generate_html_file (line 80) | def generate_html_file(self, form_values):
    method open_html_file (line 100) | def open_html_file(self, fp):
    method callback (line 103) | def callback(self, objs):
  class AddClusterAsRelease (line 114) | class AddClusterAsRelease(AddObjectAsEntity):
    method __init__ (line 119) | def __init__(self):
    method extract_discnumber (line 123) | def extract_discnumber(self, metadata):
    method set_form_values (line 183) | def set_form_values(self, cluster):
  class AddFileAsRecording (line 207) | class AddFileAsRecording(AddObjectAsEntity):
    method set_form_values (line 212) | def set_form_values(self, track):
  class AddFileAsRelease (line 219) | class AddFileAsRelease(AddObjectAsEntity):
    method set_form_values (line 224) | def set_form_values(self, track):

FILE: plugins/albumartist_website/albumartist_website.py
  class AlbumArtistWebsite (line 17) | class AlbumArtistWebsite:
    class ArtistWebsiteQueue (line 19) | class ArtistWebsiteQueue(LockableObject):
      method __init__ (line 21) | def __init__(self):
      method __contains__ (line 25) | def __contains__(self, name):
      method __iter__ (line 28) | def __iter__(self):
      method __getitem__ (line 31) | def __getitem__(self, name):
      method __setitem__ (line 37) | def __setitem__(self, name, value):
      method append (line 42) | def append(self, name, value):
      method remove (line 53) | def remove(self, name):
    method __init__ (line 62) | def __init__(self):
    method add_artist_website (line 66) | def add_artist_website(self, album, track_metadata, track_node, releas...
    method website_add_track (line 77) | def website_add_track(self, album, track, artistId):
    method website_process (line 89) | def website_process(self, artistId, response, reply, error):
    method add_websites_to_track (line 103) | def add_websites_to_track(self, track, urls):
    method album_add_request (line 112) | def album_add_request(self, album):
    method album_remove_request (line 115) | def album_remove_request(self, album):
    method artist_process_metadata (line 119) | def artist_process_metadata(self, artistId, response):
    method artist_get_relations (line 145) | def artist_get_relations(self, response):

FILE: plugins/albumartistextension/albumartistextension.py
  function add_artist_std_name (line 48) | def add_artist_std_name(album, album_metadata, release_metadata):
  function metadata_error (line 109) | def metadata_error(album_id, metadata_element):

FILE: plugins/amazon/amazon.py
  class CoverArtProviderAmazon (line 95) | class CoverArtProviderAmazon(CoverArtProvider):
    method __init__ (line 102) | def __init__(self, coverart):
    method enabled (line 106) | def enabled(self):
    method queue_images (line 110) | def queue_images(self):
    method _queue_from_asin_relation (line 118) | def _queue_from_asin_relation(self, url):
    method _queue_from_asin (line 131) | def _queue_from_asin(self):
    method _queue_asin (line 138) | def _queue_asin(self, server_info, asin):

FILE: plugins/bpm/__init__.py
  class FileBPM (line 41) | class FileBPM(BaseAction):
    method __init__ (line 44) | def __init__(self):
    method _cleanup (line 49) | def _cleanup(self):
    method _add_file_to_queue (line 52) | def _add_file_to_queue(self, file):
    method callback (line 58) | def callback(self, objs):
    method _calculate_bpm (line 66) | def _calculate_bpm(self, file, settings):
    method _calculate_bpm_callback (line 77) | def _calculate_bpm_callback(self, file, result=None, error=None):
    method _get_file_bpm (line 90) | def _get_file_bpm(self, path, settings):
  class BPMOptionsPage (line 124) | class BPMOptionsPage(OptionsPage):
    method __init__ (line 135) | def __init__(self, parent=None):
    method load (line 142) | def load(self):
    method save (line 146) | def save(self):
    method update_parameters (line 150) | def update_parameters(self):

FILE: plugins/bpm/ui_options_bpm.py
  class Ui_BPMOptionsPage (line 14) | class Ui_BPMOptionsPage(object):
    method setupUi (line 15) | def setupUi(self, BPMOptionsPage):
    method retranslateUi (line 93) | def retranslateUi(self, BPMOptionsPage):

FILE: plugins/classical_extras/__init__.py
  function write_log (line 153) | def write_log(release_id, log_type, message, *args):
  function close_log (line 261) | def close_log(release_id, caller):
  function _node_name (line 355) | def _node_name(n):
  function _read_xml (line 359) | def _read_xml(stream):
  function get_preferred_artist_language (line 381) | def get_preferred_artist_language(config):
  function parse_data (line 393) | def parse_data(release_id, obj, response_list, *match):
  function create_dict_from_ref_list (line 529) | def create_dict_from_ref_list(options, release_id, ref_list, keys, tags):
  function get_references_from_file (line 544) | def get_references_from_file(release_id, path, filename):
  function get_preserved_tags (line 612) | def get_preserved_tags():
  function get_options (line 619) | def get_options(release_id, album, track):
  function plugin_options (line 989) | def plugin_options(option_type):
  function option_settings (line 1012) | def option_settings(config_settings):
  function get_aliases (line 1025) | def get_aliases(self, release_id, album, options, releaseXmlNode):
  function get_artists (line 1148) | def get_artists(options, release_id, tm, relations, relation_type):
  function create_artist_data (line 1179) | def create_artist_data(release_id, options, log_options, tm, relations,
  function get_series (line 1316) | def get_series(options, release_id, relations):
  function apply_artist_style (line 1374) | def apply_artist_style(
  function set_work_artists (line 1433) | def set_work_artists(self, release_id, album, track, writerList, tm, cou...
  function is_latin (line 1630) | def is_latin(uchr):
  function only_roman_chars (line 1639) | def only_roman_chars(unistr):
  function get_roman (line 1646) | def get_roman(string):
  function remove_middle (line 1662) | def remove_middle(performer):
  function unsort (line 1673) | def unsort(performer):
  function _reverse_sortname (line 1686) | def _reverse_sortname(sortname):
  function stripsir (line 1704) | def stripsir(performer):
  function replace_roman_numerals (line 1729) | def replace_roman_numerals(s):
  function from_roman (line 1741) | def from_roman(s):
  function turbo_lcs (line 1777) | def turbo_lcs(release_id, multi_list):
  function longest_common_substring (line 1838) | def longest_common_substring(s1, s2):
  function longest_common_sequence (line 1863) | def longest_common_sequence(list1, list2, minstart=0, maxstart=0):
  function substart_finder (line 1888) | def substart_finder(mylist, pattern):
  function get_ui_tags (line 1895) | def get_ui_tags():
  function map_tags (line 1911) | def map_tags(options, release_id, album, tm):
  function sort_suffix (line 2504) | def sort_suffix(tag):
  function append_tag (line 2513) | def append_tag(release_id, tm, tag, source, separators=None):
  function get_artist_credit (line 2598) | def get_artist_credit(options, release_id, obj):
  function get_aliases_and_credits (line 2623) | def get_aliases_and_credits(
  function get_relation_credits (line 2662) | def get_relation_credits(
  function composer_last_names (line 2729) | def composer_last_names(self, release_id, tm, album):
  function add_list_uniquely (line 2788) | def add_list_uniquely(list_to, list_from):
  function str_to_list (line 2811) | def str_to_list(s):
  function list_to_str (line 2830) | def list_to_str(l):
  function interpret (line 2841) | def interpret(tag):
  function time_to_secs (line 2856) | def time_to_secs(a):
  function seq_last_names (line 2873) | def seq_last_names(self, album):
  function year (line 2893) | def year(date):
  function blank_if_none (line 2907) | def blank_if_none(val):
  function strip_excess_punctuation (line 2919) | def strip_excess_punctuation(s):
  class ExtraArtists (line 2988) | class ExtraArtists():
    method __init__ (line 2991) | def __init__(self):
    method add_artist_info (line 3036) | def add_artist_info(
    method ensemble_type (line 3400) | def ensemble_type(self, performer):
    method process_album (line 3432) | def process_album(self, release_id, album):
    method write_metadata (line 3516) | def write_metadata(self, release_id, options, album, track):
    method infer_genres (line 3585) | def infer_genres(self, release_id, options, track, tm):
    method append_tag (line 3708) | def append_tag(self, release_id, tm, tag, source):
    method set_performer (line 3725) | def set_performer(self, release_id, album, track, performerList, tm):
  class PartLevels (line 4026) | class PartLevels():
    class WorksQueue (line 4028) | class WorksQueue(LockableObject):
      method __init__ (line 4031) | def __init__(self):
      method __contains__ (line 4035) | def __contains__(self, name):
      method __iter__ (line 4038) | def __iter__(self):
      method __getitem__ (line 4041) | def __getitem__(self, name):
      method __setitem__ (line 4047) | def __setitem__(self, name, value):
      method append (line 4052) | def append(self, name, value):
      method remove (line 4063) | def remove(self, name):
    method __init__ (line 4074) | def __init__(self):
    method add_work_info (line 4159) | def add_work_info(
    method build_work_info (line 4335) | def build_work_info(self, release_id, options, trackXmlNode, album, tr...
    method get_sk_tags (line 4545) | def get_sk_tags(self, release_id, album, track, tm, options):
    method check_cache (line 4680) | def check_cache(self, tm, album, track, workId_tuple, not_in_cache):
    method work_not_in_cache (line 4700) | def work_not_in_cache(self, release_id, album, track, workId_tuple):
    method work_add_track (line 4741) | def work_add_track(self, album, track, workId, tries, user_data=True):
    method work_process (line 4818) | def work_process(self, workId, tries, response, reply, error):
    method work_process_metadata (line 5152) | def work_process_metadata(self, release_id, workId, wid, track, respon...
    method work_process_relations (line 5280) | def work_process_relations(
    method album_add_request (line 5425) | def album_add_request(release_id, album):
    method album_remove_request (line 5441) | def album_remove_request(release_id, album):
    method process_album (line 5460) | def process_album(self, release_id, album):
    method create_trackback (line 5694) | def create_trackback(self, release_id, album, parentId):
    method append_trackback (line 5717) | def append_trackback(self, release_id, album, parentId, child):
    method level_calc (line 5770) | def level_calc(self, release_id, trackback, height):
    method process_trackback (line 5801) | def process_trackback(
    method process_trackback_children (line 5901) | def process_trackback_children(
    method derive_from_structure (line 6017) | def derive_from_structure(
    method create_work_levels (line 6178) | def create_work_levels(self, release_id, name_type, tracks, track, tra...
    method level0_warn (line 6343) | def level0_warn(self, release_id, tm, level):
    method set_metadata (line 6366) | def set_metadata(
    method write_tags (line 6447) | def write_tags(self, release_id, track, tm, workId):
    method make_annotations (line 6481) | def make_annotations(self, release_id, track, wid):
    method derive_from_title (line 6555) | def derive_from_title(release_id, track, title):
    method process_work_artists (line 6600) | def process_work_artists(
    method extend_metadata (line 6661) | def extend_metadata(self, release_id, top_info, track, ref_height, dep...
    method publish_metadata (line 7158) | def publish_metadata(self, release_id, album, track, movement_info={}):
    method append_tag (line 7412) | def append_tag(self, release_id, tm, tag, source, sep=None):
    method strip_parent_from_work (line 7442) | def strip_parent_from_work(
    method diff_pair (line 7650) | def diff_pair(
    method canonize_opus (line 8112) | def canonize_opus(release_id, track, s):
    method canonize_key (line 8142) | def canonize_key(release_id, track, s):
    method canonize_synonyms (line 8178) | def canonize_synonyms(release_id, tuples, s):
    method find_synonyms (line 8207) | def find_synonyms(self, release_id, track, reg_item):
    method listify (line 8232) | def listify(self, release_id, track, s):
    method get_text_tuples (line 8386) | def get_text_tuples(self, release_id, track, text_type):
    method stencil (line 8470) | def stencil(release_id, matches_tuple, test_string):
    method boil (line 8513) | def boil(self, release_id, s):
  class ClassicalExtrasOptionsPage (line 8553) | class ClassicalExtrasOptionsPage(OptionsPage):
    method __init__ (line 8584) | def __init__(self, parent=None):
    method load (line 8589) | def load(self):
    method save (line 8651) | def save(self):

FILE: plugins/classical_extras/const.py
  function tag_strings (line 1044) | def tag_strings(pre):

FILE: plugins/classical_extras/suffixtree.py
  class SuffixTreeNode (line 21) | class SuffixTreeNode:
    method __init__ (line 27) | def __init__(self, start=0, end=END_OF_STRING):
    method add_child (line 47) | def add_child(self, key, start, end):
    method add_exisiting_node_as_child (line 64) | def add_exisiting_node_as_child(self, key, node):
    method get_edge_length (line 75) | def get_edge_length(self, current_index):
    method __str__ (line 84) | def __str__(self):
  class SuffixTree (line 88) | class SuffixTree:
    method __init__ (line 93) | def __init__(self):
    method append_string (line 106) | def append_string(self, input_string, special_char):
    method find_longest_common_substrings (line 204) | def find_longest_common_substrings(self, special_char):
  function multi_lcs (line 251) | def multi_lcs(strings_list):

FILE: plugins/classical_extras/ui_options_classical_extras.py
  class Ui_ClassicalExtrasOptionsPage (line 11) | class Ui_ClassicalExtrasOptionsPage(object):
    method setupUi (line 12) | def setupUi(self, ClassicalExtrasOptionsPage):
    method retranslateUi (line 4189) | def retranslateUi(self, ClassicalExtrasOptionsPage):

FILE: plugins/classicdiscnumber/classicdiscnumber.py
  function add_discnumbers (line 11) | def add_discnumbers(tagger, metadata, track, release):

FILE: plugins/collect_artists/collect_artists.py
  class CollectArtists (line 12) | class CollectArtists(BaseAction):
    method callback (line 15) | def callback(self, objs):

FILE: plugins/compatible_TXXX/compatible_TXXX.py
  function build_compliant_TXXX (line 22) | def build_compliant_TXXX(self, encoding, desc, values):
  class MP3FileCompliant (line 42) | class MP3FileCompliant(MP3File):
  class TrueAudioFileCompliant (line 48) | class TrueAudioFileCompliant(TrueAudioFile):
  class DSFFileCompliant (line 54) | class DSFFileCompliant(DSFFile):
  class AiffFileCompliant (line 60) | class AiffFileCompliant(AiffFile):

FILE: plugins/critiquebrainz/critiquebrainz.py
  function result_review (line 51) | def result_review(album, metadata, data, reply, error):
  function process_releasegroup (line 81) | def process_releasegroup(album, metadata, release):
  function process_recording (line 97) | def process_recording(album, metadata, track, release):

FILE: plugins/cuesheet/cuesheet.py
  function msfToMs (line 21) | def msfToMs(msf):
  class CuesheetTrack (line 26) | class CuesheetTrack(list):
    method __init__ (line 28) | def __init__(self, cuesheet, index):
    method set (line 33) | def set(self, *args):
    method find (line 36) | def find(self, prefix):
    method getTrackNumber (line 39) | def getTrackNumber(self):
    method getLength (line 42) | def getLength(self):
    method getField (line 51) | def getField(self, prefix):
    method getArtist (line 57) | def getArtist(self):
    method getTitle (line 60) | def getTitle(self):
    method setArtist (line 63) | def setArtist(self, artist):
  class Cuesheet (line 78) | class Cuesheet(object):
    method __init__ (line 80) | def __init__(self, filename):
    method read (line 84) | def read(self):
    method unquote (line 88) | def unquote(self, string):
    method quote (line 96) | def quote(self, string):
    method parse (line 101) | def parse(self, lines):
    method write (line 125) | def write(self):
  class GenerateCuesheet (line 143) | class GenerateCuesheet(BaseAction):
    method callback (line 146) | def callback(self, objs):

FILE: plugins/decade/__init__.py
  function decade (line 37) | def decade(date, shorten=True):
  function script_decade (line 82) | def script_decade(parser, value, shorten=True):

FILE: plugins/decode_cyrillic/decode_cyrillic.py
  class DecodeCyrillic (line 59) | class DecodeCyrillic(BaseAction):
    method unmangle (line 62) | def unmangle(self, tag, value):
    method callback (line 71) | def callback(self, objs):

FILE: plugins/decode_greek_cyrillic/decode_greek1253.py
  class DecodeGreek (line 53) | class DecodeGreek(BaseAction):
    method unmangle (line 55) | def unmangle(self, tag, value):
    method callback (line 63) | def callback(self, objs):

FILE: plugins/deezerart/__init__.py
  function is_similar (line 27) | def is_similar(str1: str, str2: str, min_similarity: float = DEFAULT_SIM...
  function is_deezer_url (line 33) | def is_deezer_url(url: str) -> bool:
  class OptionsPage (line 37) | class OptionsPage(providers.ProviderOptions):
    method load (line 46) | def load(self):
    method save (line 52) | def save(self):
  class Provider (line 57) | class Provider(providers.CoverArtProvider):
    method __init__ (line 62) | def __init__(self, coverart):
    method queue_images (line 69) | def queue_images(self):
    method error (line 87) | def error(self, msg):
    method log_debug (line 90) | def log_debug(self, msg: Any, *args):
    method _url_callback (line 93) | def _url_callback(self, url: str):
    method _queue_from_url (line 98) | def _queue_from_url(self, album: obj.APIObject, error: QtNet.QNetworkR...
    method _queue_from_search (line 113) | def _queue_from_search(self, results: List[obj.APIObject], error: Opti...
    method _artist (line 146) | def _artist(self) -> str:

FILE: plugins/deezerart/deezer/client.py
  class SearchOptions (line 21) | class SearchOptions(NamedTuple('SearchOptions', [('artist', str), ('albu...
    method __str__ (line 26) | def __str__(self):
  class Client (line 35) | class Client:
    method __init__ (line 36) | def __init__(self, webservice: WebService):
    method advanced_search (line 40) | def advanced_search(self, options: SearchOptions, callback: SearchCall...
    method obj_from_url (line 64) | def obj_from_url(self, url: str, callback: APIURLCallback[obj.APIObjec...
    method api_url (line 78) | def api_url(url: str) -> str:
    method _remove_language_path (line 84) | def _remove_language_path(path: str) -> str:

FILE: plugins/deezerart/deezer/obj.py
  class APIObject (line 10) | class APIObject:
    method __init__ (line 16) | def __init__(self, **kwargs):
    method __eq__ (line 22) | def __eq__(self, other):
  class Artist (line 29) | class Artist(APIObject):
  class CoverSize (line 36) | class CoverSize(enum.Enum):
  class Album (line 47) | class Album(APIObject):
    method cover_url (line 53) | def cover_url(self, cover_size: CoverSize) -> str:
  class Track (line 60) | class Track(APIObject):
  function parse_json (line 70) | def parse_json(data: Union[str, Mapping[str, Any]]) -> APIObject:
  function _dict_to_object (line 88) | def _dict_to_object(data: Mapping[str, Any]) -> Optional[APIObject]:

FILE: plugins/deezerart/options.py
  class Ui_Form (line 14) | class Ui_Form(object):
    method setupUi (line 15) | def setupUi(self, Form):
    method retranslateUi (line 49) | def retranslateUi(self, Form):

FILE: plugins/discnumber/discnumber.py
  function remove_discnumbers (line 19) | def remove_discnumbers(tagger, metadata, release):

FILE: plugins/enhanced_titles/__init__.py
  function flatten_values (line 82) | def flatten_values(dictionary):
  class ReleaseGroupHelper (line 94) | class ReleaseGroupHelper(MBAPIHelper):
    method get_release_group_by_id (line 98) | def get_release_group_by_id(self, release_id, handler, inc = None):
  class SortTagger (line 104) | class SortTagger:
    method _select_alias (line 112) | def _select_alias(self, aliases, name):
    method _response_handler (line 141) | def _response_handler(self, document, reply, error, metadata = None, f...
    method _swapprefix (line 165) | def _swapprefix(self, metadata, field):
    method set_track_titlesort (line 178) | def set_track_titlesort(self, album, metadata, track, release):
    method set_album_titlesort (line 192) | def set_album_titlesort(self, album, metadata, release):
  class LangFunctions (line 207) | class LangFunctions:
    method __init__ (line 217) | def __init__(self):
    method _format_languages (line 223) | def _format_languages(self, languages):
    method _create_prefixes_list (line 231) | def _create_prefixes_list(self, languages = None, is_title = False):
    method _title_case (line 256) | def _title_case(self, text, lower_case_words):
    method find_languages (line 285) | def find_languages(self, metadata):
    method find_prefixes (line 300) | def find_prefixes(self, languages):
    method find_minor_words (line 315) | def find_minor_words(self, languages):
    method swapprefix_lang (line 339) | def swapprefix_lang(self, parser, text, *languages):
    method delprefix_lang (line 369) | def delprefix_lang(self, parser, text, *languages):
    method title_lang (line 398) | def title_lang(self, parser, text, *languages):
  class EnhancedTitlesOptions (line 430) | class EnhancedTitlesOptions(OptionsPage):
    method __init__ (line 445) | def __init__(self, parent = None):
    method load (line 450) | def load(self):
    method save (line 456) | def save(self):

FILE: plugins/enhanced_titles/ui_options_enhanced_titles.py
  class Ui_EnhancedTitlesOptions (line 22) | class Ui_EnhancedTitlesOptions(object):
    method setupUi (line 23) | def setupUi(self, EnhancedTitlesOptions):
    method retranslateUi (line 108) | def retranslateUi(self, EnhancedTitlesOptions):

FILE: plugins/fanarttv/__init__.py
  function cover_sort_key (line 52) | def cover_sort_key(cover):
  function encode_queryarg (line 60) | def encode_queryarg(arg):
  class FanartTvOptionsPage (line 64) | class FanartTvOptionsPage(ProviderOptions):
    method load (line 73) | def load(self):
    method save (line 83) | def save(self):
  class FanartTvCoverArtImage (line 94) | class FanartTvCoverArtImage(CoverArtImage):
  class CoverArtProviderFanartTv (line 102) | class CoverArtProviderFanartTv(CoverArtProvider):
    method enabled (line 110) | def enabled(self):
    method queue_images (line 114) | def queue_images(self):
    method _client_key (line 135) | def _client_key(self):
    method _json_downloaded (line 138) | def _json_downloaded(self, release_group_id, data, reply, error):
    method _select_and_add_cover_art (line 172) | def _select_and_add_cover_art(self, covers, types):

FILE: plugins/fanarttv/ui_options_fanarttv.py
  class Ui_FanartTvOptionsPage (line 14) | class Ui_FanartTvOptionsPage(object):
    method setupUi (line 15) | def setupUi(self, FanartTvOptionsPage):
    method retranslateUi (line 65) | def retranslateUi(self, FanartTvOptionsPage):

FILE: plugins/featartist/featartist.py
  function remove_featartists (line 13) | def remove_featartists(tagger, metadata, track, release):

FILE: plugins/featartistsintitles/featartistsintitles.py
  function move_album_featartists (line 13) | def move_album_featartists(tagger, metadata, release):
  function move_track_featartists (line 23) | def move_track_featartists(tagger, metadata, track, release):

FILE: plugins/fix_tracknums/fix_tracknums.py
  class FixedTrack (line 73) | class FixedTrack:
    method __init__ (line 75) | def __init__(self, tracknumber=0, title=None, title_num1=0, title_num2...
  class FixTrackNumsUsingTitles (line 82) | class FixTrackNumsUsingTitles(BaseAction):
    method callback (line 86) | def callback(self, objs):
  class FixTrackNumsUsingSeq (line 175) | class FixTrackNumsUsingSeq(BaseAction):
    method callback (line 178) | def callback(self, objs):

FILE: plugins/format_performer_tags/__init__.py
  function get_word_dict (line 49) | def get_word_dict(settings):
  function rewrite_tag (line 56) | def rewrite_tag(key, values, metadata, word_dict, settings):
  function format_performer_tags (line 122) | def format_performer_tags(album, metadata, *args):
  class FormatPerformerTagsOptionsPage (line 128) | class FormatPerformerTagsOptionsPage(OptionsPage):
    method __init__ (line 154) | def __init__(self, parent=None):
    method load (line 187) | def load(self):
    method save (line 264) | def save(self):
    method restore_defaults (line 267) | def restore_defaults(self):
    method _set_settings (line 271) | def _set_settings(self, settings):
    method update_examples (line 321) | def update_examples(self):
    method build_example (line 342) | def build_example(credits, word_dict, settings):

FILE: plugins/format_performer_tags/ui_options_format_performer_tags.py
  class Ui_FormatPerformerTagsOptionsPage (line 14) | class Ui_FormatPerformerTagsOptionsPage(object):
    method setupUi (line 15) | def setupUi(self, FormatPerformerTagsOptionsPage):
    method retranslateUi (line 285) | def retranslateUi(self, FormatPerformerTagsOptionsPage):

FILE: plugins/genre_mapper/__init__.py
  class GenreMappingPairs (line 66) | class GenreMappingPairs():
    method refresh (line 70) | def refresh(cls):
  class GenreMapperOptionsPage (line 104) | class GenreMapperOptionsPage(OptionsPage):
    method __init__ (line 117) | def __init__(self, parent=None):
    method load (line 122) | def load(self):
    method save (line 134) | def save(self):
    method _set_enabled_state (line 142) | def _set_enabled_state(self, *args):
  function track_genre_mapper (line 146) | def track_genre_mapper(album, metadata, *args):

FILE: plugins/genre_mapper/ui_options_genre_mapper.py
  class Ui_GenreMapperOptionsPage (line 13) | class Ui_GenreMapperOptionsPage(object):
    method setupUi (line 14) | def setupUi(self, GenreMapperOptionsPage):
    method retranslateUi (line 114) | def retranslateUi(self, GenreMapperOptionsPage):

FILE: plugins/haikuattrs/haikuattrs.py
  class AttrInfo (line 47) | class AttrInfo(Structure):
    method __repr__ (line 52) | def __repr__(self):
  function get_numeric_type (line 91) | def get_numeric_type(attr):
  function read_attr (line 94) | def read_attr(fd, attr):
  function remove_attr (line 119) | def remove_attr(fd, attr):
  function write_attr (line 122) | def write_attr(fd, attr, attr_value):
  function get_attribute_value (line 144) | def get_attribute_value(tag, file):
  function set_attrs_from_metadata (line 160) | def set_attrs_from_metadata(file):
  function set_attrs_from_metadata_finished (line 181) | def set_attrs_from_metadata_finished(file, result=None, error=None):
  function load_attrs_to_metadata (line 188) | def load_attrs_to_metadata(file):
  function load_attrs_to_metadata_finished (line 209) | def load_attrs_to_metadata_finished(file, result=None, error=None):
  function on_file_load_processor (line 217) | def on_file_load_processor(file):
  function on_file_save_processor (line 222) | def on_file_save_processor(file):

FILE: plugins/happidev_lyrics/happidev_lyrics.py
  class HappidevLyricsMetadataProcessor (line 34) | class HappidevLyricsMetadataProcessor:
    method __init__ (line 40) | def __init__(self):
    method process_metadata (line 45) | def process_metadata(self, album, metadata, track, release):
    method _request (line 69) | def _request(self, ws, path, callback, queryargs=None, important=False):
    method process_search_response (line 78) | def process_search_response(self, album, metadata, response, reply, er...
    method process_lyrics_response (line 101) | def process_lyrics_response(self, album, metadata, response, reply, er...
    method _handle_error (line 122) | def _handle_error(album, error, response):
  class HappidevLyricsOptionsPage (line 131) | class HappidevLyricsOptionsPage(OptionsPage):
    method __init__ (line 139) | def __init__(self, parent=None):
    method load (line 162) | def load(self):
    method save (line 165) | def save(self):

FILE: plugins/hyphen_unicode/hyphen_unicode.py
  function sanitize (line 62) | def sanitize(char):
  function ascii (line 68) | def ascii(word):
  function main (line 72) | def main(tagger, metadata, *args):

FILE: plugins/instruments/instruments.py
  function _iterate_instruments (line 34) | def _iterate_instruments(instrument_list: str) -> Generator[str, None, N...
  function _strip_instrument_prefixes (line 51) | def _strip_instrument_prefixes(instrument: str) -> Optional[str]:
  function add_instruments (line 75) | def add_instruments(tagger, metadata_, *args):

FILE: plugins/keep/keep.py
  function keep (line 22) | def keep(parser, *keeptags):

FILE: plugins/key_wheel_converter/key_wheel_converter.py
  class log (line 41) | class log():
    method debug (line 47) | def debug(*args, **kwargs):
  function register_script_function (line 54) | def register_script_function(*args, **kwargs):
  class KeyMap (line 60) | class KeyMap():
  function _matcher (line 140) | def _matcher(text, out_type):
  function _parse_input (line 158) | def _parse_input(text):
  function key2camelot (line 209) | def key2camelot(parser, text):
  function key2openkey (line 297) | def key2openkey(parser, text):
  function key2standard (line 355) | def key2standard(parser, text, use_symbol=''):
  function key2traktor (line 470) | def key2traktor(parser, text):

FILE: plugins/lastfm/__init__.py
  function parse_ignored_tags (line 46) | def parse_ignored_tags(ignore_tags_setting):
  function matches_ignored (line 60) | def matches_ignored(ignore_tags, tag):
  function _tags_finalize (line 72) | def _tags_finalize(album, metadata, tags, next_):
  function _tags_downloaded (line 84) | def _tags_downloaded(album, metadata, min_usage, ignore, next_, current,...
  function get_tags (line 129) | def get_tags(album, metadata, queryargs, min_usage, ignore, next_, curre...
  function encode_str (line 152) | def encode_str(s):
  function get_queryargs (line 157) | def get_queryargs(queryargs):
  function get_track_tags (line 163) | def get_track_tags(album, metadata, artist, track, min_usage,
  function get_artist_tags (line 174) | def get_artist_tags(album, metadata, artist, min_usage,
  function process_track (line 184) | def process_track(album, metadata, track, release):
  class LastfmOptionsPage (line 206) | class LastfmOptionsPage(OptionsPage):
    method __init__ (line 221) | def __init__(self, parent=None):
    method load (line 226) | def load(self):
    method save (line 234) | def save(self):

FILE: plugins/lastfm/ui_options_lastfm.py
  class Ui_LastfmOptionsPage (line 14) | class Ui_LastfmOptionsPage(object):
    method setupUi (line 15) | def setupUi(self, LastfmOptionsPage):
    method retranslateUi (line 99) | def retranslateUi(self, LastfmOptionsPage):

FILE: plugins/loadasnat/loadasnat.py
  class LoadAsNat (line 38) | class LoadAsNat(BaseAction):
    method callback (line 41) | def callback(self, objs):

FILE: plugins/losslessfuncs/__init__.py
  function is_lossless (line 34) | def is_lossless(parser):
  function is_lossy (line 56) | def is_lossy(parser):

FILE: plugins/lrclib_lyrics/__init__.py
  function get_lyrics (line 65) | def get_lyrics(track, file):
  function response_handler (line 92) | def response_handler(metadata, document, reply, error):
  function get_lrc_file_name (line 113) | def get_lrc_file_name(file):
  function export_lrc_file (line 131) | def export_lrc_file(file):
  class ImportLrc (line 150) | class ImportLrc(BaseAction):
    method callback (line 153) | def callback(self, objs):
  class LrclibLyricsOptions (line 171) | class LrclibLyricsOptions(OptionsPage):
    method __init__ (line 191) | def __init__(self, parent=None):
    method load (line 196) | def load(self):
    method save (line 210) | def save(self):
    method update_lrc_name_field_state (line 219) | def update_lrc_name_field_state(self):

FILE: plugins/lrclib_lyrics/option_lrclib_lyrics.py
  class Ui_OptionLrclibLyrics (line 14) | class Ui_OptionLrclibLyrics(object):
    method setupUi (line 15) | def setupUi(self, OptionLrclibLyrics):
    method retranslateUi (line 62) | def retranslateUi(self, OptionLrclibLyrics):

FILE: plugins/matroska_tagger/matroska_tagger.py
  function _tool_path (line 167) | def _tool_path(name, tooldir):
  function _subprocess_flags (line 176) | def _subprocess_flags():
  function _run (line 184) | def _run(args, **kwargs):
  function _get_tooldir (line 201) | def _get_tooldir():
  function _write_simple (line 210) | def _write_simple(w, name, value):
  function _write_simple_with_sort (line 217) | def _write_simple_with_sort(w, name, value, sort_value=None):
  function _write_original_artist (line 230) | def _write_original_artist(w, value):
  function _sort_field_for (line 241) | def _sort_field_for(mkv_name, target_type_value):
  function _tags_xml (line 254) | def _tags_xml(metadata):
  function _parse_tags_xml (line 353) | def _parse_tags_xml(xml_path, metadata):
  class _MatroskaFileBase (line 471) | class _MatroskaFileBase(File):
    method supports_tag (line 480) | def supports_tag(cls, name):
    method _load (line 483) | def _load(self, filename):
    method _save (line 548) | def _save(self, filename, metadata):
  class MKVFile (line 604) | class MKVFile(_MatroskaFileBase):
  class MKAFile (line 609) | class MKAFile(_MatroskaFileBase):
  class _MkvOptionsPage (line 619) | class _MkvOptionsPage(OptionsPage):
    method __init__ (line 625) | def __init__(self, parent=None):
    method _browse (line 673) | def _browse(self):
    method _test_tools (line 680) | def _test_tools(self):
    method load (line 694) | def load(self):
    method save (line 699) | def save(self):
  function _default_tooldir (line 710) | def _default_tooldir():

FILE: plugins/mod/__init__.py
  class FieldAccess (line 50) | class FieldAccess(Enum):
  class MagicBytes (line 58) | class MagicBytes(bytes):
    method __new__ (line 61) | def __new__(cls, value, offset: int = 0):
  class ModuleFile (line 67) | class ModuleFile(File):
    method supports_tag (line 83) | def supports_tag(cls, name: str) -> bool:
    method _load (line 86) | def _load(self, filename: str) -> Metadata:
    method _save (line 96) | def _save(self, filename: str, metadata: Metadata):
    method _ensure_format (line 102) | def _ensure_format(self, f: RawIOBase) -> MagicBytes:
    method _magic_matches (line 111) | def _magic_matches(self, f: RawIOBase, magic: MagicBytes) -> bool:
    method _parse_file (line 116) | def _parse_file(self, f: RawIOBase, metadata: Metadata, magic: MagicBy...
    method _write_file (line 121) | def _write_file(self, f: RawIOBase, metadata: Metadata):
    method _decode_text (line 127) | def _decode_text(self, data: bytes) -> str:
    method _encode_text (line 130) | def _encode_text(self, text: str, length: int = None, fillchar: str = ...
  class MODFile (line 136) | class MODFile(ModuleFile):
  class ExtendedModuleFile (line 147) | class ExtendedModuleFile(ModuleFile):
    method _parse_file (line 158) | def _parse_file(self, f: RawIOBase, metadata: Metadata, magic: MagicBy...
  class ImpulseTrackerFile (line 168) | class ImpulseTrackerFile(ModuleFile):
    method _parse_file (line 179) | def _parse_file(self, f: RawIOBase, metadata: Metadata, magic: MagicBy...
  class AHXFile (line 187) | class AHXFile(ModuleFile):
    method supports_tag (line 195) | def supports_tag(cls, name: str) -> bool:
    method _parse_file (line 198) | def _parse_file(self, f: RawIOBase, metadata: Metadata, magic: MagicBy...
    method _write_file (line 202) | def _write_file(self, f: RawIOBase, metadata: Metadata):
    method _seek_names_offset (line 211) | def _seek_names_offset(self, f: RawIOBase) -> int:
    method _skip_samples (line 221) | def _skip_samples(self, f: RawIOBase, count: int):
    method _read_string (line 228) | def _read_string(self, f: RawIOBase) -> bytes:
  class MEDFile (line 238) | class MEDFile(ModuleFile):
    method _parse_file (line 250) | def _parse_file(self, f: RawIOBase, metadata: Metadata, magic: MagicBy...
  class MTMFile (line 256) | class MTMFile(ModuleFile):
  class S3MFile (line 267) | class S3MFile(ModuleFile):
  class ULTFile (line 279) | class ULTFile(ModuleFile):
    method _parse_file (line 295) | def _parse_file(self, f: RawIOBase, metadata: Metadata, magic: MagicBy...
  class Composer669File (line 300) | class Composer669File(ModuleFile):
    method _parse_file (line 313) | def _parse_file(self, f: RawIOBase, metadata: Metadata, magic: MagicBy...
  class OktalyzerFile (line 319) | class OktalyzerFile(ModuleFile):

FILE: plugins/moodbars/__init__.py
  function generate_moodbar_for_files (line 50) | def generate_moodbar_for_files(files, format, tagger):
  class MoodBar (line 88) | class MoodBar(BaseAction):
    method _add_file_to_queue (line 91) | def _add_file_to_queue(self, file):
    method callback (line 96) | def callback(self, objs):
    method _generate_moodbar (line 104) | def _generate_moodbar(self, file):
    method _moodbar_callback (line 111) | def _moodbar_callback(self, file, result=None, error=None):
  class MoodbarOptionsPage (line 124) | class MoodbarOptionsPage(OptionsPage):
    method __init__ (line 141) | def __init__(self, parent=None):
    method load (line 146) | def load(self):
    method save (line 156) | def save(self):

FILE: plugins/moodbars/ui_options_moodbar.py
  class Ui_MoodbarOptionsPage (line 14) | class Ui_MoodbarOptionsPage(object):
    method setupUi (line 15) | def setupUi(self, MoodbarOptionsPage):
    method retranslateUi (line 59) | def retranslateUi(self, MoodbarOptionsPage):

FILE: plugins/musixmatch/__init__.py
  function handle_result (line 21) | def handle_result(album, metadata, data, reply, error):
  function process_track (line 44) | def process_track(album, metadata, track, release):
  class MusixmatchOptionsPage (line 68) | class MusixmatchOptionsPage(OptionsPage):
    method __init__ (line 76) | def __init__(self, parent=None):
    method load (line 81) | def load(self):
    method save (line 84) | def save(self):

FILE: plugins/musixmatch/ui_options_musixmatch.py
  class Ui_MusixmatchOptionsPage (line 4) | class Ui_MusixmatchOptionsPage(object):
    method setupUi (line 6) | def setupUi(self, MusixmatchOptionsPage):
    method retranslateUi (line 32) | def retranslateUi(self, MusixmatchOptionsPage):

FILE: plugins/no_release/no_release.py
  class Ui_NoReleaseOptionsPage (line 19) | class Ui_NoReleaseOptionsPage(object):
    method setupUi (line 21) | def setupUi(self, NoReleaseOptionsPage):
    method retranslateUi (line 49) | def retranslateUi(self, NoReleaseOptionsPage):
  function strip_release_specific_metadata (line 55) | def strip_release_specific_metadata(metadata):
  class NoReleaseAction (line 62) | class NoReleaseAction(BaseAction):
    method callback (line 65) | def callback(self, objs):
  class NoReleaseOptionsPage (line 76) | class NoReleaseOptionsPage(OptionsPage):
    method __init__ (line 86) | def __init__(self, parent=None):
    method load (line 91) | def load(self):
    method save (line 95) | def save(self):
  function no_release_album_processor (line 100) | def no_release_album_processor(tagger, metadata, release):
  function no_release_track_processor (line 105) | def no_release_track_processor(tagger, metadata, track, release):

FILE: plugins/non_ascii_equivalents/non_ascii_equivalents.py
  function sanitize (line 128) | def sanitize(char):
  function to_ascii (line 134) | def to_ascii(word):
  function main (line 138) | def main(tagger, metadata, *args):

FILE: plugins/padded/padded.py
  function add_padded_tn (line 18) | def add_padded_tn(album, metadata, track, release):
  function add_padded_dn (line 26) | def add_padded_dn(album, metadata, track, release):

FILE: plugins/papercdcase/papercdcase.py
  function urlencode (line 46) | def urlencode(s):
  function build_papercdcase_url (line 57) | def build_papercdcase_url(artist, album, tracks):
  class PaperCdCase (line 74) | class PaperCdCase(BaseAction):
    method callback (line 77) | def callback(self, objs):

FILE: plugins/performer_tag_replace/__init__.py
  function _update_track_metadata (line 53) | def _update_track_metadata(track_metadata, replacements):
  function performer_tag_replace (line 92) | def performer_tag_replace(album, metadata, track_metadata, *args):
  class PerformerTagReplaceOptionsPage (line 133) | class PerformerTagReplaceOptionsPage(OptionsPage):
    method __init__ (line 144) | def __init__(self, parent=None):
    method load (line 149) | def load(self):
    method save (line 157) | def save(self):

FILE: plugins/performer_tag_replace/ui_options_performer_tag_replace.py
  class Ui_PerformerTagReplaceOptionsPage (line 8) | class Ui_PerformerTagReplaceOptionsPage(object):
    method setupUi (line 9) | def setupUi(self, PerformerTagReplaceOptionsPage):
    method retranslateUi (line 95) | def retranslateUi(self, PerformerTagReplaceOptionsPage):

FILE: plugins/persistent_variables/__init__.py
  class PersistentVariables (line 82) | class PersistentVariables:
    method clear_album_vars (line 87) | def clear_album_vars(cls, album):
    method set_album_var (line 92) | def set_album_var(cls, album, key, value):
    method unset_album_var (line 100) | def unset_album_var(cls, album, key):
    method unset_album_dict (line 105) | def unset_album_dict(cls, album):
    method get_album_var (line 110) | def get_album_var(cls, album, key):
    method clear_session_vars (line 116) | def clear_session_vars(cls):
    method set_session_var (line 120) | def set_session_var(cls, key, value):
    method unset_session_var (line 125) | def unset_session_var(cls, key):
    method get_session_var (line 129) | def get_session_var(cls, key):
    method get_album_dict (line 133) | def get_album_dict(cls, album):
    method get_session_dict (line 139) | def get_session_dict(cls):
  function _get_album_id (line 143) | def _get_album_id(parser):
  function func_set_s (line 154) | def func_set_s(parser, name, value):
  function func_unset_s (line 162) | def func_unset_s(parser, name):
  function func_get_s (line 167) | def func_get_s(parser, name):
  function func_clear_s (line 171) | def func_clear_s(parser):
  function func_unset_a (line 176) | def func_unset_a(parser, name):
  function func_set_a (line 184) | def func_set_a(parser, name, value):
  function func_get_a (line 192) | def func_get_a(parser, name):
  function func_clear_a (line 200) | def func_clear_a(parser):
  function initialize_album_dict (line 208) | def initialize_album_dict(album, album_metadata, release_metadata):
  function destroy_album_dict (line 214) | def destroy_album_dict(album):
  class ViewVariables (line 220) | class ViewVariables(BaseAction):
    method callback (line 223) | def callback(self, objs):
  class ViewVariablesDialog (line 232) | class ViewVariablesDialog(QtWidgets.QDialog):
    method __init__ (line 234) | def __init__(self, obj, parent=None):
    method add_separator_row (line 275) | def add_separator_row(self, table, i, title, count):
    method get_table_items (line 283) | def get_table_items(self, table, i):

FILE: plugins/persistent_variables/ui_variables_dialog.py
  class Ui_VariablesDialog (line 9) | class Ui_VariablesDialog(object):
    method setupUi (line 10) | def setupUi(self, VariablesDialog):
    method retranslateUi (line 54) | def retranslateUi(self, VariablesDialog):

FILE: plugins/playlist/playlist.py
  function get_safe_filename (line 35) | def get_safe_filename(filename):
  class PlaylistEntry (line 43) | class PlaylistEntry(list):
    method __init__ (line 45) | def __init__(self, playlist, index):
    method add (line 50) | def add(self, entry_row):
  class Playlist (line 54) | class Playlist(object):
    method __init__ (line 56) | def __init__(self, filename):
    method add_header (line 61) | def add_header(self, header):
    method write (line 64) | def write(self):
  class GeneratePlaylist (line 75) | class GeneratePlaylist(BaseAction):
    method callback (line 78) | def callback(self, objs):

FILE: plugins/post_tagging_actions/__init__.py
  class ActionLoader (line 84) | class ActionLoader:
    method __init__ (line 92) | def __init__(self):
    method _create_options (line 97) | def _create_options(self, command, *other_options):
    method _create_action (line 105) | def _create_action(self, priority, commands, album, options):
    method _replace_variables (line 118) | def _replace_variables(self, variables, item):
    method add_actions (line 137) | def add_actions(self, album, tracks):
    method load_actions (line 151) | def load_actions(self):
  class ActionRunner (line 164) | class ActionRunner:
    method __init__ (line 174) | def __init__(self):
    method _create_widget (line 200) | def _create_widget(self, window):
    method _update_widget (line 206) | def _update_widget(self):
    method _refresh_tags (line 214) | def _refresh_tags(self, future_objects, album):
    method _run_process (line 226) | def _run_process(self, command):
    method _update_executing_count (line 241) | def _update_executing_count(self, future_objects):
    method _execute (line 248) | def _execute(self):
    method stop (line 274) | def stop(self):
  class ExecuteAlbumActions (line 289) | class ExecuteAlbumActions(BaseAction):
    method callback (line 293) | def callback(self, objs):
  class ExecuteTrackActions (line 299) | class ExecuteTrackActions(BaseAction):
    method callback (line 303) | def callback(self, objs):
  class PostTaggingActionsOptions (line 311) | class PostTaggingActionsOptions(OptionsPage):
    method __init__ (line 326) | def __init__(self, parent = None):
    method _open_file_dialog (line 350) | def _open_file_dialog(self):
    method _reset_ui (line 361) | def _reset_ui(self):
    method _add_action_to_table (line 367) | def _add_action_to_table(self):
    method _remove_action_from_table (line 379) | def _remove_action_from_table(self):
    method _move_action_up (line 384) | def _move_action_up(self):
    method _move_action_down (line 391) | def _move_action_down(self):
    method _swap_table_rows (line 398) | def _swap_table_rows(self, row1, row2):
    method load (line 405) | def load(self):
    method save (line 417) | def save(self):
  class ActionsStatus (line 432) | class ActionsStatus(QtWidgets.QWidget, Ui_ActionsStatus):
    method __init__ (line 439) | def __init__(self):
    method update_actions_count (line 450) | def update_actions_count(self, count):

FILE: plugins/post_tagging_actions/actions_status.py
  class Ui_ActionsStatus (line 14) | class Ui_ActionsStatus(object):
    method setupUi (line 15) | def setupUi(self, ActionsStatus):
    method retranslateUi (line 35) | def retranslateUi(self, ActionsStatus):

FILE: plugins/post_tagging_actions/options_post_tagging_actions.py
  class Ui_PostTaggingActions (line 14) | class Ui_PostTaggingActions(object):
    method setupUi (line 15) | def setupUi(self, PostTaggingActions):
    method retranslateUi (line 164) | def retranslateUi(self, PostTaggingActions):

FILE: plugins/release_type/release_type.py
  function add_release_type (line 15) | def add_release_type(tagger, metadata, release):

FILE: plugins/releasetag_aggregations/releasetag_aggregations.py
  function get_parent_release (line 51) | def get_parent_release(file):
  function iter_release_values (line 60) | def iter_release_values(name, file):
  function iter_release_values_multi (line 69) | def iter_release_values_multi(name, file):
  function try_iter_numeric (line 78) | def try_iter_numeric(values, skip_non_numeric=False):
  function aggregate_release_tags (line 91) | def aggregate_release_tags(parser, name, aggregate_func, multi=False):
  function format_number (line 101) | def format_number(value, precision=2):
  function mode (line 117) | def mode(values):
  function average (line 125) | def average(values, precision=2):
  function natsort_min (line 136) | def natsort_min(values, precision=2):
  function natsort_max (line 141) | def natsort_max(values, precision=2):
  function distinct (line 146) | def distinct(values, separator=MULTI_VALUED_JOINER):
  function func_album_all (line 155) | def func_album_all(parser, name):
  function func_album_mode (line 172) | def func_album_mode(parser, name):
  function func_album_multi_mode (line 181) | def func_album_multi_mode(parser, name):
  function func_album_min (line 189) | def func_album_min(parser, name, precision="2"):
  function releasetag_multi_min (line 199) | def releasetag_multi_min(parser, name, precision="2"):
  function func_album_max (line 208) | def func_album_max(parser, name, precision="2"):
  function releasetag_multi_max (line 218) | def releasetag_multi_max(parser, name, precision="2"):
  function func_album_avg (line 228) | def func_album_avg(parser, name, precision="2"):
  function func_album_multi_avg (line 239) | def func_album_multi_avg(parser, name, precision="2"):
  function func_album_distinct (line 248) | def func_album_distinct(parser, name, separator=MULTI_VALUED_JOINER):
  function func_album_multi_distinct (line 258) | def func_album_multi_distinct(parser, name, separator=MULTI_VALUED_JOINER):

FILE: plugins/remove_perfect_albums/remove_perfect_albums.py
  class RemovePerfectAlbums (line 15) | class RemovePerfectAlbums(BaseAction):
    method callback (line 18) | def callback(self, objs):

FILE: plugins/reorder_sides/reorder_sides.py
  function tracknumber_to_side (line 82) | def tracknumber_to_side(tracknumber):
  function get_side_info (line 99) | def get_side_info(release):
  function find_side (line 170) | def find_side(side_info, metadata):
  function analyze_release (line 194) | def analyze_release(tagger, metadata, release):
  function reorder_sides (line 204) | def reorder_sides(tagger, metadata, *args):

FILE: plugins/replace_forbidden_symbols/replace_forbidden_symbols.py
  function sanitize (line 60) | def sanitize(char):
  function fix_forbidden (line 66) | def fix_forbidden(word):
  function replace_forbidden (line 70) | def replace_forbidden(value):
  function script_replace_forbidden (line 74) | def script_replace_forbidden(parser, value):
  function main (line 78) | def main(tagger, metadata, *args):

FILE: plugins/replaygain2/__init__.py
  class OpusMode (line 127) | class OpusMode(IntEnum):
  function rsgain_found (line 134) | def rsgain_found(rsgain_command, window):
  function build_options (line 141) | def build_options(config):
  function parse_result (line 153) | def parse_result(line):
  function format_r128 (line 165) | def format_r128(result, config):
  function update_metadata (line 171) | def update_metadata(metadata, track_result, album_result, is_nat, opus_m...
  function calculate_replaygain (line 195) | def calculate_replaygain(input_objs, options):
  function isinstanceany (line 288) | def isinstanceany(obj, types):
  class ScanCluster (line 292) | class ScanCluster(BaseAction):
    method callback (line 295) | def callback(self, objs):
    method _replaygain_callback (line 316) | def _replaygain_callback(self, files, result=None, error=None):
  class ScanTracks (line 324) | class ScanTracks(BaseAction):
    method callback (line 327) | def callback(self, objs):
    method _replaygain_callback (line 346) | def _replaygain_callback(self, tracks, result=None, error=None):
  class ScanAlbums (line 357) | class ScanAlbums(BaseAction):
    method callback (line 360) | def callback(self, objs):
    method _format_progress (line 382) | def _format_progress(self):
    method _albumgain_callback (line 389) | def _albumgain_callback(self, album, result=None, error=None):
  class ReplayGain2OptionsPage (line 413) | class ReplayGain2OptionsPage(OptionsPage):
    method __init__ (line 431) | def __init__(self, parent=None):
    method load (line 447) | def load(self):
    method save (line 458) | def save(self):
    method rsgain_command_browse (line 469) | def rsgain_command_browse(self):

FILE: plugins/replaygain2/ui_options_replaygain2.py
  class Ui_ReplayGain2OptionsPage (line 14) | class Ui_ReplayGain2OptionsPage(object):
    method setupUi (line 15) | def setupUi(self, ReplayGain2OptionsPage):
    method retranslateUi (line 147) | def retranslateUi(self, ReplayGain2OptionsPage):

FILE: plugins/save_and_rewrite_header/save_and_rewrite_header.py
  class save_and_rewrite_header (line 41) | class save_and_rewrite_header(BaseAction):
    method __init__ (line 45) | def __init__(self):
    method callback (line 53) | def callback(self, obj):

FILE: plugins/script_logger/__init__.py
  function logline (line 79) | def logline(_parser, text: str, level=None):

FILE: plugins/search_engine_lookup/__init__.py
  function show_popup (line 63) | def show_popup(title='', content='', window=None):
  function lookup_error (line 73) | def lookup_error():
  function do_lookup (line 78) | def do_lookup(text):
  function lookup_cover_art (line 87) | def lookup_cover_art(title, artist):
  class SearchEngineLookupTest (line 92) | class SearchEngineLookupTest(BaseAction):
    method callback (line 95) | def callback(self, cluster_list):
  class AlbumCoverArtLookup (line 116) | class AlbumCoverArtLookup(BaseAction):
    method callback (line 119) | def callback(self, album):
  class TrackCoverArtLookup (line 127) | class TrackCoverArtLookup(BaseAction):
    method callback (line 130) | def callback(self, track):
  class SearchEngineEditDialog (line 138) | class SearchEngineEditDialog(QtWidgets.QDialog):
    method __init__ (line 140) | def __init__(self, parent=None, edit_provider='', edit_url='', titles=...
    method setup_actions (line 159) | def setup_actions(self):
    method check_validation (line 165) | def check_validation(self):
    method get_output (line 175) | def get_output(self):
    method accept (line 178) | def accept(self):
    method title_text_changed (line 182) | def title_text_changed(self, text):
    method url_text_changed (line 186) | def url_text_changed(self, text):
  class SearchEngineLookupOptionsPage (line 191) | class SearchEngineLookupOptionsPage(OptionsPage):
    method __init__ (line 204) | def __init__(self, parent=None):
    method setup_actions (line 213) | def setup_actions(self):
    method additional_words_changed (line 221) | def additional_words_changed(self, text):
    method load (line 224) | def load(self):
    method select_provider (line 241) | def select_provider(self, list_item):
    method add_provider (line 250) | def add_provider(self):
    method edit_provider (line 254) | def edit_provider(self):
    method edit_provider_dialog (line 261) | def edit_provider_dialog(self, provider_id='', provider='', url=''):
    method delete_provider (line 273) | def delete_provider(self):
    method test_provider (line 296) | def test_provider(self):
    method update_list (line 302) | def update_list(self, current_item=None):
    method save (line 318) | def save(self):
    method _set_settings (line 321) | def _set_settings(self, settings):

FILE: plugins/search_engine_lookup/ui_options_search_engine_editor.py
  class Ui_SearchEngineEditorDialog (line 14) | class Ui_SearchEngineEditorDialog(object):
    method setupUi (line 15) | def setupUi(self, SearchEngineEditorDialog):
    method retranslateUi (line 94) | def retranslateUi(self, SearchEngineEditorDialog):

FILE: plugins/search_engine_lookup/ui_options_search_engine_lookup.py
  class Ui_SearchEngineLookupOptionsPage (line 14) | class Ui_SearchEngineLookupOptionsPage(object):
    method setupUi (line 15) | def setupUi(self, SearchEngineLookupOptionsPage):
    method retranslateUi (line 93) | def retranslateUi(self, SearchEngineLookupOptionsPage):

FILE: plugins/smart_title_case/smart_title_case.py
  function match_word (line 53) | def match_word(match):
  function string_title_match (line 59) | def string_title_match(match_word, string):
  function string_cleanup (line 62) | def string_cleanup(string):
  function string_title_case (line 67) | def string_title_case(string):
  function artist_title_case (line 99) | def artist_title_case(text, artists, artists_upper):
  function title_case (line 116) | def title_case(tagger, metadata, *args):

FILE: plugins/sort_multivalue_tags/sort_multivalue_tags.py
  function sort_multivalue_tags (line 50) | def sort_multivalue_tags(tagger, metadata, track, release):

FILE: plugins/soundtrack/soundtrack.py
  function soundtrack (line 19) | def soundtrack(tagger, metadata, release):

FILE: plugins/standardise_feat/standardise_feat.py
  function standardise_feat (line 17) | def standardise_feat(artists_str, artists_list):
  function standardise_track_artist (line 44) | def standardise_track_artist(tagger, metadata, track, release):
  function standardise_album_artist (line 49) | def standardise_album_artist(tagger, metadata, release):

FILE: plugins/standardise_performers/standardise_performers.py
  function standardise_performers (line 38) | def standardise_performers(album, metadata, *args):

FILE: plugins/submit_folksonomy_tags/__init__.py
  function tag_submit_handler (line 107) | def tag_submit_handler(document, reply, error, tagger):
  function process_tag_aliases (line 141) | def process_tag_aliases(tag_input):
  function process_objs_to_track_list (line 158) | def process_objs_to_track_list(objs):
  function handle_submit_process (line 173) | def handle_submit_process(tagger, track_list, target_tag):
  function upload_tags_to_mbz (line 293) | def upload_tags_to_mbz(data, tagger):
  class TagSubmitPlugin_OptionsPage (line 349) | class TagSubmitPlugin_OptionsPage(OptionsPage):
    method __init__ (line 355) | def __init__(self, parent=None):
    method on_destructive_selected (line 361) | def on_destructive_selected(self):
    method load (line 371) | def load(self):
    method save (line 396) | def save(self):
  class SubmitTrackTagsMenuAction (line 413) | class SubmitTrackTagsMenuAction(BaseAction):
    method callback (line 416) | def callback(self, objs):
  class SubmitReleaseTagsMenuAction (line 423) | class SubmitReleaseTagsMenuAction(BaseAction):
    method callback (line 426) | def callback(self, objs):
  class SubmitRGTagsMenuAction (line 433) | class SubmitRGTagsMenuAction(BaseAction):
    method callback (line 436) | def callback(self, objs):
  class SubmitRATagsMenuAction (line 443) | class SubmitRATagsMenuAction(BaseAction):
    method callback (line 446) | def callback(self, objs):

FILE: plugins/submit_folksonomy_tags/ui_config.py
  class TagSubmitPluginOptionsUI (line 21) | class TagSubmitPluginOptionsUI():
    method __init__ (line 23) | def __init__(self, page):
    method add_row (line 113) | def add_row(self, find_entry="", replace_entry=""):
    method delete_rows (line 126) | def delete_rows(self):
    method rows_to_tuple_list (line 133) | def rows_to_tuple_list(self):

FILE: plugins/submit_isrc/__init__.py
  function validate_isrc (line 105) | def validate_isrc(isrc):
  function show_popup (line 120) | def show_popup(title, content, window=None):
  class SubmitAlbumISRCs (line 137) | class SubmitAlbumISRCs(BaseAction):
    method callback (line 140) | def callback(self, album):
    method submission_handler (line 239) | def submission_handler(self, document, reply, error):

FILE: plugins/tangoinfo/__init__.py
  class WrappedWSGetRequest (line 49) | class WrappedWSGetRequest(WSGetRequest):  # noqa
    method __init__ (line 50) | def __init__(self, *args, **kwargs):
  class TangoInfoTagger (line 58) | class TangoInfoTagger:
    class TangoInfoScrapeQueue (line 60) | class TangoInfoScrapeQueue(LockableObject):
      method __init__ (line 62) | def __init__(self):
      method __contains__ (line 66) | def __contains__(self, name):
      method __iter__ (line 69) | def __iter__(self):
      method __getitem__ (line 72) | def __getitem__(self, name):
      method __setitem__ (line 78) | def __setitem__(self, name, value):
      method append (line 83) | def append(self, name, value):
      method pop (line 94) | def pop(self, name):
    method __init__ (line 103) | def __init__(self):
    method add_tangoinfo_data (line 107) | def add_tangoinfo_data(self, album, track_metadata, track, release):
    method website_add_track (line 144) | def website_add_track(self, album, track, barcode, tint, zeros=0):
    method website_process (line 188) | def website_process(self, barcode, zeros, response_bytes, reply, error):
    method album_add_request (line 264) | def album_add_request(self, album):
    method album_remove_request (line 267) | def album_remove_request(self, album):
    method extract_data (line 271) | def extract_data(self, barcode, response):

FILE: plugins/theaudiodb/__init__.py
  class TheAudioDbOptionsPage (line 58) | class TheAudioDbOptionsPage(ProviderOptions):
    method load (line 67) | def load(self):
    method save (line 77) | def save(self):
  class TheAudioDbCoverArtImage (line 87) | class TheAudioDbCoverArtImage(CoverArtImage):
    method parse_url (line 94) | def parse_url(self, url):
  class CoverArtProviderTheAudioDb (line 101) | class CoverArtProviderTheAudioDb(CoverArtProvider):
    method __init__ (line 109) | def __init__(self, coverart):
    method enabled (line 113) | def enabled(self):
    method queue_images (line 116) | def queue_images(self):
    method _json_downloaded (line 135) | def _json_downloaded(self, data, reply, error):
    method _select_and_add_cover_art (line 175) | def _select_and_add_cover_art(self, url, types):

FILE: plugins/theaudiodb/ui_options_theaudiodb.py
  class Ui_TheAudioDbOptionsPage (line 14) | class Ui_TheAudioDbOptionsPage(object):
    method setupUi (line 15) | def setupUi(self, TheAudioDbOptionsPage):
    method retranslateUi (line 58) | def retranslateUi(self, TheAudioDbOptionsPage):

FILE: plugins/titlecase/titlecase.py
  function iswbound (line 20) | def iswbound(char):
  function utitle (line 27) | def utitle(string):
  function title (line 47) | def title(string):
  function title_case (line 63) | def title_case(tagger, metadata, *args):

FILE: plugins/tracks2clipboard/tracks2clipboard.py
  class CopyClusterToClipboard (line 16) | class CopyClusterToClipboard(BaseAction):
    method callback (line 19) | def callback(self, objs):

FILE: plugins/viewvariables/__init__.py
  class ViewVariables (line 27) | class ViewVariables(BaseAction):
    method callback (line 30) | def callback(self, objs):
  class ViewVariablesDialog (line 39) | class ViewVariablesDialog(QtWidgets.QDialog):
    method __init__ (line 41) | def __init__(self, obj, parent=None):
    method _display_metadata (line 59) | def _display_metadata(self, metadata):
    method add_separator_row (line 100) | def add_separator_row(self, table, i, title):
    method get_table_items (line 107) | def get_table_items(self, table, i):

FILE: plugins/viewvariables/ui_variables_dialog.py
  class Ui_VariablesDialog (line 14) | class Ui_VariablesDialog(object):
    method setupUi (line 15) | def setupUi(self, VariablesDialog):
    method retranslateUi (line 59) | def retranslateUi(self, VariablesDialog):

FILE: plugins/wikidata/__init__.py
  function parse_ignored_tags (line 31) | def parse_ignored_tags(ignore_tags_setting):
  function matches_ignored (line 47) | def matches_ignored(ignore_tags, tag):
  class Wikidata (line 60) | class Wikidata:
    method __init__ (line 66) | def __init__(self):
    method process_release (line 97) | def process_release(self, album, metadata, release):
    method process_request (line 116) | def process_request(self, metadata, album, item_id, item_type):
    method musicbrainz_release_lookup (line 151) | def musicbrainz_release_lookup(self, item_id, metadata, response, repl...
    method process_wikidata (line 194) | def process_wikidata(self, genre_source_type, wikidata_url, item_id):
    method parse_wikidata_response (line 204) | def parse_wikidata_response(self, item, item_id, genre_source_type, re...
    method process_track (line 295) | def process_track(self, album, metadata, track, release):
    method update_settings (line 321) | def update_settings(self):
  class WikidataOptionsPage (line 353) | class WikidataOptionsPage(OptionsPage):
    method __init__ (line 368) | def __init__(self, parent=None):
    method load (line 379) | def load(self):
    method save (line 390) | def save(self):

FILE: plugins/wikidata/ui_options_wikidata.py
  class Ui_WikidataOptionsPage (line 14) | class Ui_WikidataOptionsPage(object):
    method setupUi (line 15) | def setupUi(self, WikidataOptionsPage):
    method retranslateUi (line 182) | def retranslateUi(self, WikidataOptionsPage):

FILE: plugins/workandmovement/__init__.py
  class Work (line 58) | class Work:
    method __init__ (line 59) | def __init__(self, title, mbid=None):
    method __str__ (line 67) | def __str__(self):
  function is_performance_work (line 81) | def is_performance_work(rel):
  function is_parent_work (line 87) | def is_parent_work(rel):
  function is_movement_like (line 93) | def is_movement_like(rel):
  function is_child_work (line 99) | def is_child_work(rel):
  function number_to_int (line 105) | def number_to_int(s):
  function parse_work_name (line 118) | def parse_work_name(title):
  function create_work_and_movement_from_title (line 122) | def create_work_and_movement_from_title(work):
  function normalize_movement_title (line 153) | def normalize_movement_title(work):
  function parse_work (line 178) | def parse_work(work_rel):
  function unset_work (line 204) | def unset_work(metadata):
  function set_work (line 213) | def set_work(metadata, work):
  function process_track (line 219) | def process_track(album, metadata, track, release):

FILE: plugins/workandmovement/roman.py
  class RomanError (line 20) | class RomanError(Exception): pass
  class OutOfRangeError (line 21) | class OutOfRangeError(RomanError): pass
  class NotIntegerError (line 22) | class NotIntegerError(RomanError): pass
  class InvalidRomanNumeralError (line 23) | class InvalidRomanNumeralError(RomanError): pass
  function toRoman (line 40) | def toRoman(n):
  function fromRoman (line 67) | def fromRoman(s):

FILE: test/plugin_test_case.py
  class FakeThreadPool (line 45) | class FakeThreadPool(QtCore.QObject):
    method start (line 47) | def start(self, runnable, priority):
  class FakeTagger (line 51) | class FakeTagger(QtCore.QObject):
    method __init__ (line 55) | def __init__(self):
    method register_cleanup (line 68) | def register_cleanup(self, func: Callable[[], Any]) -> None:
    method run_cleanup (line 71) | def run_cleanup(self) -> None:
    method emit (line 75) | def emit(self, *args) -> None:
    method get_release_group_by_id (line 78) | def get_release_group_by_id(self, rg_id: str) -> ReleaseGroup:
  class PluginTestCase (line 82) | class PluginTestCase(unittest.TestCase):
    method setUp (line 83) | def setUp(self) -> None:
    method tearDown (line 104) | def tearDown(self) -> None:
    method init_config (line 109) | def init_config() -> None:
    method set_config_values (line 121) | def set_config_values(
    method mktmpdir (line 136) | def mktmpdir(self, ignore_errors: bool = False) -> None:
    method copy_file_tmp (line 141) | def copy_file_tmp(self, filepath: str, ext: Optional[str] = None) -> str:
    method remove_file_tmp (line 149) | def remove_file_tmp(filepath: str) -> None:
    method _test_plugin_install (line 153) | def _test_plugin_install(self, name: str, module: str) -> ModuleType:
    method unload_plugin (line 170) | def unload_plugin(self, plugin_name: str) -> None:

FILE: test/test_add_to_collection.py
  class TestAddToCollection (line 13) | class TestAddToCollection(PluginTestCase):
    method install_plugin (line 24) | def install_plugin(self) -> None:
    method create_file (line 29) | def create_file(self, file_name: str, album_id: Union[str, None] = Non...
    method tearDown (line 37) | def tearDown(self) -> None:
    method test_hooks_installed (line 41) | def test_hooks_installed(self) -> None:
    method test_file_save (line 53) | def test_file_save(self) -> None:
    method test_two_files_save (line 71) | def test_two_files_save(self) -> None:
    method test_no_collection_id_setting (line 97) | def test_no_collection_id_setting(self) -> None:
    method test_no_user_collection (line 110) | def test_no_user_collection(self) -> None:
    method test_no_release (line 125) | def test_no_release(self) -> None:

FILE: test/test_doctest.py
  function load_tests (line 4) | def load_tests(loader, tests, ignore):

FILE: test/test_generate.py
  class GenerateTestCase (line 13) | class GenerateTestCase(unittest.TestCase):
    method setUp (line 23) | def setUp(self):
    method with_suppressed_stdout (line 31) | def with_suppressed_stdout(func, *args, **kwargs):
    method test_generate_json (line 40) | def test_generate_json(self):
    method test_generate_zip (line 59) | def test_generate_zip(self):
    method test_valid_json (line 76) | def test_valid_json(self):

FILE: test/test_keep.py
  class TestKeep (line 10) | class TestKeep(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method test_keep_simple (line 14) | def test_keep_simple(self):
    method test_keep_mbid (line 27) | def test_keep_mbid(self):
    method test_keep_nonfiletags (line 40) | def test_keep_nonfiletags(self):
    method _description_test (line 53) | def _description_test(self, tagname):
    method test_keep_performer (line 67) | def test_keep_performer(self):
    method test_keep_lyrics (line 71) | def test_keep_lyrics(self):
    method test_keep_comment (line 75) | def test_keep_comment(self):
    method test_keep_with_description (line 79) | def test_keep_with_description(self):
Condensed preview — 157 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,456K chars).
[
  {
    "path": ".github/workflows/test.yml",
    "chars": 629,
    "preview": "name: Test\n\non: [push, pull_request]\n\njobs:\n  unittest:\n\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        "
  },
  {
    "path": ".gitignore",
    "chars": 195,
    "preview": "# Generated by the script\nplugins.json\nplugins/*.zip\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# "
  },
  {
    "path": ".prospector.yml",
    "chars": 899,
    "preview": "# Configuration for prospector, mainly used by Codacy.\n\npep8:\n  # Please see comments in setup.cfg as to why we disable "
  },
  {
    "path": ".pylintrc",
    "chars": 18060,
    "preview": "[MASTER]\n\n# A comma-separated list of package or module names from where C extensions may\n# be loaded. Extensions are lo"
  },
  {
    "path": "README.md",
    "chars": 966,
    "preview": "# MusicBrainz Picard Plugins\n\nThis repository hosts plugins for [MusicBrainz Picard](https://picard.musicbrainz.org/). I"
  },
  {
    "path": "build_ui.py",
    "chars": 893,
    "preview": "#!/usr/bin/env python3\n\nimport glob\nimport os\n\nfrom PyQt5 import uic\n\n\nos.chdir(os.path.dirname(__file__))\nplugin_dir = "
  },
  {
    "path": "generate.py",
    "chars": 4217,
    "preview": "#!/usr/bin/env python3\n\nfrom __future__ import print_function\nimport argparse\nimport os\nimport json\nimport zipfile\n\nfrom"
  },
  {
    "path": "get_plugin_data.py",
    "chars": 1371,
    "preview": "# -*- coding: utf-8 -*-\n\nfrom __future__ import print_function\nimport ast\n\nKNOWN_DATA = [\n    'PLUGIN_NAME',\n    'PLUGIN"
  },
  {
    "path": "plugins/abbreviate_artistsort/abbreviate_artistsort.py",
    "chars": 10988,
    "preview": "# -*- coding: utf-8 -*-\n\n# This is the Sort Multivalue Tags plugin for MusicBrainz Picard.\n# Copyright (C) 2013 Sophist\n"
  },
  {
    "path": "plugins/acousticbrainz/__init__.py",
    "chars": 18224,
    "preview": "# -*- coding: utf-8 -*-\n# AcousticBrainz plugin for Picard\n#\n# Copyright (C) 2021 Wargreen <wargreen@lebib.org>\n# Copyri"
  },
  {
    "path": "plugins/acousticbrainz/ui_options_acousticbrainz_tags.py",
    "chars": 6071,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/acousticbrainz/ui_options_acousti"
  },
  {
    "path": "plugins/acousticbrainz/ui_options_acousticbrainz_tags.ui",
    "chars": 4675,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>AcousticBrainzOptionsPage</class>\n <widget class=\"QWid"
  },
  {
    "path": "plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py",
    "chars": 4361,
    "preview": "# -*- coding: utf-8 -*-\n# Acousticbrainz Tonal/Rhythm plugin for Picard\n# Copyright (C) 2015  Sophist\n#\n# This program i"
  },
  {
    "path": "plugins/add_to_collection/README.md",
    "chars": 451,
    "preview": "# Add to Collection\n\nThis plugin allows you to add any saved release to one of your user release [collections](https://m"
  },
  {
    "path": "plugins/add_to_collection/__init__.py",
    "chars": 199,
    "preview": "from picard.plugins.add_to_collection.manifest import *\nfrom picard.plugins.add_to_collection import options, post_save_"
  },
  {
    "path": "plugins/add_to_collection/manifest.py",
    "chars": 427,
    "preview": "PLUGIN_NAME = \"Add to Collection\"\nPLUGIN_AUTHOR = \"Dvir Yitzchaki (dvirtz@gmail.com)\"\nPLUGIN_DESCRIPTION = \"Adds any sav"
  },
  {
    "path": "plugins/add_to_collection/options.py",
    "chars": 1526,
    "preview": "from picard.collection import Collection, user_collections\nfrom picard.ui.options import OptionsPage, register_options_p"
  },
  {
    "path": "plugins/add_to_collection/override_module.py",
    "chars": 327,
    "preview": "from contextlib import contextmanager\nfrom typing import Generator\n\n\n@contextmanager\ndef override_module(obj: object) ->"
  },
  {
    "path": "plugins/add_to_collection/post_save_processor.py",
    "chars": 1047,
    "preview": "from picard import log\nfrom picard.collection import Collection, user_collections\nfrom picard.file import File, register"
  },
  {
    "path": "plugins/add_to_collection/settings.py",
    "chars": 516,
    "preview": "from picard.config import TextOption, get_config\nfrom typing import Optional\n\nCOLLECTION_ID = \"add_to_collection_id\"\n\n\nd"
  },
  {
    "path": "plugins/add_to_collection/ui_add_to_collection_options.py",
    "chars": 1759,
    "preview": "from PyQt5 import QtCore, QtWidgets\n\n\nclass Ui_AddToCollectionOptions(object):\n    def setupUi(self, AddToCollectionOpti"
  },
  {
    "path": "plugins/additional_artists_details/__init__.py",
    "chars": 24110,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"Additional Artists Details\n\"\"\"\n# Copyright (C) 2023-2024 Bob Swift (rdswift)\n#\n# This program"
  },
  {
    "path": "plugins/additional_artists_details/docs/README.md",
    "chars": 5278,
    "preview": "# Additional Artists Details\n\n## Overview\n\nThis plugin provides specialized album and track variables with artist detail"
  },
  {
    "path": "plugins/additional_artists_details/options_additional_artists_details.ui",
    "chars": 5451,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<ui version=\"4.0\">\r\n <class>AdditionalArtistsDetailsOptionsPage</class>\r\n <widge"
  },
  {
    "path": "plugins/additional_artists_details/ui_options_additional_artists_details.py",
    "chars": 6649,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file './plugins/additional_artists_details/opti"
  },
  {
    "path": "plugins/additional_artists_variables/additional_artists_variables.py",
    "chars": 14829,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2018-2023 Bob Swift (rdswift)\n# Copyright (C) 2023 Ruud van Asseldonk (ruuda)\n"
  },
  {
    "path": "plugins/addrelease/addrelease.py",
    "chars": 8202,
    "preview": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = \"Add Cluster As Release\"\nPLUGIN_AUTHOR = 'Frederik \"Freso\" S. Olesen, Lukáš Lalin"
  },
  {
    "path": "plugins/albumartist_website/albumartist_website.py",
    "chars": 6949,
    "preview": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = 'Album Artist Website'\nPLUGIN_AUTHOR = 'Sophist, Sambhav Kothari, Philipp Wolfer'"
  },
  {
    "path": "plugins/albumartistextension/albumartistextension.py",
    "chars": 5379,
    "preview": "PLUGIN_NAME = 'AlbumArtist Extension'\nPLUGIN_AUTHOR = 'Bob Swift (rdswift)'\nPLUGIN_DESCRIPTION = '''\nThis plugin provide"
  },
  {
    "path": "plugins/amazon/amazon.py",
    "chars": 5142,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Picard, the next-generation MusicBrainz tagger\n# Copyright (C) 2007 Oliver Charles\n# Copyrig"
  },
  {
    "path": "plugins/bpm/__init__.py",
    "chars": 5217,
    "preview": "# -*- coding: utf-8 -*-\n\n# Changelog:\n# [2015-09-15] Initial version\n# [2017-11-24] Qt5, Python3 for Picard-plugins bran"
  },
  {
    "path": "plugins/bpm/ui_options_bpm.py",
    "chars": 5753,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/bpm/ui_options_bpm.ui'\n#\n# Create"
  },
  {
    "path": "plugins/bpm/ui_options_bpm.ui",
    "chars": 5328,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>BPMOptionsPage</class>\n <widget class=\"QWidget\" name=\""
  },
  {
    "path": "plugins/classical_extras/Readme.md",
    "chars": 103924,
    "preview": "# General Information\nThis is the documentation for version 2.0.11 of \"classical\\_extras\". There may be beta versions la"
  },
  {
    "path": "plugins/classical_extras/__init__.py",
    "chars": 383508,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2018 Mark Evens\n#\n# This program is free software; you can redistribute it and"
  },
  {
    "path": "plugins/classical_extras/const.py",
    "chars": 32734,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"\nDeclare constants for Picard Classical Extras plugin\nv2.0.2\n\"\"\"\n# Copyright (C) 2018 Mark Ev"
  },
  {
    "path": "plugins/classical_extras/options_classical_extras.ui",
    "chars": 539052,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>ClassicalExtrasOptionsPage</class>\n <widget class=\"QWi"
  },
  {
    "path": "plugins/classical_extras/suffixtree.py",
    "chars": 10860,
    "preview": "# -*- coding: utf-8\n\n\"\"\"\nSearch longest common substrings using generalized suffix trees built with Ukkonen's algorithm\n"
  },
  {
    "path": "plugins/classical_extras/ui_options_classical_extras.py",
    "chars": 397379,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'M:\\Documents\\Mark's documents\\Music\\Picar"
  },
  {
    "path": "plugins/classicdiscnumber/classicdiscnumber.py",
    "chars": 779,
    "preview": "PLUGIN_NAME = 'Classic Disc Numbers'\nPLUGIN_AUTHOR = 'Lukas Lalinsky'\nPLUGIN_DESCRIPTION = '''Moves disc numbers and sub"
  },
  {
    "path": "plugins/collect_artists/collect_artists.py",
    "chars": 1963,
    "preview": "PLUGIN_NAME = \"Collect Album Artists\"\nPLUGIN_AUTHOR = \"johbi\"\nPLUGIN_DESCRIPTION = \"Adds a context menu shortcut to coll"
  },
  {
    "path": "plugins/compatible_TXXX/compatible_TXXX.py",
    "chars": 2130,
    "preview": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = u\"Compatible TXXX frames\"\nPLUGIN_AUTHOR = u'Tungol'\nPLUGIN_DESCRIPTION = \"\"\"This "
  },
  {
    "path": "plugins/critiquebrainz/critiquebrainz.py",
    "chars": 4632,
    "preview": "# -*- coding: utf-8 -*-\n# Critiquebrainz plugin for Picard\n# Copyright (C) 2022  Tobias Sarner\n#\n# This program is free "
  },
  {
    "path": "plugins/cuesheet/cuesheet.py",
    "chars": 6893,
    "preview": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = \"Generate Cuesheet\"\nPLUGIN_AUTHOR = \"Lukáš Lalinský, Sambhav Kothari\"\nPLUGIN_DESC"
  },
  {
    "path": "plugins/decade/__init__.py",
    "chars": 2516,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2019 Philipp Wolfer\n#\n# This program is free software; you can redistribute it"
  },
  {
    "path": "plugins/decode_cyrillic/decode_cyrillic.py",
    "chars": 3920,
    "preview": "# -*- coding: utf-8 -*-\n\n# This is the Decode Cyrillic plugin for MusicBrainz Picard.\n# Copyright (C) 2015 aeontech\n#\n# "
  },
  {
    "path": "plugins/decode_greek_cyrillic/decode_greek1253.py",
    "chars": 3732,
    "preview": "# -*- coding: utf-8 -*-\n#This is not my work. I just changed the language to Greek.\n#All the credits goes to the origina"
  },
  {
    "path": "plugins/deezerart/__init__.py",
    "chars": 5942,
    "preview": "PLUGIN_NAME = \"Deezer cover art\"\nPLUGIN_AUTHOR = \"Fabio Forni <livingsilver94>\"\nPLUGIN_DESCRIPTION = \"Fetch cover arts f"
  },
  {
    "path": "plugins/deezerart/deezer/__init__.py",
    "chars": 42,
    "preview": "from .client import Client, SearchOptions\n"
  },
  {
    "path": "plugins/deezerart/deezer/client.py",
    "chars": 3255,
    "preview": "import json\nfrom functools import partial\nfrom typing import Callable, List, NamedTuple, Optional, TypeVar\nfrom urllib.p"
  },
  {
    "path": "plugins/deezerart/deezer/obj.py",
    "chars": 2303,
    "preview": "import enum\nimport json\nfrom typing import Any, List, Mapping, Optional, Union\n\n# This module is a perfect target for da"
  },
  {
    "path": "plugins/deezerart/options.py",
    "chars": 2681,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/deezerart/options.ui'\n#\n# Created"
  },
  {
    "path": "plugins/deezerart/options.ui",
    "chars": 1961,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>Form</class>\n <widget class=\"QWidget\" name=\"Form\">\n  <"
  },
  {
    "path": "plugins/discnumber/discnumber.py",
    "chars": 964,
    "preview": "PLUGIN_NAME = 'Disc Numbers'\nPLUGIN_AUTHOR = 'Lukas Lalinsky'\nPLUGIN_DESCRIPTION = '''Moves disc numbers and subtitles f"
  },
  {
    "path": "plugins/enhanced_titles/__init__.py",
    "chars": 19910,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2024 Giorgio Fontanive (twodoorcoupe)\n#\n# This program is free software; you c"
  },
  {
    "path": "plugins/enhanced_titles/options_enhanced_titles.ui",
    "chars": 4983,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<ui version=\"4.0\">\r\n <class>EnhancedTitlesOptions</class>\r\n <widget class=\"QWidg"
  },
  {
    "path": "plugins/enhanced_titles/ui_options_enhanced_titles.py",
    "chars": 6369,
    "preview": "# -*- coding: utf-8 -*-\n\n################################################################################\n## Form genera"
  },
  {
    "path": "plugins/fanarttv/__init__.py",
    "chars": 6522,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2015-2021 Philipp Wolfer\n#\n# This program is free software; you can redistribu"
  },
  {
    "path": "plugins/fanarttv/ui_options_fanarttv.py",
    "chars": 4775,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/fanarttv/ui_options_fanarttv.ui'\n"
  },
  {
    "path": "plugins/fanarttv/ui_options_fanarttv.ui",
    "chars": 4649,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>FanartTvOptionsPage</class>\n <widget class=\"QWidget\" n"
  },
  {
    "path": "plugins/featartist/featartist.py",
    "chars": 578,
    "preview": "PLUGIN_NAME = 'Feat. Artists Removed'\nPLUGIN_AUTHOR = 'Lukas Lalinsky, Bryan Toth'\nPLUGIN_DESCRIPTION = 'Removes feat. a"
  },
  {
    "path": "plugins/featartistsintitles/featartistsintitles.py",
    "chars": 1310,
    "preview": "PLUGIN_NAME = 'Feat. Artists in Titles'\nPLUGIN_AUTHOR = 'Lukas Lalinsky, Michael Wiencek, Bryan Toth, JeromyNix (Nobahdi"
  },
  {
    "path": "plugins/fix_tracknums/fix_tracknums.py",
    "chars": 7112,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\n# Fix Track Numbers plugin for MusicBrainz Picard\n# Copyright (C) 2017 Jo"
  },
  {
    "path": "plugins/format_performer_tags/__init__.py",
    "chars": 16673,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2018 Bob Swift (rdswift)\n#\n# This program is free software; you can redistribu"
  },
  {
    "path": "plugins/format_performer_tags/docs/HISTORY.md",
    "chars": 1454,
    "preview": "# Format Performer Tags\n\n## Contributors\n\nThe following people have contributed to the development of this plugin.\n\n* Bo"
  },
  {
    "path": "plugins/format_performer_tags/docs/README.md",
    "chars": 10093,
    "preview": "# Format Performer Tags \\[[Download](https://github.com/rdswift/picard-plugins/raw/2.0_RDS_Plugins/plugins/format_perfor"
  },
  {
    "path": "plugins/format_performer_tags/ui_options_format_performer_tags.py",
    "chars": 24091,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/format_performer_tags/ui_options_"
  },
  {
    "path": "plugins/format_performer_tags/ui_options_format_performer_tags.ui",
    "chars": 25707,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>FormatPerformerTagsOptionsPage</class>\n <widget class="
  },
  {
    "path": "plugins/genre_mapper/__init__.py",
    "chars": 7102,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2022-2024 Bob Swift (rdswift)\n#\n# This program is free software; you can redis"
  },
  {
    "path": "plugins/genre_mapper/options_genre_mapper.ui",
    "chars": 7172,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>GenreMapperOptionsPage</class>\n <widget class=\"QWidget"
  },
  {
    "path": "plugins/genre_mapper/ui_options_genre_mapper.py",
    "chars": 8444,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file '.\\plugins\\genre_mapper\\options_genre_mapp"
  },
  {
    "path": "plugins/haikuattrs/haikuattrs.py",
    "chars": 8936,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (c) 2019, 2021 Philipp Wolfer\n#\n# This program is free software; you can redistrib"
  },
  {
    "path": "plugins/happidev_lyrics/happidev_lyrics.py",
    "chars": 6484,
    "preview": "from functools import partial\nfrom urllib.parse import (\n    quote,\n    urlencode,\n    urlparse,\n)\n\nfrom PyQt5 import Qt"
  },
  {
    "path": "plugins/hyphen_unicode/hyphen_unicode.py",
    "chars": 2608,
    "preview": "# -*- coding: utf-8 -*-\n\n# Copyright (C) 2016 Anderson Mesquita <andersonvom@gmail.com>\n# Copyright (C) 2019 Alan Swanso"
  },
  {
    "path": "plugins/instruments/instruments.py",
    "chars": 3012,
    "preview": "# MusicBrainz Picard plugin to add an ~instruments tag.\n# Copyright (C) 2019  David Mandelberg\n#\n# This program is free "
  },
  {
    "path": "plugins/keep/keep.py",
    "chars": 1189,
    "preview": "PLUGIN_NAME = \"Keep tags\"\nPLUGIN_AUTHOR = \"Wieland Hoffmann\"\nPLUGIN_DESCRIPTION = \"\"\"\nAdds a $keep() function to delete "
  },
  {
    "path": "plugins/key_wheel_converter/key_wheel_converter.py",
    "chars": 15631,
    "preview": "# -*- coding: utf-8 -*-\n\"\"\"Key Wheel Converter Plugin\n\"\"\"\n#\n# Copyright (C) 2022-2025 Bob Swift (rdswift)\n#\n# This progr"
  },
  {
    "path": "plugins/lastfm/__init__.py",
    "chars": 8565,
    "preview": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = 'Last.fm'\nPLUGIN_AUTHOR = 'Lukáš Lalinský, Philipp Wolfer'\nPLUGIN_DESCRIPTION = '"
  },
  {
    "path": "plugins/lastfm/ui_options_lastfm.py",
    "chars": 6008,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/lastfm/ui_options_lastfm.ui'\n#\n# "
  },
  {
    "path": "plugins/lastfm/ui_options_lastfm.ui",
    "chars": 6000,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>LastfmOptionsPage</class>\n <widget class=\"QWidget\" nam"
  },
  {
    "path": "plugins/loadasnat/loadasnat.py",
    "chars": 2800,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2017, 2019, 2021 Philipp Wolfer\n#\n# This program is free software; you can red"
  },
  {
    "path": "plugins/losslessfuncs/__init__.py",
    "chars": 2349,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2014, 2017, 2021, 2023-2024 Philipp Wolfer\n#\n# This program is free software; "
  },
  {
    "path": "plugins/lrclib_lyrics/__init__.py",
    "chars": 9313,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2024 Giorgio Fontanive (twodoorcoupe)\n#\n# This program is free software; you c"
  },
  {
    "path": "plugins/lrclib_lyrics/option_lrclib_lyrics.py",
    "chars": 4238,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'option_lrclib_lyrics.ui'\n#\n# Created by: "
  },
  {
    "path": "plugins/lrclib_lyrics/option_lrclib_lyrics.ui",
    "chars": 3048,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>OptionLrclibLyrics</class>\n <widget class=\"QWidget\" na"
  },
  {
    "path": "plugins/matroska_tagger/matroska_tagger.py",
    "chars": 22589,
    "preview": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = \"Matroska Tagger\"\nPLUGIN_AUTHOR = \"Ben McLean\"\nPLUGIN_VERSION = \"0.1\"\nPLUGIN_API_"
  },
  {
    "path": "plugins/mod/__init__.py",
    "chars": 11386,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2022, 2025 Philipp Wolfer\n#\n# This program is free software; you can redistrib"
  },
  {
    "path": "plugins/moodbars/__init__.py",
    "chars": 7190,
    "preview": "# -*- coding: utf-8 -*-\n\n# Changelog:\n# [2015-09-24] Initial version with support for Ogg Vorbis, FLAC, WAV and MP3, tes"
  },
  {
    "path": "plugins/moodbars/ui_options_moodbar.py",
    "chars": 3320,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/moodbars/ui_options_moodbar.ui'\n#"
  },
  {
    "path": "plugins/moodbars/ui_options_moodbar.ui",
    "chars": 2248,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>MoodbarOptionsPage</class>\n <widget class=\"QWidget\" na"
  },
  {
    "path": "plugins/musixmatch/README",
    "chars": 88,
    "preview": "Installation:\n        Obtain API key from Musixmatch (https://developer.musixmatch.com)\n"
  },
  {
    "path": "plugins/musixmatch/__init__.py",
    "chars": 2792,
    "preview": "PLUGIN_NAME = 'Musixmatch Lyrics'\nPLUGIN_AUTHOR = 'm-yn, Sambhav Kothari, Philipp Wolfer'\nPLUGIN_DESCRIPTION = 'Fetch fi"
  },
  {
    "path": "plugins/musixmatch/ui_options_musixmatch.py",
    "chars": 1589,
    "preview": "from PyQt5 import QtCore, QtWidgets\n\n\nclass Ui_MusixmatchOptionsPage(object):\n\n    def setupUi(self, MusixmatchOptionsPa"
  },
  {
    "path": "plugins/no_release/no_release.py",
    "chars": 4803,
    "preview": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = 'No release'\nPLUGIN_AUTHOR = 'Johannes Weißl, Philipp Wolfer'\nPLUGIN_DESCRIPTION "
  },
  {
    "path": "plugins/non_ascii_equivalents/non_ascii_equivalents.py",
    "chars": 3458,
    "preview": "# -*- coding: utf-8 -*-\n\n# Copyright (C) 2016 Anderson Mesquita <andersonvom@gmail.com>\n#\n# This program is free softwar"
  },
  {
    "path": "plugins/padded/padded.py",
    "chars": 1206,
    "preview": "PLUGIN_NAME = \"Padded disc and tracknumbers\"\nPLUGIN_AUTHOR = \"Wieland Hoffmann\"\nPLUGIN_DESCRIPTION = \"\"\"\nAdds padded dis"
  },
  {
    "path": "plugins/papercdcase/papercdcase.py",
    "chars": 3189,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2015, 2019 Philipp Wolfer\n#\n# This program is free software; you can redistrib"
  },
  {
    "path": "plugins/performer_tag_replace/__init__.py",
    "chars": 7276,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2018-2025 Bob Swift (rdswift)\n#\n# This program is free software; you can redis"
  },
  {
    "path": "plugins/performer_tag_replace/options_performer_tag_replace.ui",
    "chars": 6712,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>PerformerTagReplaceOptionsPage</class>\n <widget class="
  },
  {
    "path": "plugins/performer_tag_replace/ui_options_performer_tag_replace.py",
    "chars": 7225,
    "preview": "# -*- coding: utf-8 -*-\n\n# Automatically generated - don't edit.\n# Use `python setup.py build_ui` to update it.\n\nfrom Py"
  },
  {
    "path": "plugins/persistent_variables/__init__.py",
    "chars": 12042,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2022 Bob Swift (rdswift)\n#\n# This program is free software; you can redistribu"
  },
  {
    "path": "plugins/persistent_variables/ui_variables_dialog.py",
    "chars": 2941,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form based on that used for the \"View Script Variables\" plugin\n\n\nfrom PyQt5 import QtCore, Qt"
  },
  {
    "path": "plugins/playlist/playlist.py",
    "chars": 5955,
    "preview": "#!/usr/bin/python\n# -*- coding: utf-8 -*-\n\n# This program is free software; you can redistribute it and/or\n# modify it u"
  },
  {
    "path": "plugins/post_tagging_actions/__init__.py",
    "chars": 18538,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2024 Giorgio Fontanive (twodoorcoupe)\n#\n# This program is free software; you c"
  },
  {
    "path": "plugins/post_tagging_actions/actions_status.py",
    "chars": 1719,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/post_tagging_actions/actions_stat"
  },
  {
    "path": "plugins/post_tagging_actions/actions_status.ui",
    "chars": 1432,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>ActionsStatus</class>\n <widget class=\"QWidget\" name=\"A"
  },
  {
    "path": "plugins/post_tagging_actions/docs/guide.md",
    "chars": 3292,
    "preview": "# Post Tagging Actions\nThis plugin lets you set up actions that run with a context menu click.\nAn action consists in a c"
  },
  {
    "path": "plugins/post_tagging_actions/options_post_tagging_actions.py",
    "chars": 12162,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/post_tagging_actions/options_post"
  },
  {
    "path": "plugins/post_tagging_actions/options_post_tagging_actions.ui",
    "chars": 11500,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<ui version=\"4.0\">\r\n <class>PostTaggingActions</class>\r\n <widget class=\"QWidget\""
  },
  {
    "path": "plugins/release_type/release_type.py",
    "chars": 1009,
    "preview": "PLUGIN_NAME = 'Release Type'\nPLUGIN_AUTHOR = 'Elliot Chance'\nPLUGIN_DESCRIPTION = 'Appends information to EPs and Single"
  },
  {
    "path": "plugins/releasetag_aggregations/releasetag_aggregations.py",
    "chars": 10008,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2021 Philipp Wolfer\n#\n# This program is free software; you can redistribute it"
  },
  {
    "path": "plugins/remove_perfect_albums/remove_perfect_albums.py",
    "chars": 879,
    "preview": "PLUGIN_NAME = 'Remove Perfect Albums'\nPLUGIN_AUTHOR = 'ichneumon, hrglgrmpf'\nPLUGIN_DESCRIPTION = '''Remove all perfectl"
  },
  {
    "path": "plugins/reorder_sides/reorder_sides.py",
    "chars": 6735,
    "preview": "# MusicBrainz Picard plugin to re-order sides of a release.\n# Copyright (C) 2016  David Mandelberg\n#\n# This program is f"
  },
  {
    "path": "plugins/replace_forbidden_symbols/replace_forbidden_symbols.py",
    "chars": 2477,
    "preview": "# -*- coding: utf-8 -*-\n\n# Copyright (C) 2019 Alex Rustler <alex_rustler@rambler.ru>\n#\n# This program is free software: "
  },
  {
    "path": "plugins/replaygain2/__init__.py",
    "chars": 16360,
    "preview": "# -*- coding: utf-8 -*-\nPLUGIN_NAME = \"ReplayGain 2.0\"\nPLUGIN_AUTHOR = \"complexlogic\"\nPLUGIN_DESCRIPTION = '''\nCalculate"
  },
  {
    "path": "plugins/replaygain2/ui_options_replaygain2.py",
    "chars": 8895,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/replaygain2/ui_options_replaygain"
  },
  {
    "path": "plugins/replaygain2/ui_options_replaygain2.ui",
    "chars": 7568,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>ReplayGain2OptionsPage</class>\n <widget class=\"QWidget"
  },
  {
    "path": "plugins/save_and_rewrite_header/save_and_rewrite_header.py",
    "chars": 2622,
    "preview": "#!/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 conte"
  },
  {
    "path": "plugins/script_logger/__init__.py",
    "chars": 3020,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2023 Bob Swift (rdswift)\n#\n# This program is free software; you can redistribu"
  },
  {
    "path": "plugins/search_engine_lookup/README.md",
    "chars": 1687,
    "preview": "# Search Engine Lookup \\[[Download](https://github.com/rdswift/picard-plugins/raw/2.0_RDS_Plugins/plugins/search_engine_"
  },
  {
    "path": "plugins/search_engine_lookup/__init__.py",
    "chars": 13318,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2020-2021, 2026 Bob Swift (rdswift)\n#\n# This program is free software; you can"
  },
  {
    "path": "plugins/search_engine_lookup/ui_options_search_engine_editor.py",
    "chars": 6409,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/search_engine_lookup/ui_options_s"
  },
  {
    "path": "plugins/search_engine_lookup/ui_options_search_engine_editor.ui",
    "chars": 6018,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>SearchEngineEditorDialog</class>\n <widget class=\"QWidg"
  },
  {
    "path": "plugins/search_engine_lookup/ui_options_search_engine_lookup.py",
    "chars": 7058,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/search_engine_lookup/ui_options_s"
  },
  {
    "path": "plugins/search_engine_lookup/ui_options_search_engine_lookup.ui",
    "chars": 6122,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>SearchEngineLookupOptionsPage</class>\n <widget class=\""
  },
  {
    "path": "plugins/smart_title_case/smart_title_case.py",
    "chars": 5723,
    "preview": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n# This is the Smart Title Case plugin for MusicBrainz Picard.\n# Copyrigh"
  },
  {
    "path": "plugins/sort_multivalue_tags/sort_multivalue_tags.py",
    "chars": 2336,
    "preview": "# -*- coding: utf-8 -*-\n\n# This is the Sort Multivalue Tags plugin for MusicBrainz Picard.\n# Copyright (C) 2013 Sophist\n"
  },
  {
    "path": "plugins/soundtrack/soundtrack.py",
    "chars": 879,
    "preview": "# -*- coding: utf-8 -*-\n\n# Copyright © 2015 Samir Benmendil <me@rmz.io>\n# This work is free. You can redistribute it and"
  },
  {
    "path": "plugins/standardise_feat/standardise_feat.py",
    "chars": 2482,
    "preview": "PLUGIN_NAME = 'Standardise Feat.'\r\nPLUGIN_AUTHOR = 'Sambhav Kothari'\r\nPLUGIN_DESCRIPTION = 'Standardises \"featuring\" joi"
  },
  {
    "path": "plugins/standardise_performers/standardise_performers.py",
    "chars": 2943,
    "preview": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = 'Standardise Performers'\nPLUGIN_AUTHOR = 'Sophist'\nPLUGIN_DESCRIPTION = '''Splits"
  },
  {
    "path": "plugins/submit_folksonomy_tags/README.md",
    "chars": 2413,
    "preview": "# Submit Folksonomy Tags - Picard Plugin\n\nA plugin that lets the user submit tags from their tracks' tags - defaults to "
  },
  {
    "path": "plugins/submit_folksonomy_tags/__init__.py",
    "chars": 21896,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2023 Flaky\n# Copyright (C) 2023 Bob Swift (rdswift)\n#\n# This program is free s"
  },
  {
    "path": "plugins/submit_folksonomy_tags/ui_config.py",
    "chars": 7543,
    "preview": "from PyQt5.QtCore import (\n    QSize,\n    Qt\n    )\n\nfrom PyQt5.QtWidgets import (\n    QGridLayout,\n    QGroupBox,\n    QH"
  },
  {
    "path": "plugins/submit_isrc/README.md",
    "chars": 1147,
    "preview": "# Submit ISRC\n\n## Overview\n\nThis plugin adds a right click option on an album to submit the ISRCs to the MusicBrainz ser"
  },
  {
    "path": "plugins/submit_isrc/__init__.py",
    "chars": 12463,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2020-2021, 2023 Bob Swift (rdswift)\n#\n# This program is free software; you can"
  },
  {
    "path": "plugins/tangoinfo/README.md",
    "chars": 2365,
    "preview": "# tango.info plugin for MusicBrainz Picard\nAutomatically get *genre, date and singers* from **tango.info**, right inside"
  },
  {
    "path": "plugins/tangoinfo/__init__.py",
    "chars": 14129,
    "preview": "# -*- coding: utf-8 -*-\nPLUGIN_NAME = \"Tango.info Adapter\"\nPLUGIN_AUTHOR = \"Felix Elsner, Sambhav Kothari, Philipp Wolfe"
  },
  {
    "path": "plugins/theaudiodb/__init__.py",
    "chars": 6740,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (c) 2015-2020 Philipp Wolfer\n#\n# This program is free software; you can redistribu"
  },
  {
    "path": "plugins/theaudiodb/ui_options_theaudiodb.py",
    "chars": 4576,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/theaudiodb/ui_options_theaudiodb."
  },
  {
    "path": "plugins/theaudiodb/ui_options_theaudiodb.ui",
    "chars": 3994,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>TheAudioDbOptionsPage</class>\n <widget class=\"QWidget\""
  },
  {
    "path": "plugins/titlecase/titlecase.py",
    "chars": 2275,
    "preview": "# -*- coding: utf-8 -*-\n# Copyright 2007 Javier Kohen\n#\n# This program is free software; you can redistribute it and/or "
  },
  {
    "path": "plugins/tracks2clipboard/tracks2clipboard.py",
    "chars": 1633,
    "preview": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = \"Copy Cluster to Clipboard\"\nPLUGIN_AUTHOR = \"Michael Elsdörfer, Sambhav Kothari\"\n"
  },
  {
    "path": "plugins/viewvariables/__init__.py",
    "chars": 4512,
    "preview": "# -*- coding: utf-8 -*-\n\nPLUGIN_NAME = 'View script variables'\nPLUGIN_AUTHOR = 'Sophist'\nPLUGIN_DESCRIPTION = '''Display"
  },
  {
    "path": "plugins/viewvariables/ui_variables_dialog.py",
    "chars": 3207,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/viewvariables/ui_variables_dialog"
  },
  {
    "path": "plugins/viewvariables/ui_variables_dialog.ui",
    "chars": 2533,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>VariablesDialog</class>\n <widget class=\"QDialog\" name="
  },
  {
    "path": "plugins/wikidata/__init__.py",
    "chars": 20556,
    "preview": "# -*- coding: utf-8 -*-\n# Copyright © 2016 Daniel sobey <dns@dns.id.au >\n\n# This work is free. You can redistribute it a"
  },
  {
    "path": "plugins/wikidata/ui_options_wikidata.py",
    "chars": 13826,
    "preview": "# -*- coding: utf-8 -*-\n\n# Form implementation generated from reading ui file 'plugins/wikidata/ui_options_wikidata.ui'\n"
  },
  {
    "path": "plugins/wikidata/ui_options_wikidata.ui",
    "chars": 12587,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>WikidataOptionsPage</class>\n <widget class=\"QWidget\" n"
  },
  {
    "path": "plugins/workandmovement/__init__.py",
    "chars": 8795,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2018-2019, 2021-2022 Philipp Wolfer\n#\n# This program is free software; you can"
  },
  {
    "path": "plugins/workandmovement/roman.py",
    "chars": 2697,
    "preview": "\"\"\"Convert to and from Roman numerals\"\"\"\n\n__author__ = \"Mark Pilgrim (f8dy@diveintopython.org)\"\n__version__ = \"1.4\"\n__da"
  },
  {
    "path": "setup.cfg",
    "chars": 575,
    "preview": "[flake8]\n# E127: continuation line over-indented for visual indent\n# E128: continuation line under-indented for visual i"
  },
  {
    "path": "test/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/plugin_test_case.py",
    "chars": 5928,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Picard, the next-generation MusicBrainz tagger\n#\n# Copyright (C) 2018 Wieland Hoffmann\n# Cop"
  },
  {
    "path": "test/test_add_to_collection.py",
    "chars": 4713,
    "preview": "import os\nfrom itertools import chain\nfrom test.plugin_test_case import PluginTestCase\nfrom typing import Union\nfrom uni"
  },
  {
    "path": "test/test_doctest.py",
    "chars": 715,
    "preview": "import doctest\n\n\ndef load_tests(loader, tests, ignore):\n    from plugins.addrelease import addrelease\n    tests.addTests"
  },
  {
    "path": "test/test_generate.py",
    "chars": 2871,
    "preview": "import doctest\nimport os\nimport glob\nimport json\nimport shutil\nimport tempfile\nimport unittest\nfrom contextlib import re"
  },
  {
    "path": "test/test_keep.py",
    "chars": 2637,
    "preview": "#!/usr/bin/env python\n# coding: utf-8\nimport unittest\n\nfrom picard.metadata import Metadata\nfrom picard.script import Sc"
  }
]

About this extraction

This page contains the full source code of the metabrainz/picard-plugins GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 157 files (2.3 MB), approximately 598.7k tokens, and a symbol index with 987 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!