From ef22ea5183234fb851e7533975cba5f281fa384d Mon Sep 17 00:00:00 2001 From: Jakob Schnitzer Date: Fri, 1 Mar 2013 16:16:28 +0100 Subject: [PATCH 01/14] convert: add missing util.displayable_path --- beetsplug/convert.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index aee2f55f0..3ce3e34a4 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -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 From 2fd3ad53622d88e1df91580b073f423645ab0447 Mon Sep 17 00:00:00 2001 From: Jakob Schnitzer Date: Fri, 1 Mar 2013 15:47:44 +0100 Subject: [PATCH 02/14] initial version of the mbsync plugin this plugin provides a faster way to query new metadata from musicbrainz. (instead of having to 're-import' the files) Currently it lacks all forms of documentation and will only work for album queries. not really tested so far so be careful --- beetsplug/mbsync.py | 100 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 beetsplug/mbsync.py diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py new file mode 100644 index 000000000..fd7c41323 --- /dev/null +++ b/beetsplug/mbsync.py @@ -0,0 +1,100 @@ +# 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 local library from MusicBrainz +""" +import logging + +from beets.plugins import BeetsPlugin +from beets import autotag, library, ui, util + +log = logging.getLogger('beets') + + +def mbsync_func(lib, opts, args): + #album = opts.album + album = True + move = True + pretend = opts.pretend + with lib.transaction(): + # Right now this only works for albums.... + _, albums = ui.commands._do_query(lib, ui.decargs(args), album) + + for a in albums: + 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) + + cur_artist, cur_album, candidates, _ = \ + autotag.match.tag_album(items, search_id=a.mb_albumid) + match = candidates[0] # There should only be one match! + # ui.commands.show_change(cur_artist, cur_album, match) + autotag.apply_metadata(match.info, match.mapping) + + for item in items: + changes = {} + for key in library.ITEM_KEYS_META: + if item.dirty[key]: + changes[key] = item.old_data[key], getattr(item, key) + if changes: + # 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 pretend: + continue + + # Move the item if it's in the library. + if move and lib.directory in util.ancestry(item.path): + lib.move(item) + lib.store(item) + + if pretend or a.id is None: # pretend or Singleton + continue + + # 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() + + +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('-a', '--album', action='store_true', + # help='choose albums instead of tracks') + 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=True, dest='move', + help="don't write updated metadata to files") + cmd.func = mbsync_func + return [cmd] From cd7305d4872b1316c5eb4c6e2e1a61018defc934 Mon Sep 17 00:00:00 2001 From: Jakob Schnitzer Date: Sun, 3 Mar 2013 11:39:34 +0100 Subject: [PATCH 03/14] mbsync: write metadata to files... --- beetsplug/mbsync.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index fd7c41323..6b453bb2e 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -24,12 +24,12 @@ log = logging.getLogger('beets') def mbsync_func(lib, opts, args): #album = opts.album - album = True - move = True + move = opts.move pretend = opts.pretend + write = opts.write with lib.transaction(): # Right now this only works for albums.... - _, albums = ui.commands._do_query(lib, ui.decargs(args), album) + albums = lib.albums(ui.decargs(args)) for a in albums: if not a.mb_albumid: @@ -43,7 +43,6 @@ def mbsync_func(lib, opts, args): cur_artist, cur_album, candidates, _ = \ autotag.match.tag_album(items, search_id=a.mb_albumid) match = candidates[0] # There should only be one match! - # ui.commands.show_change(cur_artist, cur_album, match) autotag.apply_metadata(match.info, match.mapping) for item in items: @@ -64,9 +63,12 @@ def mbsync_func(lib, opts, args): # Move the item if it's in the library. if move and lib.directory in util.ancestry(item.path): lib.move(item) + + if write: + item.write() lib.store(item) - if pretend or a.id is None: # pretend or Singleton + if pretend: continue # Update album structure to reflect an item in it. @@ -94,7 +96,7 @@ class MBSyncPlugin(BeetsPlugin): default=True, dest='move', help="don't move files in library") cmd.parser.add_option('-W', '--nowrite', action='store_false', - default=True, dest='move', + default=True, dest='write', help="don't write updated metadata to files") cmd.func = mbsync_func return [cmd] From 78a99c23fa58e3fa48deb50147a15eaa3bac0fbd Mon Sep 17 00:00:00 2001 From: Jakob Schnitzer Date: Sun, 3 Mar 2013 12:25:53 +0100 Subject: [PATCH 04/14] mbsync: documentation --- docs/plugins/index.rst | 2 ++ docs/plugins/mbsync.rst | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 docs/plugins/mbsync.rst diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 2217f4d91..3c9eb4d9f 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -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. diff --git a/docs/plugins/mbsync.rst b/docs/plugins/mbsync.rst new file mode 100644 index 000000000..47312646a --- /dev/null +++ b/docs/plugins/mbsync.rst @@ -0,0 +1,22 @@ +MBSync Plugin +============= + +The ``mbsync`` lets you fetch metadata from MusicBrainz for albums 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. This only work for album queries at the moment. +To only preview the changes that would be made, use the ``-p`` (``--pretend``) +flag. By default all the new metadata will be written to the files and the files +will be moved according to their new metadata. This behaviour can be changed +with the ``-W`` (``--nowrite``) and ``-M`` (``--nomove``) command line options. From 3a9c9d53da9f45639dc7612c73a4da2076c1e557 Mon Sep 17 00:00:00 2001 From: Jakob Schnitzer Date: Sun, 3 Mar 2013 14:16:08 +0100 Subject: [PATCH 05/14] mbsync: add support for singletons I can't really guarantee this works right now since I have no singletons in my collection to test it --- beetsplug/mbsync.py | 49 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 6b453bb2e..48d102df8 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -23,13 +23,48 @@ log = logging.getLogger('beets') def mbsync_func(lib, opts, args): - #album = opts.album move = opts.move pretend = opts.pretend write = opts.write + if opts.album and opts.singleton: + return + with lib.transaction(): - # Right now this only works for albums.... - albums = lib.albums(ui.decargs(args)) + singletons = [item for item in lib.items(ui.decargs(args)) + if item.album_id is None] if not opts.album else [] + albums = lib.albums(ui.decargs(args)) if not opts.singleton else [] + + for s in singletons: + if not s.mb_trackid: + log.info(u'Skipping singleton {0}: has no mb_trackid' + .format(s.title)) + continue + + old_data = dict(s.record) + candidates, _ = autotag.match.tag_item(s, search_id=s.mb_trackid) + match = candidates[0] + autotag.apply_item_metadata(s, match.info) + changes = {} + for key in library.ITEM_KEYS_META: + if s.dirty[key]: + changes[key] = old_data[key], getattr(s, key) + if changes: + # Something changed. + ui.print_obj(s, 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 pretend: + continue + + # Move the item if it's in the library. + if move and lib.directory in util.ancestry(s.path): + lib.move(s) + + if write: + s.write() + lib.store(s) for a in albums: if not a.mb_albumid: @@ -40,7 +75,7 @@ def mbsync_func(lib, opts, args): for item in items: item.old_data = dict(item.record) - cur_artist, cur_album, candidates, _ = \ + _, _, candidates, _ = \ autotag.match.tag_album(items, search_id=a.mb_albumid) match = candidates[0] # There should only be one match! autotag.apply_metadata(match.info, match.mapping) @@ -88,8 +123,10 @@ class MBSyncPlugin(BeetsPlugin): def commands(self): cmd = ui.Subcommand('mbsync', help='update metadata from musicbrainz') - #cmd.parser.add_option('-a', '--album', action='store_true', - # help='choose albums instead of tracks') + cmd.parser.add_option('-a', '--album', action='store_true', + help='only query for albums') + cmd.parser.add_option('-s', '--singleton', action='store_true', + help='only query for singletons') 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', From 49d3ca4f020a9375dd68d476f03e7985cf57b146 Mon Sep 17 00:00:00 2001 From: Jakob Schnitzer Date: Sun, 3 Mar 2013 14:21:34 +0100 Subject: [PATCH 06/14] mbsync: update docs --- docs/plugins/mbsync.rst | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/plugins/mbsync.rst b/docs/plugins/mbsync.rst index 47312646a..95733e9d9 100644 --- a/docs/plugins/mbsync.rst +++ b/docs/plugins/mbsync.rst @@ -1,21 +1,25 @@ MBSync Plugin ============= -The ``mbsync`` lets you fetch metadata from MusicBrainz for albums 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. +The ``mbsync`` lets you fetch metadata from MusicBrainz for albums and +singletons 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. This only work for album queries at the moment. +for a part of your collection. By default this will use the given query to +search for albums and singletons. You can use the ``-a`` (``--album``) and +``-s`` (``--singleton``) command line flags to only search for albums or +singletons respectively. + To only preview the changes that would be made, use the ``-p`` (``--pretend``) flag. By default all the new metadata will be written to the files and the files will be moved according to their new metadata. This behaviour can be changed From d647ea0f0d7a9b12a99f2eaad3d216331226b9de Mon Sep 17 00:00:00 2001 From: Jakob Schnitzer Date: Sun, 3 Mar 2013 20:32:28 +0100 Subject: [PATCH 07/14] mbsync: a little refactoring --- beetsplug/mbsync.py | 99 +++++++++++++++-------------------------- docs/plugins/mbsync.rst | 8 ++-- 2 files changed, 41 insertions(+), 66 deletions(-) diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 48d102df8..a751d4d27 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -22,17 +22,38 @@ from beets import autotag, library, ui, util log = logging.getLogger('beets') +def _print_and_apply_changes(lib, item, move, pretend, write): + 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 + + # 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) + + if write: + item.write() + lib.store(item) + + def mbsync_func(lib, opts, args): move = opts.move pretend = opts.pretend write = opts.write - if opts.album and opts.singleton: - return with lib.transaction(): - singletons = [item for item in lib.items(ui.decargs(args)) - if item.album_id is None] if not opts.album else [] - albums = lib.albums(ui.decargs(args)) if not opts.singleton else [] + singletons = lib.items(ui.decargs(args + ['singleton'])) + albums = lib.albums(ui.decargs(args)) for s in singletons: if not s.mb_trackid: @@ -40,31 +61,11 @@ def mbsync_func(lib, opts, args): .format(s.title)) continue - old_data = dict(s.record) + s.old_data = dict(s.record) candidates, _ = autotag.match.tag_item(s, search_id=s.mb_trackid) match = candidates[0] autotag.apply_item_metadata(s, match.info) - changes = {} - for key in library.ITEM_KEYS_META: - if s.dirty[key]: - changes[key] = old_data[key], getattr(s, key) - if changes: - # Something changed. - ui.print_obj(s, 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 pretend: - continue - - # Move the item if it's in the library. - if move and lib.directory in util.ancestry(s.path): - lib.move(s) - - if write: - s.write() - lib.store(s) + _print_and_apply_changes(lib, s, move, pretend, write) for a in albums: if not a.mb_albumid: @@ -81,39 +82,17 @@ def mbsync_func(lib, opts, args): autotag.apply_metadata(match.info, match.mapping) for item in items: - changes = {} - for key in library.ITEM_KEYS_META: - if item.dirty[key]: - changes[key] = item.old_data[key], getattr(item, key) - if changes: - # Something changed. - ui.print_obj(item, lib) - for key, (oldval, newval) in changes.iteritems(): - ui.commands._showdiff(key, oldval, newval) + _print_and_apply_changes(lib, item, move, pretend, write) - # If we're just pretending, then don't move or save. - if pretend: - 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 the item if it's in the library. - if move and lib.directory in util.ancestry(item.path): - lib.move(item) - - if write: - item.write() - lib.store(item) - - if pretend: - continue - - # 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() + # 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() class MBSyncPlugin(BeetsPlugin): @@ -123,10 +102,6 @@ class MBSyncPlugin(BeetsPlugin): def commands(self): cmd = ui.Subcommand('mbsync', help='update metadata from musicbrainz') - cmd.parser.add_option('-a', '--album', action='store_true', - help='only query for albums') - cmd.parser.add_option('-s', '--singleton', action='store_true', - help='only query for singletons') 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', diff --git a/docs/plugins/mbsync.rst b/docs/plugins/mbsync.rst index 95733e9d9..4023618b0 100644 --- a/docs/plugins/mbsync.rst +++ b/docs/plugins/mbsync.rst @@ -15,10 +15,10 @@ Usage ----- Enable the plugin and then run ``beet mbsync QUERY`` to fetch updated metadata -for a part of your collection. By default this will use the given query to -search for albums and singletons. You can use the ``-a`` (``--album``) and -``-s`` (``--singleton``) command line flags to only search for albums or -singletons respectively. +for a part of your collection. Since the MusicBrainZ API allows for more +efficient queries for full albums this will by run separately for all albums and +all singletons(tracks that are not part of an album) so it will use the given +query to search for both albums and singletons. To only preview the changes that would be made, use the ``-p`` (``--pretend``) flag. By default all the new metadata will be written to the files and the files From 54e070d06bdf609ee29e9de360a5de973f9af2a6 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 3 Mar 2013 16:29:31 -0800 Subject: [PATCH 08/14] mbsync: use SingletonQuery for item updates --- beets/library.py | 62 +++++++++++++++++++++------------------------ beetsplug/mbsync.py | 13 +++++----- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/beets/library.py b/beets/library.py index f07ea7a53..390d4e1dd 100644 --- a/beets/library.py +++ b/beets/library.py @@ -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: diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index a751d4d27..44a1a8771 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -12,7 +12,7 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Update local library from MusicBrainz +"""Update library's tags using MusicBrainz. """ import logging @@ -52,10 +52,10 @@ def mbsync_func(lib, opts, args): write = opts.write with lib.transaction(): - singletons = lib.items(ui.decargs(args + ['singleton'])) - albums = lib.albums(ui.decargs(args)) - - for s in singletons: + # Process matching singletons. + singletons_query = library.get_query(ui.decargs(args), 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)) @@ -67,7 +67,8 @@ def mbsync_func(lib, opts, args): autotag.apply_item_metadata(s, match.info) _print_and_apply_changes(lib, s, move, pretend, write) - for a in albums: + # Process matching albums. + for a in lib.albums(ui.decargs(args)): if not a.mb_albumid: log.info(u'Skipping album {0}: has no mb_albumid'.format(a.id)) continue From 5f3ebde6bb09dc1acfd7686695217092d8a4759b Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 3 Mar 2013 16:41:48 -0800 Subject: [PATCH 09/14] mbsync: docs/changelog As discussed on #115, this has the "reimport" docs refer to the mbsync plugin. --- docs/changelog.rst | 4 ++++ docs/plugins/mbsync.rst | 39 +++++++++++++++++++++++---------------- docs/reference/cli.rst | 12 ++++++++---- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f7ad1b07b..49ce4edfa 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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. diff --git a/docs/plugins/mbsync.rst b/docs/plugins/mbsync.rst index 4023618b0..764be9d86 100644 --- a/docs/plugins/mbsync.rst +++ b/docs/plugins/mbsync.rst @@ -1,26 +1,33 @@ MBSync Plugin ============= -The ``mbsync`` lets you fetch metadata from MusicBrainz for albums and -singletons 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. +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. Since the MusicBrainZ API allows for more -efficient queries for full albums this will by run separately for all albums and -all singletons(tracks that are not part of an album) so it will use the given -query to search for both albums and singletons. +for a part of your collection (or omit the query to run over your whole +library). -To only preview the changes that would be made, use the ``-p`` (``--pretend``) -flag. By default all the new metadata will be written to the files and the files -will be moved according to their new metadata. This behaviour can be changed -with the ``-W`` (``--nowrite``) and ``-M`` (``--nomove``) command line options. +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 all the new metadata will be written to the files and the files + will be moved according to their new metadata. This behavior can be changed + with the ``-W`` (``--nowrite``) and ``-M`` (``--nomove``) command line + options. diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index 2add6aaba..575c92968 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -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 From a8d999a1014d7c001d5b4c4751dd969383d19f44 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 3 Mar 2013 16:52:14 -0800 Subject: [PATCH 10/14] mbsync: split album/item functions; shorter txns The main change here is to use shorter transactions -- one per matching entity -- rather than one large one. This avoids very long transactions when the network happens to move slowly. --- beetsplug/mbsync.py | 72 +++++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 44a1a8771..96dc79319 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -46,42 +46,44 @@ def _print_and_apply_changes(lib, item, move, pretend, write): lib.store(item) -def mbsync_func(lib, opts, args): - move = opts.move - pretend = opts.pretend - write = opts.write +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 - with lib.transaction(): - # Process matching singletons. - singletons_query = library.get_query(ui.decargs(args), 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) - candidates, _ = autotag.match.tag_item(s, search_id=s.mb_trackid) - match = candidates[0] + s.old_data = dict(s.record) + candidates, _ = autotag.match.tag_item(s, search_id=s.mb_trackid) + match = candidates[0] + with lib.transaction(): autotag.apply_item_metadata(s, match.info) _print_and_apply_changes(lib, s, move, pretend, write) - # Process matching albums. - for a in lib.albums(ui.decargs(args)): - 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) +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 - _, _, candidates, _ = \ - autotag.match.tag_album(items, search_id=a.mb_albumid) - match = candidates[0] # There should only be one match! + items = list(a.items()) + for item in items: + item.old_data = dict(item.record) + + _, _, candidates, _ = \ + autotag.match.tag_album(items, search_id=a.mb_albumid) + match = candidates[0] # There should only be one match! + + with lib.transaction(): autotag.apply_metadata(match.info, match.mapping) - for item in items: _print_and_apply_changes(lib, item, move, pretend, write) @@ -96,6 +98,18 @@ def mbsync_func(lib, opts, args): 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__() From 72263a1cf71dbcb54022ae6dae14a3debfe41f6e Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 3 Mar 2013 17:08:07 -0800 Subject: [PATCH 11/14] mbsync: use ID lookups instead of full match logic This change uses _album_for_id and _track_for_id instead of the full autotag.match.* functions. This should be faster (requiring fewer calls to the MusicBrainz API) while also being more predictable. It also won't, for example, use acoustic fingerprinting even if the chroma plugin is installed. Finally, this change catches the error case in which MBIDs are erroneous. This can happen, for example, if the user has some track MBIDs left over from before the NGS transition. --- beetsplug/mbsync.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 96dc79319..bbf653f93 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -18,6 +18,7 @@ import logging from beets.plugins import BeetsPlugin from beets import autotag, library, ui, util +from beets.autotag import hooks log = logging.getLogger('beets') @@ -54,14 +55,20 @@ def mbsync_singletons(lib, query, move, pretend, write): 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)) + .format(s.title)) continue s.old_data = dict(s.record) - candidates, _ = autotag.match.tag_item(s, search_id=s.mb_trackid) - match = candidates[0] + + # 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, match.info) + autotag.apply_item_metadata(s, track_info) _print_and_apply_changes(lib, s, move, pretend, write) @@ -78,12 +85,24 @@ def mbsync_albums(lib, query, move, pretend, write): for item in items: item.old_data = dict(item.record) - _, _, candidates, _ = \ - autotag.match.tag_album(items, search_id=a.mb_albumid) - match = candidates[0] # There should only be one match! + # 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(match.info, match.mapping) + autotag.apply_metadata(album_info, mapping) for item in items: _print_and_apply_changes(lib, item, move, pretend, write) From 5f68d037936da16cb2448c58583580bb9827a5e6 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 3 Mar 2013 17:13:54 -0800 Subject: [PATCH 12/14] mbsync: don't write tags if import.write is off This will avoid surprising users with import.write turned off. --- beetsplug/mbsync.py | 3 ++- docs/plugins/mbsync.rst | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index bbf653f93..3313c6f07 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -19,6 +19,7 @@ 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') @@ -142,7 +143,7 @@ class MBSyncPlugin(BeetsPlugin): default=True, dest='move', help="don't move files in library") cmd.parser.add_option('-W', '--nowrite', action='store_false', - default=True, dest='write', + default=config['import']['write'], dest='write', help="don't write updated metadata to files") cmd.func = mbsync_func return [cmd] diff --git a/docs/plugins/mbsync.rst b/docs/plugins/mbsync.rst index 764be9d86..4bb3da32f 100644 --- a/docs/plugins/mbsync.rst +++ b/docs/plugins/mbsync.rst @@ -27,7 +27,9 @@ 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 all the new metadata will be written to the files and the files - will be moved according to their new metadata. This behavior can be changed - with the ``-W`` (``--nowrite``) and ``-M`` (``--nomove``) command line - options. +* 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. From 18688008a440467cb55e5d77b96ec85d1a3fdc29 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 3 Mar 2013 17:19:05 -0800 Subject: [PATCH 13/14] mbsync: avoid spurious stores/moves As _print_and_apply_changes itself does for items, we now shortcut modifications (metadata and filesystem) for albums when no changes are required for a given album. This avoids effectively doing a "beet move" on an album even when nothing has changed. --- beetsplug/mbsync.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index 3313c6f07..e8fdf6c7a 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -25,12 +25,15 @@ 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 + return False # Something changed. ui.print_obj(item, lib) @@ -47,6 +50,8 @@ def _print_and_apply_changes(lib, item, move, pretend, write): item.write() lib.store(item) + return True + def mbsync_singletons(lib, query, move, pretend, write): """Synchronize matching singleton items. @@ -104,8 +109,13 @@ def mbsync_albums(lib, query, move, pretend, write): # Apply. with lib.transaction(): autotag.apply_metadata(album_info, mapping) + changed = False for item in items: - _print_and_apply_changes(lib, item, move, pretend, write) + 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. From 1a7ec6dc7976ec13f226da7f7626fcc2c8829977 Mon Sep 17 00:00:00 2001 From: Adrian Sampson Date: Sun, 3 Mar 2013 17:46:16 -0800 Subject: [PATCH 14/14] mbsync: fix redundant album art movement Since we explicitly move album art later in the process, implicitly moving it with items can cause a double-move (and thus a "file not found" error). --- beetsplug/mbsync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/mbsync.py b/beetsplug/mbsync.py index e8fdf6c7a..97ecee3ae 100644 --- a/beetsplug/mbsync.py +++ b/beetsplug/mbsync.py @@ -44,7 +44,7 @@ def _print_and_apply_changes(lib, item, move, pretend, write): 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) + lib.move(item, with_album=False) if write: item.write()