mirror of
https://github.com/beetbox/beets.git
synced 2025-12-11 02:53:58 +01:00
Merge branch 'master' of https://github.com/sampsyo/beets
This commit is contained in:
commit
72a06e96d0
7 changed files with 239 additions and 38 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
159
beetsplug/mbsync.py
Normal 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]
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
35
docs/plugins/mbsync.rst
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue