From e58e04f0ab18a7d9b3ea8a4a5589fa0f55d93c85 Mon Sep 17 00:00:00 2001 From: Ali Graham Date: Thu, 25 Feb 2016 12:44:09 +1030 Subject: [PATCH 1/8] move and split fetchart valid_image --- beets/util/artresizer.py | 40 +++++++++++++++++ beetsplug/fetchart.py | 93 ++++++++++++++-------------------------- 2 files changed, 72 insertions(+), 61 deletions(-) diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 1b6a5903e..919a10883 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -173,6 +173,46 @@ class ArtResizer(object): log.debug(u"artresizer: method is {0}", self.method) self.can_compare = self._can_compare() + def valid_size(self, size, enforce_ratio = False, minwidth = None): + """If size constraints exist, check whether the provided image size + matches them. + """ + # Check minimum size. + if minwidth and size[0] < minwidth: + log.debug('image too small ({} < {})', + size[0], minwidth) + return False + + # Check aspect ratio. + if enforce_ratio and size[0] != size[1]: + log.debug('image is not square ({} != {})', + size[0], size[1]) + return False + + return True + + def must_resize(self, size, maxwidth = None): + """Determine whether the provided image size means that the image + will need to be scaled to fit the maximum width. + """ + if not maxwidth: + return False + + if not size: + log.warning(u'Could not get size of image (please see ' + u'documentation for dependencies).') + return False + + if size[0] <= maxwidth: + log.debug('dump values ({} > {})', + size[0], maxwidth) + log.debug(u'Image does not need to be resized.') + return False + + log.debug('Image needs resizing ({} > {})', + size[0], maxwidth) + return True + def resize(self, maxwidth, path_in, path_out=None): """Manipulate an image file according to the method, returning a new path. For PIL or IMAGEMAGIC methods, resizes the image to a diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 57d8e4c46..efa2f5bde 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -42,10 +42,6 @@ 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 @@ -535,51 +531,6 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): self._log.debug('error fetching art: {}', exc) return None - def _is_valid_image_candidate(self, candidate): - """Determine whether the given candidate artwork is valid based on - its dimensions (width and ratio). - - 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) - self._log.debug('image size: {}', size) - - if not size: - self._log.warning(u'Could not get size of image (please see ' - u'documentation for dependencies). ' - u'The configuration options `minwidth` and ' - u'`enforce_ratio` may be violated.') - return CANDIDATE_EXACT - - # Check minimum size. - if self.minwidth and size[0] < self.minwidth: - self._log.debug('image too small ({} < {})', - size[0], self.minwidth) - return CANDIDATE_BAD - - # Check aspect ratio. - if self.enforce_ratio and size[0] != size[1]: - self._log.debug('image is not square ({} != {})', - size[0], size[1]) - return CANDIDATE_BAD - - # Check maximum size. - if self.maxwidth and size[0] > self.maxwidth: - self._log.debug('image needs resizing ({} > {})', - size[0], self.maxwidth) - return CANDIDATE_DOWNSCALE - - return CANDIDATE_EXACT - def art_for_album(self, album, paths, local_only=False): """Given an Album object, returns a path to downloaded art for the album (or None if no art is found). If `maxwidth`, then images are @@ -588,7 +539,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): are made. """ out = None - check = None + size = None # Local art. cover_names = self.config['cover_names'].as_str_seq() @@ -597,11 +548,21 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): if paths: for path in paths: candidate = self.fs_source.get(path, cover_names, cautious) - check = self._is_valid_image_candidate(candidate) - if check: - out = candidate - self._log.debug('found local image {}', out) - break + if not candidate: + continue + + # get_size returns None if no local imaging backend is available + size = ArtResizer.shared.get_size(candidate) + self._log.debug('image size: {}', size) + if size: + valid = ArtResizer.shared.valid_size(size, + self.enforce_ratio, self.minwidth) + if not valid: + continue + + out = candidate + self._log.debug('found local image {}', out) + break # Web art sources. remote_priority = self.config['remote_priority'].get(bool) @@ -610,13 +571,23 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): if self.maxwidth: url = ArtResizer.shared.proxy_url(self.maxwidth, url) candidate = self._fetch_image(url) - check = self._is_valid_image_candidate(candidate) - if check: - out = candidate - self._log.debug('using remote image {}', out) - break + if not candidate: + continue - if self.maxwidth and out and check == CANDIDATE_DOWNSCALE: + # get_size returns None if no local imaging backend is available + size = ArtResizer.shared.get_size(candidate) + self._log.debug('image size: {}', size) + if size: + valid = ArtResizer.shared.valid_size(size, + self.enforce_ratio, self.minwidth) + if not valid: + continue + + out = candidate + self._log.debug('using remote image {}', out) + break + + if self.maxwidth and out and ArtResizer.shared.must_resize(size, self.maxwidth): out = ArtResizer.shared.resize(self.maxwidth, out) return out From 04c12a50ac0124c77cdc02e0dd6251004e76c6db Mon Sep 17 00:00:00 2001 From: Ali Graham Date: Thu, 25 Feb 2016 12:44:56 +1030 Subject: [PATCH 2/8] add copy_album_art_maxwidth config option to convert plugin --- beetsplug/convert.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 6145fdb74..0e0ad5817 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -29,6 +29,7 @@ from beets import ui, util, plugins, config from beets.plugins import BeetsPlugin from beets.util.confit import ConfigTypeError from beets import art +from beets.util.artresizer import ArtResizer _fs_lock = threading.Lock() _temp_files = [] # Keep track of temporary transcoded files for deletion. @@ -132,6 +133,7 @@ class ConvertPlugin(BeetsPlugin): u'paths': {}, u'never_convert_lossy_files': False, u'copy_album_art': False, + u'copy_album_art_maxwidth': 0, }) self.import_stages = [self.auto_convert] @@ -305,8 +307,8 @@ class ConvertPlugin(BeetsPlugin): dest=converted, keepnew=False) def copy_album_art(self, album, dest_dir, path_formats, pretend=False): - """Copies the associated cover art of the album. Album must have at - least one track. + """Copies or converts the associated cover art of the album. Album must + have at least one track. """ if not album or not album.artpath: return @@ -336,14 +338,33 @@ class ConvertPlugin(BeetsPlugin): util.displayable_path(album.artpath)) return - if pretend: - self._log.info(u'cp {0} {1}', - util.displayable_path(album.artpath), - util.displayable_path(dest)) + resize = False + maxwidth = None + + if self.config['copy_album_art_maxwidth']: + maxwidth = self.config['copy_album_art_maxwidth'].get(int) + size = ArtResizer.shared.get_size(album.artpath) + if size: + resize = ArtResizer.shared.must_resize(size, maxwidth) + + if resize: + if pretend: + self._log.info(u'resize_artwork {0} {1}', + util.displayable_path(album.artpath), + util.displayable_path(dest)) + else: + self._log.info(u'Converting cover art to {0}', + util.displayable_path(dest)) + ArtResizer.shared.resize(maxwidth, album.artpath, dest) else: - self._log.info(u'Copying cover art to {0}', - util.displayable_path(dest)) - util.copy(album.artpath, dest) + if pretend: + self._log.info(u'cp {0} {1}', + util.displayable_path(album.artpath), + util.displayable_path(dest)) + else: + self._log.info(u'Copying cover art to {0}', + util.displayable_path(dest)) + util.copy(album.artpath, dest) def convert_func(self, lib, opts, args): if not opts.dest: From 1fc6efe3b4b0bad39379144ab2e3b8318545c151 Mon Sep 17 00:00:00 2001 From: Ali Graham Date: Thu, 25 Feb 2016 18:45:53 +1030 Subject: [PATCH 3/8] fix indentation --- beetsplug/convert.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 0e0ad5817..a99d1983d 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -342,10 +342,10 @@ class ConvertPlugin(BeetsPlugin): maxwidth = None if self.config['copy_album_art_maxwidth']: - maxwidth = self.config['copy_album_art_maxwidth'].get(int) - size = ArtResizer.shared.get_size(album.artpath) - if size: - resize = ArtResizer.shared.must_resize(size, maxwidth) + maxwidth = self.config['copy_album_art_maxwidth'].get(int) + size = ArtResizer.shared.get_size(album.artpath) + if size: + resize = ArtResizer.shared.must_resize(size, maxwidth) if resize: if pretend: From bc21afab4276fc27d2d874fd8563c4baf77b6b6a Mon Sep 17 00:00:00 2001 From: Ali Graham Date: Thu, 25 Feb 2016 18:50:41 +1030 Subject: [PATCH 4/8] better text for artwork resize/copy logs --- beetsplug/convert.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index a99d1983d..f02fd6442 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -348,22 +348,16 @@ class ConvertPlugin(BeetsPlugin): resize = ArtResizer.shared.must_resize(size, maxwidth) if resize: - if pretend: - self._log.info(u'resize_artwork {0} {1}', - util.displayable_path(album.artpath), - util.displayable_path(dest)) - else: - self._log.info(u'Converting cover art to {0}', - util.displayable_path(dest)) + self._log.info(u'Resizing cover art from {0} to {1}', + util.displayable_path(album.artpath), + util.displayable_path(dest)) + if not pretend: ArtResizer.shared.resize(maxwidth, album.artpath, dest) else: - if pretend: - self._log.info(u'cp {0} {1}', - util.displayable_path(album.artpath), - util.displayable_path(dest)) - else: - self._log.info(u'Copying cover art to {0}', - util.displayable_path(dest)) + self._log.info(u'Copying cover art from {0} to {1}', + util.displayable_path(album.artpath), + util.displayable_path(dest)) + if not pretend: util.copy(album.artpath, dest) def convert_func(self, lib, opts, args): From a1f80275c4d86954b4b4b451b3efde2fe382c30d Mon Sep 17 00:00:00 2001 From: Ali Graham Date: Thu, 25 Feb 2016 19:41:00 +1030 Subject: [PATCH 5/8] revert a chunk of the work to be the way it was --- beets/util/artresizer.py | 40 ----------------- beetsplug/convert.py | 6 ++- beetsplug/fetchart.py | 93 ++++++++++++++++++++++++++-------------- 3 files changed, 66 insertions(+), 73 deletions(-) diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 919a10883..1b6a5903e 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -173,46 +173,6 @@ class ArtResizer(object): log.debug(u"artresizer: method is {0}", self.method) self.can_compare = self._can_compare() - def valid_size(self, size, enforce_ratio = False, minwidth = None): - """If size constraints exist, check whether the provided image size - matches them. - """ - # Check minimum size. - if minwidth and size[0] < minwidth: - log.debug('image too small ({} < {})', - size[0], minwidth) - return False - - # Check aspect ratio. - if enforce_ratio and size[0] != size[1]: - log.debug('image is not square ({} != {})', - size[0], size[1]) - return False - - return True - - def must_resize(self, size, maxwidth = None): - """Determine whether the provided image size means that the image - will need to be scaled to fit the maximum width. - """ - if not maxwidth: - return False - - if not size: - log.warning(u'Could not get size of image (please see ' - u'documentation for dependencies).') - return False - - if size[0] <= maxwidth: - log.debug('dump values ({} > {})', - size[0], maxwidth) - log.debug(u'Image does not need to be resized.') - return False - - log.debug('Image needs resizing ({} > {})', - size[0], maxwidth) - return True - def resize(self, maxwidth, path_in, path_out=None): """Manipulate an image file according to the method, returning a new path. For PIL or IMAGEMAGIC methods, resizes the image to a diff --git a/beetsplug/convert.py b/beetsplug/convert.py index f02fd6442..0f2eb8d4d 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -344,8 +344,12 @@ class ConvertPlugin(BeetsPlugin): if self.config['copy_album_art_maxwidth']: maxwidth = self.config['copy_album_art_maxwidth'].get(int) size = ArtResizer.shared.get_size(album.artpath) + self._log.debug('image size: {}', size) if size: - resize = ArtResizer.shared.must_resize(size, maxwidth) + resize = size[0] > maxwidth + else: + self._log.warning(u'Could not get size of image (please see ' + u'documentation for dependencies).') if resize: self._log.info(u'Resizing cover art from {0} to {1}', diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index efa2f5bde..57d8e4c46 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -42,6 +42,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 @@ -531,6 +535,51 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): self._log.debug('error fetching art: {}', exc) return None + def _is_valid_image_candidate(self, candidate): + """Determine whether the given candidate artwork is valid based on + its dimensions (width and ratio). + + 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) + self._log.debug('image size: {}', size) + + if not size: + self._log.warning(u'Could not get size of image (please see ' + u'documentation for dependencies). ' + u'The configuration options `minwidth` and ' + u'`enforce_ratio` may be violated.') + return CANDIDATE_EXACT + + # Check minimum size. + if self.minwidth and size[0] < self.minwidth: + self._log.debug('image too small ({} < {})', + size[0], self.minwidth) + return CANDIDATE_BAD + + # Check aspect ratio. + if self.enforce_ratio and size[0] != size[1]: + self._log.debug('image is not square ({} != {})', + size[0], size[1]) + return CANDIDATE_BAD + + # Check maximum size. + if self.maxwidth and size[0] > self.maxwidth: + self._log.debug('image needs resizing ({} > {})', + size[0], self.maxwidth) + return CANDIDATE_DOWNSCALE + + return CANDIDATE_EXACT + def art_for_album(self, album, paths, local_only=False): """Given an Album object, returns a path to downloaded art for the album (or None if no art is found). If `maxwidth`, then images are @@ -539,7 +588,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): are made. """ out = None - size = None + check = None # Local art. cover_names = self.config['cover_names'].as_str_seq() @@ -548,21 +597,11 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): if paths: for path in paths: candidate = self.fs_source.get(path, cover_names, cautious) - if not candidate: - continue - - # get_size returns None if no local imaging backend is available - size = ArtResizer.shared.get_size(candidate) - self._log.debug('image size: {}', size) - if size: - valid = ArtResizer.shared.valid_size(size, - self.enforce_ratio, self.minwidth) - if not valid: - continue - - out = candidate - self._log.debug('found local image {}', out) - break + check = self._is_valid_image_candidate(candidate) + if check: + out = candidate + self._log.debug('found local image {}', out) + break # Web art sources. remote_priority = self.config['remote_priority'].get(bool) @@ -571,23 +610,13 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): if self.maxwidth: url = ArtResizer.shared.proxy_url(self.maxwidth, url) candidate = self._fetch_image(url) - if not candidate: - continue + check = self._is_valid_image_candidate(candidate) + if check: + out = candidate + self._log.debug('using remote image {}', out) + break - # get_size returns None if no local imaging backend is available - size = ArtResizer.shared.get_size(candidate) - self._log.debug('image size: {}', size) - if size: - valid = ArtResizer.shared.valid_size(size, - self.enforce_ratio, self.minwidth) - if not valid: - continue - - out = candidate - self._log.debug('using remote image {}', out) - break - - if self.maxwidth and out and ArtResizer.shared.must_resize(size, self.maxwidth): + if self.maxwidth and out and check == CANDIDATE_DOWNSCALE: out = ArtResizer.shared.resize(self.maxwidth, out) return out From b59792c7a8c7f197f791310c630029d4d38a55a6 Mon Sep 17 00:00:00 2001 From: Ali Graham Date: Thu, 25 Feb 2016 19:41:28 +1030 Subject: [PATCH 6/8] doc note for new copy_album_art_maxwidth option --- docs/plugins/convert.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index 7480bacbb..b992663a4 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -62,6 +62,9 @@ file. The available options are: Default: none (system default), - **copy_album_art**: Copy album art when copying or transcoding albums matched using the ``-a`` option. Default: ``no``. +- **copy_album_art_maxwidth**: Downscale album art if it's too big. The + resize operation reduces image width to at most ``maxwidth`` pixels. The + height is recomputed so that the aspect ratio is preserved. - **dest**: The directory where the files will be converted (or copied) to. Default: none. - **embed**: Embed album art in converted items. Default: ``yes``. From a98bc481cd3e9718e58cf551a11905c6ea3e832b Mon Sep 17 00:00:00 2001 From: Ali Graham Date: Sat, 27 Feb 2016 19:02:10 +1030 Subject: [PATCH 7/8] tweaks for style, option name --- beetsplug/convert.py | 8 ++++---- docs/plugins/convert.rst | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 0f2eb8d4d..81c6f4eb8 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -133,7 +133,7 @@ class ConvertPlugin(BeetsPlugin): u'paths': {}, u'never_convert_lossy_files': False, u'copy_album_art': False, - u'copy_album_art_maxwidth': 0, + u'album_art_maxwidth': 0, }) self.import_stages = [self.auto_convert] @@ -341,8 +341,8 @@ class ConvertPlugin(BeetsPlugin): resize = False maxwidth = None - if self.config['copy_album_art_maxwidth']: - maxwidth = self.config['copy_album_art_maxwidth'].get(int) + if self.config['album_art_maxwidth']: + maxwidth = self.config['album_art_maxwidth'].get(int) size = ArtResizer.shared.get_size(album.artpath) self._log.debug('image size: {}', size) if size: @@ -358,7 +358,7 @@ class ConvertPlugin(BeetsPlugin): if not pretend: ArtResizer.shared.resize(maxwidth, album.artpath, dest) else: - self._log.info(u'Copying cover art from {0} to {1}', + self._log.info(u'cp {0} {1}', util.displayable_path(album.artpath), util.displayable_path(dest)) if not pretend: diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index b992663a4..b93227b62 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -62,9 +62,9 @@ file. The available options are: Default: none (system default), - **copy_album_art**: Copy album art when copying or transcoding albums matched using the ``-a`` option. Default: ``no``. -- **copy_album_art_maxwidth**: Downscale album art if it's too big. The - resize operation reduces image width to at most ``maxwidth`` pixels. The - height is recomputed so that the aspect ratio is preserved. +- **album_art_maxwidth**: Downscale album art if it's too big. The resize + operation reduces image width to at most ``maxwidth`` pixels. The height is + recomputed so that the aspect ratio is preserved. - **dest**: The directory where the files will be converted (or copied) to. Default: none. - **embed**: Embed album art in converted items. Default: ``yes``. From 9f59e136f983f34a9aaf0eb57c5e0d0696cc20f1 Mon Sep 17 00:00:00 2001 From: Ali Graham Date: Sat, 27 Feb 2016 19:16:13 +1030 Subject: [PATCH 8/8] changelog entry for new convert.album_art_maxwidth option --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3803894b2..77be3019a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,12 @@ Changelog 1.3.18 (in development) ----------------------- +New features: + +* :doc:`/plugins/convert`: A new `album_art_maxwidth` option which will + downsize destination images if the `copy_album_art` switch is true and the + image is too wide. + Fixes: * Fix a problem with the :ref:`stats-cmd` in exact mode when filenames on