Repository: beetbox/beets
Pull request: Added option to embedart using an image URL [small PR]
## Description

Fixes [#83](https://github.com/beetbox/beets/issues/831)

Added another option `-u` to provide an image URL. The plugin can now download the image, check if it is valid, and then embed the picture as cover art. 

## To Do

- [x] Documentation. (If you've add a new command-line flag, for example, find the appropriate page under `docs/` to describe it.)
- [x] Changelog. (Add an entry to `docs/changelog.rst` near the top of the document.)
- [x] Tests. (Encouraged but not strictly required.)


Problem statement:
FetchArt / EmbedArt - fetch art from specific url
if not found or if set in options prompt for a url from where to get the album art

.. so that we dont have to download it and use the `[-f IMAGE]` option :)

Hints from issue discussion:
Thanks for the suggestion! In the mean time, have you considered a simple shell script that just downloads a file with `curl` and then invokes `beet embedart`? Something as simple as:

```
#!/bin/sh
curl -o temp.jpg $1
beet embedart -f temp.jpg $2
rm temp.jpg
```

could work.

thanks!
i guess i'm stuck on windows, but i'll figure it out ;)
(i'm anyway trying to automate my whole process, like unzipping archives, using beets and then importing the tagged music into itunes using the COM :) if it works as i hope, i'll also write something for os x, for a friend of mine. i guess i could say beets is the heart of the whole idea B-))

to develop this further: this could be an additional source for `fetchart`, e.g. `url`. which alway prompts for an url but otherwise has the same flow.

Commit message: Add additional error handling

Patch to review:
diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py
index 6db46f8c17..5df9f2e5a4 100644
--- a/beetsplug/embedart.py
+++ b/beetsplug/embedart.py
@@ -15,14 +15,15 @@
 """Allows beets to embed album art into file metadata."""
 
 import os.path
+from io import BytesIO
 
+import requests
+
+from beets import art, config, ui
 from beets.plugins import BeetsPlugin
-from beets import ui
-from beets.ui import print_, decargs
-from beets.util import syspath, normpath, displayable_path, bytestring_path
+from beets.ui import decargs, print_
+from beets.util import bytestring_path, displayable_path, normpath, syspath
 from beets.util.artresizer import ArtResizer
-from beets import config
-from beets import art
 
 
 def _confirm(objs, album):
@@ -75,15 +76,17 @@ def __init__(self):
 
     def commands(self):
         # Embed command.
-        embed_cmd = ui.Subcommand(
-            'embedart', help='embed image files into file metadata'
-        )
-        embed_cmd.parser.add_option(
-            '-f', '--file', metavar='PATH', help='the image file to embed'
-        )
-        embed_cmd.parser.add_option(
-            "-y", "--yes", action="store_true", help="skip confirmation"
-        )
+        embed_cmd = ui.Subcommand('embedart',
+                                  help='embed image files into file metadata')
+        embed_cmd.parser.add_option('-f', '--file', metavar='PATH',
+                                    help='the image file to embed')
+
+        embed_cmd.parser.add_option("-y", "--yes", action="store_true",
+                                    help="skip confirmation")
+
+        embed_cmd.parser.add_option('-u', '--url', action="store_true",
+                                    help='the URL of the image file to embed')
+
         maxwidth = self.config['maxwidth'].get(int)
         quality = self.config['quality'].get(int)
         compare_threshold = self.config['compare_threshold'].get(int)
@@ -107,13 +110,38 @@ def embed_func(lib, opts, args):
                     art.embed_item(self._log, item, imagepath, maxwidth,
                                    None, compare_threshold, ifempty,
                                    quality=quality)
+            elif opts.url:
+                from PIL import Image
+                try:
+                    response = requests.get(opts.url, timeout=5)
+                    response.raise_for_status()
+                except requests.exceptions.RequestException as e:
+                    self._log.error("Error: {}".format(e))
+                try:
+                    img = Image.open(BytesIO(response.content))
+                except OSError as e:
+                    self._log.error("Error: {}".format(e))
+                if img.format:
+                    with open('temp.jpg', 'wb') as f:
+                        f.write(response.content)
+                    opts.file = 'temp.jpg'
+                    items = lib.items(decargs(args))
+                    # Confirm with user.
+                    if not opts.yes and not _confirm(items, not opts.url):
+                        return
+                    for item in items:
+                        art.embed_item(self._log, item, opts.file, maxwidth,
+                                       None, compare_threshold, ifempty,
+                                       quality=quality)
+                    os.remove(opts.file)
+                else:
+                    self._log.error('Invalid image file')
+                    return
             else:
                 albums = lib.albums(decargs(args))
-
                 # Confirm with user.
                 if not opts.yes and not _confirm(albums, not opts.file):
                     return
-
                 for album in albums:
                     art.embed_album(self._log, album, maxwidth,
                                     False, compare_threshold, ifempty,


Code review comments:
diff_hunk: @@ -107,13 +110,38 @@ def embed_func(lib, opts, args):
                     art.embed_item(self._log, item, imagepath, maxwidth,
                                    None, compare_threshold, ifempty,
                                    quality=quality)
+            elif opts.url:
+                from PIL import Image
+                try:
+                    response = requests.get(opts.url, timeout=5)
+                    response.raise_for_status()
+                except requests.exceptions.RequestException as e:
+                    self._log.error("Error: {}".format(e))
+                try:
+                    img = Image.open(BytesIO(response.content))
+                except OSError as e:
+                    self._log.error("Error: {}".format(e))
+                if img.format:
+                    with open('temp.jpg', 'wb') as f:
+                        f.write(response.content)
+                    opts.file = 'temp.jpg'
path: beetsplug/embedart.py
text: @user1:
I'm still not sure if it hurts to handle errors here. What happens if the file can't be written. Disk full, permission problem or something like that. Maybe I'm overcautious but  yeah...

@author:
Good point....added more error handling.