Commits

Fabrice Laporte committed f4e1b13 Merge

Merge branch 'master' of https://github.com/sampsyo/beets

Comments (0)

Files changed (7)

         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.
 
         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):
     # 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)))
         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:

beetsplug/convert.py

     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

beetsplug/mbsync.py

+# 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]

docs/changelog.rst

 
 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.

docs/plugins/index.rst

    convert
    info
    smartplaylist
+   mbsync
 
 Autotagger Extensions
 ''''''''''''''''''''''
 * :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.

docs/plugins/mbsync.rst

+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.

docs/reference/cli.rst

     ^^^^^^^^^^^
 
     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
     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