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)¶
You can add new items or albums to the library:
- 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:
- 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 = {}¶
Optional Types for non-fixed (i.e., flexible and computed) fields.
- _fields = {}¶
A mapping indicating available “fixed” fields on this type. The keys are field names and the values are Type objects.
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.
If check_revision is true, the database is only queried loaded when a transaction has been committed since the item was last loaded.
- 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 adict
-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, raise_=False)¶
Get the value for a field, or default. Alternatively, raise a KeyError if the field is not available.
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, replacements=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()
andwrite()
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()
, andstore()
(conditionally).
The
Item
class supplements the normal model interface so that they interacting with the filesystem as well:- move(operation=MoveOperation.MOVE, 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', 'discogs_artistid', 'discogs_labelid', 'year', 'month', 'day', 'disctotal', 'comp', 'mb_albumid', 'mb_albumartistid', 'albumtype', 'albumtypes', '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, 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)¶
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.