Library Database API

This page describes the internal API of beets’ core database features. It doesn’t exhaustively document the API, but is aimed at giving an overview of the architecture to orient anyone who wants to dive into the code.

The Library object is the central repository for data in beets. It represents a database containing songs, which are Item instances, and groups of items, which are Album instances.

The Library Class

The Library is typically instantiated as a singleton. A single invocation of beets usually has only one Library. It’s powered by dbcore.Database under the hood, which handles the SQLite abstraction, something like a very minimal ORM. The library is also responsible for handling queries to retrieve stored objects.

class beets.library.Library(path, directory[, path_formats[, replacements]])

A database of music containing songs and albums.

__init__(path='library.blb', directory='~/Music', path_formats=(('default', '$artist/$album/$track $title'), ), replacements=None)

x.__init__(…) initializes x; see help(type(x)) for signature

You can add new items or albums to the library:

add(obj)

Add the Item or Album object to the library database. Return the object’s new id.

add_album(items)

Create a new album consisting of a list of items.

The items are added to the database if they don’t yet have an ID. Return a new Album object. The list items must not be empty.

And there are methods for querying the database:

items(query=None, sort=None)

Get Item objects matching the query.

albums(query=None, sort=None)

Get Album objects matching the query.

get_item(id)

Fetch an Item by its ID. Returns None if no match is found.

get_album(item_or_id)

Given an album ID or an item associated with an album, return an Album object for the album. If no such album exists, returns None.

Any modifications must go through a Transaction which you get can using this method:

transaction()

Get a Transaction object for interacting directly with the underlying SQLite database.

Model Classes

The two model entities in beets libraries, Item and Album, share a base class, LibModel, that provides common functionality. That class itself specialises dbcore.Model which provides an ORM-like abstraction.

To get or change the metadata of a model (an item or album), either access its attributes (e.g., print(album.year) or album.year = 2012) or use the dict-like interface (e.g. item['artist']).

Model base

Models use dirty-flags to track when the object’s metadata goes out of sync with the database. The dirty dictionary maps field names to booleans indicating whether the field has been written since the object was last synchronized (via load or store) with the database.

class beets.library.LibModel(db=None, **values)

Shared concrete functionality for Items and Albums.

classmethod all_keys()

Get a list of available keys for objects of this type. Includes fixed and computed fields.

__init__(db=None, **values)

Create a new object with an optional Database association and initial field values.

_types = {}
_fields = {}

There are CRUD-like methods for interacting with the database:

store(fields=None)

Save the object’s metadata into the library database. :param fields: the fields to be stored. If not specified, all fields will be.

load()

Refresh the object’s metadata from the library database.

remove()

Remove the object’s associated rows from the database.

add(lib=None)

Add the object to the library database. This object must be associated with a database; you can provide one via the db parameter or use the currently associated database.

The object’s id and added fields are set along with any current field values.

The base class dbcore.Model has a dict-like interface, so normal the normal mapping API is supported:

keys(computed=False)

Get a list of available field names for this object. The computed parameter controls whether computed (plugin-provided) fields are included in the key list.

update(values)

Assign all values in the given dict.

items()

Iterate over (key, value) pairs that this object contains. Computed fields are not included.

get(key, default=None)

Get the value for a given key or default if it does not exist.

Item

Each Item object represents a song or track. (We use the more generic term item because, one day, beets might support non-music media.) An item can either be purely abstract, in which case it’s just a bag of metadata fields, or it can have an associated file (indicated by item.path).

In terms of the underlying SQLite database, items are backed by a single table called items with one column per metadata fields. The metadata fields currently in use are listed in library.py in Item._fields.

To read and write a file’s tags, we use the MediaFile library. To make changes to either the database or the tags on a file, you update an item’s fields (e.g., item.title = "Let It Be") and then call item.write().

Items also track their modification times (mtimes) to help detect when they become out of sync with on-disk metadata, mainly to speed up the update (which needs to check whether the database is in sync with the filesystem). This feature turns out to be sort of complicated.

For any Item, there are two mtimes: the on-disk mtime (maintained by the OS) and the database mtime (maintained by beets). Correspondingly, there is on-disk metadata (ID3 tags, for example) and DB metadata. The goal with the mtime is to ensure that the on-disk and DB mtimes match when the on-disk and DB metadata are in sync; this lets beets do a quick mtime check and avoid rereading files in some circumstances.

Specifically, beets attempts to maintain the following invariant:

If the on-disk metadata differs from the DB metadata, then the on-disk mtime must be greater than the DB mtime.

As a result, it is always valid for the DB mtime to be zero (assuming that real disk mtimes are always positive). However, whenever possible, beets tries to set db_mtime = disk_mtime at points where it knows the metadata is synchronized. When it is possible that the metadata is out of sync, beets can then just set db_mtime = 0 to return to a consistent state.

This leads to the following implementation policy:

  • On every write of disk metadata (Item.write()), the DB mtime is updated to match the post-write disk mtime.
  • Same for metadata reads (Item.read()).
  • On every modification to DB metadata (item.field = ...), the DB mtime is reset to zero.
class beets.library.Item(db=None, **values)
__init__(db=None, **values)

Create a new object with an optional Database association and initial field values.

classmethod from_path(path)

Creates a new item from the media file at the specified path.

get_album()

Get the Album object that this item belongs to, if any, or None if the item is a singleton or is not associated with a library.

destination(fragment=False, basedir=None, platform=None, path_formats=None)

Returns the path in the library directory designated for the item (i.e., where the file ought to be). fragment makes this method return just the path fragment underneath the root library directory; the path is also returned as Unicode instead of encoded as a bytestring. basedir can override the library’s base directory for the destination.

current_mtime()

Returns the current mtime of the file, rounded to the nearest integer.

The methods read() and write() are complementary: one reads a file’s tags and updates the item’s metadata fields accordingly while the other takes the item’s fields and writes them to the file’s tags.

read(read_path=None)

Read the metadata from the associated file.

If read_path is specified, read metadata from that file instead. Updates all the properties in _media_fields from the media file.

Raises a ReadError if the file could not be read.

write(path=None, tags=None, id3v23=None)

Write the item’s metadata to a media file.

All fields in _media_fields are written to disk according to the values on this object.

path is the path of the mediafile to write the data to. It defaults to the item’s path.

tags is a dictionary of additional metadata the should be written to the file. (These tags need not be in _media_fields.)

id3v23 will override the global id3v23 config option if it is set to something other than None.

Can raise either a ReadError or a WriteError.

try_write(*args, **kwargs)

Calls write() but catches and logs FileOperationError exceptions.

Returns False an exception was caught and True otherwise.

try_sync(write, move, with_album=True)

Synchronize the item with the database and, possibly, updates its tags on disk and its path (by moving the file).

write indicates whether to write new tags into the file. Similarly, move controls whether the path should be updated. In the latter case, files are only moved when they are inside their library’s directory (if any).

Similar to calling write(), move(), and store() (conditionally).

The Item class supplements the normal model interface so that they interacting with the filesystem as well:

move(operation=<MoveOperation.MOVE: 0>, basedir=None, with_album=True, store=True)

Move the item to its designated location within the library directory (provided by destination()). Subdirectories are created as needed. If the operation succeeds, the item’s path field is updated to reflect the new location.

Instead of moving the item it can also be copied, linked or hardlinked depending on operation which should be an instance of util.MoveOperation.

basedir overrides the library base directory for the destination.

If the item is in an album and with_album is True, the album is given an opportunity to move its art.

By default, the item is stored to the database if it is in the database, so any dirty fields prior to the move() call will be written as a side effect. If store is False however, the item won’t be stored and you’ll have to manually store it after invoking this method.

remove(delete=False, with_album=True)

Removes the item. If delete, then the associated file is removed from disk. If with_album, then the item’s album (if any) is removed if it the item was the last in the album.

Album

An Album is a collection of Items in the database. Every item in the database has either zero or one associated albums (accessible via item.album_id). An item that has no associated album is called a singleton. Changing fields on an album (e.g. album.year = 2012) updates the album itself and also changes the same field in all associated items.

An Album object keeps track of album-level metadata, which is (mostly) a subset of the track-level metadata. The album-level metadata fields are listed in Album._fields. For those fields that are both item-level and album-level (e.g., year or albumartist), every item in an album should share the same value. Albums use an SQLite table called albums, in which each column is an album metadata field.

class beets.library.Album(db=None, **values)

Provides access to information about albums stored in a library. Reflects the library’s “albums” table, including album art.

__init__(db=None, **values)

Create a new object with an optional Database association and initial field values.

item_dir()

Returns the directory containing the album’s first item, provided that such an item exists.

Albums extend the normal model interface to also forward changes to their items:

item_keys = ['added', 'albumartist', 'albumartist_sort', 'albumartist_credit', 'album', 'genre', 'style', 'discogs_albumid', 'year', 'month', 'day', 'disctotal', 'comp', 'mb_albumid', 'mb_albumartistid', 'albumtype', 'label', 'mb_releasegroupid', 'asin', 'catalognum', 'script', 'language', 'country', 'albumstatus', 'albumdisambig', 'releasegroupdisambig', 'rg_album_gain', 'rg_album_peak', 'r128_album_gain', 'original_year', 'original_month', 'original_day']

List of keys that are set on an album’s items.

store(fields=None)

Update the database with the album information. The album’s tracks are also updated. :param fields: The fields to be stored. If not specified, all fields will be.

try_sync(write, move)

Synchronize the album and its items with the database. Optionally, also write any new tags into the files and update their paths.

write indicates whether to write tags to the item files, and move controls whether files (both audio and album art) are moved.

move(operation=<MoveOperation.MOVE: 0>, basedir=None, store=True)

Move, copy, link or hardlink (depending on operation) all items to their destination. Any album art moves along with them.

basedir overrides the library base directory for the destination.

operation should be an instance of util.MoveOperation.

By default, the album is stored to the database, persisting any modifications to its metadata. If store is False however, the album is not stored automatically, and you’ll have to manually store it after invoking this method.

remove(delete=False, with_items=True)

Removes this album and all its associated items from the library. If delete, then the items’ files are also deleted from disk, along with any album art. The directories containing the album are also removed (recursively) if empty. Set with_items to False to avoid removing the album’s items.

Albums also manage album art, image files that are associated with each album:

set_art(path, copy=True)

Sets the album’s cover art to the image at the given path. The image is copied (or moved) into place, replacing any existing art.

Sends an ‘art_set’ event with self as the sole argument.

move_art(operation=<MoveOperation.MOVE: 0>)

Move, copy, link or hardlink (depending on operation) any existing album art so that it remains in the same directory as the items.

operation should be an instance of util.MoveOperation.

art_destination(image, item_dir=None)

Returns a path to the destination for the album art image for the album. image is the path of the image that will be moved there (used for its extension).

The path construction uses the existing path of the album’s items, so the album must contain at least one item or item_dir must be provided.

Transactions

The Library class provides the basic methods necessary to access and manipulate its contents. To perform more complicated operations atomically, or to interact directly with the underlying SQLite database, you must use a transaction (see this blog post for motivation). For example:

lib = Library()
with lib.transaction() as tx:
    items = lib.items(query)
    lib.add_album(list(items))
class beets.dbcore.db.Transaction(db)

A context manager for safe, concurrent access to the database. All SQL commands should be executed through a transaction.

mutate(statement, subvals=())

Execute an SQL statement with substitution values and return the row ID of the last affected row.

query(statement, subvals=())

Execute an SQL statement with substitution values and return a list of rows from the database.

script(statements)

Execute a string containing multiple SQL statements.

Queries

To access albums and items in a library, we use Queries. In beets, the Query abstract base class represents a criterion that matches items or albums in the database. Every subclass of Query must implement two methods, which implement two different ways of identifying matching items/albums.

The clause() method should return an SQLite WHERE clause that matches appropriate albums/items. This allows for efficient batch queries. Correspondingly, the match(item) method should take an Item object and return a boolean, indicating whether or not a specific item matches the criterion. This alternate implementation allows clients to determine whether items that have already been fetched from the database match the query.

There are many different types of queries. Just as an example, FieldQuery determines whether a certain field matches a certain value (an equality query). AndQuery (like its abstract superclass, CollectionQuery) takes a set of other query objects and bundles them together, matching only albums/items that match all constituent queries.

Beets has a human-writable plain-text query syntax that can be parsed into Query objects. Calling AndQuery.from_strings parses a list of query parts into a query object that can then be used with Library objects.