or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration.mdimport-autotag.mdindex.mdlibrary-management.mdplugin-system.mdquery-system.mduser-interface.mdutilities-templates.md

plugin-system.mddocs/

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