A comprehensive music library management system and command-line application for organizing and maintaining digital music collections
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
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.
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)
"""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
"""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
"""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
"""# 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 conversionfrom 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')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')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')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')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}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..10from 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 []# 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: 30class PluginConflictError(Exception):
"""Raised when plugins conflict with each other."""
class PluginLoadError(Exception):
"""Raised when plugin cannot be loaded."""Common plugin issues:
Install with Tessl CLI
npx tessl i tessl/pypi-beets