{"owner":"metabrainz","repo":"picard","hasSkills":true,"totalSkillsCount":2,"totalTokensCount":4504,"categories":["plugin-manifest","root-instruction"],"hasMcp":false,"mcpConfig":null,"found":["scripts/tools/README.md","AGENTS.md"],"skills":{"scripts/tools/README.md":"# scripts/tools/\n\nDeveloper utility scripts for MusicBrainz Picard. These are not shipped with\nreleases — they assist with development, releases, and asset generation.\n\n## Scripts\n\n| Script | Description |\n|--------|-------------|\n| `authors-between-releases.py` | List code contributors and translators between two git tags, for release notes. |\n| `changelog-for-version.py` | Extract changelog entries for a given version from git history. |\n| `check_settings.py` | Check for references to undefined option settings in the codebase. |\n| `detect_qt_shadowing.py` | Detect class attributes that shadow inherited Qt methods. |\n| `fix-header.py` | Regenerate source file copyright/license headers from git log. |\n| `generate_icons.sh` | Generate PNG icons (multiple sizes) from an SVG source file. |\n| `generate_match_icons.py` | Generate match quality bookmark PNGs (normal + pending, 1× + 2×). |\n| `multiple_imports_on_one_line.py` | Find import statements with multiple names on one line. |\n| `patch-version.sh` | Patch the version string with git commit count/hash for dev builds. |\n| `pull-shared-translations.py` | Pull shared translation strings from Weblate/upstream. |\n| `tag-release.sh` | Create a git tag for the current release version. |\n","AGENTS.md":"# AI Assistant Context for MusicBrainz Picard\n\n> **Purpose:** Essential patterns, conventions, and gotchas for AI assistants.\n> For architecture, dependencies, and component details, explore the codebase\n> or read existing docs (`README.md`, `CONTRIBUTING.md`, `INSTALL.md`).\n\n**What is Picard?** Cross-platform audio tagger for MusicBrainz. Tags music files using the MusicBrainz database, supports fingerprinting (AcoustID), and has a powerful plugin system.\n\n---\n\n## Quick Facts\n\n- **Language:** Python 3.10+, PyQt6, Mutagen\n- **Size:** Event-driven MVC with plugin system\n- **Entry Point:** `picard/tagger.py:main()` → `Tagger` singleton\n- **Tests:** `test/` directory (pytest)\n- **Docs:** <https://picard-docs.musicbrainz.org/>\n- **Tickets:** <https://tickets.metabrainz.org/projects/PICARD>\n- **Chat:** [Matrix #musicbrainz-picard-dev](https://matrix.to/#/#musicbrainz-picard-dev:chatbrainz.org)\n\n---\n\n## Critical Patterns & Gotchas\n\n### Threading Rules\n```python\n# ❌ NEVER access UI from background threads\ndef background_task():\n    self.label.setText(\"Done\")  # CRASH!\n\n\n# ✅ Use signals or run_task callback\nfrom picard.util.thread import run_task\n\n\ndef callback(result):\n    self.label.setText(result)  # Safe - runs in main thread\n\n\nrun_task(heavy_operation, callback=callback)\n```\n\n### Metadata Changes\n```python\n# ❌ Silent changes won't update UI\nmetadata['artist'] = 'New Artist'\n\n# ✅ Always emit signals\nmetadata['artist'] = 'New Artist'\nfile.metadata_updated.emit()\n```\n\n### Metadata Access\n```python\n# Metadata is always stored internally as a list of strings.\n\n# ❌ Don't use get() + split on MULTI_VALUED_JOINER to get individual values\nvalues = metadata.get('artist', '').split(MULTI_VALUED_JOINER)\n\n# ✅ Use getall() to get individual values as a list\nvalues = metadata.getall('artist')  # Returns: ['Artist 1', 'Artist 2']\n\n# ✅ Use get() or [] only for display (returns joined string)\ndisplay = metadata['artist']  # Returns: 'Artist 1; Artist 2'\n\n# ✅ Set multiple values by passing a list (don't join them)\nmetadata['artist'] = ['Artist 1', 'Artist 2']\n```\n\n### Configuration Access\n```python\n# ✅ Always use config.setting\nfrom picard import config\n\nvalue = config.setting['option_name']\n\n# ❌ Don't access internal structures directly\n```\n\n### Async Operations\n```python\n# ✅ Use thread pool for I/O\nfrom picard.util.thread import run_task\n\nrun_task(process_file, file, callback)\n\n# ❌ Don't block the main thread\nresult = slow_network_call()  # UI freezes!\n```\n\n### Import Rules\n```python\n# ❌ Don't use inline imports (unless breaking circular dependencies)\ndef my_function():\n    from picard.some_module import something\n\n    return something()\n\n\n# ❌ Don't put multiple imports on one line\nfrom picard.plugin3.validator import generate_uuid, validate_manifest_dict\n\n# ✅ Imports go at the top of the file, each imported name on its own line with trailing commas\nfrom picard.plugin3.validator import (\n    generate_uuid,\n    validate_manifest_dict,\n)\n```\n\n**Exception:** Inline imports are acceptable only to break circular dependencies. Place them as close to usage as possible with a comment explaining why.\n\n---\n\n## Plugin System (v3)\n\n### Structure\n```text\nmy_plugin/\n├── __init__.py       # Plugin code\n├── MANIFEST.toml     # Metadata (UPPERCASE!)\n└── ui_options.py     # Optional settings\n```\n\n### MANIFEST.toml\n```toml\nuuid = \"unique-uuid\"\nname = \"Plugin Name\"\nauthors = [\"Your Name\"]\ndescription = \"Description\"\napi = [\"3.0\"]\ncategories = [\"metadata\"]\nlicense = \"GPL-2.0-or-later\"\nlicense_url = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n```\n\n### Plugin Code\n```python\nfrom picard.plugin3.api import PluginApi\n\n\ndef enable(api: PluginApi):\n    \"\"\"Called when plugin is enabled.\"\"\"\n    api.plugin_config.register_option(\"my_option\", \"default\")\n    api.register_track_metadata_processor(process_metadata)\n\n\ndef process_metadata(api, album, metadata, track, release):\n    metadata['custom'] = api.plugin_config['my_option']\n```\n\n### Plugin Management CLI\n```bash\n# List installed plugins\npicard-cli plugins list\n\n# Install plugin from registry\npicard-cli plugins install plugin-name\n\n# Install from URL or local path\npicard-cli plugins install https://github.com/user/plugin.git\npicard-cli plugins install /path/to/plugin\n\n# Update plugins\npicard-cli plugins update plugin-name\n\n# Remove plugin\npicard-cli plugins remove plugin-name\n\n# Search registry\npicard-cli plugins search keyword\n```\n\n**Key Files:**\n- API: `picard/plugin3/api_impl.py`\n- Manager: `picard/plugin3/manager/__init__.py`\n- CLI: `picard/plugin3/cli.py`\n- Example: <https://github.com/rdswift/picard-plugin-format-performer-tags>\n\n**Documentation:**\n- Plugin v3 docs: `docs/PLUGINSV3/` (API, CLI, manifest, translations, etc.)\n- Migration guide: `docs/Plugin2to3MigrationGuide.md`\n- Plugin registry: <https://github.com/metabrainz/picard-plugins-registry>\n\n---\n\n## Development Workflow\n\n### Setup\n```bash\ngit clone https://github.com/metabrainz/picard.git\ncd picard\n\n# Preferred\nuv sync\n\n# Alternative\npip install -r requirements.txt\n\npython setup.py build_ui  # Compile Qt forms\npython tagger.py          # Run from source\n```\n\n### Before Committing\n```bash\n# 1. Format code\nruff format picard/\n\n# 2. Check for issues\nruff check picard/\n\n# 3. Run tests\npytest test/\n\n# Or specific tests\npytest test/test_metadata.py\n```\n\n**Important:** Always run `ruff format` after making code changes to ensure consistent formatting.\n\n### AI Assistant Guidelines\nWhen making code changes:\n1. **Imports go at the top of files** - local (inline) imports are acceptable only to break circular dependencies; in that case, place them as close to usage as possible with a comment explaining why\n2. **Bug fixes: test first** - write a test that reproduces the bug before fixing it, then verify the fix makes the test pass; keep the test as a permanent regression test when feasible\n3. **Run `ruff format` after all changes** - ensures code follows project style guidelines\n4. **Run `ruff check` to catch issues** - fix any linting errors before committing\n5. **Never commit editor or agent-specific files** - Do not `git add` files from AI tool directories (`.kiro/`, `.cursor/`, `.aider*`, etc.) or any files not part of the project. Review `git diff --cached` before committing to verify only intended files are staged. When in doubt, ask the user before committing.\n6. **Don't push to remote** - without explicit user approval\n7. **Never use `git commit --no-verify`** - pre-commit hooks run `ruff format` and `ruff check` automatically; skipping them leads to CI failures. If using fixup/rebase workflows, always run `ruff format` and `ruff check` manually before the final push.\n8. **Run `ty check` on modified files** - verify no new type errors are introduced (pre-existing errors in the codebase can be ignored).\n9. **`gh pr edit` may fail with a GraphQL Projects deprecation error** - use `gh api repos/OWNER/REPO/pulls/NUMBER -X PATCH -F \"body=@file.md\"` as a workaround to update PR descriptions.\n\n### Contributing\n1. **Create ticket first:** <https://tickets.metabrainz.org/projects/PICARD>\n2. Create feature branch\n3. Make changes\n4. Run pre-commit checks (above)\n5. **PR title must start with:** `PICARD-XXXX:` (ticket number)\n6. **Use the PR template** - fill in `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Problem, Solution, AI Usage, Action sections)\n7. **Don't auto-create tickets/PRs** - let humans handle this\n8. **Don't push to remote** - without explicit user approval\n\n---\n\n## Code Style\n\n### Conventions\n- **Standard:** PEP 8, 120 char lines (flexible)\n- **Linter:** Ruff\n- **Type hints:** Recommended for new code (existing code has limited coverage)\n- **Naming:** `PascalCase` classes, `snake_case` functions, `UPPER_SNAKE` constants\n\n### Membership Tests and Control Flow\n```python\n# Use a set literal (not a tuple/list) for membership tests against a fixed\n# set of constants. Python optimizes `in {...}`. See the ruff \"literal-membership\" rule.\n\n# ❌ Tuple/list literal for membership test\nif state in (State.CHANGED, State.NORMAL):\n    ...\nassert suffix in ('.pyc', '.pyo')\n\n# ✅ Set literal\nif state in {State.CHANGED, State.NORMAL}:\n    ...\nassert suffix in {'.pyc', '.pyo'}\n\n\n# Prefer a single membership test over chained equality checks.\n\n# ❌ Multiple equality checks\nif name == 'performer' or name == 'comment':\n    ...\n\n# ✅ Single membership test\nif name in {'performer', 'comment'}:\n    ...\n\n\n# Collapse a nested `else:` + `if` into a single `elif` to reduce indentation.\n# See the ruff \"collapsible-else-if\" rule.\n\n# ❌ Nested if inside else\nif a:\n    do_x()\nelse:\n    if b:\n        do_y()\n    else:\n        do_z()\n\n# ✅ elif\nif a:\n    do_x()\nelif b:\n    do_y()\nelse:\n    do_z()\n```\n\nThe preview ruff rules can be checked with:\n```bash\nuv run ruff check --preview --select literal-membership,collapsible-else-if\n```\n\n### Internationalization (i18n)\n```python\n# Import translation functions\nfrom picard.i18n import gettext as _\nfrom picard.i18n import N_\n\n# Translatable strings (runtime)\nmessage = _(\"Save file\")\nerror = _(\"Failed to load: %s\") % filename\n\n# Mark for extraction only (constants, class attributes)\nTITLE = N_(\"Options\")  # Translated at usage: _(TITLE)\n\n# Plugin translations (Plugin v3)\nfrom picard.plugin3.api import t_\n\n\nclass ManifestTranslations:\n    NAME = t_(\"manifest.name\", \"My Plugin\")\n    DESC = t_(\"manifest.description\", \"Plugin description\")\n```\n\n**Translation workflow:**\n- Strings marked with `_()` or `N_()` are extracted to `po/picard.pot`\n- Regenerate: `python setup.py regen_pot_file`\n- Translations managed via [Weblate](https://translations.metabrainz.org/projects/picard/)\n- See `po/README.md` for details\n\n#### Gotcha: `picard/const/` uses a separate gettext domain\n\nPicard has **four** gettext domains (see `picard/i18n/gettext.py`), not one.\nStrings are extracted per source location, and each domain must be looked up\nwith its **own** translation function at runtime:\n\n| Where the string is defined | `N_()`-marked into | Translate at runtime with |\n|---|---|---|\n| Anywhere **except** `picard/const/` | `po/picard.pot` (`main`) | `gettext()` / `_()` |\n| `picard/const/` (e.g. `tags.py`, `scripts.py`, `cover_processing.py`, `languages.py`, `locales.py`, `defaults.py`) | `po/constants/` (`constants`) | `gettext_constants()` |\n| `picard/const/countries.py` (`RELEASE_COUNTRIES`) | `po/countries/` (`countries`) | `gettext_countries()` |\n| `picard/const/attributes.py` (`MB_ATTRIBUTES`) | `po/attributes/` (`attributes`) | `pgettext_attributes()` / `gettext_attributes()` |\n\n`setup.py` deliberately excludes `const` from the main pot and extracts it\ninto `po/constants/constants.pot`. So a `const/`-sourced string translated\nwith the plain `_()` is looked up in the **wrong** domain, silently misses,\nand renders in English even though a translation exists.\n\n```python\nfrom picard.i18n import gettext as _\nfrom picard.i18n import gettext_constants\n\n# TagVar.longdesc / shortdesc, SCRIPTS[...], CoverResizeMode.title,\n# DEFAULT_SCRIPT_NAME, ... all originate in picard/const/\n\n# ❌ Wrong domain — always English, even in fr/de/... UI\ntitle = _(tagvar.shortdesc)\nname = _(DEFAULT_SCRIPT_NAME)  # or storing the raw N_() string\n\n# ✅ Constants domain\ntitle = gettext_constants(tagvar.shortdesc)\nname = gettext_constants(DEFAULT_SCRIPT_NAME)\n```\n\nRule of thumb: **if the string's `N_()` lives in `picard/const/`, translate it\nwith `gettext_constants()`** (or the countries/attributes helper). This bug is\neasy to reintroduce — it has recurred across PRs #3453 and #3457 (PICARD-3448).\nWhen touching translated `const/` strings, verify the msgid resolves against\n`po/constants/*.po` (or `po/countries/`, `po/attributes/`).\n\n### Qt UI Files\n```bash\n# ❌ Don't edit picard/ui/ui_*.py directly (auto-generated)\n\n# ✅ Edit .ui files with Qt Designer\nqt6-tools designer ui/options_metadata.ui\n\n# ✅ Regenerate Python files\npython setup.py build_ui\n```\n\n### Qt Signals\n```python\nfrom PyQt6.QtCore import pyqtSignal\n\n\nclass MyWidget(QWidget):\n    # Define signals\n    value_changed = pyqtSignal(str)\n    item_selected = pyqtSignal(object)\n\n    def some_action(self):\n        # Emit signals\n        self.value_changed.emit(\"new value\")\n```\n\nUse the `QSignalBlocker` context manager to suppress signals temporarily,\nrather than manual `blockSignals(True)`/`blockSignals(False)` calls. It is\nexception-safe (signals are always unblocked, even if an exception is raised)\nand more concise.\n\n```python\nfrom PyQt6.QtCore import QSignalBlocker\n\n\n# ❌ Manual, not exception-safe\nwidget.blockSignals(True)\nwidget.setChecked(True)\nwidget.blockSignals(False)\n\n# ✅ Context manager\nwith QSignalBlocker(widget):\n    widget.setChecked(True)\n```\n\n### Readability\n- Use descriptive names\n- Comment complex logic\n- Break long functions\n- Follow existing style in the file\n\n---\n\n## Key Locations\n\n### Core Components\n- **Main app:** `picard/tagger.py`\n- **File handling:** `picard/file.py`\n- **Album/Track:** `picard/album.py`, `picard/track.py`\n- **Metadata:** `picard/metadata.py`\n\n### Format Handlers\n- **Registry:** `picard/formats/registry.py`\n- **ID3 (MP3):** `picard/formats/id3.py`\n- **Vorbis (FLAC/Ogg):** `picard/formats/vorbis.py`\n- **MP4:** `picard/formats/mp4.py`\n\n### Plugin System\n- **API:** `picard/plugin3/api_impl.py`\n- **Manager:** `picard/plugin3/manager/__init__.py`\n- **Registry:** `picard/plugin3/registry.py`\n\n### UI\n- **Main window:** `picard/ui/mainwindow/`\n- **Options:** `picard/ui/options/`\n- **Metadata box:** `picard/ui/metadatabox/`\n- **Script editor:** `picard/ui/scripteditor/`\n\n### Scripting\n- **Parser:** `picard/script/parser.py`\n- **Functions:** `picard/script/functions.py`\n\n### Web Services\n- **HTTP client:** `picard/webservice/__init__.py`\n- **APIs:** `picard/webservice/api_helpers.py`\n\n---\n\n## Common Tasks\n\n### Add Audio Format\n1. Create handler in `picard/formats/`\n2. Inherit from `File`\n3. Implement `_load()` and `_save()`\n4. Define `EXTENSIONS` and `NAME`\n5. Call `register_format(MyFormat)`\n\n### Add Script Function\n```python\nfrom picard.script import script_function\n\n\n@script_function\ndef func_myfunction(parser, arg1, arg2='default'):\n    \"\"\"$myfunction(arg1,arg2) - Description\"\"\"\n    return result\n```\n\n### Add Option\n1. Define in `picard/options.py`\n2. Create UI page in `picard/ui/options/`\n3. Implement `load()` and `save()` methods\n4. Register with `register_options_page()`\n\n### Add Metadata Processor (Core)\n```python\nfrom picard.extension_points.metadata import register_track_metadata_processor\n\n\ndef my_processor(album, metadata, track, release):\n    metadata['custom'] = 'value'\n\n\nregister_track_metadata_processor(my_processor)\n```\n\n**For plugins:** Use `api.register_track_metadata_processor()` instead (see Plugin System section).\n\n---\n\n## Testing\n\nHow to run the suite and how to write tests — the two coexisting test styles, the\n`subtest_cases` decorator for table-driven tests, and the shared `qapp` fixture for\nQt widgets — is documented in [test/README.md](test/README.md). That document is the\nsource of truth; follow it rather than relying on a copy here.\n\nPoints worth repeating because they are easy to get wrong:\n\n- Match the test style of the file you are editing. Do not convert a\n  `PicardTestCase` class to plain pytest (or the reverse) as a side effect of an\n  unrelated change.\n- `@pytest.mark.parametrize` does not work on `PicardTestCase` subclasses; the test\n  errors with a missing-argument `TypeError`. Use the `subtest_cases` decorator (or\n  `self.subTest()`) there.\n- Never create a local `QApplication`/`QCoreApplication`; it crashes under\n  `pytest-randomly` ordering.\n\n---\n\n## Debugging\n\n```bash\n# Enable debug logging\npicard --debug\n\n# Enable specific debug options (comma-separated)\npicard --debug-opts=option1,option2\n```\n\n```python\n# In code\nfrom picard import log\n\nlog.debug('Debug: %s', value)\nlog.error('Error', exc_info=True)\n```\n\n---\n\n## Resources\n\n- **User Docs:** <https://picard-docs.musicbrainz.org/>\n- **Website:** <https://picard.musicbrainz.org/>\n- **GitHub:** <https://github.com/metabrainz/picard>\n- **Forum:** <https://community.metabrainz.org/c/picard>\n- **Contributing:** See `CONTRIBUTING.md`\n- **Installation:** See `INSTALL.md`\n\n---\n\n**Note:** This file contains only essential patterns. For architecture, dependencies, and component details, explore the codebase directly or read existing documentation (`README.md`, `CONTRIBUTING.md`, `INSTALL.md`, user docs).\n"},"files":{"scripts/tools/README.md":"# scripts/tools/\n\nDeveloper utility scripts for MusicBrainz Picard. These are not shipped with\nreleases — they assist with development, releases, and asset generation.\n\n## Scripts\n\n| Script | Description |\n|--------|-------------|\n| `authors-between-releases.py` | List code contributors and translators between two git tags, for release notes. |\n| `changelog-for-version.py` | Extract changelog entries for a given version from git history. |\n| `check_settings.py` | Check for references to undefined option settings in the codebase. |\n| `detect_qt_shadowing.py` | Detect class attributes that shadow inherited Qt methods. |\n| `fix-header.py` | Regenerate source file copyright/license headers from git log. |\n| `generate_icons.sh` | Generate PNG icons (multiple sizes) from an SVG source file. |\n| `generate_match_icons.py` | Generate match quality bookmark PNGs (normal + pending, 1× + 2×). |\n| `multiple_imports_on_one_line.py` | Find import statements with multiple names on one line. |\n| `patch-version.sh` | Patch the version string with git commit count/hash for dev builds. |\n| `pull-shared-translations.py` | Pull shared translation strings from Weblate/upstream. |\n| `tag-release.sh` | Create a git tag for the current release version. |\n","AGENTS.md":"# AI Assistant Context for MusicBrainz Picard\n\n> **Purpose:** Essential patterns, conventions, and gotchas for AI assistants.\n> For architecture, dependencies, and component details, explore the codebase\n> or read existing docs (`README.md`, `CONTRIBUTING.md`, `INSTALL.md`).\n\n**What is Picard?** Cross-platform audio tagger for MusicBrainz. Tags music files using the MusicBrainz database, supports fingerprinting (AcoustID), and has a powerful plugin system.\n\n---\n\n## Quick Facts\n\n- **Language:** Python 3.10+, PyQt6, Mutagen\n- **Size:** Event-driven MVC with plugin system\n- **Entry Point:** `picard/tagger.py:main()` → `Tagger` singleton\n- **Tests:** `test/` directory (pytest)\n- **Docs:** <https://picard-docs.musicbrainz.org/>\n- **Tickets:** <https://tickets.metabrainz.org/projects/PICARD>\n- **Chat:** [Matrix #musicbrainz-picard-dev](https://matrix.to/#/#musicbrainz-picard-dev:chatbrainz.org)\n\n---\n\n## Critical Patterns & Gotchas\n\n### Threading Rules\n```python\n# ❌ NEVER access UI from background threads\ndef background_task():\n    self.label.setText(\"Done\")  # CRASH!\n\n\n# ✅ Use signals or run_task callback\nfrom picard.util.thread import run_task\n\n\ndef callback(result):\n    self.label.setText(result)  # Safe - runs in main thread\n\n\nrun_task(heavy_operation, callback=callback)\n```\n\n### Metadata Changes\n```python\n# ❌ Silent changes won't update UI\nmetadata['artist'] = 'New Artist'\n\n# ✅ Always emit signals\nmetadata['artist'] = 'New Artist'\nfile.metadata_updated.emit()\n```\n\n### Metadata Access\n```python\n# Metadata is always stored internally as a list of strings.\n\n# ❌ Don't use get() + split on MULTI_VALUED_JOINER to get individual values\nvalues = metadata.get('artist', '').split(MULTI_VALUED_JOINER)\n\n# ✅ Use getall() to get individual values as a list\nvalues = metadata.getall('artist')  # Returns: ['Artist 1', 'Artist 2']\n\n# ✅ Use get() or [] only for display (returns joined string)\ndisplay = metadata['artist']  # Returns: 'Artist 1; Artist 2'\n\n# ✅ Set multiple values by passing a list (don't join them)\nmetadata['artist'] = ['Artist 1', 'Artist 2']\n```\n\n### Configuration Access\n```python\n# ✅ Always use config.setting\nfrom picard import config\n\nvalue = config.setting['option_name']\n\n# ❌ Don't access internal structures directly\n```\n\n### Async Operations\n```python\n# ✅ Use thread pool for I/O\nfrom picard.util.thread import run_task\n\nrun_task(process_file, file, callback)\n\n# ❌ Don't block the main thread\nresult = slow_network_call()  # UI freezes!\n```\n\n### Import Rules\n```python\n# ❌ Don't use inline imports (unless breaking circular dependencies)\ndef my_function():\n    from picard.some_module import something\n\n    return something()\n\n\n# ❌ Don't put multiple imports on one line\nfrom picard.plugin3.validator import generate_uuid, validate_manifest_dict\n\n# ✅ Imports go at the top of the file, each imported name on its own line with trailing commas\nfrom picard.plugin3.validator import (\n    generate_uuid,\n    validate_manifest_dict,\n)\n```\n\n**Exception:** Inline imports are acceptable only to break circular dependencies. Place them as close to usage as possible with a comment explaining why.\n\n---\n\n## Plugin System (v3)\n\n### Structure\n```text\nmy_plugin/\n├── __init__.py       # Plugin code\n├── MANIFEST.toml     # Metadata (UPPERCASE!)\n└── ui_options.py     # Optional settings\n```\n\n### MANIFEST.toml\n```toml\nuuid = \"unique-uuid\"\nname = \"Plugin Name\"\nauthors = [\"Your Name\"]\ndescription = \"Description\"\napi = [\"3.0\"]\ncategories = [\"metadata\"]\nlicense = \"GPL-2.0-or-later\"\nlicense_url = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n```\n\n### Plugin Code\n```python\nfrom picard.plugin3.api import PluginApi\n\n\ndef enable(api: PluginApi):\n    \"\"\"Called when plugin is enabled.\"\"\"\n    api.plugin_config.register_option(\"my_option\", \"default\")\n    api.register_track_metadata_processor(process_metadata)\n\n\ndef process_metadata(api, album, metadata, track, release):\n    metadata['custom'] = api.plugin_config['my_option']\n```\n\n### Plugin Management CLI\n```bash\n# List installed plugins\npicard-cli plugins list\n\n# Install plugin from registry\npicard-cli plugins install plugin-name\n\n# Install from URL or local path\npicard-cli plugins install https://github.com/user/plugin.git\npicard-cli plugins install /path/to/plugin\n\n# Update plugins\npicard-cli plugins update plugin-name\n\n# Remove plugin\npicard-cli plugins remove plugin-name\n\n# Search registry\npicard-cli plugins search keyword\n```\n\n**Key Files:**\n- API: `picard/plugin3/api_impl.py`\n- Manager: `picard/plugin3/manager/__init__.py`\n- CLI: `picard/plugin3/cli.py`\n- Example: <https://github.com/rdswift/picard-plugin-format-performer-tags>\n\n**Documentation:**\n- Plugin v3 docs: `docs/PLUGINSV3/` (API, CLI, manifest, translations, etc.)\n- Migration guide: `docs/Plugin2to3MigrationGuide.md`\n- Plugin registry: <https://github.com/metabrainz/picard-plugins-registry>\n\n---\n\n## Development Workflow\n\n### Setup\n```bash\ngit clone https://github.com/metabrainz/picard.git\ncd picard\n\n# Preferred\nuv sync\n\n# Alternative\npip install -r requirements.txt\n\npython setup.py build_ui  # Compile Qt forms\npython tagger.py          # Run from source\n```\n\n### Before Committing\n```bash\n# 1. Format code\nruff format picard/\n\n# 2. Check for issues\nruff check picard/\n\n# 3. Run tests\npytest test/\n\n# Or specific tests\npytest test/test_metadata.py\n```\n\n**Important:** Always run `ruff format` after making code changes to ensure consistent formatting.\n\n### AI Assistant Guidelines\nWhen making code changes:\n1. **Imports go at the top of files** - local (inline) imports are acceptable only to break circular dependencies; in that case, place them as close to usage as possible with a comment explaining why\n2. **Bug fixes: test first** - write a test that reproduces the bug before fixing it, then verify the fix makes the test pass; keep the test as a permanent regression test when feasible\n3. **Run `ruff format` after all changes** - ensures code follows project style guidelines\n4. **Run `ruff check` to catch issues** - fix any linting errors before committing\n5. **Never commit editor or agent-specific files** - Do not `git add` files from AI tool directories (`.kiro/`, `.cursor/`, `.aider*`, etc.) or any files not part of the project. Review `git diff --cached` before committing to verify only intended files are staged. When in doubt, ask the user before committing.\n6. **Don't push to remote** - without explicit user approval\n7. **Never use `git commit --no-verify`** - pre-commit hooks run `ruff format` and `ruff check` automatically; skipping them leads to CI failures. If using fixup/rebase workflows, always run `ruff format` and `ruff check` manually before the final push.\n8. **Run `ty check` on modified files** - verify no new type errors are introduced (pre-existing errors in the codebase can be ignored).\n9. **`gh pr edit` may fail with a GraphQL Projects deprecation error** - use `gh api repos/OWNER/REPO/pulls/NUMBER -X PATCH -F \"body=@file.md\"` as a workaround to update PR descriptions.\n\n### Contributing\n1. **Create ticket first:** <https://tickets.metabrainz.org/projects/PICARD>\n2. Create feature branch\n3. Make changes\n4. Run pre-commit checks (above)\n5. **PR title must start with:** `PICARD-XXXX:` (ticket number)\n6. **Use the PR template** - fill in `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Problem, Solution, AI Usage, Action sections)\n7. **Don't auto-create tickets/PRs** - let humans handle this\n8. **Don't push to remote** - without explicit user approval\n\n---\n\n## Code Style\n\n### Conventions\n- **Standard:** PEP 8, 120 char lines (flexible)\n- **Linter:** Ruff\n- **Type hints:** Recommended for new code (existing code has limited coverage)\n- **Naming:** `PascalCase` classes, `snake_case` functions, `UPPER_SNAKE` constants\n\n### Membership Tests and Control Flow\n```python\n# Use a set literal (not a tuple/list) for membership tests against a fixed\n# set of constants. Python optimizes `in {...}`. See the ruff \"literal-membership\" rule.\n\n# ❌ Tuple/list literal for membership test\nif state in (State.CHANGED, State.NORMAL):\n    ...\nassert suffix in ('.pyc', '.pyo')\n\n# ✅ Set literal\nif state in {State.CHANGED, State.NORMAL}:\n    ...\nassert suffix in {'.pyc', '.pyo'}\n\n\n# Prefer a single membership test over chained equality checks.\n\n# ❌ Multiple equality checks\nif name == 'performer' or name == 'comment':\n    ...\n\n# ✅ Single membership test\nif name in {'performer', 'comment'}:\n    ...\n\n\n# Collapse a nested `else:` + `if` into a single `elif` to reduce indentation.\n# See the ruff \"collapsible-else-if\" rule.\n\n# ❌ Nested if inside else\nif a:\n    do_x()\nelse:\n    if b:\n        do_y()\n    else:\n        do_z()\n\n# ✅ elif\nif a:\n    do_x()\nelif b:\n    do_y()\nelse:\n    do_z()\n```\n\nThe preview ruff rules can be checked with:\n```bash\nuv run ruff check --preview --select literal-membership,collapsible-else-if\n```\n\n### Internationalization (i18n)\n```python\n# Import translation functions\nfrom picard.i18n import gettext as _\nfrom picard.i18n import N_\n\n# Translatable strings (runtime)\nmessage = _(\"Save file\")\nerror = _(\"Failed to load: %s\") % filename\n\n# Mark for extraction only (constants, class attributes)\nTITLE = N_(\"Options\")  # Translated at usage: _(TITLE)\n\n# Plugin translations (Plugin v3)\nfrom picard.plugin3.api import t_\n\n\nclass ManifestTranslations:\n    NAME = t_(\"manifest.name\", \"My Plugin\")\n    DESC = t_(\"manifest.description\", \"Plugin description\")\n```\n\n**Translation workflow:**\n- Strings marked with `_()` or `N_()` are extracted to `po/picard.pot`\n- Regenerate: `python setup.py regen_pot_file`\n- Translations managed via [Weblate](https://translations.metabrainz.org/projects/picard/)\n- See `po/README.md` for details\n\n#### Gotcha: `picard/const/` uses a separate gettext domain\n\nPicard has **four** gettext domains (see `picard/i18n/gettext.py`), not one.\nStrings are extracted per source location, and each domain must be looked up\nwith its **own** translation function at runtime:\n\n| Where the string is defined | `N_()`-marked into | Translate at runtime with |\n|---|---|---|\n| Anywhere **except** `picard/const/` | `po/picard.pot` (`main`) | `gettext()` / `_()` |\n| `picard/const/` (e.g. `tags.py`, `scripts.py`, `cover_processing.py`, `languages.py`, `locales.py`, `defaults.py`) | `po/constants/` (`constants`) | `gettext_constants()` |\n| `picard/const/countries.py` (`RELEASE_COUNTRIES`) | `po/countries/` (`countries`) | `gettext_countries()` |\n| `picard/const/attributes.py` (`MB_ATTRIBUTES`) | `po/attributes/` (`attributes`) | `pgettext_attributes()` / `gettext_attributes()` |\n\n`setup.py` deliberately excludes `const` from the main pot and extracts it\ninto `po/constants/constants.pot`. So a `const/`-sourced string translated\nwith the plain `_()` is looked up in the **wrong** domain, silently misses,\nand renders in English even though a translation exists.\n\n```python\nfrom picard.i18n import gettext as _\nfrom picard.i18n import gettext_constants\n\n# TagVar.longdesc / shortdesc, SCRIPTS[...], CoverResizeMode.title,\n# DEFAULT_SCRIPT_NAME, ... all originate in picard/const/\n\n# ❌ Wrong domain — always English, even in fr/de/... UI\ntitle = _(tagvar.shortdesc)\nname = _(DEFAULT_SCRIPT_NAME)  # or storing the raw N_() string\n\n# ✅ Constants domain\ntitle = gettext_constants(tagvar.shortdesc)\nname = gettext_constants(DEFAULT_SCRIPT_NAME)\n```\n\nRule of thumb: **if the string's `N_()` lives in `picard/const/`, translate it\nwith `gettext_constants()`** (or the countries/attributes helper). This bug is\neasy to reintroduce — it has recurred across PRs #3453 and #3457 (PICARD-3448).\nWhen touching translated `const/` strings, verify the msgid resolves against\n`po/constants/*.po` (or `po/countries/`, `po/attributes/`).\n\n### Qt UI Files\n```bash\n# ❌ Don't edit picard/ui/ui_*.py directly (auto-generated)\n\n# ✅ Edit .ui files with Qt Designer\nqt6-tools designer ui/options_metadata.ui\n\n# ✅ Regenerate Python files\npython setup.py build_ui\n```\n\n### Qt Signals\n```python\nfrom PyQt6.QtCore import pyqtSignal\n\n\nclass MyWidget(QWidget):\n    # Define signals\n    value_changed = pyqtSignal(str)\n    item_selected = pyqtSignal(object)\n\n    def some_action(self):\n        # Emit signals\n        self.value_changed.emit(\"new value\")\n```\n\nUse the `QSignalBlocker` context manager to suppress signals temporarily,\nrather than manual `blockSignals(True)`/`blockSignals(False)` calls. It is\nexception-safe (signals are always unblocked, even if an exception is raised)\nand more concise.\n\n```python\nfrom PyQt6.QtCore import QSignalBlocker\n\n\n# ❌ Manual, not exception-safe\nwidget.blockSignals(True)\nwidget.setChecked(True)\nwidget.blockSignals(False)\n\n# ✅ Context manager\nwith QSignalBlocker(widget):\n    widget.setChecked(True)\n```\n\n### Readability\n- Use descriptive names\n- Comment complex logic\n- Break long functions\n- Follow existing style in the file\n\n---\n\n## Key Locations\n\n### Core Components\n- **Main app:** `picard/tagger.py`\n- **File handling:** `picard/file.py`\n- **Album/Track:** `picard/album.py`, `picard/track.py`\n- **Metadata:** `picard/metadata.py`\n\n### Format Handlers\n- **Registry:** `picard/formats/registry.py`\n- **ID3 (MP3):** `picard/formats/id3.py`\n- **Vorbis (FLAC/Ogg):** `picard/formats/vorbis.py`\n- **MP4:** `picard/formats/mp4.py`\n\n### Plugin System\n- **API:** `picard/plugin3/api_impl.py`\n- **Manager:** `picard/plugin3/manager/__init__.py`\n- **Registry:** `picard/plugin3/registry.py`\n\n### UI\n- **Main window:** `picard/ui/mainwindow/`\n- **Options:** `picard/ui/options/`\n- **Metadata box:** `picard/ui/metadatabox/`\n- **Script editor:** `picard/ui/scripteditor/`\n\n### Scripting\n- **Parser:** `picard/script/parser.py`\n- **Functions:** `picard/script/functions.py`\n\n### Web Services\n- **HTTP client:** `picard/webservice/__init__.py`\n- **APIs:** `picard/webservice/api_helpers.py`\n\n---\n\n## Common Tasks\n\n### Add Audio Format\n1. Create handler in `picard/formats/`\n2. Inherit from `File`\n3. Implement `_load()` and `_save()`\n4. Define `EXTENSIONS` and `NAME`\n5. Call `register_format(MyFormat)`\n\n### Add Script Function\n```python\nfrom picard.script import script_function\n\n\n@script_function\ndef func_myfunction(parser, arg1, arg2='default'):\n    \"\"\"$myfunction(arg1,arg2) - Description\"\"\"\n    return result\n```\n\n### Add Option\n1. Define in `picard/options.py`\n2. Create UI page in `picard/ui/options/`\n3. Implement `load()` and `save()` methods\n4. Register with `register_options_page()`\n\n### Add Metadata Processor (Core)\n```python\nfrom picard.extension_points.metadata import register_track_metadata_processor\n\n\ndef my_processor(album, metadata, track, release):\n    metadata['custom'] = 'value'\n\n\nregister_track_metadata_processor(my_processor)\n```\n\n**For plugins:** Use `api.register_track_metadata_processor()` instead (see Plugin System section).\n\n---\n\n## Testing\n\nHow to run the suite and how to write tests — the two coexisting test styles, the\n`subtest_cases` decorator for table-driven tests, and the shared `qapp` fixture for\nQt widgets — is documented in [test/README.md](test/README.md). That document is the\nsource of truth; follow it rather than relying on a copy here.\n\nPoints worth repeating because they are easy to get wrong:\n\n- Match the test style of the file you are editing. Do not convert a\n  `PicardTestCase` class to plain pytest (or the reverse) as a side effect of an\n  unrelated change.\n- `@pytest.mark.parametrize` does not work on `PicardTestCase` subclasses; the test\n  errors with a missing-argument `TypeError`. Use the `subtest_cases` decorator (or\n  `self.subTest()`) there.\n- Never create a local `QApplication`/`QCoreApplication`; it crashes under\n  `pytest-randomly` ordering.\n\n---\n\n## Debugging\n\n```bash\n# Enable debug logging\npicard --debug\n\n# Enable specific debug options (comma-separated)\npicard --debug-opts=option1,option2\n```\n\n```python\n# In code\nfrom picard import log\n\nlog.debug('Debug: %s', value)\nlog.error('Error', exc_info=True)\n```\n\n---\n\n## Resources\n\n- **User Docs:** <https://picard-docs.musicbrainz.org/>\n- **Website:** <https://picard.musicbrainz.org/>\n- **GitHub:** <https://github.com/metabrainz/picard>\n- **Forum:** <https://community.metabrainz.org/c/picard>\n- **Contributing:** See `CONTRIBUTING.md`\n- **Installation:** See `INSTALL.md`\n\n---\n\n**Note:** This file contains only essential patterns. For architecture, dependencies, and component details, explore the codebase directly or read existing documentation (`README.md`, `CONTRIBUTING.md`, `INSTALL.md`, user docs).\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/metabrainz/picard/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AI Assistant Context for MusicBrainz Picard\n\n> **Purpose:** Essential patterns, conventions, and gotchas for AI assistants.\n> For architecture, dependencies, and component details, explore the codebase\n> or read existing docs (`README.md`, `CONTRIBUTING.md`, `INSTALL.md`).\n\n**What is Picard?** Cross-platform audio tagger for MusicBrainz. Tags music files using the MusicBrainz database, supports fingerprinting (AcoustID), and has a powerful plugin system.\n\n---\n\n## Quick Facts\n\n- **Language:** Python 3.10+, PyQt6, Mutagen\n- **Size:** Event-driven MVC with plugin system\n- **Entry Point:** `picard/tagger.py:main()` → `Tagger` singleton\n- **Tests:** `test/` directory (pytest)\n- **Docs:** <https://picard-docs.musicbrainz.org/>\n- **Tickets:** <https://tickets.metabrainz.org/projects/PICARD>\n- **Chat:** [Matrix #musicbrainz-picard-dev](https://matrix.to/#/#musicbrainz-picard-dev:chatbrainz.org)\n\n---\n\n## Critical Patterns & Gotchas\n\n### Threading Rules\n```python\n# ❌ NEVER access UI from background threads\ndef background_task():\n    self.label.setText(\"Done\")  # CRASH!\n\n\n# ✅ Use signals or run_task callback\nfrom picard.util.thread import run_task\n\n\ndef callback(result):\n    self.label.setText(result)  # Safe - runs in main thread\n\n\nrun_task(heavy_operation, callback=callback)\n```\n\n### Metadata Changes\n```python\n# ❌ Silent changes won't update UI\nmetadata['artist'] = 'New Artist'\n\n# ✅ Always emit signals\nmetadata['artist'] = 'New Artist'\nfile.metadata_updated.emit()\n```\n\n### Metadata Access\n```python\n# Metadata is always stored internally as a list of strings.\n\n# ❌ Don't use get() + split on MULTI_VALUED_JOINER to get individual values\nvalues = metadata.get('artist', '').split(MULTI_VALUED_JOINER)\n\n# ✅ Use getall() to get individual values as a list\nvalues = metadata.getall('artist')  # Returns: ['Artist 1', 'Artist 2']\n\n# ✅ Use get() or [] only for display (returns joined string)\ndisplay = metadata['artist']  # Returns: 'Artist 1; Artist 2'\n\n# ✅ Set multiple values by passing a list (don't join them)\nmetadata['artist'] = ['Artist 1', 'Artist 2']\n```\n\n### Configuration Access\n```python\n# ✅ Always use config.setting\nfrom picard import config\n\nvalue = config.setting['option_name']\n\n# ❌ Don't access internal structures directly\n```\n\n### Async Operations\n```python\n# ✅ Use thread pool for I/O\nfrom picard.util.thread import run_task\n\nrun_task(process_file, file, callback)\n\n# ❌ Don't block the main thread\nresult = slow_network_call()  # UI freezes!\n```\n\n### Import Rules\n```python\n# ❌ Don't use inline imports (unless breaking circular dependencies)\ndef my_function():\n    from picard.some_module import something\n\n    return something()\n\n\n# ❌ Don't put multiple imports on one line\nfrom picard.plugin3.validator import generate_uuid, validate_manifest_dict\n\n# ✅ Imports go at the top of the file, each imported name on its own line with trailing commas\nfrom picard.plugin3.validator import (\n    generate_uuid,\n    validate_manifest_dict,\n)\n```\n\n**Exception:** Inline imports are acceptable only to break circular dependencies. Place them as close to usage as possible with a comment explaining why.\n\n---\n\n## Plugin System (v3)\n\n### Structure\n```text\nmy_plugin/\n├── __init__.py       # Plugin code\n├── MANIFEST.toml     # Metadata (UPPERCASE!)\n└── ui_options.py     # Optional settings\n```\n\n### MANIFEST.toml\n```toml\nuuid = \"unique-uuid\"\nname = \"Plugin Name\"\nauthors = [\"Your Name\"]\ndescription = \"Description\"\napi = [\"3.0\"]\ncategories = [\"metadata\"]\nlicense = \"GPL-2.0-or-later\"\nlicense_url = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n```\n\n### Plugin Code\n```python\nfrom picard.plugin3.api import PluginApi\n\n\ndef enable(api: PluginApi):\n    \"\"\"Called when plugin is enabled.\"\"\"\n    api.plugin_config.register_option(\"my_option\", \"default\")\n    api.register_track_metadata_processor(process_metadata)\n\n\ndef process_metadata(api, album, metadata, track, release):\n    metadata['custom'] = api.plugin_config['my_option']\n```\n\n### Plugin Management CLI\n```bash\n# List installed plugins\npicard-cli plugins list\n\n# Install plugin from registry\npicard-cli plugins install plugin-name\n\n# Install from URL or local path\npicard-cli plugins install https://github.com/user/plugin.git\npicard-cli plugins install /path/to/plugin\n\n# Update plugins\npicard-cli plugins update plugin-name\n\n# Remove plugin\npicard-cli plugins remove plugin-name\n\n# Search registry\npicard-cli plugins search keyword\n```\n\n**Key Files:**\n- API: `picard/plugin3/api_impl.py`\n- Manager: `picard/plugin3/manager/__init__.py`\n- CLI: `picard/plugin3/cli.py`\n- Example: <https://github.com/rdswift/picard-plugin-format-performer-tags>\n\n**Documentation:**\n- Plugin v3 docs: `docs/PLUGINSV3/` (API, CLI, manifest, translations, etc.)\n- Migration guide: `docs/Plugin2to3MigrationGuide.md`\n- Plugin registry: <https://github.com/metabrainz/picard-plugins-registry>\n\n---\n\n## Development Workflow\n\n### Setup\n```bash\ngit clone https://github.com/metabrainz/picard.git\ncd picard\n\n# Preferred\nuv sync\n\n# Alternative\npip install -r requirements.txt\n\npython setup.py build_ui  # Compile Qt forms\npython tagger.py          # Run from source\n```\n\n### Before Committing\n```bash\n# 1. Format code\nruff format picard/\n\n# 2. Check for issues\nruff check picard/\n\n# 3. Run tests\npytest test/\n\n# Or specific tests\npytest test/test_metadata.py\n```\n\n**Important:** Always run `ruff format` after making code changes to ensure consistent formatting.\n\n### AI Assistant Guidelines\nWhen making code changes:\n1. **Imports go at the top of files** - local (inline) imports are acceptable only to break circular dependencies; in that case, place them as close to usage as possible with a comment explaining why\n2. **Bug fixes: test first** - write a test that reproduces the bug before fixing it, then verify the fix makes the test pass; keep the test as a permanent regression test when feasible\n3. **Run `ruff format` after all changes** - ensures code follows project style guidelines\n4. **Run `ruff check` to catch issues** - fix any linting errors before committing\n5. **Never commit editor or agent-specific files** - Do not `git add` files from AI tool directories (`.kiro/`, `.cursor/`, `.aider*`, etc.) or any files not part of the project. Review `git diff --cached` before committing to verify only intended files are staged. When in doubt, ask the user before committing.\n6. **Don't push to remote** - without explicit user approval\n7. **Never use `git commit --no-verify`** - pre-commit hooks run `ruff format` and `ruff check` automatically; skipping them leads to CI failures. If using fixup/rebase workflows, always run `ruff format` and `ruff check` manually before the final push.\n8. **Run `ty check` on modified files** - verify no new type errors are introduced (pre-existing errors in the codebase can be ignored).\n9. **`gh pr edit` may fail with a GraphQL Projects deprecation error** - use `gh api repos/OWNER/REPO/pulls/NUMBER -X PATCH -F \"body=@file.md\"` as a workaround to update PR descriptions.\n\n### Contributing\n1. **Create ticket first:** <https://tickets.metabrainz.org/projects/PICARD>\n2. Create feature branch\n3. Make changes\n4. Run pre-commit checks (above)\n5. **PR title must start with:** `PICARD-XXXX:` (ticket number)\n6. **Use the PR template** - fill in `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Problem, Solution, AI Usage, Action sections)\n7. **Don't auto-create tickets/PRs** - let humans handle this\n8. **Don't push to remote** - without explicit user approval\n\n---\n\n## Code Style\n\n### Conventions\n- **Standard:** PEP 8, 120 char lines (flexible)\n- **Linter:** Ruff\n- **Type hints:** Recommended for new code (existing code has limited coverage)\n- **Naming:** `PascalCase` classes, `snake_case` functions, `UPPER_SNAKE` constants\n\n### Membership Tests and Control Flow\n```python\n# Use a set literal (not a tuple/list) for membership tests against a fixed\n# set of constants. Python optimizes `in {...}`. See the ruff \"literal-membership\" rule.\n\n# ❌ Tuple/list literal for membership test\nif state in (State.CHANGED, State.NORMAL):\n    ...\nassert suffix in ('.pyc', '.pyo')\n\n# ✅ Set literal\nif state in {State.CHANGED, State.NORMAL}:\n    ...\nassert suffix in {'.pyc', '.pyo'}\n\n\n# Prefer a single membership test over chained equality checks.\n\n# ❌ Multiple equality checks\nif name == 'performer' or name == 'comment':\n    ...\n\n# ✅ Single membership test\nif name in {'performer', 'comment'}:\n    ...\n\n\n# Collapse a nested `else:` + `if` into a single `elif` to reduce indentation.\n# See the ruff \"collapsible-else-if\" rule.\n\n# ❌ Nested if inside else\nif a:\n    do_x()\nelse:\n    if b:\n        do_y()\n    else:\n        do_z()\n\n# ✅ elif\nif a:\n    do_x()\nelif b:\n    do_y()\nelse:\n    do_z()\n```\n\nThe preview ruff rules can be checked with:\n```bash\nuv run ruff check --preview --select literal-membership,collapsible-else-if\n```\n\n### Internationalization (i18n)\n```python\n# Import translation functions\nfrom picard.i18n import gettext as _\nfrom picard.i18n import N_\n\n# Translatable strings (runtime)\nmessage = _(\"Save file\")\nerror = _(\"Failed to load: %s\") % filename\n\n# Mark for extraction only (constants, class attributes)\nTITLE = N_(\"Options\")  # Translated at usage: _(TITLE)\n\n# Plugin translations (Plugin v3)\nfrom picard.plugin3.api import t_\n\n\nclass ManifestTranslations:\n    NAME = t_(\"manifest.name\", \"My Plugin\")\n    DESC = t_(\"manifest.description\", \"Plugin description\")\n```\n\n**Translation workflow:**\n- Strings marked with `_()` or `N_()` are extracted to `po/picard.pot`\n- Regenerate: `python setup.py regen_pot_file`\n- Translations managed via [Weblate](https://translations.metabrainz.org/projects/picard/)\n- See `po/README.md` for details\n\n#### Gotcha: `picard/const/` uses a separate gettext domain\n\nPicard has **four** gettext domains (see `picard/i18n/gettext.py`), not one.\nStrings are extracted per source location, and each domain must be looked up\nwith its **own** translation function at runtime:\n\n| Where the string is defined | `N_()`-marked into | Translate at runtime with |\n|---|---|---|\n| Anywhere **except** `picard/const/` | `po/picard.pot` (`main`) | `gettext()` / `_()` |\n| `picard/const/` (e.g. `tags.py`, `scripts.py`, `cover_processing.py`, `languages.py`, `locales.py`, `defaults.py`) | `po/constants/` (`constants`) | `gettext_constants()` |\n| `picard/const/countries.py` (`RELEASE_COUNTRIES`) | `po/countries/` (`countries`) | `gettext_countries()` |\n| `picard/const/attributes.py` (`MB_ATTRIBUTES`) | `po/attributes/` (`attributes`) | `pgettext_attributes()` / `gettext_attributes()` |\n\n`setup.py` deliberately excludes `const` from the main pot and extracts it\ninto `po/constants/constants.pot`. So a `const/`-sourced string translated\nwith the plain `_()` is looked up in the **wrong** domain, silently misses,\nand renders in English even though a translation exists.\n\n```python\nfrom picard.i18n import gettext as _\nfrom picard.i18n import gettext_constants\n\n# TagVar.longdesc / shortdesc, SCRIPTS[...], CoverResizeMode.title,\n# DEFAULT_SCRIPT_NAME, ... all originate in picard/const/\n\n# ❌ Wrong domain — always English, even in fr/de/... UI\ntitle = _(tagvar.shortdesc)\nname = _(DEFAULT_SCRIPT_NAME)  # or storing the raw N_() string\n\n# ✅ Constants domain\ntitle = gettext_constants(tagvar.shortdesc)\nname = gettext_constants(DEFAULT_SCRIPT_NAME)\n```\n\nRule of thumb: **if the string's `N_()` lives in `picard/const/`, translate it\nwith `gettext_constants()`** (or the countries/attributes helper). This bug is\neasy to reintroduce — it has recurred across PRs #3453 and #3457 (PICARD-3448).\nWhen touching translated `const/` strings, verify the msgid resolves against\n`po/constants/*.po` (or `po/countries/`, `po/attributes/`).\n\n### Qt UI Files\n```bash\n# ❌ Don't edit picard/ui/ui_*.py directly (auto-generated)\n\n# ✅ Edit .ui files with Qt Designer\nqt6-tools designer ui/options_metadata.ui\n\n# ✅ Regenerate Python files\npython setup.py build_ui\n```\n\n### Qt Signals\n```python\nfrom PyQt6.QtCore import pyqtSignal\n\n\nclass MyWidget(QWidget):\n    # Define signals\n    value_changed = pyqtSignal(str)\n    item_selected = pyqtSignal(object)\n\n    def some_action(self):\n        # Emit signals\n        self.value_changed.emit(\"new value\")\n```\n\nUse the `QSignalBlocker` context manager to suppress signals temporarily,\nrather than manual `blockSignals(True)`/`blockSignals(False)` calls. It is\nexception-safe (signals are always unblocked, even if an exception is raised)\nand more concise.\n\n```python\nfrom PyQt6.QtCore import QSignalBlocker\n\n\n# ❌ Manual, not exception-safe\nwidget.blockSignals(True)\nwidget.setChecked(True)\nwidget.blockSignals(False)\n\n# ✅ Context manager\nwith QSignalBlocker(widget):\n    widget.setChecked(True)\n```\n\n### Readability\n- Use descriptive names\n- Comment complex logic\n- Break long functions\n- Follow existing style in the file\n\n---\n\n## Key Locations\n\n### Core Components\n- **Main app:** `picard/tagger.py`\n- **File handling:** `picard/file.py`\n- **Album/Track:** `picard/album.py`, `picard/track.py`\n- **Metadata:** `picard/metadata.py`\n\n### Format Handlers\n- **Registry:** `picard/formats/registry.py`\n- **ID3 (MP3):** `picard/formats/id3.py`\n- **Vorbis (FLAC/Ogg):** `picard/formats/vorbis.py`\n- **MP4:** `picard/formats/mp4.py`\n\n### Plugin System\n- **API:** `picard/plugin3/api_impl.py`\n- **Manager:** `picard/plugin3/manager/__init__.py`\n- **Registry:** `picard/plugin3/registry.py`\n\n### UI\n- **Main window:** `picard/ui/mainwindow/`\n- **Options:** `picard/ui/options/`\n- **Metadata box:** `picard/ui/metadatabox/`\n- **Script editor:** `picard/ui/scripteditor/`\n\n### Scripting\n- **Parser:** `picard/script/parser.py`\n- **Functions:** `picard/script/functions.py`\n\n### Web Services\n- **HTTP client:** `picard/webservice/__init__.py`\n- **APIs:** `picard/webservice/api_helpers.py`\n\n---\n\n## Common Tasks\n\n### Add Audio Format\n1. Create handler in `picard/formats/`\n2. Inherit from `File`\n3. Implement `_load()` and `_save()`\n4. Define `EXTENSIONS` and `NAME`\n5. Call `register_format(MyFormat)`\n\n### Add Script Function\n```python\nfrom picard.script import script_function\n\n\n@script_function\ndef func_myfunction(parser, arg1, arg2='default'):\n    \"\"\"$myfunction(arg1,arg2) - Description\"\"\"\n    return result\n```\n\n### Add Option\n1. Define in `picard/options.py`\n2. Create UI page in `picard/ui/options/`\n3. Implement `load()` and `save()` methods\n4. Register with `register_options_page()`\n\n### Add Metadata Processor (Core)\n```python\nfrom picard.extension_points.metadata import register_track_metadata_processor\n\n\ndef my_processor(album, metadata, track, release):\n    metadata['custom'] = 'value'\n\n\nregister_track_metadata_processor(my_processor)\n```\n\n**For plugins:** Use `api.register_track_metadata_processor()` instead (see Plugin System section).\n\n---\n\n## Testing\n\nHow to run the suite and how to write tests — the two coexisting test styles, the\n`subtest_cases` decorator for table-driven tests, and the shared `qapp` fixture for\nQt widgets — is documented in [test/README.md](test/README.md). That document is the\nsource of truth; follow it rather than relying on a copy here.\n\nPoints worth repeating because they are easy to get wrong:\n\n- Match the test style of the file you are editing. Do not convert a\n  `PicardTestCase` class to plain pytest (or the reverse) as a side effect of an\n  unrelated change.\n- `@pytest.mark.parametrize` does not work on `PicardTestCase` subclasses; the test\n  errors with a missing-argument `TypeError`. Use the `subtest_cases` decorator (or\n  `self.subTest()`) there.\n- Never create a local `QApplication`/`QCoreApplication`; it crashes under\n  `pytest-randomly` ordering.\n\n---\n\n## Debugging\n\n```bash\n# Enable debug logging\npicard --debug\n\n# Enable specific debug options (comma-separated)\npicard --debug-opts=option1,option2\n```\n\n```python\n# In code\nfrom picard import log\n\nlog.debug('Debug: %s', value)\nlog.error('Error', exc_info=True)\n```\n\n---\n\n## Resources\n\n- **User Docs:** <https://picard-docs.musicbrainz.org/>\n- **Website:** <https://picard.musicbrainz.org/>\n- **GitHub:** <https://github.com/metabrainz/picard>\n- **Forum:** <https://community.metabrainz.org/c/picard>\n- **Contributing:** See `CONTRIBUTING.md`\n- **Installation:** See `INSTALL.md`\n\n---\n\n**Note:** This file contains only essential patterns. For architecture, dependencies, and component details, explore the codebase directly or read existing documentation (`README.md`, `CONTRIBUTING.md`, `INSTALL.md`, user docs).\n","isInternal":false,"tokens":4221,"sizeBytes":16480},{"name":"README.md","path":"scripts/tools/README.md","rawUrl":"https://raw.githubusercontent.com/metabrainz/picard/HEAD/scripts/tools/README.md","title":"tools Documentation","category":"plugin-manifest","format":"markdown","content":"# scripts/tools/\n\nDeveloper utility scripts for MusicBrainz Picard. These are not shipped with\nreleases — they assist with development, releases, and asset generation.\n\n## Scripts\n\n| Script | Description |\n|--------|-------------|\n| `authors-between-releases.py` | List code contributors and translators between two git tags, for release notes. |\n| `changelog-for-version.py` | Extract changelog entries for a given version from git history. |\n| `check_settings.py` | Check for references to undefined option settings in the codebase. |\n| `detect_qt_shadowing.py` | Detect class attributes that shadow inherited Qt methods. |\n| `fix-header.py` | Regenerate source file copyright/license headers from git log. |\n| `generate_icons.sh` | Generate PNG icons (multiple sizes) from an SVG source file. |\n| `generate_match_icons.py` | Generate match quality bookmark PNGs (normal + pending, 1× + 2×). |\n| `multiple_imports_on_one_line.py` | Find import statements with multiple names on one line. |\n| `patch-version.sh` | Patch the version string with git commit count/hash for dev builds. |\n| `pull-shared-translations.py` | Pull shared translation strings from Weblate/upstream. |\n| `tag-release.sh` | Create a git tag for the current release version. |\n","isInternal":false,"tokens":283,"sizeBytes":1253}],"systemPromptSnippet":"<yakaai_skills repo=\"metabrainz/picard\">\n<!-- File: AGENTS.md (Tokens: ~4221 | Category: root-instruction) -->\n# AI Assistant Context for MusicBrainz Picard\n\n> **Purpose:** Essential patterns, conventions, and gotchas for AI assistants.\n> For architecture, dependencies, and component details, explore the codebase\n> or read existing docs (`README.md`, `CONTRIBUTING.md`, `INSTALL.md`).\n\n**What is Picard?** Cross-platform audio tagger for MusicBrainz. Tags music files using the MusicBrainz database, supports fingerprinting (AcoustID), and has a powerful plugin system.\n\n---\n\n## Quick Facts\n\n- **Language:** Python 3.10+, PyQt6, Mutagen\n- **Size:** Event-driven MVC with plugin system\n- **Entry Point:** `picard/tagger.py:main()` → `Tagger` singleton\n- **Tests:** `test/` directory (pytest)\n- **Docs:** <https://picard-docs.musicbrainz.org/>\n- **Tickets:** <https://tickets.metabrainz.org/projects/PICARD>\n- **Chat:** [Matrix #musicbrainz-picard-dev](https://matrix.to/#/#musicbrainz-picard-dev:chatbrainz.org)\n\n---\n\n## Critical Patterns & Gotchas\n\n### Threading Rules\n```python\n# ❌ NEVER access UI from background threads\ndef background_task():\n    self.label.setText(\"Done\")  # CRASH!\n\n\n# ✅ Use signals or run_task callback\nfrom picard.util.thread import run_task\n\n\ndef callback(result):\n    self.label.setText(result)  # Safe - runs in main thread\n\n\nrun_task(heavy_operation, callback=callback)\n```\n\n### Metadata Changes\n```python\n# ❌ Silent changes won't update UI\nmetadata['artist'] = 'New Artist'\n\n# ✅ Always emit signals\nmetadata['artist'] = 'New Artist'\nfile.metadata_updated.emit()\n```\n\n### Metadata Access\n```python\n# Metadata is always stored internally as a list of strings.\n\n# ❌ Don't use get() + split on MULTI_VALUED_JOINER to get individual values\nvalues = metadata.get('artist', '').split(MULTI_VALUED_JOINER)\n\n# ✅ Use getall() to get individual values as a list\nvalues = metadata.getall('artist')  # Returns: ['Artist 1', 'Artist 2']\n\n# ✅ Use get() or [] only for display (returns joined string)\ndisplay = metadata['artist']  # Returns: 'Artist 1; Artist 2'\n\n# ✅ Set multiple values by passing a list (don't join them)\nmetadata['artist'] = ['Artist 1', 'Artist 2']\n```\n\n### Configuration Access\n```python\n# ✅ Always use config.setting\nfrom picard import config\n\nvalue = config.setting['option_name']\n\n# ❌ Don't access internal structures directly\n```\n\n### Async Operations\n```python\n# ✅ Use thread pool for I/O\nfrom picard.util.thread import run_task\n\nrun_task(process_file, file, callback)\n\n# ❌ Don't block the main thread\nresult = slow_network_call()  # UI freezes!\n```\n\n### Import Rules\n```python\n# ❌ Don't use inline imports (unless breaking circular dependencies)\ndef my_function():\n    from picard.some_module import something\n\n    return something()\n\n\n# ❌ Don't put multiple imports on one line\nfrom picard.plugin3.validator import generate_uuid, validate_manifest_dict\n\n# ✅ Imports go at the top of the file, each imported name on its own line with trailing commas\nfrom picard.plugin3.validator import (\n    generate_uuid,\n    validate_manifest_dict,\n)\n```\n\n**Exception:** Inline imports are acceptable only to break circular dependencies. Place them as close to usage as possible with a comment explaining why.\n\n---\n\n## Plugin System (v3)\n\n### Structure\n```text\nmy_plugin/\n├── __init__.py       # Plugin code\n├── MANIFEST.toml     # Metadata (UPPERCASE!)\n└── ui_options.py     # Optional settings\n```\n\n### MANIFEST.toml\n```toml\nuuid = \"unique-uuid\"\nname = \"Plugin Name\"\nauthors = [\"Your Name\"]\ndescription = \"Description\"\napi = [\"3.0\"]\ncategories = [\"metadata\"]\nlicense = \"GPL-2.0-or-later\"\nlicense_url = \"https://www.gnu.org/licenses/gpl-2.0.html\"\n```\n\n### Plugin Code\n```python\nfrom picard.plugin3.api import PluginApi\n\n\ndef enable(api: PluginApi):\n    \"\"\"Called when plugin is enabled.\"\"\"\n    api.plugin_config.register_option(\"my_option\", \"default\")\n    api.register_track_metadata_processor(process_metadata)\n\n\ndef process_metadata(api, album, metadata, track, release):\n    metadata['custom'] = api.plugin_config['my_option']\n```\n\n### Plugin Management CLI\n```bash\n# List installed plugins\npicard-cli plugins list\n\n# Install plugin from registry\npicard-cli plugins install plugin-name\n\n# Install from URL or local path\npicard-cli plugins install https://github.com/user/plugin.git\npicard-cli plugins install /path/to/plugin\n\n# Update plugins\npicard-cli plugins update plugin-name\n\n# Remove plugin\npicard-cli plugins remove plugin-name\n\n# Search registry\npicard-cli plugins search keyword\n```\n\n**Key Files:**\n- API: `picard/plugin3/api_impl.py`\n- Manager: `picard/plugin3/manager/__init__.py`\n- CLI: `picard/plugin3/cli.py`\n- Example: <https://github.com/rdswift/picard-plugin-format-performer-tags>\n\n**Documentation:**\n- Plugin v3 docs: `docs/PLUGINSV3/` (API, CLI, manifest, translations, etc.)\n- Migration guide: `docs/Plugin2to3MigrationGuide.md`\n- Plugin registry: <https://github.com/metabrainz/picard-plugins-registry>\n\n---\n\n## Development Workflow\n\n### Setup\n```bash\ngit clone https://github.com/metabrainz/picard.git\ncd picard\n\n# Preferred\nuv sync\n\n# Alternative\npip install -r requirements.txt\n\npython setup.py build_ui  # Compile Qt forms\npython tagger.py          # Run from source\n```\n\n### Before Committing\n```bash\n# 1. Format code\nruff format picard/\n\n# 2. Check for issues\nruff check picard/\n\n# 3. Run tests\npytest test/\n\n# Or specific tests\npytest test/test_metadata.py\n```\n\n**Important:** Always run `ruff format` after making code changes to ensure consistent formatting.\n\n### AI Assistant Guidelines\nWhen making code changes:\n1. **Imports go at the top of files** - local (inline) imports are acceptable only to break circular dependencies; in that case, place them as close to usage as possible with a comment explaining why\n2. **Bug fixes: test first** - write a test that reproduces the bug before fixing it, then verify the fix makes the test pass; keep the test as a permanent regression test when feasible\n3. **Run `ruff format` after all changes** - ensures code follows project style guidelines\n4. **Run `ruff check` to catch issues** - fix any linting errors before committing\n5. **Never commit editor or agent-specific files** - Do not `git add` files from AI tool directories (`.kiro/`, `.cursor/`, `.aider*`, etc.) or any files not part of the project. Review `git diff --cached` before committing to verify only intended files are staged. When in doubt, ask the user before committing.\n6. **Don't push to remote** - without explicit user approval\n7. **Never use `git commit --no-verify`** - pre-commit hooks run `ruff format` and `ruff check` automatically; skipping them leads to CI failures. If using fixup/rebase workflows, always run `ruff format` and `ruff check` manually before the final push.\n8. **Run `ty check` on modified files** - verify no new type errors are introduced (pre-existing errors in the codebase can be ignored).\n9. **`gh pr edit` may fail with a GraphQL Projects deprecation error** - use `gh api repos/OWNER/REPO/pulls/NUMBER -X PATCH -F \"body=@file.md\"` as a workaround to update PR descriptions.\n\n### Contributing\n1. **Create ticket first:** <https://tickets.metabrainz.org/projects/PICARD>\n2. Create feature branch\n3. Make changes\n4. Run pre-commit checks (above)\n5. **PR title must start with:** `PICARD-XXXX:` (ticket number)\n6. **Use the PR template** - fill in `.github/PULL_REQUEST_TEMPLATE.md` (Summary, Problem, Solution, AI Usage, Action sections)\n7. **Don't auto-create tickets/PRs** - let humans handle this\n8. **Don't push to remote** - without explicit user approval\n\n---\n\n## Code Style\n\n### Conventions\n- **Standard:** PEP 8, 120 char lines (flexible)\n- **Linter:** Ruff\n- **Type hints:** Recommended for new code (existing code has limited coverage)\n- **Naming:** `PascalCase` classes, `snake_case` functions, `UPPER_SNAKE` constants\n\n### Membership Tests and Control Flow\n```python\n# Use a set literal (not a tuple/list) for membership tests against a fixed\n# set of constants. Python optimizes `in {...}`. See the ruff \"literal-membership\" rule.\n\n# ❌ Tuple/list literal for membership test\nif state in (State.CHANGED, State.NORMAL):\n    ...\nassert suffix in ('.pyc', '.pyo')\n\n# ✅ Set literal\nif state in {State.CHANGED, State.NORMAL}:\n    ...\nassert suffix in {'.pyc', '.pyo'}\n\n\n# Prefer a single membership test over chained equality checks.\n\n# ❌ Multiple equality checks\nif name == 'performer' or name == 'comment':\n    ...\n\n# ✅ Single membership test\nif name in {'performer', 'comment'}:\n    ...\n\n\n# Collapse a nested `else:` + `if` into a single `elif` to reduce indentation.\n# See the ruff \"collapsible-else-if\" rule.\n\n# ❌ Nested if inside else\nif a:\n    do_x()\nelse:\n    if b:\n        do_y()\n    else:\n        do_z()\n\n# ✅ elif\nif a:\n    do_x()\nelif b:\n    do_y()\nelse:\n    do_z()\n```\n\nThe preview ruff rules can be checked with:\n```bash\nuv run ruff check --preview --select literal-membership,collapsible-else-if\n```\n\n### Internationalization (i18n)\n```python\n# Import translation functions\nfrom picard.i18n import gettext as _\nfrom picard.i18n import N_\n\n# Translatable strings (runtime)\nmessage = _(\"Save file\")\nerror = _(\"Failed to load: %s\") % filename\n\n# Mark for extraction only (constants, class attributes)\nTITLE = N_(\"Options\")  # Translated at usage: _(TITLE)\n\n# Plugin translations (Plugin v3)\nfrom picard.plugin3.api import t_\n\n\nclass ManifestTranslations:\n    NAME = t_(\"manifest.name\", \"My Plugin\")\n    DESC = t_(\"manifest.description\", \"Plugin description\")\n```\n\n**Translation workflow:**\n- Strings marked with `_()` or `N_()` are extracted to `po/picard.pot`\n- Regenerate: `python setup.py regen_pot_file`\n- Translations managed via [Weblate](https://translations.metabrainz.org/projects/picard/)\n- See `po/README.md` for details\n\n#### Gotcha: `picard/const/` uses a separate gettext domain\n\nPicard has **four** gettext domains (see `picard/i18n/gettext.py`), not one.\nStrings are extracted per source location, and each domain must be looked up\nwith its **own** translation function at runtime:\n\n| Where the string is defined | `N_()`-marked into | Translate at runtime with |\n|---|---|---|\n| Anywhere **except** `picard/const/` | `po/picard.pot` (`main`) | `gettext()` / `_()` |\n| `picard/const/` (e.g. `tags.py`, `scripts.py`, `cover_processing.py`, `languages.py`, `locales.py`, `defaults.py`) | `po/constants/` (`constants`) | `gettext_constants()` |\n| `picard/const/countries.py` (`RELEASE_COUNTRIES`) | `po/countries/` (`countries`) | `gettext_countries()` |\n| `picard/const/attributes.py` (`MB_ATTRIBUTES`) | `po/attributes/` (`attributes`) | `pgettext_attributes()` / `gettext_attributes()` |\n\n`setup.py` deliberately excludes `const` from the main pot and extracts it\ninto `po/constants/constants.pot`. So a `const/`-sourced string translated\nwith the plain `_()` is looked up in the **wrong** domain, silently misses,\nand renders in English even though a translation exists.\n\n```python\nfrom picard.i18n import gettext as _\nfrom picard.i18n import gettext_constants\n\n# TagVar.longdesc / shortdesc, SCRIPTS[...], CoverResizeMode.title,\n# DEFAULT_SCRIPT_NAME, ... all originate in picard/const/\n\n# ❌ Wrong domain — always English, even in fr/de/... UI\ntitle = _(tagvar.shortdesc)\nname = _(DEFAULT_SCRIPT_NAME)  # or storing the raw N_() string\n\n# ✅ Constants domain\ntitle = gettext_constants(tagvar.shortdesc)\nname = gettext_constants(DEFAULT_SCRIPT_NAME)\n```\n\nRule of thumb: **if the string's `N_()` lives in `picard/const/`, translate it\nwith `gettext_constants()`** (or the countries/attributes helper). This bug is\neasy to reintroduce — it has recurred across PRs #3453 and #3457 (PICARD-3448).\nWhen touching translated `const/` strings, verify the msgid resolves against\n`po/constants/*.po` (or `po/countries/`, `po/attributes/`).\n\n### Qt UI Files\n```bash\n# ❌ Don't edit picard/ui/ui_*.py directly (auto-generated)\n\n# ✅ Edit .ui files with Qt Designer\nqt6-tools designer ui/options_metadata.ui\n\n# ✅ Regenerate Python files\npython setup.py build_ui\n```\n\n### Qt Signals\n```python\nfrom PyQt6.QtCore import pyqtSignal\n\n\nclass MyWidget(QWidget):\n    # Define signals\n    value_changed = pyqtSignal(str)\n    item_selected = pyqtSignal(object)\n\n    def some_action(self):\n        # Emit signals\n        self.value_changed.emit(\"new value\")\n```\n\nUse the `QSignalBlocker` context manager to suppress signals temporarily,\nrather than manual `blockSignals(True)`/`blockSignals(False)` calls. It is\nexception-safe (signals are always unblocked, even if an exception is raised)\nand more concise.\n\n```python\nfrom PyQt6.QtCore import QSignalBlocker\n\n\n# ❌ Manual, not exception-safe\nwidget.blockSignals(True)\nwidget.setChecked(True)\nwidget.blockSignals(False)\n\n# ✅ Context manager\nwith QSignalBlocker(widget):\n    widget.setChecked(True)\n```\n\n### Readability\n- Use descriptive names\n- Comment complex logic\n- Break long functions\n- Follow existing style in the file\n\n---\n\n## Key Locations\n\n### Core Components\n- **Main app:** `picard/tagger.py`\n- **File handling:** `picard/file.py`\n- **Album/Track:** `picard/album.py`, `picard/track.py`\n- **Metadata:** `picard/metadata.py`\n\n### Format Handlers\n- **Registry:** `picard/formats/registry.py`\n- **ID3 (MP3):** `picard/formats/id3.py`\n- **Vorbis (FLAC/Ogg):** `picard/formats/vorbis.py`\n- **MP4:** `picard/formats/mp4.py`\n\n### Plugin System\n- **API:** `picard/plugin3/api_impl.py`\n- **Manager:** `picard/plugin3/manager/__init__.py`\n- **Registry:** `picard/plugin3/registry.py`\n\n### UI\n- **Main window:** `picard/ui/mainwindow/`\n- **Options:** `picard/ui/options/`\n- **Metadata box:** `picard/ui/metadatabox/`\n- **Script editor:** `picard/ui/scripteditor/`\n\n### Scripting\n- **Parser:** `picard/script/parser.py`\n- **Functions:** `picard/script/functions.py`\n\n### Web Services\n- **HTTP client:** `picard/webservice/__init__.py`\n- **APIs:** `picard/webservice/api_helpers.py`\n\n---\n\n## Common Tasks\n\n### Add Audio Format\n1. Create handler in `picard/formats/`\n2. Inherit from `File`\n3. Implement `_load()` and `_save()`\n4. Define `EXTENSIONS` and `NAME`\n5. Call `register_format(MyFormat)`\n\n### Add Script Function\n```python\nfrom picard.script import script_function\n\n\n@script_function\ndef func_myfunction(parser, arg1, arg2='default'):\n    \"\"\"$myfunction(arg1,arg2) - Description\"\"\"\n    return result\n```\n\n### Add Option\n1. Define in `picard/options.py`\n2. Create UI page in `picard/ui/options/`\n3. Implement `load()` and `save()` methods\n4. Register with `register_options_page()`\n\n### Add Metadata Processor (Core)\n```python\nfrom picard.extension_points.metadata import register_track_metadata_processor\n\n\ndef my_processor(album, metadata, track, release):\n    metadata['custom'] = 'value'\n\n\nregister_track_metadata_processor(my_processor)\n```\n\n**For plugins:** Use `api.register_track_metadata_processor()` instead (see Plugin System section).\n\n---\n\n## Testing\n\nHow to run the suite and how to write tests — the two coexisting test styles, the\n`subtest_cases` decorator for table-driven tests, and the shared `qapp` fixture for\nQt widgets — is documented in [test/README.md](test/README.md). That document is the\nsource of truth; follow it rather than relying on a copy here.\n\nPoints worth repeating because they are easy to get wrong:\n\n- Match the test style of the file you are editing. Do not convert a\n  `PicardTestCase` class to plain pytest (or the reverse) as a side effect of an\n  unrelated change.\n- `@pytest.mark.parametrize` does not work on `PicardTestCase` subclasses; the test\n  errors with a missing-argument `TypeError`. Use the `subtest_cases` decorator (or\n  `self.subTest()`) there.\n- Never create a local `QApplication`/`QCoreApplication`; it crashes under\n  `pytest-randomly` ordering.\n\n---\n\n## Debugging\n\n```bash\n# Enable debug logging\npicard --debug\n\n# Enable specific debug options (comma-separated)\npicard --debug-opts=option1,option2\n```\n\n```python\n# In code\nfrom picard import log\n\nlog.debug('Debug: %s', value)\nlog.error('Error', exc_info=True)\n```\n\n---\n\n## Resources\n\n- **User Docs:** <https://picard-docs.musicbrainz.org/>\n- **Website:** <https://picard.musicbrainz.org/>\n- **GitHub:** <https://github.com/metabrainz/picard>\n- **Forum:** <https://community.metabrainz.org/c/picard>\n- **Contributing:** See `CONTRIBUTING.md`\n- **Installation:** See `INSTALL.md`\n\n---\n\n**Note:** This file contains only essential patterns. For architecture, dependencies, and component details, explore the codebase directly or read existing documentation (`README.md`, `CONTRIBUTING.md`, `INSTALL.md`, user docs).\n\n<!-- File: scripts/tools/README.md (Tokens: ~283 | Category: plugin-manifest) -->\n# scripts/tools/\n\nDeveloper utility scripts for MusicBrainz Picard. These are not shipped with\nreleases — they assist with development, releases, and asset generation.\n\n## Scripts\n\n| Script | Description |\n|--------|-------------|\n| `authors-between-releases.py` | List code contributors and translators between two git tags, for release notes. |\n| `changelog-for-version.py` | Extract changelog entries for a given version from git history. |\n| `check_settings.py` | Check for references to undefined option settings in the codebase. |\n| `detect_qt_shadowing.py` | Detect class attributes that shadow inherited Qt methods. |\n| `fix-header.py` | Regenerate source file copyright/license headers from git log. |\n| `generate_icons.sh` | Generate PNG icons (multiple sizes) from an SVG source file. |\n| `generate_match_icons.py` | Generate match quality bookmark PNGs (normal + pending, 1× + 2×). |\n| `multiple_imports_on_one_line.py` | Find import statements with multiple names on one line. |\n| `patch-version.sh` | Patch the version string with git commit count/hash for dev builds. |\n| `pull-shared-translations.py` | Pull shared translation strings from Weblate/upstream. |\n| `tag-release.sh` | Create a git tag for the current release version. |\n</yakaai_skills>"}