0
# Plugin System
1
2
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.
3
4
## Capabilities
5
6
### BeetsPlugin Base Class
7
8
The foundation class that all beets plugins inherit from, providing the core plugin interface and lifecycle management.
9
10
```python { .api }
11
class BeetsPlugin:
12
def __init__(self, name: str):
13
"""
14
Initialize a beets plugin.
15
16
Parameters:
17
- name: Plugin name for identification and configuration
18
"""
19
20
def commands(self) -> List[Subcommand]:
21
"""
22
Return list of CLI commands provided by this plugin.
23
24
Returns:
25
List of Subcommand objects that will be added to beets CLI
26
"""
27
28
def template_funcs(self) -> Dict[str, Callable]:
29
"""
30
Return template functions for path formatting.
31
32
Returns:
33
Dictionary mapping function names to callable functions
34
"""
35
36
def item_types(self) -> Dict[str, Type]:
37
"""
38
Return field types for Item model extensions.
39
40
Returns:
41
Dictionary mapping field names to database type objects
42
"""
43
44
def album_types(self) -> Dict[str, Type]:
45
"""
46
Return field types for Album model extensions.
47
48
Returns:
49
Dictionary mapping field names to database type objects
50
"""
51
52
def queries(self) -> Dict[str, Type]:
53
"""
54
Return custom query types for database searches.
55
56
Returns:
57
Dictionary mapping query prefixes to Query class types
58
"""
59
60
def album_distance(self, items: List[Item], album_info: AlbumInfo) -> float:
61
"""
62
Calculate custom distance for album matching.
63
64
Parameters:
65
- items: List of Item objects being matched
66
- album_info: AlbumInfo candidate from metadata source
67
68
Returns:
69
Distance value (lower means better match)
70
"""
71
72
def track_distance(self, item: Item, track_info: TrackInfo) -> float:
73
"""
74
Calculate custom distance for track matching.
75
76
Parameters:
77
- item: Item object being matched
78
- track_info: TrackInfo candidate from metadata source
79
80
Returns:
81
Distance value (lower means better match)
82
"""
83
```
84
85
### MetadataSourcePlugin Base Class
86
87
Specialized base class for plugins that provide metadata from external sources.
88
89
```python { .api }
90
class MetadataSourcePlugin(BeetsPlugin):
91
"""Base class for plugins that provide metadata sources."""
92
93
def get_albums(self, query: str) -> Iterator[AlbumInfo]:
94
"""
95
Search for album metadata.
96
97
Parameters:
98
- query: Search query string
99
100
Returns:
101
Iterator of AlbumInfo objects matching the query
102
"""
103
104
def get_tracks(self, query: str) -> Iterator[TrackInfo]:
105
"""
106
Search for track metadata.
107
108
Parameters:
109
- query: Search query string
110
111
Returns:
112
Iterator of TrackInfo objects matching the query
113
"""
114
115
def candidates(self, items: List[Item], artist: str, album: str) -> Iterator[AlbumInfo]:
116
"""
117
Get album candidates for a set of items.
118
119
Parameters:
120
- items: List of Item objects to find matches for
121
- artist: Artist hint for search
122
- album: Album hint for search
123
124
Returns:
125
Iterator of AlbumInfo candidates
126
"""
127
128
def item_candidates(self, item: Item, artist: str, title: str) -> Iterator[TrackInfo]:
129
"""
130
Get track candidates for a single item.
131
132
Parameters:
133
- item: Item object to find matches for
134
- artist: Artist hint for search
135
- title: Title hint for search
136
137
Returns:
138
Iterator of TrackInfo candidates
139
"""
140
```
141
142
### Plugin Management Functions
143
144
Functions for loading, discovering, and managing plugins.
145
146
```python { .api }
147
def load_plugins(names: List[str]) -> None:
148
"""
149
Load specified plugins by name.
150
151
Parameters:
152
- names: List of plugin names to load
153
"""
154
155
def find_plugins() -> List[str]:
156
"""
157
Discover all available plugins.
158
159
Returns:
160
List of plugin names that can be loaded
161
"""
162
163
def commands() -> List[Subcommand]:
164
"""
165
Get all CLI commands from loaded plugins.
166
167
Returns:
168
List of Subcommand objects from all plugins
169
"""
170
171
def types(model_cls: Type) -> Dict[str, Type]:
172
"""
173
Get field types from all plugins for a model class.
174
175
Parameters:
176
- model_cls: Item or Album class
177
178
Returns:
179
Dictionary of field name to type mappings
180
"""
181
182
def named_queries(model_cls: Type) -> Dict[str, Query]:
183
"""
184
Get named queries from all plugins for a model class.
185
186
Parameters:
187
- model_cls: Item or Album class
188
189
Returns:
190
Dictionary of query name to Query object mappings
191
"""
192
```
193
194
### Event System
195
196
Plugin event system for hooking into beets operations.
197
198
```python { .api }
199
def send(event: str, **kwargs) -> None:
200
"""
201
Send event to all registered plugin listeners.
202
203
Parameters:
204
- event: Event name string
205
- **kwargs: Event-specific parameters
206
"""
207
208
def listen(event: str, func: Callable) -> None:
209
"""
210
Register function to listen for specific event.
211
212
Parameters:
213
- event: Event name to listen for
214
- func: Callback function for event
215
"""
216
```
217
218
#### Common Plugin Events
219
220
```python { .api }
221
# Library events
222
'library_opened' # Library instance created
223
'database_change' # Database schema changed
224
225
# Import events
226
'import_begin' # Import process starting
227
'import_task_start' # Individual import task starting
228
'import_task_choice' # User making import choice
229
'import_task_files' # Files being processed
230
'album_imported' # Album successfully imported
231
'item_imported' # Item successfully imported
232
'import_task_end' # Import task completed
233
234
# Item/Album events
235
'item_copied' # Item file copied
236
'item_moved' # Item file moved
237
'item_removed' # Item removed from library
238
'item_written' # Item metadata written to file
239
'album_removed' # Album removed from library
240
241
# CLI events
242
'cli_exit' # CLI command completed
243
'before_choose_candidate' # Before metadata choice UI
244
'after_convert' # After audio conversion
245
```
246
247
## Plugin Examples
248
249
### Basic Command Plugin
250
251
```python
252
from beets.plugins import BeetsPlugin
253
from beets.ui import Subcommand, decargs
254
255
class HelloPlugin(BeetsPlugin):
256
"""Simple plugin adding a hello command."""
257
258
def commands(self):
259
hello_cmd = Subcommand('hello', help='Say hello')
260
hello_cmd.func = self.hello_command
261
return [hello_cmd]
262
263
def hello_command(self, lib, opts, args):
264
"""Implementation of hello command."""
265
name = args[0] if args else 'World'
266
print(f"Hello, {name}!")
267
268
# Register plugin
269
hello_plugin = HelloPlugin('hello')
270
```
271
272
### Field Extension Plugin
273
274
```python
275
from beets.plugins import BeetsPlugin
276
from beets.dbcore.types import STRING, INTEGER
277
278
class CustomFieldsPlugin(BeetsPlugin):
279
"""Plugin adding custom fields to items."""
280
281
def item_types(self):
282
return {
283
'myrating': INTEGER, # Custom rating field
284
'notes': STRING, # Notes field
285
'customtag': STRING, # Custom tag field
286
}
287
288
def album_types(self):
289
return {
290
'albumrating': INTEGER, # Album-level rating
291
}
292
293
# Register plugin
294
fields_plugin = CustomFieldsPlugin('customfields')
295
```
296
297
### Event Listener Plugin
298
299
```python
300
from beets.plugins import BeetsPlugin
301
from beets import plugins
302
303
class LoggingPlugin(BeetsPlugin):
304
"""Plugin that logs various beets events."""
305
306
def __init__(self, name):
307
super().__init__(name)
308
309
# Register event listeners
310
plugins.listen('item_imported', self.on_item_imported)
311
plugins.listen('album_imported', self.on_album_imported)
312
plugins.listen('item_moved', self.on_item_moved)
313
314
def on_item_imported(self, lib, item):
315
"""Called when an item is imported."""
316
print(f"Imported: {item.artist} - {item.title}")
317
318
def on_album_imported(self, lib, album):
319
"""Called when an album is imported."""
320
print(f"Album imported: {album.albumartist} - {album.album}")
321
322
def on_item_moved(self, item, source, destination):
323
"""Called when an item file is moved."""
324
print(f"Moved: {source} -> {destination}")
325
326
logging_plugin = LoggingPlugin('logging')
327
```
328
329
### Metadata Source Plugin
330
331
```python
332
from beets.plugins import MetadataSourcePlugin
333
from beets.autotag.hooks import AlbumInfo, TrackInfo
334
import requests
335
336
class CustomAPIPlugin(MetadataSourcePlugin):
337
"""Plugin providing metadata from custom API."""
338
339
def get_albums(self, query):
340
"""Search for albums via custom API."""
341
response = requests.get(f'https://api.example.com/albums?q={query}')
342
343
for result in response.json()['albums']:
344
yield AlbumInfo(
345
album=result['title'],
346
artist=result['artist'],
347
year=result.get('year'),
348
tracks=[
349
TrackInfo(
350
title=track['title'],
351
artist=track.get('artist', result['artist']),
352
track=track['number'],
353
length=track.get('duration')
354
) for track in result.get('tracks', [])
355
]
356
)
357
358
def get_tracks(self, query):
359
"""Search for individual tracks."""
360
response = requests.get(f'https://api.example.com/tracks?q={query}')
361
362
for result in response.json()['tracks']:
363
yield TrackInfo(
364
title=result['title'],
365
artist=result['artist'],
366
length=result.get('duration'),
367
trackid=str(result['id'])
368
)
369
370
api_plugin = CustomAPIPlugin('customapi')
371
```
372
373
### Template Function Plugin
374
375
```python
376
from beets.plugins import BeetsPlugin
377
import re
378
379
class TemplatePlugin(BeetsPlugin):
380
"""Plugin adding custom template functions."""
381
382
def template_funcs(self):
383
return {
384
'acronym': self.acronymize,
385
'sanitize': self.sanitize_filename,
386
'shorten': self.shorten_text,
387
}
388
389
def acronymize(self, text):
390
"""Convert text to acronym (first letters)."""
391
words = text.split()
392
return ''.join(word[0].upper() for word in words if word)
393
394
def sanitize_filename(self, text):
395
"""Remove/replace invalid filename characters."""
396
# Remove invalid filename characters
397
return re.sub(r'[<>:"/\\|?*]', '', text)
398
399
def shorten_text(self, text, length=20):
400
"""Shorten text to specified length."""
401
if len(text) <= length:
402
return text
403
return text[:length-3] + '...'
404
405
template_plugin = TemplatePlugin('templates')
406
407
# Usage in path formats:
408
# paths:
409
# default: $albumartist/$album/$track $title
410
# short: %acronym{$albumartist}/%shorten{$album,15}/$track %sanitize{$title}
411
```
412
413
### Query Type Plugin
414
415
```python
416
from beets.plugins import BeetsPlugin
417
from beets.dbcore.query import StringQuery
418
419
class MyRatingQuery(StringQuery):
420
"""Custom query for rating ranges."""
421
422
def __init__(self, field, pattern):
423
# Convert rating range (e.g., "8..10") to appropriate query
424
if '..' in pattern:
425
start, end = pattern.split('..')
426
# Implement range logic
427
pattern = f">={start} AND <={end}"
428
super().__init__(field, pattern)
429
430
class RatingPlugin(BeetsPlugin):
431
"""Plugin adding rating query support."""
432
433
def queries(self):
434
return {
435
'myrating': MyRatingQuery,
436
}
437
438
rating_plugin = RatingPlugin('rating')
439
440
# Usage: beet list myrating:8..10
441
```
442
443
## Plugin Configuration
444
445
### Plugin-Specific Configuration
446
447
```python
448
from beets import config
449
450
class ConfigurablePlugin(BeetsPlugin):
451
"""Plugin with configuration options."""
452
453
def __init__(self, name):
454
super().__init__(name)
455
456
# Set default configuration values
457
config[name].add({
458
'enabled': True,
459
'api_key': '',
460
'timeout': 10,
461
'format': 'json'
462
})
463
464
def get_config(self, key, default=None):
465
"""Get plugin configuration value."""
466
return config[self.name][key].get(default)
467
468
def commands(self):
469
# Only provide commands if enabled
470
if self.get_config('enabled', True):
471
return [self.create_command()]
472
return []
473
```
474
475
### Plugin Loading Configuration
476
477
```yaml
478
# Configuration in config.yaml
479
plugins:
480
- fetchart
481
- lyrics
482
- discogs
483
- mycustomplugin
484
485
# Plugin-specific configuration
486
fetchart:
487
auto: yes
488
sources: coverart lastfm amazon
489
490
mycustomplugin:
491
enabled: true
492
api_key: "your-api-key"
493
timeout: 30
494
```
495
496
## Built-in Plugin Categories
497
498
### Metadata Sources
499
- **discogs**: Discogs database integration
500
- **spotify**: Spotify Web API integration
501
- **lastgenre**: Last.fm genre tagging
502
- **acousticbrainz**: AcousticBrainz audio analysis
503
504
### File Management
505
- **fetchart**: Album artwork fetching
506
- **embedart**: Embed artwork in files
507
- **duplicates**: Find and remove duplicates
508
- **missing**: Find missing files
509
- **badfiles**: Check file integrity
510
511
### Audio Processing
512
- **replaygain**: Calculate ReplayGain values
513
- **convert**: Audio format conversion
514
- **chroma**: Chromaprint fingerprinting
515
- **bpm**: Tempo detection
516
517
### Import Helpers
518
- **importadded**: Track import timestamps
519
- **importfeeds**: RSS/Atom feed monitoring
520
- **fromfilename**: Guess metadata from filenames
521
522
### External Integration
523
- **web**: Web interface
524
- **mpdupdate**: MPD integration
525
- **kodiupdate**: Kodi library updates
526
- **plexupdate**: Plex library updates
527
528
## Error Handling
529
530
```python { .api }
531
class PluginConflictError(Exception):
532
"""Raised when plugins conflict with each other."""
533
534
class PluginLoadError(Exception):
535
"""Raised when plugin cannot be loaded."""
536
```
537
538
Common plugin issues:
539
- Missing dependencies for plugin functionality
540
- Configuration errors or missing API keys
541
- Conflicts between plugins modifying same fields
542
- Network timeouts for web-based plugins
543
- Permission errors for file operations