CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-beets

A comprehensive music library management system and command-line application for organizing and maintaining digital music collections

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

plugin-system.mddocs/

Plugin System

Extensible plugin architecture with base classes, event system, and plugin management for extending beets functionality through custom commands, metadata sources, and processing hooks. The plugin system is how beets achieves its extensive functionality through 78+ built-in plugins.

Capabilities

BeetsPlugin Base Class

The foundation class that all beets plugins inherit from, providing the core plugin interface and lifecycle management.

class BeetsPlugin:
    def __init__(self, name: str):
        """
        Initialize a beets plugin.
        
        Parameters:
        - name: Plugin name for identification and configuration
        """
    
    def commands(self) -> List[Subcommand]:
        """
        Return list of CLI commands provided by this plugin.
        
        Returns:
        List of Subcommand objects that will be added to beets CLI
        """
    
    def template_funcs(self) -> Dict[str, Callable]:
        """
        Return template functions for path formatting.
        
        Returns:
        Dictionary mapping function names to callable functions
        """
    
    def item_types(self) -> Dict[str, Type]:
        """
        Return field types for Item model extensions.
        
        Returns:
        Dictionary mapping field names to database type objects
        """
    
    def album_types(self) -> Dict[str, Type]:
        """
        Return field types for Album model extensions.
        
        Returns:
        Dictionary mapping field names to database type objects
        """
    
    def queries(self) -> Dict[str, Type]:
        """
        Return custom query types for database searches.
        
        Returns:
        Dictionary mapping query prefixes to Query class types
        """
    
    def album_distance(self, items: List[Item], album_info: AlbumInfo) -> float:
        """
        Calculate custom distance for album matching.
        
        Parameters:
        - items: List of Item objects being matched
        - album_info: AlbumInfo candidate from metadata source
        
        Returns:
        Distance value (lower means better match)
        """
    
    def track_distance(self, item: Item, track_info: TrackInfo) -> float:
        """
        Calculate custom distance for track matching.
        
        Parameters:
        - item: Item object being matched
        - track_info: TrackInfo candidate from metadata source
        
        Returns:
        Distance value (lower means better match)
        """

MetadataSourcePlugin Base Class

Specialized base class for plugins that provide metadata from external sources.

class MetadataSourcePlugin(BeetsPlugin):
    """Base class for plugins that provide metadata sources."""
    
    def get_albums(self, query: str) -> Iterator[AlbumInfo]:
        """
        Search for album metadata.
        
        Parameters:
        - query: Search query string
        
        Returns:
        Iterator of AlbumInfo objects matching the query
        """
    
    def get_tracks(self, query: str) -> Iterator[TrackInfo]:
        """
        Search for track metadata.
        
        Parameters:
        - query: Search query string
        
        Returns:
        Iterator of TrackInfo objects matching the query
        """
    
    def candidates(self, items: List[Item], artist: str, album: str) -> Iterator[AlbumInfo]:
        """
        Get album candidates for a set of items.
        
        Parameters:
        - items: List of Item objects to find matches for
        - artist: Artist hint for search
        - album: Album hint for search
        
        Returns:
        Iterator of AlbumInfo candidates
        """
    
    def item_candidates(self, item: Item, artist: str, title: str) -> Iterator[TrackInfo]:
        """
        Get track candidates for a single item.
        
        Parameters:
        - item: Item object to find matches for
        - artist: Artist hint for search
        - title: Title hint for search
        
        Returns:
        Iterator of TrackInfo candidates
        """

Plugin Management Functions

Functions for loading, discovering, and managing plugins.

def load_plugins(names: List[str]) -> None:
    """
    Load specified plugins by name.
    
    Parameters:
    - names: List of plugin names to load
    """

def find_plugins() -> List[str]:
    """
    Discover all available plugins.
    
    Returns:
    List of plugin names that can be loaded
    """

def commands() -> List[Subcommand]:
    """
    Get all CLI commands from loaded plugins.
    
    Returns:
    List of Subcommand objects from all plugins
    """

def types(model_cls: Type) -> Dict[str, Type]:
    """
    Get field types from all plugins for a model class.
    
    Parameters:
    - model_cls: Item or Album class
    
    Returns:
    Dictionary of field name to type mappings
    """

def named_queries(model_cls: Type) -> Dict[str, Query]:
    """
    Get named queries from all plugins for a model class.
    
    Parameters:
    - model_cls: Item or Album class
    
    Returns:
    Dictionary of query name to Query object mappings
    """

Event System

Plugin event system for hooking into beets operations.

def send(event: str, **kwargs) -> None:
    """
    Send event to all registered plugin listeners.
    
    Parameters:
    - event: Event name string
    - **kwargs: Event-specific parameters
    """

def listen(event: str, func: Callable) -> None:
    """
    Register function to listen for specific event.
    
    Parameters:
    - event: Event name to listen for
    - func: Callback function for event
    """

Common Plugin Events

# Library events
'library_opened'        # Library instance created
'database_change'       # Database schema changed

# Import events
'import_begin'          # Import process starting
'import_task_start'     # Individual import task starting  
'import_task_choice'    # User making import choice
'import_task_files'     # Files being processed
'album_imported'        # Album successfully imported
'item_imported'         # Item successfully imported
'import_task_end'       # Import task completed

# Item/Album events
'item_copied'           # Item file copied
'item_moved'            # Item file moved
'item_removed'          # Item removed from library
'item_written'          # Item metadata written to file
'album_removed'         # Album removed from library

# CLI events
'cli_exit'              # CLI command completed
'before_choose_candidate'  # Before metadata choice UI
'after_convert'         # After audio conversion

Plugin Examples

Basic Command Plugin

from beets.plugins import BeetsPlugin
from beets.ui import Subcommand, decargs

class HelloPlugin(BeetsPlugin):
    """Simple plugin adding a hello command."""
    
    def commands(self):
        hello_cmd = Subcommand('hello', help='Say hello')
        hello_cmd.func = self.hello_command
        return [hello_cmd]
    
    def hello_command(self, lib, opts, args):
        """Implementation of hello command."""
        name = args[0] if args else 'World'
        print(f"Hello, {name}!")

# Register plugin
hello_plugin = HelloPlugin('hello')

Field Extension Plugin

from beets.plugins import BeetsPlugin
from beets.dbcore.types import STRING, INTEGER

class CustomFieldsPlugin(BeetsPlugin):
    """Plugin adding custom fields to items."""
    
    def item_types(self):
        return {
            'myrating': INTEGER,      # Custom rating field
            'notes': STRING,          # Notes field
            'customtag': STRING,      # Custom tag field
        }
    
    def album_types(self):
        return {
            'albumrating': INTEGER,   # Album-level rating
        }

# Register plugin  
fields_plugin = CustomFieldsPlugin('customfields')

Event Listener Plugin

from beets.plugins import BeetsPlugin
from beets import plugins

class LoggingPlugin(BeetsPlugin):
    """Plugin that logs various beets events."""
    
    def __init__(self, name):
        super().__init__(name)
        
        # Register event listeners
        plugins.listen('item_imported', self.on_item_imported)
        plugins.listen('album_imported', self.on_album_imported)
        plugins.listen('item_moved', self.on_item_moved)
    
    def on_item_imported(self, lib, item):
        """Called when an item is imported."""
        print(f"Imported: {item.artist} - {item.title}")
    
    def on_album_imported(self, lib, album):
        """Called when an album is imported."""
        print(f"Album imported: {album.albumartist} - {album.album}")
    
    def on_item_moved(self, item, source, destination):
        """Called when an item file is moved."""
        print(f"Moved: {source} -> {destination}")

logging_plugin = LoggingPlugin('logging')

Metadata Source Plugin

from beets.plugins import MetadataSourcePlugin
from beets.autotag.hooks import AlbumInfo, TrackInfo
import requests

class CustomAPIPlugin(MetadataSourcePlugin):
    """Plugin providing metadata from custom API."""
    
    def get_albums(self, query):
        """Search for albums via custom API."""
        response = requests.get(f'https://api.example.com/albums?q={query}')
        
        for result in response.json()['albums']:
            yield AlbumInfo(
                album=result['title'],
                artist=result['artist'],
                year=result.get('year'),
                tracks=[
                    TrackInfo(
                        title=track['title'],
                        artist=track.get('artist', result['artist']),
                        track=track['number'],
                        length=track.get('duration')
                    ) for track in result.get('tracks', [])
                ]
            )
    
    def get_tracks(self, query):
        """Search for individual tracks."""
        response = requests.get(f'https://api.example.com/tracks?q={query}')
        
        for result in response.json()['tracks']:
            yield TrackInfo(
                title=result['title'],
                artist=result['artist'],
                length=result.get('duration'),
                trackid=str(result['id'])
            )

api_plugin = CustomAPIPlugin('customapi')

Template Function Plugin

from beets.plugins import BeetsPlugin
import re

class TemplatePlugin(BeetsPlugin):
    """Plugin adding custom template functions."""
    
    def template_funcs(self):
        return {
            'acronym': self.acronymize,
            'sanitize': self.sanitize_filename,
            'shorten': self.shorten_text,
        }
    
    def acronymize(self, text):
        """Convert text to acronym (first letters)."""
        words = text.split()
        return ''.join(word[0].upper() for word in words if word)
    
    def sanitize_filename(self, text):
        """Remove/replace invalid filename characters."""
        # Remove invalid filename characters
        return re.sub(r'[<>:"/\\|?*]', '', text)
    
    def shorten_text(self, text, length=20):
        """Shorten text to specified length."""
        if len(text) <= length:
            return text
        return text[:length-3] + '...'

template_plugin = TemplatePlugin('templates')

# Usage in path formats:
# paths:
#   default: $albumartist/$album/$track $title
#   short: %acronym{$albumartist}/%shorten{$album,15}/$track %sanitize{$title}

Query Type Plugin

from beets.plugins import BeetsPlugin
from beets.dbcore.query import StringQuery

class MyRatingQuery(StringQuery):
    """Custom query for rating ranges."""
    
    def __init__(self, field, pattern):
        # Convert rating range (e.g., "8..10") to appropriate query
        if '..' in pattern:
            start, end = pattern.split('..')
            # Implement range logic
            pattern = f">={start} AND <={end}"
        super().__init__(field, pattern)

class RatingPlugin(BeetsPlugin):
    """Plugin adding rating query support."""
    
    def queries(self):
        return {
            'myrating': MyRatingQuery,
        }

rating_plugin = RatingPlugin('rating')

# Usage: beet list myrating:8..10

Plugin Configuration

Plugin-Specific Configuration

from beets import config

class ConfigurablePlugin(BeetsPlugin):
    """Plugin with configuration options."""
    
    def __init__(self, name):
        super().__init__(name)
        
        # Set default configuration values
        config[name].add({
            'enabled': True,
            'api_key': '',
            'timeout': 10,
            'format': 'json'
        })
    
    def get_config(self, key, default=None):
        """Get plugin configuration value."""
        return config[self.name][key].get(default)
    
    def commands(self):
        # Only provide commands if enabled
        if self.get_config('enabled', True):
            return [self.create_command()]
        return []

Plugin Loading Configuration

# Configuration in config.yaml
plugins: 
  - fetchart
  - lyrics
  - discogs
  - mycustomplugin

# Plugin-specific configuration
fetchart:
  auto: yes
  sources: coverart lastfm amazon

mycustomplugin:
  enabled: true
  api_key: "your-api-key"
  timeout: 30

Built-in Plugin Categories

Metadata Sources

  • discogs: Discogs database integration
  • spotify: Spotify Web API integration
  • lastgenre: Last.fm genre tagging
  • acousticbrainz: AcousticBrainz audio analysis

File Management

  • fetchart: Album artwork fetching
  • embedart: Embed artwork in files
  • duplicates: Find and remove duplicates
  • missing: Find missing files
  • badfiles: Check file integrity

Audio Processing

  • replaygain: Calculate ReplayGain values
  • convert: Audio format conversion
  • chroma: Chromaprint fingerprinting
  • bpm: Tempo detection

Import Helpers

  • importadded: Track import timestamps
  • importfeeds: RSS/Atom feed monitoring
  • fromfilename: Guess metadata from filenames

External Integration

  • web: Web interface
  • mpdupdate: MPD integration
  • kodiupdate: Kodi library updates
  • plexupdate: Plex library updates

Error Handling

class PluginConflictError(Exception):
    """Raised when plugins conflict with each other."""

class PluginLoadError(Exception):
    """Raised when plugin cannot be loaded."""

Common plugin issues:

  • Missing dependencies for plugin functionality
  • Configuration errors or missing API keys
  • Conflicts between plugins modifying same fields
  • Network timeouts for web-based plugins
  • Permission errors for file operations

Install with Tessl CLI

npx tessl i tessl/pypi-beets

docs

configuration.md

import-autotag.md

index.md

library-management.md

plugin-system.md

query-system.md

user-interface.md

utilities-templates.md

tile.json