updateMerge branch 'master' of https://github.com/sampsyo/beets into editor

This commit is contained in:
jmwatte 2015-11-05 15:13:07 +01:00
commit ab3bc356b2
11 changed files with 121 additions and 37 deletions

View file

@ -718,6 +718,7 @@ class ImportTask(BaseImportTask):
if replaced_album:
self.album.added = replaced_album.added
self.album.update(replaced_album._values_flex)
self.album.artpath = replaced_album.artpath
self.album.store()
log.debug(
u'Reimported album: added {0}, flexible '

View file

@ -39,8 +39,7 @@ import os
urllib3_logger = logging.getLogger('requests.packages.urllib3')
urllib3_logger.setLevel(logging.CRITICAL)
USER_AGENT = 'beets/{0} +http://beets.radbox.org/'.format(beets.__version__) \
.encode('utf8')
USER_AGENT = u'beets/{0} +http://beets.radbox.org/'.format(beets.__version__)
# Exceptions that discogs_client should really handle but does not.
CONNECTION_ERRORS = (ConnectionError, socket.error, httplib.HTTPException,
@ -66,8 +65,8 @@ class DiscogsPlugin(BeetsPlugin):
def setup(self, session=None):
"""Create the `discogs_client` field. Authenticate if necessary.
"""
c_key = self.config['apikey'].get(unicode).encode('utf8')
c_secret = self.config['apisecret'].get(unicode).encode('utf8')
c_key = self.config['apikey'].get(unicode)
c_secret = self.config['apisecret'].get(unicode)
# Get the OAuth token from a file or log in.
try:
@ -77,8 +76,8 @@ class DiscogsPlugin(BeetsPlugin):
# No token yet. Generate one.
token, secret = self.authenticate(c_key, c_secret)
else:
token = tokendata['token'].encode('utf8')
secret = tokendata['secret'].encode('utf8')
token = tokendata['token']
secret = tokendata['secret']
self.discogs_client = Client(USER_AGENT, c_key, c_secret,
token, secret)
@ -121,7 +120,7 @@ class DiscogsPlugin(BeetsPlugin):
with open(self._tokenfile(), 'w') as f:
json.dump({'token': token, 'secret': secret}, f)
return token.encode('utf8'), secret.encode('utf8')
return token, secret
def album_distance(self, items, album_info, mapping):
"""Returns the album distance.
@ -151,8 +150,8 @@ class DiscogsPlugin(BeetsPlugin):
return self.candidates(items, artist, album, va_likely)
else:
return []
except CONNECTION_ERRORS as e:
self._log.debug(u'HTTP Connection Error: {0}', e)
except CONNECTION_ERRORS:
self._log.debug('Connection error in album search', exc_info=True)
return []
def album_for_id(self, album_id):
@ -182,8 +181,8 @@ class DiscogsPlugin(BeetsPlugin):
self.reset_auth()
return self.album_for_id(album_id)
return None
except CONNECTION_ERRORS as e:
self._log.debug(u'HTTP Connection Error: {0}', e)
except CONNECTION_ERRORS:
self._log.debug('Connection error in album lookup', exc_info=True)
return None
return self.get_album_info(result)
@ -204,9 +203,9 @@ class DiscogsPlugin(BeetsPlugin):
try:
releases = self.discogs_client.search(query,
type='release').page(1)
except CONNECTION_ERRORS as exc:
self._log.debug("Communication error while searching for {0!r}: "
"{1}".format(query, exc))
except CONNECTION_ERRORS:
self._log.debug("Communication error while searching for {0!r}",
query, exc_info=True)
return []
return [self.get_album_info(release) for release in releases[:5]]

View file

@ -271,7 +271,7 @@ class DuplicatesPlugin(BeetsPlugin):
Return same number of items, with the head item modified.
"""
fields = [f for sublist in Item.get_fields() for f in sublist]
fields = Item.all_keys()
for f in fields:
for o in objs[1:]:
if getattr(objs[0], f, None) in (None, ''):

View file

@ -63,7 +63,6 @@ class EmbedCoverArtPlugin(BeetsPlugin):
maxwidth = self.config['maxwidth'].get(int)
compare_threshold = self.config['compare_threshold'].get(int)
ifempty = self.config['ifempty'].get(bool)
remove_art_file = self.config['remove_art_file'].get(bool)
def embed_func(lib, opts, args):
if opts.file:
@ -79,14 +78,7 @@ class EmbedCoverArtPlugin(BeetsPlugin):
for album in lib.albums(decargs(args)):
art.embed_album(self._log, album, maxwidth, False,
compare_threshold, ifempty)
if remove_art_file and album.artpath is not None:
if os.path.isfile(album.artpath):
self._log.debug(u'Removing album art file '
u'for {0}', album)
os.remove(album.artpath)
album.artpath = None
album.store()
self.remove_artfile(album)
embed_cmd.func = embed_func
@ -141,3 +133,15 @@ class EmbedCoverArtPlugin(BeetsPlugin):
art.embed_album(self._log, album, max_width, True,
self.config['compare_threshold'].get(int),
self.config['ifempty'].get(bool))
self.remove_artfile(album)
def remove_artfile(self, album):
"""Possibly delete the album art file for an album (if the
appropriate configuration option is enabled.
"""
if self.config['remove_art_file'] and album.artpath:
if os.path.isfile(album.artpath):
self._log.debug('Removing album art file for {0}', album)
os.remove(album.artpath)
album.artpath = None
album.store()

View file

@ -41,6 +41,10 @@ IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg']
CONTENT_TYPES = ('image/jpeg', 'image/png')
DOWNLOAD_EXTENSION = '.jpg'
CANDIDATE_BAD = 0
CANDIDATE_EXACT = 1
CANDIDATE_DOWNSCALE = 2
def _logged_get(log, *args, **kwargs):
"""Like `requests.get`, but logs the effective URL to the specified
@ -430,6 +434,9 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
def fetch_art(self, session, task):
"""Find art for the album being imported."""
if task.is_album: # Only fetch art for full albums.
if task.album.artpath and os.path.isfile(task.album.artpath):
# Album already has art (probably a re-import); skip it.
return
if task.choice_flag == importer.action.ASIS:
# For as-is imports, don't search Web sources for art.
local = True
@ -505,11 +512,18 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
return None
def _is_valid_image_candidate(self, candidate):
if not candidate:
return False
"""Determine whether the given candidate artwork is valid based on
its dimensions (width and ratio).
if not (self.enforce_ratio or self.minwidth):
return True
Return `CANDIDATE_BAD` if the file is unusable.
Return `CANDIDATE_EXACT` if the file is usable as-is.
Return `CANDIDATE_DOWNSCALE` if the file must be resized.
"""
if not candidate:
return CANDIDATE_BAD
if not (self.enforce_ratio or self.minwidth or self.maxwidth):
return CANDIDATE_EXACT
# get_size returns None if no local imaging backend is available
size = ArtResizer.shared.get_size(candidate)
@ -519,10 +533,14 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
u'documentation for dependencies. '
u'The configuration options `minwidth` and '
u'`enforce_ratio` may be violated.')
return True
return CANDIDATE_EXACT
return size and size[0] >= self.minwidth and \
(not self.enforce_ratio or size[0] == size[1])
if (not self.minwidth or size[0] >= self.minwidth) and (
not self.enforce_ratio or size[0] == size[1]):
if not self.maxwidth or size[0] > self.maxwidth:
return CANDIDATE_DOWNSCALE
return CANDIDATE_EXACT
return CANDIDATE_BAD
def art_for_album(self, album, paths, local_only=False):
"""Given an Album object, returns a path to downloaded art for the
@ -532,6 +550,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
are made.
"""
out = None
check = None
# Local art.
cover_names = self.config['cover_names'].as_str_seq()
@ -540,7 +559,8 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
if paths:
for path in paths:
candidate = self.fs_source.get(path, cover_names, cautious)
if self._is_valid_image_candidate(candidate):
check = self._is_valid_image_candidate(candidate)
if check:
out = candidate
self._log.debug('found local image {}', out)
break
@ -552,12 +572,13 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):
if self.maxwidth:
url = ArtResizer.shared.proxy_url(self.maxwidth, url)
candidate = self._fetch_image(url)
if self._is_valid_image_candidate(candidate):
check = self._is_valid_image_candidate(candidate)
if check:
out = candidate
self._log.debug('using remote image {}', out)
break
if self.maxwidth and out:
if self.maxwidth and out and check == CANDIDATE_DOWNSCALE:
out = ArtResizer.shared.resize(self.maxwidth, out)
return out

View file

@ -111,11 +111,13 @@ class MetaSyncPlugin(BeetsPlugin):
# Instantiate the meta sources
for player in sources:
try:
meta_source_instances[player] = \
META_SOURCES[player](self.config, self._log)
cls = META_SOURCES[player]
except KeyError:
self._log.error(u'Unknown metadata source \'{0}\''.format(
player))
try:
meta_source_instances[player] = cls(self.config, self._log)
except (ImportError, ConfigValueError) as e:
self._log.error(u'Failed to instantiate metadata source '
u'\'{0}\': {1}'.format(player, e))

View file

@ -94,7 +94,8 @@ class Itunes(MetaSource):
# Make the iTunes library queryable using the path
self.collection = {_norm_itunes_path(track['Location']): track
for track in raw_library['Tracks'].values()}
for track in raw_library['Tracks'].values()
if 'Location' in track}
def sync_from_source(self, item):
result = self.collection.get(util.bytestring_path(item.path).lower())

View file

@ -20,10 +20,21 @@ Fixes:
when there were not. :bug:`1652`
* :doc:`plugins/lastgenre`: Clean up the reggae related genres somewhat.
Thanks to :user:`Freso`. :bug:`1661`
* The importer now correctly moves album art files when re-importing.
:bug:`314`
* :doc:`/plugins/fetchart`: In auto mode, skips albums that already have
art attached to them so as not to interfere with re-imports. :bug:`314`
* :doc:`plugins/fetchart`: The plugin now only resizes album art if necessary,
rather than always by default. :bug:`1264`
* :doc:`plugins/fetchart`: Fix a bug where a database reference to a
non-existent album art file would prevent the command from fetching new art.
:bug:`1126`
* :doc:`/plugins/thumbnails`: Fix a crash with Unicode paths. :bug:`1686`
* :doc:`/plugins/embedart`: The ``remove_art_file`` option now works on import
(as well as with the explicit command). :bug:`1662` :bug:`1675`
* :doc:`/plugins/metasync`: Fix a crash when syncing with recent versions of
iTunes. :bug:`1700`
* :doc:`/plugins/duplicates`: Fix a crash when merging items. :bug:`1699`
1.3.15 (October 17, 2015)

View file

@ -208,6 +208,23 @@ Use the ``%asciify{}`` function in your path formats. See
:ref:`template-functions`.
.. _move-dir:
…point beets at a new music directory?
--------------------------------------
If you want to move your music from one directory to another, the best way is
to let beets do it for you. First, edit your configuration and set the
``directory`` setting to the new place. Then, type ``beet move`` to have beets
move all your files.
If you've already moved your music *outside* of beets, you have a few options:
- Move the music back (with an ordinary ``mv``) and then use the above steps.
- Delete your database and re-create it from the new paths using ``beet import -AWMC``.
- Resort to manually modifying the SQLite database (not recommended).
Why does beets…
===============

View file

@ -21,6 +21,7 @@ import os
import shutil
import responses
from mock import patch
from test import _common
from test._common import unittest
@ -422,6 +423,12 @@ class ArtForAlbumTest(UseThePlugin):
else:
self.assertIsNone(local_artpath)
def _assertImageResized(self, image_file, should_resize):
self.image_file = image_file
with patch.object(ArtResizer.shared, 'resize') as mock_resize:
self.plugin.art_for_album(None, [''], True)
self.assertEqual(mock_resize.called, should_resize)
def _require_backend(self):
"""Skip the test if the art resizer doesn't have ImageMagick or
PIL (so comparisons and measurements are unavailable).
@ -445,6 +452,12 @@ class ArtForAlbumTest(UseThePlugin):
self.plugin.enforce_ratio = False
self._assertImageIsValidArt(self.IMG_500x490, True)
def test_resize_if_necessary(self):
self._require_backend()
self.plugin.maxwidth = 300
self._assertImageResized(self.IMG_225x225, False)
self._assertImageResized(self.IMG_348x348, True)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)

View file

@ -1592,6 +1592,21 @@ class ReimportTest(unittest.TestCase, ImportHelper):
self.importer.run()
self.assertEqual(self._item().added, 4747.0)
def test_reimported_item_preserves_art(self):
self._setup_session()
art_source = os.path.join(_common.RSRC, 'abbey.jpg')
replaced_album = self._album()
replaced_album.set_art(art_source)
replaced_album.store()
old_artpath = replaced_album.artpath
self.importer.run()
new_album = self._album()
new_artpath = new_album.art_destination(art_source)
self.assertEqual(new_album.artpath, new_artpath)
self.assertTrue(os.path.exists(new_artpath))
if new_artpath != old_artpath:
self.assertFalse(os.path.exists(old_artpath))
class ImportPretendTest(_common.TestCase, ImportHelper):
""" Test the pretend commandline option