{"owner":"metabrainz","repo":"picard","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md"],"skills":{"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### 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### 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\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\n```bash\n# All tests\npytest test/\n\n# Specific file\npytest test/test_metadata.py\n\n# With coverage\npytest --cov=picard test/\n```\n\n### Test Structure\n```python\nfrom test.picardtestcase import PicardTestCase\n\n\nclass TestMyFeature(PicardTestCase):\n    def setUp(self):\n        super().setUp()\n\n    def test_something(self):\n        self.assertEqual(expected, actual)\n```\n\n### Qt Widget Tests\nA session-scoped `qapp` fixture in `test/conftest.py` provides a shared\n`QApplication` for the entire test run. Always use it for widget tests:\n\n```python\n@pytest.fixture()\ndef my_widget(qapp):\n    from picard.ui.widgets.mywidget import MyWidget\n\n    return MyWidget()\n```\n\n**Never** create a local `QApplication` or `QCoreApplication` in tests — it causes\ncrashes with random test ordering. For unittest-based tests that need an event loop,\nuse `QCoreApplication.instance()` and only create one if it returns `None`.\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":{"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### 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### 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\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\n```bash\n# All tests\npytest test/\n\n# Specific file\npytest test/test_metadata.py\n\n# With coverage\npytest --cov=picard test/\n```\n\n### Test Structure\n```python\nfrom test.picardtestcase import PicardTestCase\n\n\nclass TestMyFeature(PicardTestCase):\n    def setUp(self):\n        super().setUp()\n\n    def test_something(self):\n        self.assertEqual(expected, actual)\n```\n\n### Qt Widget Tests\nA session-scoped `qapp` fixture in `test/conftest.py` provides a shared\n`QApplication` for the entire test run. Always use it for widget tests:\n\n```python\n@pytest.fixture()\ndef my_widget(qapp):\n    from picard.ui.widgets.mywidget import MyWidget\n\n    return MyWidget()\n```\n\n**Never** create a local `QApplication` or `QCoreApplication` in tests — it causes\ncrashes with random test ordering. For unittest-based tests that need an event loop,\nuse `QCoreApplication.instance()` and only create one if it returns `None`.\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","title":"AGENTS.md","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### 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### 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\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\n```bash\n# All tests\npytest test/\n\n# Specific file\npytest test/test_metadata.py\n\n# With coverage\npytest --cov=picard test/\n```\n\n### Test Structure\n```python\nfrom test.picardtestcase import PicardTestCase\n\n\nclass TestMyFeature(PicardTestCase):\n    def setUp(self):\n        super().setUp()\n\n    def test_something(self):\n        self.assertEqual(expected, actual)\n```\n\n### Qt Widget Tests\nA session-scoped `qapp` fixture in `test/conftest.py` provides a shared\n`QApplication` for the entire test run. Always use it for widget tests:\n\n```python\n@pytest.fixture()\ndef my_widget(qapp):\n    from picard.ui.widgets.mywidget import MyWidget\n\n    return MyWidget()\n```\n\n**Never** create a local `QApplication` or `QCoreApplication` in tests — it causes\ncrashes with random test ordering. For unittest-based tests that need an event loop,\nuse `QCoreApplication.instance()` and only create one if it returns `None`.\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","category":"root","tokens":3216}]}