0
# Import and Autotag System
1
2
Automated music import with metadata matching, user interaction for ambiguous matches, and integration with multiple metadata sources for comprehensive tagging. The import system is the primary way music gets into a beets library with correct metadata.
3
4
## Capabilities
5
6
### Import Session Management
7
8
Central class that controls the import process, handles user interaction, and manages import decisions.
9
10
```python { .api }
11
class ImportSession:
12
def __init__(self, lib: Library, loghandler: logging.Handler = None, config: dict = None):
13
"""
14
Initialize an import session.
15
16
Parameters:
17
- lib: Library instance to import into
18
- loghandler: Optional logging handler for import messages
19
- config: Optional configuration overrides
20
"""
21
22
def run(self) -> None:
23
"""
24
Execute the import process for all configured paths.
25
Processes files, matches metadata, and handles user interaction.
26
"""
27
28
def choose_match(self, task: ImportTask) -> action:
29
"""
30
Handle match selection for an import task.
31
32
Parameters:
33
- task: ImportTask with potential matches
34
35
Returns:
36
Action constant (APPLY, ASIS, SKIP, etc.)
37
"""
38
39
def choose_item(self, task: ImportTask, item: Item) -> action:
40
"""
41
Handle individual item decisions during import.
42
43
Parameters:
44
- task: ImportTask containing the item
45
- item: Specific Item being processed
46
47
Returns:
48
Action constant for this item
49
"""
50
51
def resolve_duplicate(self, task: ImportTask, found_duplicates: List[Item]) -> action:
52
"""
53
Handle duplicate resolution during import.
54
55
Parameters:
56
- task: ImportTask that found duplicates
57
- found_duplicates: List of existing items that match
58
59
Returns:
60
Action to take for the duplicates
61
"""
62
```
63
64
### Import Task Types
65
66
Different task types for handling various import scenarios.
67
68
```python { .api }
69
class ImportTask:
70
"""Base class for import tasks representing album imports."""
71
72
def __init__(self, toppath: str, paths: List[str], items: List[Item]):
73
"""
74
Initialize import task.
75
76
Parameters:
77
- toppath: Top-level directory being imported
78
- paths: List of file paths in the album
79
- items: List of Item objects created from files
80
"""
81
82
def lookup_candidates(self) -> List[AlbumInfo]:
83
"""
84
Look up metadata candidates for this album.
85
86
Returns:
87
List of AlbumInfo objects with potential matches
88
"""
89
90
def match_candidates(self, candidates: List[AlbumInfo]) -> List[Tuple[AlbumInfo, Distance]]:
91
"""
92
Score metadata candidates against the actual items.
93
94
Parameters:
95
- candidates: List of AlbumInfo objects to score
96
97
Returns:
98
List of (AlbumInfo, Distance) tuples sorted by match quality
99
"""
100
101
class SingletonImportTask(ImportTask):
102
"""Import task for individual tracks (not part of an album)."""
103
104
def lookup_candidates(self) -> List[TrackInfo]:
105
"""
106
Look up metadata candidates for this single track.
107
108
Returns:
109
List of TrackInfo objects with potential matches
110
"""
111
112
class ArchiveImportTask(ImportTask):
113
"""Import task for compressed archives (ZIP, RAR, etc.)."""
114
115
def extract(self, dest: str) -> str:
116
"""
117
Extract archive contents to destination directory.
118
119
Parameters:
120
- dest: Destination directory for extraction
121
122
Returns:
123
Path to extracted contents
124
"""
125
```
126
127
### Import Actions
128
129
Constants defining possible actions during import process.
130
131
```python { .api }
132
# Import action constants
133
SKIP = 'skip' # Skip importing this item/album
134
ASIS = 'asis' # Import as-is without metadata changes
135
TRACKS = 'tracks' # Import as individual tracks, not album
136
APPLY = 'apply' # Apply the selected metadata match
137
ALBUMS = 'albums' # Group items into albums during import
138
RETAG = 'retag' # Re-tag existing items with new metadata
139
```
140
141
### Core Autotag Functions
142
143
High-level functions for automated metadata tagging.
144
145
```python { .api }
146
def tag_album(items: List[Item], search_artist: str = None, search_album: str = None) -> Tuple[List[AlbumInfo], List[TrackInfo]]:
147
"""
148
Automatically tag an album by searching metadata sources.
149
150
Parameters:
151
- items: List of Item objects representing album tracks
152
- search_artist: Optional artist hint for search
153
- search_album: Optional album title hint for search
154
155
Returns:
156
Tuple of (album_matches, track_matches) with scoring information
157
"""
158
159
def tag_item(item: Item, search_artist: str = None, search_title: str = None) -> Tuple[List[TrackInfo], Distance]:
160
"""
161
Automatically tag a single item by searching metadata sources.
162
163
Parameters:
164
- item: Item object to tag
165
- search_artist: Optional artist hint for search
166
- search_title: Optional title hint for search
167
168
Returns:
169
Tuple of (track_matches, best_distance) with match information
170
"""
171
172
def apply_metadata(info: Union[AlbumInfo, TrackInfo], items: List[Item]) -> None:
173
"""
174
Apply metadata from AlbumInfo or TrackInfo to items.
175
176
Parameters:
177
- info: AlbumInfo or TrackInfo object with metadata
178
- items: List of Item objects to update
179
"""
180
```
181
182
### Metadata Information Classes
183
184
Classes representing metadata retrieved from various sources.
185
186
```python { .api }
187
class AlbumInfo:
188
"""Album metadata information from external sources."""
189
190
# Core album fields
191
album: str # Album title
192
artist: str # Album artist
193
albumtype: str # Album type (album, single, EP, etc.)
194
albumtypes: List[str] # List of album types
195
va: bool # Various artists album
196
year: int # Release year
197
month: int # Release month
198
day: int # Release day
199
country: str # Release country
200
label: str # Record label
201
catalognum: str # Catalog number
202
asin: str # Amazon ASIN
203
albumdisambig: str # Disambiguation string
204
releasegroupid: str # Release group ID
205
albumid: str # Album ID from source
206
albumstatus: str # Release status
207
media: str # Media format
208
albumdisambig: str # Album disambiguation
209
210
# Track information
211
tracks: List[TrackInfo] # List of track metadata
212
213
def __init__(self, **kwargs):
214
"""Initialize with metadata fields as keyword arguments."""
215
216
class TrackInfo:
217
"""Track metadata information from external sources."""
218
219
# Core track fields
220
title: str # Track title
221
artist: str # Track artist
222
length: float # Duration in seconds
223
track: int # Track number
224
disc: int # Disc number
225
medium: int # Medium number
226
medium_index: int # Index within medium
227
medium_total: int # Total tracks on medium
228
artist_sort: str # Artist sort name
229
disctitle: str # Disc title
230
trackid: str # Track ID from source
231
artistid: str # Artist ID from source
232
233
def __init__(self, **kwargs):
234
"""Initialize with metadata fields as keyword arguments."""
235
236
class Distance:
237
"""Represents similarity measurement between items and metadata."""
238
239
def __init__(self):
240
self.album_distance: float = 0.0 # Album-level distance
241
self.tracks: List[float] = [] # Per-track distances
242
self.unmatched_tracks: int = 0 # Number of unmatched tracks
243
self.extra_tracks: int = 0 # Number of extra tracks
244
245
@property
246
def distance(self) -> float:
247
"""Overall distance score (lower is better match)."""
248
249
@property
250
def max_distance(self) -> float:
251
"""Maximum possible distance for normalization."""
252
```
253
254
### Match Classes
255
256
Classes representing matching results with scoring information.
257
258
```python { .api }
259
class AlbumMatch:
260
"""Represents a potential album match with distance scoring."""
261
262
def __init__(self, info: AlbumInfo, distance: Distance, items: List[Item]):
263
"""
264
Initialize album match.
265
266
Parameters:
267
- info: AlbumInfo object with metadata
268
- distance: Distance object with match scoring
269
- items: List of Item objects being matched
270
"""
271
272
@property
273
def goodness(self) -> float:
274
"""Match quality score (higher is better)."""
275
276
class TrackMatch:
277
"""Represents a potential track match with distance scoring."""
278
279
def __init__(self, info: TrackInfo, distance: float, item: Item):
280
"""
281
Initialize track match.
282
283
Parameters:
284
- info: TrackInfo object with metadata
285
- distance: Distance score for this match
286
- item: Item object being matched
287
"""
288
289
# Match quality levels
290
class Recommendation:
291
"""Enumeration of match recommendation levels."""
292
STRONG = 'strong' # High confidence match
293
MEDIUM = 'medium' # Moderate confidence match
294
LOW = 'low' # Low confidence match
295
NONE = 'none' # No good matches found
296
```
297
298
## Import Workflow Examples
299
300
### Basic Import Session
301
302
```python
303
from beets.library import Library
304
from beets.importer import ImportSession
305
306
# Create library and import session
307
lib = Library('/path/to/library.db', '/music')
308
session = ImportSession(lib)
309
310
# Configure paths to import
311
session.paths = ['/path/to/new/music']
312
313
# Run import process (interactive)
314
session.run()
315
```
316
317
### Programmatic Import Control
318
319
```python
320
from beets.importer import ImportSession, action
321
from beets.library import Library
322
323
class AutoImportSession(ImportSession):
324
"""Custom import session with automatic decisions."""
325
326
def choose_match(self, task):
327
"""Always apply the best match if confidence is high."""
328
if task.rec == task.rec.STRONG:
329
return action.APPLY
330
else:
331
return action.SKIP
332
333
def choose_item(self, task, item):
334
"""Auto-apply strong track matches."""
335
if task.item_match.goodness > 0.8:
336
return action.APPLY
337
else:
338
return action.ASIS
339
340
# Use custom session
341
lib = Library('/path/to/library.db', '/music')
342
session = AutoImportSession(lib)
343
session.paths = ['/path/to/new/music']
344
session.run()
345
```
346
347
### Manual Tagging
348
349
```python
350
from beets.autotag import tag_album, tag_item, apply_metadata
351
from beets.library import Item
352
353
# Tag an album manually
354
items = [Item.from_path(p) for p in album_paths]
355
album_matches, track_matches = tag_album(items)
356
357
if album_matches:
358
best_match = album_matches[0] # Highest scoring match
359
apply_metadata(best_match.info, items)
360
361
# Add to library
362
for item in items:
363
lib.add(item)
364
365
# Tag individual track
366
item = Item.from_path('/path/to/track.mp3')
367
track_matches, distance = tag_item(item)
368
369
if track_matches and distance.distance < 0.1: # Good match
370
apply_metadata(track_matches[0], [item])
371
lib.add(item)
372
```
373
374
### Custom Metadata Sources
375
376
```python
377
from beets.plugins import MetadataSourcePlugin
378
from beets.autotag.hooks import AlbumInfo, TrackInfo
379
380
class CustomMetadataSource(MetadataSourcePlugin):
381
"""Example custom metadata source plugin."""
382
383
def get_albums(self, query: str) -> Iterator[AlbumInfo]:
384
"""Search for albums from custom source."""
385
# Implement custom album search
386
results = self.search_custom_api(query)
387
388
for result in results:
389
yield AlbumInfo(
390
album=result['title'],
391
artist=result['artist'],
392
year=result['year'],
393
tracks=[
394
TrackInfo(
395
title=track['title'],
396
artist=track['artist'],
397
track=track['number']
398
) for track in result['tracks']
399
]
400
)
401
402
def get_tracks(self, query: str) -> Iterator[TrackInfo]:
403
"""Search for individual tracks."""
404
# Implement custom track search
405
pass
406
```
407
408
## Configuration Options
409
410
### Import Configuration
411
412
```python
413
# Access import configuration
414
from beets import config
415
416
# Common import settings
417
write_tags = config['import']['write'].get(bool) # Write tags to files
418
copy_files = config['import']['copy'].get(bool) # Copy vs move files
419
resume = config['import']['resume'].get(bool) # Resume interrupted imports
420
incremental = config['import']['incremental'].get(bool) # Skip unchanged directories
421
quiet_fallback = config['import']['quiet_fallback'].get(bool) # Auto-accept on timeout
422
423
# Metadata source configuration
424
sources = config['import']['sources'].get(list) # Enabled metadata sources
425
search_ids = config['import']['search_ids'].get(list) # IDs to search for
426
```
427
428
### Path Configuration
429
430
```python
431
# Path format configuration
432
path_formats = config['paths'].get(dict)
433
434
# Example path formats
435
default_format = config['paths']['default'].get(str) # Default path template
436
album_format = config['paths']['albumtype:album'].get(str) # Album-specific format
437
comp_format = config['paths']['comp'].get(str) # Compilation format
438
```
439
440
## Error Handling
441
442
```python { .api }
443
class ImportAbortError(Exception):
444
"""Raised when import process is aborted by user."""
445
446
class ImportDuplicateAlbumError(Exception):
447
"""Raised when attempting to import duplicate album."""
448
```
449
450
Common import issues:
451
- Corrupt or unreadable audio files
452
- Network timeouts when fetching metadata
453
- Duplicate detection conflicts
454
- Insufficient disk space for copying files
455
- Permission errors when moving/copying files