Repository: beetbox/beets

Issue:
convert: In auto mode, option to keep originals instead of converted files
Hey All,

I'm looking to use beets to manage my lossless library, but I have devices in the house that do not support high bit-rate formats (Sonos devices). So I'm looking to use Beets to import music into it's database and at the same time create a copy of the Lossless files, convert them into lossy/.mp3 and store in a different location.

Couple questions:
1. Can this all be done with one Beets instance / database or should I use one for the lossy and one for the lossless?
2. How would this be configured? I'm using the below configuration and it only keeps the MP3 version and ignores the FLACs.

```
plugins: fetchart embedart convert scrub replaygain lastgenre chroma web
directory: /music
library: /config/musiclibrary.blb
art_filename: albumart
threaded: yes
original_date: no
per_disc_numbering: no

convert:
    auto: yes
    ffmpeg: /usr/bin/ffmpeg
    opts: -ab 320k -ac 2 -ar 48000
    max_bitrate: 320
    threads: 5
    copy_album_art: yes
    dest: /music/lossy

paths:
    default: $albumartist/$album%aunique{}/$track - $title
    singleton: Non-Album/$artist - $title
    comp: Compilations/$album%aunique{}/$track - $title
    albumtype_soundtrack: Soundtracks/$album/$track $title

import:
    write: yes
    copy: yes
    move: no
    resume: ask
    incremental: yes
    quiet_fallback: skip
    timid: no
    log: /config/beet.log

lastgenre:
    auto: yes
    source: album

embedart:
    auto: yes

fetchart:
    auto: yes

replaygain:
    auto: no

scrub:
    auto: yes

replace:
    '^\.': _
    '[\x00-\x1f]': _
    '[<>:"\?\*\|]': _
    '[\xE8-\xEB]': e
    '[\xEC-\xEF]': i
    '[\xE2-\xE6]': a
    '[\xF2-\xF6]': o
    '[\xF8]': o
    '\.$': _
    '\s+$': ''

web:
    host: 0.0.0.0
    port: 8337
```



Discussion:
With this setup, beets will convert files before adding them to the library, as described in [the documentation](http://beets.readthedocs.org/en/latest/plugins/convert.html):

> - auto: Import transcoded versions of your files automatically during imports. With this option enabled, the importer will transcode all (in the default configuration) non-MP3 files over the maximum bitrate before adding them to your library. Default: `no`.

I don't know of any way to automate the conversion process using the plugins readily available with beets. Maybe in the near future with a combination of `convert` and `hook` (see #1603 ).

However, if you're feeling bleeding edge or want to experiment a bit, you can always try the [beets-alternatives](https://github.com/geigerzaehler/beets-alternatives) plugin. Note however that it hasn't been updated since may 2015 and that support might be a bit lacking.

Maybe you could take a look at #1620 #1110 (In #1620 I reference a feature request that was very similar to yours, but I can't find it anymore :disappointed:). 

The usual way, FWIW, is not to have a beets library for your lossy files. Just keep the lossless originals in your beets library, and periodically `beet convert` to export things for your device.

@nathdwek has a great point, though: maybe the `auto` function should have a simpler mode where it keeps the old files, not the transcodes, in the library.

@sampsyo @nathdwek  that'd actually be perfect. Wouldn't mind having the lossy in sort of an 'out of band' mode. Primarily interested in maintaining the higher quality sources. Would need the option to copy without transcoding in the event that the new import was already an MP3 at 320k. Ideally the originals would have their tags adjusted first, then new ones transcoded with the tags copied over. 

Good call; turning this into a feature request.

Fortunately, the plugin already has bitrate filters, so keeping 320kbps MP3s shouldn't be a problem.

Also, if there could be a way to keep tabs on what has been converted in the past within the database, with maybe a reference link to where it was saved to, that could be helpful for libraries with some transcoded / exported and some not.

Yeah that's basically what beets-alternative does. Seems like a few people would have been interested by it lately. However, it would still require some effort to bring it in and resume support.

I don't know however if it would be a good call to try and provide these features from scratch however, given that a working solution is right there.

Just as an example, something like a "converted version" flexible attribute wouldn't be hard to add to the `convert` plugin. I'm not sure about the rest of the `alternatives` features.

Just searching for exactly this to export / convert my flac to mp3 for my car mp3 player and wondered how it was going? :)
In general, there's never really anything to report except what you see on GitHub. So, in summary, we could use some help with addressing this if you have time!
Unless you'll take some pascal code I'll not be much help, but its always surprising how sensitive coders are to timescales. I wasn't meaning to hurry anyone along, just thought I may have missed an update or an alternate way of getting an mp3 copy of my source flac files. I'll keep searching for an alternate way. All the best.
Sorry; I didn't mean to sound offended! Just trying to give some context: work in this project generally doesn't get done until someone comes up with the extra time to claim a ticket.

Resolving patch:
diff --git a/beetsplug/convert.py b/beetsplug/convert.py
--- a/beetsplug/convert.py
+++ b/beetsplug/convert.py
@@ -138,6 +138,7 @@ def __init__(self):
             },
             'max_bitrate': 500,
             'auto': False,
+            'auto_keep': False,
             'tmpdir': None,
             'quiet': False,
             'embed': True,
@@ -148,7 +149,7 @@ def __init__(self):
             'album_art_maxwidth': 0,
             'delete_originals': False,
         })
-        self.early_import_stages = [self.auto_convert]
+        self.early_import_stages = [self.auto_convert, self.auto_convert_keep]
 
         self.register_listener('import_task_files', self._cleanup)
 
@@ -184,6 +185,16 @@ def auto_convert(self, config, task):
             par_map(lambda item: self.convert_on_import(config.lib, item),
                     task.imported_items())
 
+    def auto_convert_keep(self, config, task):
+        if self.config['auto_keep']:
+            empty_opts = self.commands()[0].parser.get_default_values()
+            (dest, threads, path_formats, fmt,
+             pretend, hardlink, link) = self._get_opts_and_config(empty_opts)
+
+            items = task.imported_items()
+            self._parallel_convert(dest, False, path_formats, fmt,
+                                   pretend, link, hardlink, threads, items)
+
     # Utilities converted from functions to methods on logging overhaul
 
     def encode(self, command, source, dest, pretend=False):
@@ -423,31 +434,8 @@ def copy_album_art(self, album, dest_dir, path_formats, pretend=False,
                     util.copy(album.artpath, dest)
 
     def convert_func(self, lib, opts, args):
-        dest = opts.dest or self.config['dest'].get()
-        if not dest:
-            raise ui.UserError('no convert destination set')
-        dest = util.bytestring_path(dest)
-
-        threads = opts.threads or self.config['threads'].get(int)
-
-        path_formats = ui.get_path_formats(self.config['paths'] or None)
-
-        fmt = opts.format or self.config['format'].as_str().lower()
-
-        if opts.pretend is not None:
-            pretend = opts.pretend
-        else:
-            pretend = self.config['pretend'].get(bool)
-
-        if opts.hardlink is not None:
-            hardlink = opts.hardlink
-            link = False
-        elif opts.link is not None:
-            hardlink = False
-            link = opts.link
-        else:
-            hardlink = self.config['hardlink'].get(bool)
-            link = self.config['link'].get(bool)
+        (dest, threads, path_formats, fmt,
+         pretend, hardlink, link) = self._get_opts_and_config(opts)
 
         if opts.album:
             albums = lib.albums(ui.decargs(args))
@@ -472,16 +460,8 @@ def convert_func(self, lib, opts, args):
                 self.copy_album_art(album, dest, path_formats, pretend,
                                     link, hardlink)
 
-        convert = [self.convert_item(dest,
-                                     opts.keep_new,
-                                     path_formats,
-                                     fmt,
-                                     pretend,
-                                     link,
-                                     hardlink)
-                   for _ in range(threads)]
-        pipe = util.pipeline.Pipeline([iter(items), convert])
-        pipe.run_parallel()
+        self._parallel_convert(dest, opts.keep_new, path_formats, fmt,
+                               pretend, link, hardlink, threads, items)
 
     def convert_on_import(self, lib, item):
         """Transcode a file automatically after it is imported into the
@@ -546,3 +526,52 @@ def _cleanup(self, task, session):
                 if os.path.isfile(path):
                     util.remove(path)
                 _temp_files.remove(path)
+
+    def _get_opts_and_config(self, opts):
+        """Returns parameters needed for convert function.
+        Get parameters from command line if available,
+        default to config if not available.
+        """
+        dest = opts.dest or self.config['dest'].get()
+        if not dest:
+            raise ui.UserError('no convert destination set')
+        dest = util.bytestring_path(dest)
+
+        threads = opts.threads or self.config['threads'].get(int)
+
+        path_formats = ui.get_path_formats(self.config['paths'] or None)
+
+        fmt = opts.format or self.config['format'].as_str().lower()
+
+        if opts.pretend is not None:
+            pretend = opts.pretend
+        else:
+            pretend = self.config['pretend'].get(bool)
+
+        if opts.hardlink is not None:
+            hardlink = opts.hardlink
+            link = False
+        elif opts.link is not None:
+            hardlink = False
+            link = opts.link
+        else:
+            hardlink = self.config['hardlink'].get(bool)
+            link = self.config['link'].get(bool)
+
+        return dest, threads, path_formats, fmt, pretend, hardlink, link
+
+    def _parallel_convert(self, dest, keep_new, path_formats, fmt,
+                          pretend, link, hardlink, threads, items):
+        """Run the convert_item function for every items on as many thread as
+        defined in threads
+        """
+        convert = [self.convert_item(dest,
+                                     keep_new,
+                                     path_formats,
+                                     fmt,
+                                     pretend,
+                                     link,
+                                     hardlink)
+                   for _ in range(threads)]
+        pipe = util.pipeline.Pipeline([iter(items), convert])
+        pipe.run_parallel()


Test patch: