Repository: beetbox/beets

Issue:
Re-importing with fetchart creates two album art files
Heya guys,

Not a bug or anything but I'm occasionally getting multiple cover art files.
I'm thinking it's when I previously had the incorrect musicbrainz ID for the album, thus the existing `cover.jpg` isn't valid.

an example: http://pastebin.com/mz13gBZ9

I'm `cover.2.jpg` is the correct art for the release of that album I have. 
I'm kind of anal here and would much rather have any existing `cover*.jpg` files deleted if it's going to get new art.

I already have `*.jpg` configured under `clutter:`, perhaps incorrectly thinking it would remove any jpgs when it checked for new artwork.

_I suspect I'm just not groking something in the beets decision tree_

fyi: fetchart is enabled and runs with these settings:

```
fetchart:                                   # get album art
    auto:               yes                 # default: yes
    cautious:           yes                 # require cover_names
    cover_names:        cover folder front picard  # be paranoid
    sources:            coverart itunes *   # prefer coverart itunes
    maxwidth:           2500                # max of 2500px wide please
    remote_priority:    no                  # prefer local
```

Re-importing with fetchart creates two album art files
Heya guys,

Not a bug or anything but I'm occasionally getting multiple cover art files.
I'm thinking it's when I previously had the incorrect musicbrainz ID for the album, thus the existing `cover.jpg` isn't valid.

an example: http://pastebin.com/mz13gBZ9

I'm `cover.2.jpg` is the correct art for the release of that album I have. 
I'm kind of anal here and would much rather have any existing `cover*.jpg` files deleted if it's going to get new art.

I already have `*.jpg` configured under `clutter:`, perhaps incorrectly thinking it would remove any jpgs when it checked for new artwork.

_I suspect I'm just not groking something in the beets decision tree_

fyi: fetchart is enabled and runs with these settings:

```
fetchart:                                   # get album art
    auto:               yes                 # default: yes
    cautious:           yes                 # require cover_names
    cover_names:        cover folder front picard  # be paranoid
    sources:            coverart itunes *   # prefer coverart itunes
    maxwidth:           2500                # max of 2500px wide please
    remote_priority:    no                  # prefer local
```



Discussion:
Hello, and thanks for reporting! Can you please try to reproduce this from the beginning so we can see exactly what might be going on? For example, steps to reproduce might look like:
1. Import this album (attached as zip file) with this config (also attached)
2. Run `fetchart`
3. Type `ls /path/to/album` to see the two files

Without knowing what you did to lead up to this situation, it's difficult to tell how things got into a bad state.

---

> I already have *.jpg configured under clutter:, perhaps incorrectly thinking it would remove any jpgs when it checked for new artwork.

Yes, clutter is unrelated.

This happens to me when I import an album multiple times.

Steps to reproduce:
1. Import an album
2. Make sure it has a cover file
3. Import the album again
4. There should now be a `cover.N.jpg` file (`N` increments each time you re-import the album)

Here's my fetchart config:

```
fetchart:
  cautious: true
  remote_priority: true
```

That's exactly what I've experienced.  Thanks @djl

I think I've tracked this down to a bug in [`Album.set_art()`](https://github.com/sampsyo/beets/blob/master/beets/library.py#L932). Rather than deleting the old file (as the docstring implies) a new file with a unique suffix is created instead.

Looking at this, it seems like a simple fix but now I'm not sure if this was done intentionally (possibly to prevent accidental data loss?) and the docstring is just incorrect.

In any case, pull request coming shortly. We can discuss it there :)

Hi there, @djl—thanks for doing some investigation. The story is much clearer now: re-importing albums with `fetchart` enabled creates double album art files. It's related to #314.

That does seem like a problem in `set_art`, but I actually don't think that's the only cause here. When re-importing, we currently don't do anything smart at all about album art. The normal workflow would pick up the file that already exists, but you have `remote_priority` enabled so it downloads another image. Re-importing creates a new Album in the database and replaces the old one.

A full solution here, and probably to #314, would need to bring over the old Album's art to the new one, and then make sure it gets deleted if it's replaced. Does that make sense?

Also, if you do tackle this, can I ask you the favor of creating a separate `delete_art` helper method? That will be helpful for, e.g., #733.

>  but you have `remote_priority` enabled so it downloads another image

I'm a little confused now.

If `remote_priority` is the trigger for this bug, how did @k-f- end up with multiple cover files? I tested fetchart without `remote_priority` and it didn't create any duplicate files.

What am I misunderstanding?

Good question—I don't exactly know how @k-f- ended up with this behavior. If you can cook up reproduction steps with your config, @k-f-, do let us know.

Hey guys,

Sorry been fighting a failing raid array all day, and haven't had a chance to get back to you.  :neckbeard: 
I do indeed have `remote_prority:   no' set.

I've been meaning to make a config I can use for dev/testing without nuking my personal stuff, so I'll do that and see if I can't replicate this for you.

Hello, and thanks for reporting! Can you please try to reproduce this from the beginning so we can see exactly what might be going on? For example, steps to reproduce might look like:
1. Import this album (attached as zip file) with this config (also attached)
2. Run `fetchart`
3. Type `ls /path/to/album` to see the two files

Without knowing what you did to lead up to this situation, it's difficult to tell how things got into a bad state.

---

> I already have *.jpg configured under clutter:, perhaps incorrectly thinking it would remove any jpgs when it checked for new artwork.

Yes, clutter is unrelated.

This happens to me when I import an album multiple times.

Steps to reproduce:
1. Import an album
2. Make sure it has a cover file
3. Import the album again
4. There should now be a `cover.N.jpg` file (`N` increments each time you re-import the album)

Here's my fetchart config:

```
fetchart:
  cautious: true
  remote_priority: true
```

That's exactly what I've experienced.  Thanks @djl

I think I've tracked this down to a bug in [`Album.set_art()`](https://github.com/sampsyo/beets/blob/master/beets/library.py#L932). Rather than deleting the old file (as the docstring implies) a new file with a unique suffix is created instead.

Looking at this, it seems like a simple fix but now I'm not sure if this was done intentionally (possibly to prevent accidental data loss?) and the docstring is just incorrect.

In any case, pull request coming shortly. We can discuss it there :)

Hi there, @djl—thanks for doing some investigation. The story is much clearer now: re-importing albums with `fetchart` enabled creates double album art files. It's related to #314.

That does seem like a problem in `set_art`, but I actually don't think that's the only cause here. When re-importing, we currently don't do anything smart at all about album art. The normal workflow would pick up the file that already exists, but you have `remote_priority` enabled so it downloads another image. Re-importing creates a new Album in the database and replaces the old one.

A full solution here, and probably to #314, would need to bring over the old Album's art to the new one, and then make sure it gets deleted if it's replaced. Does that make sense?

Also, if you do tackle this, can I ask you the favor of creating a separate `delete_art` helper method? That will be helpful for, e.g., #733.

>  but you have `remote_priority` enabled so it downloads another image

I'm a little confused now.

If `remote_priority` is the trigger for this bug, how did @k-f- end up with multiple cover files? I tested fetchart without `remote_priority` and it didn't create any duplicate files.

What am I misunderstanding?

Good question—I don't exactly know how @k-f- ended up with this behavior. If you can cook up reproduction steps with your config, @k-f-, do let us know.

Hey guys,

Sorry been fighting a failing raid array all day, and haven't had a chance to get back to you.  :neckbeard: 
I do indeed have `remote_prority:   no' set.

I've been meaning to make a config I can use for dev/testing without nuking my personal stuff, so I'll do that and see if I can't replicate this for you.


Resolving patch:
diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py
--- a/beetsplug/fetchart.py
+++ b/beetsplug/fetchart.py
@@ -41,6 +41,10 @@
 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
@@ -505,11 +509,18 @@ def _fetch_image(self, url):
             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 False
+            return CANDIDATE_BAD
 
-        if not (self.enforce_ratio or self.minwidth):
-            return True
+        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 +530,14 @@ def _is_valid_image_candidate(self, candidate):
                               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 +547,7 @@ def art_for_album(self, album, paths, local_only=False):
         are made.
         """
         out = None
+        check = None
 
         # Local art.
         cover_names = self.config['cover_names'].as_str_seq()
@@ -540,7 +556,8 @@ def art_for_album(self, album, paths, local_only=False):
         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 +569,13 @@ def art_for_album(self, album, paths, local_only=False):
                 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


Test patch:
diff --git a/test/test_art.py b/test/test_art.py
--- a/test/test_art.py
+++ b/test/test_art.py
@@ -21,6 +21,7 @@
 import shutil
 
 import responses
+from mock import patch
 
 from test import _common
 from test._common import unittest
@@ -422,6 +423,12 @@ def _assertImageIsValidArt(self, image_file, should_exist):
         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 @@ def test_respect_enforce_ratio_no(self):
         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__)