Repository: beetbox/beets

Issue:
ffmpeg passed wrong argument due to apostrophe
### Problem

When I try to use "beet convert" on an album with an apostrophe ffmpeg is sent the file path using " â€™ " in place of " ' ", perhaps this is a unicode error? 

Running beet convert verbose (`-vv`) mode:

```sh
D:\>beet -vv convert bbc radio earned
```

Led to this problem:

```
convert: Encoding D:\Music\Music\FLAC\Compilations\BBC Radio 1’s Live Lounge 2015\09 Earned It.flac
convert: Encoding D:\Music\Music\FLAC\Compilations\BBC Radio 1â€™s Live Lounge 2015\09 Earned It.flac failed. Cleaning up...
convert: Command ['ffmpeg', '-i', 'D:\\Music\\Music\\FLAC\\Compilations\\BBC Radio 1â€™s Live Lounge 2015\\09 Earned It.flac', '-y', '-vn', '-q:a', '1', 'D:\\Music\\Converted Music\\Compilations\\BBC Radio 1â€™s Live Lounge 2015\\09 Earned It.mp3'] exited with status 1: ffmpeg version N-83507-g8fa18e0 Copyright (c) 2000-2017 the FFmpeg developers
  built with gcc 5.4.0 (GCC)
  configuration: --enable-gpl --enable-version3 --enable-cuda --enable-cuvid --enable-d3d11va --enable-dxva2 --enable-libmfx --enable-nvenc --enable-avisynth --enable-bzlib --enable-fontconfig --enable-frei0r --enable-gnutls --enable-iconv --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libfreetype --enable-libgme --enable-libgsm --enable-libilbc --enable-libmodplug --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopus --enable-librtmp --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvo-amrwbenc --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxavs --enable-libxvid --enable-libzimg --enable-lzma --enable-zlib
  libavutil      55. 47.100 / 55. 47.100
  libavcodec     57. 80.100 / 57. 80.100
  libavformat    57. 66.102 / 57. 66.102
  libavdevice    57.  2.100 / 57.  2.100
  libavfilter     6. 73.100 /  6. 73.100
  libswscale      4.  3.101 /  4.  3.101
  libswresample   2.  4.100 /  2.  4.100
  libpostproc    54.  2.100 / 54.  2.100
D:\Music\Music\FLAC\Compilations\BBC Radio 1â€™s Live Lounge 2015\09 Earned It.flac: No such file or directory

```

Here's a link to the music files that trigger the bug (if relevant): 

```sh
D:\>beet ls bbc radio earned -p
D:\Music\Music\FLAC\Compilations\BBC Radio 1’s Live Lounge 2015\09 Earned It.flac
```
### Setup

* OS: Windows 10
* Python version: 3.6.0
* beets version: 1.4.3
* Turning off plugins made problem go away (yes/no):  it's a plugin problem

ffmpeg is installed correctly and can convert other files.

My configuration (output of `beet config`) is:

```yaml
directory: D:/Music/Music
library: D:/Music/Music/windows-library.blb
threaded: yes

import:
    copy: no
    move: yes
    quiet_fallback: skip
    log: beetlog.txt

plugins: fetchart convert missing info duplicates scrub embedart fromfilename
embedart:
    ifempty: yes
    maxwidth: 0
    auto: yes
    compare_threshold: 0
    remove_art_file: no
thumbnails:
    auto: yes

paths:
    default: $format/$albumartist/$album/$track $title
    singleton: $format/$albumartist/Other/$track $title
    comp: $format/Compilations/$album/$track $title
convert:
    never_convert_lossy_files: yes
    auto: no
    embed: yes
    copy_album_art: yes
    dest: D:/Music/Converted Music

    paths:
        default: $albumartist/$album/$track $title
        singleton: $albumartist/Other/$track $title
        comp: Compilations/$album/$track $title
    format: mp3
    formats:
        mp3: ffmpeg -i $source -y -vn -q:a 1 $dest
        aac:
            command: ffmpeg -i $source -y -vn -acodec libfaac -aq 100 $dest
            extension: m4a
        alac:
            command: ffmpeg -i $source -y -vn -acodec alac $dest
            extension: m4a
        flac: ffmpeg -i $source -y -vn -acodec flac $dest
        opus: ffmpeg -i $source -y -vn -acodec libopus -ab 96k $dest
        ogg: ffmpeg -i $source -y -vn -acodec libvorbis -aq 3 $dest
        wma: ffmpeg -i $source -y -vn -acodec wmav2 -vn $dest
    pretend: no
    threads: 4
    max_bitrate: 500
    tmpdir:
    quiet: no
    album_art_maxwidth: 0
fetchart:
    auto: yes
    minwidth: 0
    maxwidth: 0
    enforce_ratio: no
    cautious: no
    cover_names:
    - cover
    - front
    - art
    - album
    - folder
    sources:
    - filesystem
    - coverart
    - itunes
    - amazon
    - albumart
    google_key: REDACTED
    google_engine: 001442825323518660753:hrh5ch1gjzm
    fanarttv_key: REDACTED
    store_source: no
scrub:
    auto: yes
duplicates:
    album: no
    checksum: ''
    copy: ''
    count: no
    delete: no
    format: ''
    full: no
    keys: []
    merge: no
    move: ''
    path: no
    tiebreak: {}
    strict: no
    tag: ''
missing:
    count: no
    total: no

```



Discussion:
Update: converting a file with an apostrophe in its filename but not path does not produce this error:

```
convert: Encoding D:\Music\Music\FLAC\Compilations\Dinner Jazz - American Classics\04 Ev'ry Time We Say Goodbye.flac
convert: Finished encoding D:\Music\Music\FLAC\Compilations\Dinner Jazz - American Classics\04 Ev'ry Time We Say Goodbye.flac
```

Resolving patch:
diff --git a/beetsplug/convert.py b/beetsplug/convert.py
--- a/beetsplug/convert.py
+++ b/beetsplug/convert.py
@@ -24,6 +24,7 @@
 import shlex
 import six
 from string import Template
+import platform
 
 from beets import ui, util, plugins, config
 from beets.plugins import BeetsPlugin
@@ -183,12 +184,22 @@ def encode(self, command, source, dest, pretend=False):
         if not quiet and not pretend:
             self._log.info(u'Encoding {0}', util.displayable_path(source))
 
-        # Substitute $source and $dest in the argument list.
+        # On Python 3, we need to construct the command to invoke as a
+        # Unicode string. On Unix, this is a little unfortunate---the OS is
+        # expecting bytes---so we use surrogate escaping and decode with the
+        # argument encoding, which is the same encoding that will then be
+        # *reversed* to recover the same bytes before invoking the OS. On
+        # Windows, we want to preserve the Unicode filename "as is."
         if not six.PY2:
             command = command.decode(util.arg_encoding(), 'surrogateescape')
-            source = source.decode(util.arg_encoding(), 'surrogateescape')
-            dest = dest.decode(util.arg_encoding(), 'surrogateescape')
+            if platform.system() == 'Windows':
+                source = source.decode(util._fsencoding())
+                dest = dest.decode(util._fsencoding())
+            else:
+                source = source.decode(util.arg_encoding(), 'surrogateescape')
+                dest = dest.decode(util.arg_encoding(), 'surrogateescape')
 
+        # Substitute $source and $dest in the argument list.
         args = shlex.split(command)
         encode_cmd = []
         for i, arg in enumerate(args):


Test patch: