This commit is contained in:
Fabrice Laporte 2013-03-04 08:38:09 +01:00
commit 72a06e96d0
7 changed files with 239 additions and 38 deletions

View file

@ -798,6 +798,33 @@ class ResultIterator(object):
row = self.rowiter.next() # May raise StopIteration.
return Item(row)
def get_query(val, album=False):
"""Takes a value which may be None, a query string, a query string
list, or a Query object, and returns a suitable Query object. album
determines whether the query is to match items or albums.
"""
if album:
default_fields = ALBUM_DEFAULT_FIELDS
all_keys = ALBUM_KEYS
else:
default_fields = ITEM_DEFAULT_FIELDS
all_keys = ITEM_KEYS
# Convert a single string into a list of space-separated
# criteria.
if isinstance(val, basestring):
val = val.split()
if val is None:
return TrueQuery()
elif isinstance(val, list) or isinstance(val, tuple):
return AndQuery.from_strings(val, default_fields, all_keys)
elif isinstance(val, Query):
return val
else:
raise ValueError('query must be None or have type Query or str')
# An abstract library.
@ -809,37 +836,6 @@ class BaseLibrary(object):
raise NotImplementedError
# Helpers.
@classmethod
def _get_query(cls, val=None, album=False):
"""Takes a value which may be None, a query string, a query
string list, or a Query object, and returns a suitable Query
object. album determines whether the query is to match items
or albums.
"""
if album:
default_fields = ALBUM_DEFAULT_FIELDS
all_keys = ALBUM_KEYS
else:
default_fields = ITEM_DEFAULT_FIELDS
all_keys = ITEM_KEYS
# Convert a single string into a list of space-separated
# criteria.
if isinstance(val, basestring):
val = val.split()
if val is None:
return TrueQuery()
elif isinstance(val, list) or isinstance(val, tuple):
return AndQuery.from_strings(val, default_fields, all_keys)
elif isinstance(val, Query):
return val
elif not isinstance(val, Query):
raise ValueError('query must be None or have type Query or str')
# Basic operations.
def add(self, item, copy=False):
@ -1358,7 +1354,7 @@ class Library(BaseLibrary):
# Querying.
def albums(self, query=None, artist=None):
query = self._get_query(query, True)
query = get_query(query, True)
if artist is not None:
# "Add" the artist to the query.
query = AndQuery((query, MatchQuery('albumartist', artist)))
@ -1372,7 +1368,7 @@ class Library(BaseLibrary):
return [Album(self, dict(res)) for res in rows]
def items(self, query=None, artist=None, album=None, title=None):
queries = [self._get_query(query, False)]
queries = [get_query(query, False)]
if artist is not None:
queries.append(MatchQuery('artist', artist))
if album is not None:

View file

@ -39,7 +39,8 @@ def encode(source, dest):
encode.wait()
if encode.returncode != 0:
# Something went wrong (probably Ctrl+C), remove temporary files
log.info(u'Encoding {0} failed. Cleaning up...'.format(source))
log.info(u'Encoding {0} failed. Cleaning up...'
.format(util.displayable_path(source)))
util.remove(dest)
util.prune_dirs(os.path.dirname(dest))
return

159
beetsplug/mbsync.py Normal file
View file

@ -0,0 +1,159 @@
# This file is part of beets.
# Copyright 2013, Jakob Schnitzer.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Update library's tags using MusicBrainz.
"""
import logging
from beets.plugins import BeetsPlugin
from beets import autotag, library, ui, util
from beets.autotag import hooks
from beets import config
log = logging.getLogger('beets')
def _print_and_apply_changes(lib, item, move, pretend, write):
"""Apply changes to an Item and preview them in the console. Return
a boolean indicating whether any changes were made.
"""
changes = {}
for key in library.ITEM_KEYS_META:
if item.dirty[key]:
changes[key] = item.old_data[key], getattr(item, key)
if not changes:
return False
# Something changed.
ui.print_obj(item, lib)
for key, (oldval, newval) in changes.iteritems():
ui.commands._showdiff(key, oldval, newval)
# If we're just pretending, then don't move or save.
if not pretend:
# Move the item if it's in the library.
if move and lib.directory in util.ancestry(item.path):
lib.move(item, with_album=False)
if write:
item.write()
lib.store(item)
return True
def mbsync_singletons(lib, query, move, pretend, write):
"""Synchronize matching singleton items.
"""
singletons_query = library.get_query(query, False)
singletons_query.subqueries.append(library.SingletonQuery(True))
for s in lib.items(singletons_query):
if not s.mb_trackid:
log.info(u'Skipping singleton {0}: has no mb_trackid'
.format(s.title))
continue
s.old_data = dict(s.record)
# Get the MusicBrainz recording info.
track_info = hooks._track_for_id(s.mb_trackid)
if not track_info:
log.info(u'Recording ID not found: {0}'.format(s.mb_trackid))
continue
# Apply.
with lib.transaction():
autotag.apply_item_metadata(s, track_info)
_print_and_apply_changes(lib, s, move, pretend, write)
def mbsync_albums(lib, query, move, pretend, write):
"""Synchronize matching albums.
"""
# Process matching albums.
for a in lib.albums(query):
if not a.mb_albumid:
log.info(u'Skipping album {0}: has no mb_albumid'.format(a.id))
continue
items = list(a.items())
for item in items:
item.old_data = dict(item.record)
# Get the MusicBrainz album information.
album_info = hooks._album_for_id(a.mb_albumid)
if not album_info:
log.info(u'Release ID not found: {0}'.format(a.mb_albumid))
continue
# Construct a track mapping according to MBIDs. This should work
# for albums that have missing or extra tracks.
mapping = {}
for item in items:
for track_info in album_info.tracks:
if item.mb_trackid == track_info.track_id:
mapping[item] = track_info
break
# Apply.
with lib.transaction():
autotag.apply_metadata(album_info, mapping)
changed = False
for item in items:
changed = changed or \
_print_and_apply_changes(lib, item, move, pretend, write)
if not changed:
# No change to any item.
continue
if not pretend:
# Update album structure to reflect an item in it.
for key in library.ALBUM_KEYS_ITEM:
setattr(a, key, getattr(items[0], key))
# Move album art (and any inconsistent items).
if move and lib.directory in util.ancestry(items[0].path):
log.debug(u'moving album {0}'.format(a.id))
a.move()
def mbsync_func(lib, opts, args):
"""Command handler for the mbsync function.
"""
move = opts.move
pretend = opts.pretend
write = opts.write
query = ui.decargs(args)
mbsync_singletons(lib, query, move, pretend, write)
mbsync_albums(lib, query, move, pretend, write)
class MBSyncPlugin(BeetsPlugin):
def __init__(self):
super(MBSyncPlugin, self).__init__()
def commands(self):
cmd = ui.Subcommand('mbsync',
help='update metadata from musicbrainz')
cmd.parser.add_option('-p', '--pretend', action='store_true',
help='show all changes but do nothing')
cmd.parser.add_option('-M', '--nomove', action='store_false',
default=True, dest='move',
help="don't move files in library")
cmd.parser.add_option('-W', '--nowrite', action='store_false',
default=config['import']['write'], dest='write',
help="don't write updated metadata to files")
cmd.func = mbsync_func
return [cmd]

View file

@ -18,6 +18,10 @@ New configuration options:
Other stuff:
* A new :doc:`/plugins/mbsync` provides a command that looks up each item and
track in MusicBrainz and updates your library to reflect it. This can help
you easily correct errors that have been fixed in the MB database. Thanks to
Jakob Schnitzer.
* :doc:`/plugins/echonest_tempo`: API errors now issue a warning instead of
exiting with an exception. We also avoid an error when track metadata
contains newlines.

View file

@ -60,6 +60,7 @@ disabled by default, but you can turn them on as described above.
convert
info
smartplaylist
mbsync
Autotagger Extensions
''''''''''''''''''''''
@ -73,6 +74,7 @@ Metadata
* :doc:`lyrics`: Automatically fetch song lyrics.
* :doc:`echonest_tempo`: Automatically fetch song tempos (bpm).
* :doc:`lastgenre`: Fetch genres based on Last.fm tags.
* :doc:`mbsync`: Fetch updated metadata from MusicBrainz
* :doc:`fetchart`: Fetch album cover art from various sources.
* :doc:`embedart`: Embed album art images into files' metadata.
* :doc:`replaygain`: Calculate volume normalization for players that support it.

35
docs/plugins/mbsync.rst Normal file
View file

@ -0,0 +1,35 @@
MBSync Plugin
=============
This plugin provides the ``mbsync`` command, which lets you fetch metadata
from MusicBrainz for albums and tracks that already have MusicBrainz IDs. This
is useful for updating tags as they are fixed in the MusicBrainz database, or
when you change your mind about some config options that change how tags are
written to files. If you have a music library that is already nicely tagged by
a program that also uses MusicBrainz like Picard, this can speed up the
initial import if you just import "as-is" and then use ``mbsync`` to get
up-to-date tags that are written to the files according to your beets
configuration.
Usage
-----
Enable the plugin and then run ``beet mbsync QUERY`` to fetch updated metadata
for a part of your collection (or omit the query to run over your whole
library).
This plugin treats albums and singletons (non-album tracks) separately. It
first processes all matching singletons and then proceeds on to full albums.
The same query is used to search for both kinds of entities.
The command has a few command-line options:
* To preview the changes that would be made without applying them, use the
``-p`` (``--pretend``) flag.
* By default, files will be moved (renamed) according to their metadata if
they are inside your beets library directory. To disable this, use the
``-M`` (``--nomove``) command-line option.
* If you have the `import.write` configuration option enabled, then this
plugin will write new metadata to files' tags. To disable this, use the
``-W`` (``--nowrite``) option.

View file

@ -105,10 +105,9 @@ right now; this is something we need to work on. Read the
^^^^^^^^^^^
The ``import`` command can also be used to "reimport" music that you've
already added to your library. This is useful for updating tags as they are
fixed in the MusicBrainz database, for when you change your mind about some
selections you made during the initial import, or if you prefer to import
everything "as-is" and then correct tags later.
already added to your library. This is useful when you change your mind
about some selections you made during the initial import, or if you prefer
to import everything "as-is" and then correct tags later.
Just point the ``beet import`` command at a directory of files that are
already catalogged in your library. Beets will automatically detect this
@ -127,6 +126,11 @@ right now; this is something we need to work on. Read the
or full albums. If you want to retag your whole library, just supply a null
query, which matches everything: ``beet import -L``
Note that, if you just want to update your files' tags according to
changes in the MusicBrainz database, the :doc:`/plugins/mbsync` is a
better choice. Reimporting uses the full matching machinery to guess
metadata matches; ``mbsync`` just relies on MusicBrainz IDs.
.. _list-cmd:
list