docs(global): add documentation

This commit is contained in:
h
2025-11-07 17:15:12 +01:00
parent ba3005d9b4
commit af227b597b
30 changed files with 2978 additions and 12 deletions

20
docs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

58
docs/conf.py Normal file
View File

@@ -0,0 +1,58 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import sys
import tomllib
sys.path.insert(0, os.path.abspath("."))
sys.path.insert(0, os.path.abspath("../."))
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "logic-plugin-manager"
copyright = "2025, h"
author = "h"
release = tomllib.load(open("../pyproject.toml", "rb"))["project"]["version"]
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.viewcode",
"sphinx.ext.napoleon",
"sphinx_autodoc_typehints",
]
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
autodoc_default_options = {
"members": True,
"member-order": "bysource",
"special-members": "__init__",
"undoc-members": True,
"exclude-members": "__weakref__",
"inherited-members": False,
}
autodoc_typehints = "description"
typehints_use_signature = True
typehints_use_signature_return = True
autodoc_member_order = "bysource"
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "furo"
html_static_path = ["_static"]
html_title = "Logic Plugin Manager"
html_theme_options = {
"sidebar_hide_name": False,
"navigation_with_keys": True,
}

467
docs/core_concepts.rst Normal file
View File

@@ -0,0 +1,467 @@
Core Concepts
=============
This page explains the key concepts and architecture of Logic Plugin Manager.
Architecture Overview
---------------------
Logic Plugin Manager is structured around three main areas:
1. **Components**: Audio Unit plugin bundles and their metadata
2. **Logic Management**: High-level interface for plugin discovery and organization
3. **Tags & Categories**: Logic Pro's categorization system
The Library's Structure
~~~~~~~~~~~~~~~~~~~~~~~~
::
logic_plugin_manager/
├── components/ # Audio Unit components
│ ├── AudioComponent # Individual plugin representation
│ ├── Component # .component bundle parser
│ └── AudioUnitType # Audio Unit type enumeration
├── logic/ # Main management interface
│ ├── Logic # Primary entry point
│ ├── Plugins # Plugin collection with search
│ └── SearchResult # Search result with scoring
└── tags/ # Category and tag management
├── Category # Plugin category management
├── MusicApps # Database interface
├── Tagpool # Plugin count tracking
├── Properties # Category sorting
└── Tagset # Plugin tag files
Understanding Audio Components
------------------------------
Audio Component Bundle Structure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
macOS Audio Unit plugins are distributed as ``.component`` bundles:
::
PluginName.component/
└── Contents/
├── Info.plist # Metadata and component definitions
├── MacOS/
│ └── PluginName # Executable binary
└── Resources/ # UI resources, presets, etc.
The ``Info.plist`` file contains an ``AudioComponents`` array defining one or more Audio Units within the bundle.
AudioComponent Class
~~~~~~~~~~~~~~~~~~~~
Each Audio Unit is represented by an ``AudioComponent`` instance with:
- **Identification**: Type, subtype, and manufacturer codes
- **Metadata**: Name, description, version
- **Categories**: Tags assigned in Logic Pro
- **Tags ID**: Unique identifier derived from codes
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
plugin = logic.plugins.get_by_name("Pro-Q 3")
# Access component information
print(f"Full Name: {plugin.full_name}")
print(f"Manufacturer: {plugin.manufacturer}")
print(f"Type Code: {plugin.type_code}")
print(f"Subtype Code: {plugin.subtype_code}")
print(f"Manufacturer Code: {plugin.manufacturer_code}")
print(f"Tags ID: {plugin.tags_id}")
Audio Unit Types
~~~~~~~~~~~~~~~~
Logic Plugin Manager recognizes five Audio Unit types:
.. list-table::
:header-rows: 1
:widths: 15 25 20 40
* - Code
- Display Name
- Alternative Name
- Description
* - ``aufx``
- Audio FX
- Effect
- Audio effect processors
* - ``aumu``
- Instrument
- Music Device
- Software instruments and synthesizers
* - ``aumf``
- MIDI-controlled Effects
- Music Effect
- Effects controlled by MIDI input
* - ``aumi``
- MIDI FX
- MIDI Generator
- MIDI processors and generators
* - ``augn``
- Generator
- Generator
- Audio generators
.. code-block:: python
from logic_plugin_manager import AudioUnitType
# Get type by code
instrument_type = AudioUnitType.from_code("aumu")
print(instrument_type.display_name) # "Instrument"
# Search for types
effect_types = AudioUnitType.search("effect")
The Logic Class
---------------
The ``Logic`` class is the main entry point for plugin management.
Initialization Process
~~~~~~~~~~~~~~~~~~~~~~
When you create a ``Logic`` instance (with ``lazy=False``, the default):
1. Loads MusicApps database (tagpool and properties files)
2. Scans ``/Library/Audio/Plug-Ins/Components`` directory
3. Parses each ``.component`` bundle's ``Info.plist``
4. Creates ``AudioComponent`` instances
5. Loads tagsets and categories for each plugin
6. Indexes plugins for fast lookups
.. code-block:: python
from logic_plugin_manager import Logic
# Full initialization
logic = Logic() # Discovers everything
# Lazy initialization (manual control)
logic = Logic(lazy=True)
logic.discover_plugins() # When ready to load plugins
logic.discover_categories() # When ready to load categories
Logic Instance Attributes
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
logic = Logic()
# Access discovered data
logic.plugins # Plugins collection
logic.components # Set of Component bundles
logic.categories # Dict of category_name -> Category
logic.musicapps # MusicApps database interface
# Configuration paths
logic.components_path # Path to Components directory
logic.tags_path # Path to tags database
Plugin Collection & Search
---------------------------
The Plugins Class
~~~~~~~~~~~~~~~~~
The ``Plugins`` class provides a searchable collection with multiple indexes:
- **By full name**: Exact manufacturer + plugin name match
- **By manufacturer**: All plugins from a vendor
- **By name**: Plugin name only
- **By codes**: Type, subtype, manufacturer codes
- **By category**: All plugins in a category
- **By tags_id**: Unique component identifier
.. code-block:: python
logic = Logic()
# Exact lookups (fast)
plugin = logic.plugins.get_by_full_name("fabfilter: pro-q 3")
plugin = logic.plugins.get_by_tags_id("61756678-65517033-46466")
# Set lookups
fabfilter = logic.plugins.get_by_manufacturer("fabfilter")
effects = logic.plugins.get_by_type_code("aufx")
eq_plugins = logic.plugins.get_by_category("Effects:EQ")
Search Algorithm
~~~~~~~~~~~~~~~~
The ``search()`` method implements a sophisticated scoring system:
**Priority Levels** (highest to lowest):
1. **Tags ID exact match** (score: 1000)
2. **Name substring match** (850-900)
3. **Full name substring match** (750-800)
4. **Fuzzy name match** (650-700, requires rapidfuzz)
5. **Manufacturer match** (580-650)
6. **Category match** (480-550)
7. **Type code match** (380-450)
8. **Subtype code match** (290-350)
9. **Manufacturer code match** (190-250)
.. code-block:: python
logic = Logic()
# Fuzzy search with scoring
results = logic.plugins.search(
"serum",
use_fuzzy=True,
fuzzy_threshold=80,
max_results=10
)
for result in results:
print(f"{result.plugin.full_name}")
print(f" Score: {result.score}")
print(f" Matched: {result.match_field}")
Categories & Tags
-----------------
Hierarchical Structure
~~~~~~~~~~~~~~~~~~~~~~
Categories in Logic Pro use colon-separated hierarchies:
::
Effects
Effects:Dynamics
Effects:Dynamics:Compressor
Effects:EQ
Instruments
Instruments:Synth
.. code-block:: python
logic = Logic()
# Navigate hierarchy
dynamics = logic.categories["Effects:Dynamics"]
parent = dynamics.parent # "Effects" category
child = parent.child("EQ") # "Effects:EQ" category
# Check properties
print(dynamics.plugin_amount)
print(dynamics.is_root)
Category Operations
~~~~~~~~~~~~~~~~~~~
.. code-block:: python
logic = Logic()
# Create new category
my_cat = logic.introduce_category("Studio Essentials")
# Manage plugins
plugin = logic.plugins.get_by_name("Pro-Q 3")
plugin.add_to_category(my_cat)
plugin.remove_from_category(my_cat)
plugin.move_to_category(my_cat) # Removes from all others
# Bulk operations
plugins_set = {plugin1, plugin2, plugin3}
logic.add_plugins_to_category(my_cat, plugins_set)
# Update plugin count
logic.sync_category_plugin_amount(my_cat)
Category Sorting
~~~~~~~~~~~~~~~~
Categories have a sort order managed by the ``Properties`` database:
.. code-block:: python
category = logic.categories["Effects:EQ"]
# Check position
print(category.index)
print(category.is_first)
print(category.is_last)
prev, next = category.neighbors
# Reorder
category.move_up(steps=2)
category.move_down()
category.move_to_top()
category.move_to_bottom()
category.move_before(other_category)
category.move_after(other_category)
category.swap(other_category)
Tagsets
-------
What are Tagsets?
~~~~~~~~~~~~~~~~~
Each ``AudioComponent`` has an associated ``.tagset`` file stored at:
``~/Music/Audio Music Apps/Databases/Tags/<tags_id>.tagset``
These XML plist files store:
- **nickname**: Custom display name
- **shortname**: Abbreviated name for UI
- **tags**: Dictionary of category assignments
.. code-block:: python
logic = Logic()
plugin = logic.plugins.get_by_name("Pro-Q 3")
# Access tagset
print(plugin.tagset.nickname)
print(plugin.tagset.shortname)
print(plugin.tagset.tags) # {"Effects:EQ": "user"}
# Modify tagset
plugin.set_nickname("My Favorite EQ")
plugin.set_shortname("PQ3")
Tag Values
~~~~~~~~~~
Category tags typically have the value ``"user"`` indicating user assignment, but can have other values from Logic Pro's internal management.
MusicApps Database
------------------
Database Files
~~~~~~~~~~~~~~
Logic Pro stores category information in two files:
**MusicApps.tagpool**
Maps category names to plugin counts:
.. code-block:: python
{
"Effects": 245,
"Effects:EQ": 18,
"Instruments": 89,
...
}
**MusicApps.properties**
Stores category sort order and preferences:
.. code-block:: python
{
"sorting": ["Effects", "Effects:Dynamics", ...],
"user_sorted": "property" # or absent for alphabetical
}
Accessing the Database
~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
logic = Logic()
musicapps = logic.musicapps
# Access tagpool
print(musicapps.tagpool.categories)
# Access properties
print(musicapps.properties.sorting)
print(musicapps.properties.user_sorted)
# Modify database
musicapps.introduce_category("New Category")
musicapps.remove_category("Old Category")
Thread Safety
-------------
Logic Plugin Manager is **not thread-safe**. The library performs file I/O operations on Logic Pro's database files without locking mechanisms.
**Recommendations:**
- Use a single ``Logic`` instance per process
- Avoid concurrent writes to categories or tagsets
- Reload data after external changes (e.g., Logic Pro modifying categories)
Performance Considerations
--------------------------
Initial Discovery
~~~~~~~~~~~~~~~~~
Loading all plugins can take several seconds depending on the number of installed components. Use ``lazy=True`` for faster startup when you don't need immediate access.
Indexing
~~~~~~~~
The ``Plugins`` collection maintains multiple indexes. Reindexing is required if you:
- Modify plugin properties externally
- Add plugins after initialization
.. code-block:: python
logic = Logic(lazy=True)
logic.discover_plugins()
# ... modify plugins externally ...
logic.plugins.reindex_all()
Search Performance
~~~~~~~~~~~~~~~~~~
- Exact lookups by indexes are O(1)
- Fuzzy search with ``rapidfuzz`` is O(n) but optimized
- Use ``max_results`` to limit computation for large result sets
Best Practices
--------------
1. **Single Instance**: Create one ``Logic`` instance and reuse it
2. **Error Handling**: Wrap operations in try-except blocks for specific exceptions
3. **Lazy Loading**: Use for CLI tools or services that don't need immediate access
4. **Reloading**: Call ``load()`` methods after external modifications to databases
.. code-block:: python
from logic_plugin_manager import (
Logic,
CategoryValidationError,
PluginLoadError
)
try:
logic = Logic()
except PluginLoadError as e:
print(f"Failed to load plugins: {e}")
# Handle gracefully
# Batch operation example
favorites = {
logic.plugins.get_by_name(name)
for name in ["Pro-Q 3", "Serum", "Valhalla VintageVerb"]
if logic.plugins.get_by_name(name)
}
if favorites:
fav_category = logic.introduce_category("Favorites")
logic.add_plugins_to_category(fav_category, favorites)
logic.sync_category_plugin_amount(fav_category)

712
docs/examples.rst Normal file
View File

@@ -0,0 +1,712 @@
Usage Examples
==============
This page provides practical examples for common use cases.
Plugin Discovery & Inspection
------------------------------
List All Plugins
~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
print(f"Total plugins: {len(logic.plugins.all())}")
for plugin in logic.plugins.all():
print(f"{plugin.full_name}")
print(f" Type: {plugin.type_name.display_name}")
print(f" Version: {plugin.version}")
print(f" Categories: {', '.join(c.name for c in plugin.categories)}")
print()
Filter by Type
~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Get all instruments
instruments = logic.plugins.get_by_type_code("aumu")
print(f"Found {len(instruments)} instruments")
# Get all effects
effects = logic.plugins.get_by_type_code("aufx")
print(f"Found {len(effects)} effects")
Find Plugins by Manufacturer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# List all manufacturers
manufacturers = set()
for plugin in logic.plugins.all():
manufacturers.add(plugin.manufacturer)
for mfr in sorted(manufacturers):
plugins = logic.plugins.get_by_manufacturer(mfr)
print(f"{mfr}: {len(plugins)} plugins")
# Get specific manufacturer's plugins
fabfilter = logic.plugins.get_by_manufacturer("fabfilter")
for plugin in fabfilter:
print(f" - {plugin.name}")
Inspect Plugin Details
~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
plugin = logic.plugins.get_by_full_name("fabfilter: pro-q 3")
if plugin:
print(f"Full Name: {plugin.full_name}")
print(f"Manufacturer: {plugin.manufacturer}")
print(f"Name: {plugin.name}")
print(f"Description: {plugin.description}")
print(f"Type: {plugin.type_name.display_name} ({plugin.type_code})")
print(f"Subtype: {plugin.subtype_code}")
print(f"Manufacturer Code: {plugin.manufacturer_code}")
print(f"Version: {plugin.version}")
print(f"Factory Function: {plugin.factory_function}")
print(f"Tags ID: {plugin.tags_id}")
print(f"Tagset Path: {plugin.tagset.path}")
if plugin.tagset.nickname:
print(f"Nickname: {plugin.tagset.nickname}")
if plugin.tagset.shortname:
print(f"Short Name: {plugin.tagset.shortname}")
print(f"Categories: {', '.join(c.name for c in plugin.categories)}")
Search & Discovery
------------------
Simple Text Search
~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Simple substring search
reverb_plugins = logic.plugins.search_simple("reverb")
print(f"Found {len(reverb_plugins)} plugins with 'reverb' in name")
for plugin in reverb_plugins:
print(f" - {plugin.full_name}")
Advanced Fuzzy Search
~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Fuzzy search with scoring
results = logic.plugins.search(
query="compressor",
use_fuzzy=True,
fuzzy_threshold=80,
max_results=10
)
for i, result in enumerate(results, 1):
print(f"{i}. {result.plugin.full_name}")
print(f" Score: {result.score:.1f}")
print(f" Matched field: {result.match_field}")
print()
Search by Multiple Criteria
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
def find_plugins(search_term, plugin_type=None, category=None):
"""Search plugins with optional filters."""
results = logic.plugins.search(search_term, use_fuzzy=True)
plugins = [r.plugin for r in results]
# Filter by type if specified
if plugin_type:
plugins = [p for p in plugins if p.type_code == plugin_type]
# Filter by category if specified
if category:
plugins = [
p for p in plugins
if any(c.name == category for c in p.categories)
]
return plugins
# Find reverb effects (not instruments)
reverbs = find_plugins("reverb", plugin_type="aufx")
# Find synthesizers in Instruments category
synths = find_plugins("synth", plugin_type="aumu", category="Instruments")
Category Management
-------------------
List All Categories
~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
print("Categories:")
for name, category in sorted(logic.categories.items()):
print(f" {name} ({category.plugin_amount} plugins)")
Create Category Hierarchy
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Create nested categories
categories_to_create = [
"My Plugins",
"My Plugins:Favorites",
"My Plugins:Favorites:Mixing",
"My Plugins:Favorites:Mastering",
]
for cat_name in categories_to_create:
if cat_name not in logic.categories:
category = logic.introduce_category(cat_name)
print(f"Created: {cat_name}")
else:
print(f"Already exists: {cat_name}")
Add Plugins to Category
~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Create or get category
favorites = logic.categories.get("Favorites")
if not favorites:
favorites = logic.introduce_category("Favorites")
# Add single plugin
plugin = logic.plugins.get_by_full_name("fabfilter: pro-q 3")
if plugin:
plugin.add_to_category(favorites)
print(f"Added {plugin.full_name} to {favorites.name}")
# Update category count
logic.sync_category_plugin_amount(favorites)
Bulk Category Assignment
~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Find all FabFilter plugins
fabfilter_plugins = logic.plugins.get_by_manufacturer("fabfilter")
# Create category
fabfilter_cat = logic.categories.get("FabFilter")
if not fabfilter_cat:
fabfilter_cat = logic.introduce_category("FabFilter")
# Bulk add
logic.add_plugins_to_category(fabfilter_cat, fabfilter_plugins)
print(f"Added {len(fabfilter_plugins)} FabFilter plugins")
Move Plugins Between Categories
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Get plugins from one category
dynamics_plugins = logic.plugins.get_by_category("Effects:Dynamics")
# Filter for compressors
compressors = {
p for p in dynamics_plugins
if "compress" in p.name.lower()
}
# Move to specific category
comp_category = logic.categories.get("Effects:Dynamics:Compressor")
if comp_category:
logic.move_plugins_to_category(comp_category, compressors)
logic.sync_category_plugin_amount(comp_category)
Organize Uncategorized Plugins
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Get plugins without categories
uncategorized = logic.plugins.get_by_category(None)
print(f"Found {len(uncategorized)} uncategorized plugins")
# Auto-categorize by manufacturer
for plugin in uncategorized:
manufacturer = plugin.manufacturer.strip()
category_name = f"By Manufacturer:{manufacturer}"
# Create category if needed
if category_name not in logic.categories:
logic.introduce_category(category_name)
category = logic.categories[category_name]
plugin.add_to_category(category)
# Update all category counts
logic.sync_all_categories_plugin_amount()
Category Sorting
~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Get category
category = logic.categories["Effects:EQ"]
# Check current position
print(f"Current index: {category.index}")
prev, next_cat = category.neighbors
if prev:
print(f"Previous: {prev.name}")
if next_cat:
print(f"Next: {next_cat.name}")
# Move category
category.move_to_top()
category.move_up(steps=5)
category.move_down()
# Move relative to another category
target = logic.categories["Effects:Delay"]
category.move_before(target)
Custom Plugin Metadata
----------------------
Set Nicknames
~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Set custom nicknames for easier identification
plugins_to_rename = {
"fabfilter: pro-q 3": "PQ3 - Main EQ",
"fabfilter: pro-c 2": "PC2 - Main Compressor",
"valhalla shimmer": "Shimmer - Ambient Verb",
}
for full_name, nickname in plugins_to_rename.items():
plugin = logic.plugins.get_by_full_name(full_name)
if plugin:
plugin.set_nickname(nickname)
print(f"Renamed: {full_name} -> {nickname}")
Set Short Names
~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Set short names for UI display
shortnames = {
"fabfilter: pro-q 3": "PQ3",
"fabfilter: pro-c 2": "PC2",
"serum": "SRM",
}
for full_name, shortname in shortnames.items():
plugin = logic.plugins.get_by_full_name(full_name)
if plugin:
plugin.set_shortname(shortname)
Batch Metadata Updates
~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Add manufacturer prefix to all plugin nicknames
for plugin in logic.plugins.all():
if not plugin.tagset.nickname:
manufacturer = plugin.manufacturer.upper()
nickname = f"[{manufacturer}] {plugin.name}"
plugin.set_nickname(nickname)
print(f"Set nickname for {plugin.full_name}")
Advanced Operations
-------------------
Export Plugin Inventory
~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
import csv
from logic_plugin_manager import Logic
logic = Logic()
# Export to CSV
with open("plugin_inventory.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow([
"Full Name", "Manufacturer", "Type", "Version",
"Categories", "Subtype Code", "Tags ID"
])
for plugin in sorted(logic.plugins.all(), key=lambda p: p.full_name):
writer.writerow([
plugin.full_name,
plugin.manufacturer,
plugin.type_name.display_name,
plugin.version,
"; ".join(c.name for c in plugin.categories),
plugin.subtype_code,
plugin.tags_id,
])
print("Exported plugin inventory to plugin_inventory.csv")
Clone Category Structure
~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
def clone_category(source: str, target: str, logic: Logic):
"""Clone plugins from source category to target category."""
# Get plugins in source
source_plugins = logic.plugins.get_by_category(source)
# Create target if needed
if target not in logic.categories:
logic.introduce_category(target)
target_category = logic.categories[target]
# Add plugins to target
logic.add_plugins_to_category(target_category, source_plugins)
print(f"Cloned {len(source_plugins)} plugins from {source} to {target}")
logic = Logic()
clone_category("Effects:EQ", "My Plugins:EQ", logic)
Find Duplicate Plugins
~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from collections import defaultdict
from logic_plugin_manager import Logic
logic = Logic()
# Group by name (ignoring manufacturer)
by_name = defaultdict(list)
for plugin in logic.plugins.all():
by_name[plugin.name.lower()].append(plugin)
# Find duplicates
duplicates = {
name: plugins
for name, plugins in by_name.items()
if len(plugins) > 1
}
print(f"Found {len(duplicates)} plugin names with multiple versions:\n")
for name, plugins in sorted(duplicates.items()):
print(f"{name}:")
for plugin in plugins:
print(f" - {plugin.full_name} (v{plugin.version})")
print()
Backup and Restore Categories
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
import json
from pathlib import Path
from logic_plugin_manager import Logic
def backup_categories(output_file: Path):
"""Backup all plugin category assignments."""
logic = Logic()
backup_data = {}
for plugin in logic.plugins.all():
backup_data[plugin.tags_id] = {
"full_name": plugin.full_name,
"categories": [c.name for c in plugin.categories],
}
with open(output_file, "w") as f:
json.dump(backup_data, f, indent=2)
print(f"Backed up {len(backup_data)} plugin assignments")
def restore_categories(backup_file: Path):
"""Restore plugin category assignments from backup."""
logic = Logic()
with open(backup_file) as f:
backup_data = json.load(f)
restored = 0
for tags_id, data in backup_data.items():
plugin = logic.plugins.get_by_tags_id(tags_id)
if plugin:
# Restore categories
categories = [
logic.categories[name]
for name in data["categories"]
if name in logic.categories
]
if categories:
plugin.set_categories(categories)
restored += 1
print(f"Restored {restored} plugin assignments")
# Usage
backup_categories(Path("categories_backup.json"))
# restore_categories(Path("categories_backup.json"))
Generate Category Report
~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
print("=" * 60)
print("CATEGORY REPORT")
print("=" * 60)
print()
# Sort categories by hierarchy
sorted_categories = sorted(logic.categories.items())
for name, category in sorted_categories:
# Calculate depth based on colons
depth = name.count(":")
indent = " " * depth
# Get plugins in this exact category
plugins = logic.plugins.get_by_category(name)
print(f"{indent}├─ {name.split(':')[-1]}")
print(f"{indent}│ Count: {category.plugin_amount}")
print(f"{indent}│ Index: {category.index}")
if plugins:
print(f"{indent}│ Plugins:")
for plugin in sorted(plugins, key=lambda p: p.full_name)[:5]:
print(f"{indent}│ - {plugin.full_name}")
if len(plugins) > 5:
print(f"{indent}│ ... and {len(plugins) - 5} more")
print()
Working with Component Bundles
-------------------------------
Inspect Component Bundles
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
print(f"Total component bundles: {len(logic.components)}\n")
for component in logic.components:
print(f"Bundle: {component.name}")
print(f" ID: {component.bundle_id}")
print(f" Version: {component.version} ({component.short_version})")
print(f" Audio Components: {len(component.audio_components)}")
for audio_comp in component.audio_components:
print(f" - {audio_comp.full_name}")
print()
Find Multi-Plugin Bundles
~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Find bundles with multiple plugins
multi_plugin_bundles = [
comp for comp in logic.components
if len(comp.audio_components) > 1
]
print(f"Found {len(multi_plugin_bundles)} bundles with multiple plugins:\n")
for component in multi_plugin_bundles:
print(f"{component.name} - {len(component.audio_components)} plugins:")
for plugin in component.audio_components:
print(f" - {plugin.name} ({plugin.type_name.display_name})")
print()
Error Handling Examples
-----------------------
Robust Plugin Search
~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic, PluginLoadError
def safe_find_plugin(plugin_name: str) -> None:
"""Safely search for a plugin with error handling."""
try:
logic = Logic()
except PluginLoadError as e:
print(f"Error loading plugins: {e}")
return
plugin = logic.plugins.get_by_full_name(plugin_name)
if plugin:
print(f"Found: {plugin.full_name}")
print(f"Type: {plugin.type_name.display_name}")
# Try to load tagset
try:
print(f"Categories: {', '.join(c.name for c in plugin.categories)}")
except Exception as e:
print(f"Could not load categories: {e}")
else:
print(f"Plugin '{plugin_name}' not found")
# Try fuzzy search
results = logic.plugins.search(plugin_name)
if results:
print(f"\nDid you mean:")
for result in results[:3]:
print(f" - {result.plugin.full_name}")
safe_find_plugin("fabfilter: pro-q 3")
Graceful Category Operations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import (
Logic,
CategoryValidationError,
MusicAppsLoadError
)
def safe_add_to_category(plugin_name: str, category_name: str):
"""Add plugin to category with error handling."""
try:
logic = Logic()
except MusicAppsLoadError as e:
print(f"Database error: {e}")
return
# Find plugin
plugin = logic.plugins.get_by_full_name(plugin_name)
if not plugin:
print(f"Plugin '{plugin_name}' not found")
return
# Get or create category
try:
category = logic.categories[category_name]
except KeyError:
try:
category = logic.introduce_category(category_name)
print(f"Created new category: {category_name}")
except Exception as e:
print(f"Could not create category: {e}")
return
# Add to category
try:
plugin.add_to_category(category)
logic.sync_category_plugin_amount(category)
print(f"Added {plugin.full_name} to {category_name}")
except Exception as e:
print(f"Error adding to category: {e}")
safe_add_to_category("fabfilter: pro-q 3", "My Favorites")

240
docs/getting_started.rst Normal file
View File

@@ -0,0 +1,240 @@
Getting Started
===============
This guide will help you get started with Logic Plugin Manager, a Python library for programmatically managing Logic Pro's audio plugins.
Installation
------------
Basic Installation
~~~~~~~~~~~~~~~~~~
Install the package using pip or uv:
.. code-block:: bash
pip install logic-plugin-manager
Or with uv:
.. code-block:: bash
uv add logic-plugin-manager
With Search Functionality
~~~~~~~~~~~~~~~~~~~~~~~~~~
For fuzzy search capabilities, install with the ``search`` extra:
.. code-block:: bash
pip install logic-plugin-manager[search]
# or
uv add logic-plugin-manager[search]
This includes the ``rapidfuzz`` dependency for advanced plugin searching.
Requirements
------------
- **Python**: 3.13 or higher
- **Operating System**: macOS only (Logic Pro specific)
- **Logic Pro**: Installed with audio plugins
The library accesses:
- Audio Components directory: ``/Library/Audio/Plug-Ins/Components``
- Tags database: ``~/Music/Audio Music Apps/Databases/Tags``
Quick Start
-----------
Basic Usage
~~~~~~~~~~~
The simplest way to start is by creating a ``Logic`` instance:
.. code-block:: python
from logic_plugin_manager import Logic
# Initialize and discover all plugins
logic = Logic()
# Access all plugins
for plugin in logic.plugins.all():
print(f"{plugin.full_name} - {plugin.type_name.display_name}")
# Access categories
for category_name, category in logic.categories.items():
print(f"{category_name}: {category.plugin_amount} plugins")
Lazy Loading
~~~~~~~~~~~~
For faster initialization when you don't need immediate access to all plugins:
.. code-block:: python
from logic_plugin_manager import Logic
# Initialize without loading plugins
logic = Logic(lazy=True)
# Manually discover plugins when needed
logic.discover_plugins()
logic.discover_categories()
Searching Plugins
~~~~~~~~~~~~~~~~~
Search for plugins by name, manufacturer, or category:
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Simple substring search
results = logic.plugins.search_simple("reverb")
# Advanced fuzzy search with scoring
results = logic.plugins.search("serum", use_fuzzy=True)
for result in results[:5]: # Top 5 results
print(f"{result.plugin.full_name} (score: {result.score})")
Working with Categories
~~~~~~~~~~~~~~~~~~~~~~~
Organize plugins into categories:
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Get plugins in a specific category
effects = logic.plugins.get_by_category("Effects")
# Get or create a category
my_category = logic.categories.get("My Favorites")
if not my_category:
my_category = logic.introduce_category("My Favorites")
# Add plugin to category
plugin = logic.plugins.get_by_full_name("fabfilter: pro-q 3")
if plugin:
plugin.add_to_category(my_category)
Custom Paths
~~~~~~~~~~~~
If your Logic Pro or components are in non-standard locations:
.. code-block:: python
from pathlib import Path
from logic_plugin_manager import Logic
logic = Logic(
components_path=Path("/custom/path/to/Components"),
tags_path=Path("~/custom/path/to/Tags").expanduser()
)
Next Steps
----------
- Learn about :doc:`core_concepts` to understand the library's architecture
- Explore :doc:`examples` for common use cases
- Check the :doc:`logic_plugin_manager` for detailed API reference
Common Patterns
---------------
Finding a Specific Plugin
~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# By full name (exact match)
plugin = logic.plugins.get_by_full_name("apple: logic eq")
# By manufacturer
fabfilter_plugins = logic.plugins.get_by_manufacturer("fabfilter")
# By audio unit type
instruments = logic.plugins.get_by_type_code("aumu")
Batch Category Operations
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
# Get all synthesizer plugins
synths = logic.plugins.search("synth", use_fuzzy=True)
synth_plugins = {result.plugin for result in synths[:20]}
# Move them to a custom category
synth_category = logic.introduce_category("Synthesizers")
logic.move_plugins_to_category(synth_category, synth_plugins)
Working with Plugin Metadata
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
from logic_plugin_manager import Logic
logic = Logic()
plugin = logic.plugins.get_by_name("Pro-Q 3")
if plugin:
# Access metadata
print(f"Manufacturer: {plugin.manufacturer}")
print(f"Type: {plugin.type_name.display_name}")
print(f"Version: {plugin.version}")
print(f"Categories: {[c.name for c in plugin.categories]}")
# Set custom nickname
plugin.set_nickname("My Favorite EQ")
# Set short name for UI display
plugin.set_shortname("PQ3")
Error Handling
--------------
The library raises specific exceptions for different error conditions:
.. code-block:: python
from logic_plugin_manager import (
Logic,
PluginLoadError,
MusicAppsLoadError,
CategoryValidationError
)
try:
logic = Logic()
except MusicAppsLoadError as e:
print(f"Could not load Logic's database: {e}")
except PluginLoadError as e:
print(f"Error loading plugins: {e}")
try:
category = logic.categories["Nonexistent"]
except KeyError:
print("Category not found")
See :doc:`logic_plugin_manager` for all exception types.

91
docs/index.rst Normal file
View File

@@ -0,0 +1,91 @@
Logic Plugin Manager
====================
**Programmatic management of Logic Pro audio plugins**
Logic Plugin Manager is a Python library for discovering, organizing, and managing macOS Audio Unit plugins used by Logic Pro. It provides programmatic access to Logic's internal tag database, enabling automated plugin organization, bulk categorization, and advanced search capabilities.
----
Features
--------
- **Plugin Discovery**: Automatically scan and index all installed Audio Unit plugins
- **Category Management**: Create, modify, and organize plugin categories programmatically
- **Advanced Search**: Fuzzy search with scoring across multiple attributes
- **Bulk Operations**: Efficiently manage large plugin collections
- **Metadata Control**: Set custom nicknames, short names, and categories
- **Type-Safe**: Fully typed Python API with comprehensive documentation
Quick Example
-------------
.. code-block:: python
from logic_plugin_manager import Logic
# Initialize and discover all plugins
logic = Logic()
# Search for plugins
results = logic.plugins.search("reverb", use_fuzzy=True)
for result in results[:5]:
print(f"{result.plugin.full_name} (score: {result.score})")
# Organize into categories
favorites = logic.introduce_category("Favorites")
plugin = logic.plugins.get_by_full_name("fabfilter: pro-q 3")
plugin.add_to_category(favorites)
Installation
------------
.. code-block:: bash
pip install logic-plugin-manager
# With search functionality
pip install logic-plugin-manager[search]
**Requirements**: Python 3.13+, macOS, Logic Pro
----
Documentation
=============
.. toctree::
:maxdepth: 2
:caption: User Guide
getting_started
core_concepts
examples
.. toctree::
:maxdepth: 2
:caption: API Reference
logic_plugin_manager
----
Indices
=======
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
----
License
=======
This project is dual-licensed:
- **Open Source (AGPL-3.0)**: Free for open source projects
- **Commercial License**: Available for closed-source/commercial use
Contact: h@kotikot.com

View File

@@ -0,0 +1,20 @@
Components
==========
The components package provides classes for working with macOS Audio Unit components and bundles.
AudioComponent
--------------
.. automodule:: logic_plugin_manager.components.audiocomponent
:members:
:show-inheritance:
:undoc-members:
Component
---------
.. automodule:: logic_plugin_manager.components.component
:members:
:show-inheritance:
:undoc-members:

View File

@@ -0,0 +1,20 @@
Logic Management
================
The logic package provides the main interface for plugin management and search functionality.
Logic Class
-----------
.. automodule:: logic_plugin_manager.logic.logic
:members:
:show-inheritance:
:undoc-members:
Plugins Collection
------------------
.. automodule:: logic_plugin_manager.logic.plugins
:members:
:show-inheritance:
:undoc-members:

View File

@@ -0,0 +1,28 @@
API Reference
=============
Core Modules
------------
.. toctree::
:maxdepth: 2
logic_plugin_manager.components
logic_plugin_manager.logic
logic_plugin_manager.tags
Defaults
--------
.. automodule:: logic_plugin_manager.defaults
:members:
:show-inheritance:
:undoc-members:
Exceptions
----------
.. automodule:: logic_plugin_manager.exceptions
:members:
:show-inheritance:
:undoc-members:

View File

@@ -0,0 +1,28 @@
Tags & Categories
=================
The tags package manages Logic Pro's category system and tag database.
Category Management
-------------------
.. automodule:: logic_plugin_manager.tags.category
:members:
:show-inheritance:
:undoc-members:
MusicApps Database
------------------
.. automodule:: logic_plugin_manager.tags.musicapps
:members:
:show-inheritance:
:undoc-members:
Tagset Files
------------
.. automodule:: logic_plugin_manager.tags.tagset
:members:
:show-inheritance:
:undoc-members:

35
docs/make.bat Normal file
View File

@@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

3
docs/requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
sphinx
furo
sphinx-autodoc-typehints