From 21c9855c4f2549031be61ca2b804f8072284eab5 Mon Sep 17 00:00:00 2001 From: Rowan Lewis Date: Thu, 12 Sep 2013 20:06:36 +1000 Subject: [PATCH 1/9] Allow the convert plugin to convert to any format, not just mp3. --- beetsplug/convert.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 6beaabbf9..e9112406e 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -19,6 +19,7 @@ import os import threading from subprocess import Popen import tempfile +from string import Template from beets.plugins import BeetsPlugin from beets import ui, util @@ -41,17 +42,29 @@ def _destination(lib, dest_dir, item, keep_new, path_formats): # occurs. return dest else: - # Otherwise, replace the extension with .mp3. - return os.path.splitext(dest)[0] + '.mp3' + # Otherwise, replace the extension. + return os.path.splitext(dest)[0] + '.' + config['convert']['extension'].get(unicode) def encode(source, dest): log.info(u'Started encoding {0}'.format(util.displayable_path(source))) - opts = config['convert']['opts'].get(unicode).split(u' ') - encode = Popen([config['convert']['ffmpeg'].get(unicode), '-i', - source, '-y'] + opts + [dest], - close_fds=True, stderr=DEVNULL) + command = config['transport']['command'].get(unicode).split(u' ') + opts = [] + + for arg in command: + opts.append(Template(arg).substitute({ + 'source': source, + 'dest': dest + })) + + encode = Popen(opts, close_fds=True, stderr=DEVNULL) + + #opts = config['convert']['opts'].get(unicode).split(u' ') + #encode = Popen([config['convert']['ffmpeg'].get(unicode), '-i', + # source, '-y'] + opts + [dest], + # close_fds=True, stderr=DEVNULL) + encode.wait() if encode.returncode != 0: # Something went wrong (probably Ctrl+C), remove temporary files @@ -106,7 +119,7 @@ def convert_item(lib, dest_dir, keep_new, path_formats): else: if keep_new: - item.path = os.path.splitext(item.path)[0] + '.mp3' + item.path = os.path.splitext(item.path)[0] + '.' + config['convert']['extension'].get(unicode) encode(dest, item.path) else: encode(item.path, dest) @@ -135,7 +148,7 @@ def convert_on_import(lib, item): library. """ if should_transcode(item): - fd, dest = tempfile.mkstemp('.mp3') + fd, dest = tempfile.mkstemp('.' + config['convert']['extension'].get(unicode)) os.close(fd) _temp_files.append(dest) # Delete the transcode later. encode(item.path, dest) @@ -180,8 +193,8 @@ class ConvertPlugin(BeetsPlugin): self.config.add({ u'dest': None, u'threads': util.cpu_count(), - u'ffmpeg': u'ffmpeg', - u'opts': u'-aq 2', + u'command': u'ffmpeg -i $source -y -aq 2 $dest', + u'extension': u'mp3', u'max_bitrate': 500, u'embed': True, u'auto': False, From 697cf3fd65f02bf2686ebe3bd13776de877c1292 Mon Sep 17 00:00:00 2001 From: Rowan Lewis Date: Thu, 12 Sep 2013 20:22:18 +1000 Subject: [PATCH 2/9] Removed reference to my testing plugin, oops. --- beetsplug/convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index e9112406e..274ca6ba1 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -49,7 +49,7 @@ def _destination(lib, dest_dir, item, keep_new, path_formats): def encode(source, dest): log.info(u'Started encoding {0}'.format(util.displayable_path(source))) - command = config['transport']['command'].get(unicode).split(u' ') + command = config['convert']['command'].get(unicode).split(u' ') opts = [] for arg in command: From 697e70f14b33cc4544da8ebc063265cc7d6b4c68 Mon Sep 17 00:00:00 2001 From: Rowan Lewis Date: Thu, 12 Sep 2013 22:21:50 +1000 Subject: [PATCH 3/9] Problem with unicode filenames sorted. --- beetsplug/convert.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 274ca6ba1..4837c83ee 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -53,6 +53,7 @@ def encode(source, dest): opts = [] for arg in command: + arg = arg.encode('utf-8') opts.append(Template(arg).substitute({ 'source': source, 'dest': dest From bfbf5a9215972e95f7f2ca833824c66c4f020c94 Mon Sep 17 00:00:00 2001 From: Rowan Lewis Date: Fri, 13 Sep 2013 07:54:36 +1000 Subject: [PATCH 4/9] Changed the configuration to allow for easier format selection. --- beetsplug/convert.py | 68 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 4837c83ee..f2a38edf5 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -43,13 +43,29 @@ def _destination(lib, dest_dir, item, keep_new, path_formats): return dest else: # Otherwise, replace the extension. - return os.path.splitext(dest)[0] + '.' + config['convert']['extension'].get(unicode) + return os.path.splitext(dest)[0] + get_file_extension() + + +def get_command(): + """Get the currently configured format command. + """ + format = config['convert']['format'].get(unicode) + + return config['convert']['formats'][format]['command'].get(unicode).split(u' ') + + +def get_file_extension(): + """Get the currently configured format file extension. + """ + format = config['convert']['format'].get(unicode) + + return u'.' + config['convert']['formats'][format]['extension'].get(unicode) def encode(source, dest): log.info(u'Started encoding {0}'.format(util.displayable_path(source))) - command = config['convert']['command'].get(unicode).split(u' ') + command = get_command() opts = [] for arg in command: @@ -60,13 +76,8 @@ def encode(source, dest): })) encode = Popen(opts, close_fds=True, stderr=DEVNULL) - - #opts = config['convert']['opts'].get(unicode).split(u' ') - #encode = Popen([config['convert']['ffmpeg'].get(unicode), '-i', - # source, '-y'] + opts + [dest], - # close_fds=True, stderr=DEVNULL) - encode.wait() + if encode.returncode != 0: # Something went wrong (probably Ctrl+C), remove temporary files log.info(u'Encoding {0} failed. Cleaning up...' @@ -74,9 +85,26 @@ def encode(source, dest): util.remove(dest) util.prune_dirs(os.path.dirname(dest)) return + log.info(u'Finished encoding {0}'.format(util.displayable_path(source))) +def validate_config(): + """Validate the format configuration, make sure all of the required values are set for the current format. + """ + format = config['convert']['format'].get(unicode) + formats = config['convert']['formats'].get() + + if format not in formats: + raise ui.UserError(u'specified format {0} not configured in formats'.format(format)) + + if 'command' not in formats[format]: + raise ui.UserError(u'specified format {0} does not have a command defined'.format(format)) + + if 'extension' not in formats[format]: + raise ui.UserError(u'specified format {0} does not have a file extension defined'.format(format)) + + def should_transcode(item): """Determine whether the item should be transcoded as part of conversion (i.e., its bitrate is high or it has the wrong format). @@ -120,7 +148,7 @@ def convert_item(lib, dest_dir, keep_new, path_formats): else: if keep_new: - item.path = os.path.splitext(item.path)[0] + '.' + config['convert']['extension'].get(unicode) + item.path = os.path.splitext(item.path)[0] + get_file_extension() encode(dest, item.path) else: encode(item.path, dest) @@ -149,7 +177,7 @@ def convert_on_import(lib, item): library. """ if should_transcode(item): - fd, dest = tempfile.mkstemp('.' + config['convert']['extension'].get(unicode)) + fd, dest = tempfile.mkstemp(get_file_extension()) os.close(fd) _temp_files.append(dest) # Delete the transcode later. encode(item.path, dest) @@ -162,8 +190,10 @@ def convert_on_import(lib, item): def convert_func(lib, opts, args): dest = opts.dest if opts.dest is not None else \ config['convert']['dest'].get() + if not dest: raise ui.UserError('no convert destination set') + dest = util.bytestring_path(dest) threads = opts.threads if opts.threads is not None else \ config['convert']['threads'].get(int) @@ -194,13 +224,27 @@ class ConvertPlugin(BeetsPlugin): self.config.add({ u'dest': None, u'threads': util.cpu_count(), - u'command': u'ffmpeg -i $source -y -aq 2 $dest', - u'extension': u'mp3', + u'format': u'mp3', + u'formats': { + u'mp3': { + u'command': u'ffmpeg -i $source -y -aq 2 $dest', + u'extension': u'mp3', + }, + u'opus': { + u'command': u'ffmpeg -i $source -y -acodec libopus -vn -ab 96k $dest', + u'extension': u'opus', + }, + u'ogg': { + u'command': u'ffmpeg -i $source -y -acodec libvorbis -vn -aq 2 $dest', + u'extension': u'ogg', + }, + }, u'max_bitrate': 500, u'embed': True, u'auto': False, u'paths': {}, }) + validate_config() self.import_stages = [self.auto_convert] def commands(self): From 53aba3ce3fb246039550281234cef12241135650 Mon Sep 17 00:00:00 2001 From: Rowan Lewis Date: Fri, 13 Sep 2013 07:57:04 +1000 Subject: [PATCH 5/9] It's called Vorbis, not OGG. --- beetsplug/convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index f2a38edf5..36caf4e64 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -234,7 +234,7 @@ class ConvertPlugin(BeetsPlugin): u'command': u'ffmpeg -i $source -y -acodec libopus -vn -ab 96k $dest', u'extension': u'opus', }, - u'ogg': { + u'vorbis': { u'command': u'ffmpeg -i $source -y -acodec libvorbis -vn -aq 2 $dest', u'extension': u'ogg', }, From d3dae9f0894382f8b0b127fb07c9e91bc557abff Mon Sep 17 00:00:00 2001 From: Rowan Lewis Date: Fri, 13 Sep 2013 08:13:30 +1000 Subject: [PATCH 6/9] Reject lossy formats under a set bitrate. --- beetsplug/convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 36caf4e64..083dc2cdf 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -110,7 +110,7 @@ def should_transcode(item): conversion (i.e., its bitrate is high or it has the wrong format). """ maxbr = config['convert']['max_bitrate'].get(int) - return item.format != 'MP3' or item.bitrate >= 1000 * maxbr + return item.format not in ['MP3', 'Opus', 'OGG'] or item.bitrate >= 1000 * maxbr def convert_item(lib, dest_dir, keep_new, path_formats): From d2327d2dcff86b8a38ddd93d5272c24347ca968a Mon Sep 17 00:00:00 2001 From: Rowan Lewis Date: Sat, 14 Sep 2013 07:27:15 +1000 Subject: [PATCH 7/9] Fixed validation, basically replace unfriendly errors with friendly errors. --- beetsplug/convert.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 083dc2cdf..af942aabb 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -93,16 +93,28 @@ def validate_config(): """Validate the format configuration, make sure all of the required values are set for the current format. """ format = config['convert']['format'].get(unicode) - formats = config['convert']['formats'].get() + formats = config['convert']['formats'] - if format not in formats: - raise ui.UserError(u'specified format {0} not configured in formats'.format(format)) + try: + formats[format].get() + except: + raise ui.UserError( + 'Format {0} does not appear to exist, please check your convert plugin configuration.'.format(format) + ) - if 'command' not in formats[format]: - raise ui.UserError(u'specified format {0} does not have a command defined'.format(format)) + try: + formats[format]['command'].get(unicode) + except: + raise ui.UserError( + 'Format {0} does not define a command, please check your convert plugin configuration.'.format(format) + ) - if 'extension' not in formats[format]: - raise ui.UserError(u'specified format {0} does not have a file extension defined'.format(format)) + try: + formats[format]['extension'].get(unicode) + except: + raise ui.UserError( + 'Format {0} does not define a file extension, please check your convert plugin configuration.'.format(format) + ) def should_transcode(item): From 52d86f0e6ae09ab29f0d9905bca846b2166aa0c9 Mon Sep 17 00:00:00 2001 From: Rowan Lewis Date: Sat, 14 Sep 2013 09:35:25 +1000 Subject: [PATCH 8/9] Added more format presets, updated documentation. --- beetsplug/convert.py | 19 ++++++++++++++++++- docs/plugins/convert.rst | 33 +++++++++++++++++++++++++-------- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index af942aabb..8b887bd9a 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -122,7 +122,8 @@ def should_transcode(item): conversion (i.e., its bitrate is high or it has the wrong format). """ maxbr = config['convert']['max_bitrate'].get(int) - return item.format not in ['MP3', 'Opus', 'OGG'] or item.bitrate >= 1000 * maxbr + + return item.format not in ['AAC', 'MP3', 'Opus', 'OGG', 'Windows Media'] or item.bitrate >= 1000 * maxbr def convert_item(lib, dest_dir, keep_new, path_formats): @@ -238,6 +239,18 @@ class ConvertPlugin(BeetsPlugin): u'threads': util.cpu_count(), u'format': u'mp3', u'formats': { + u'aac': { + u'command': u'ffmpeg -i $source -y -acodec libfaac -aq 100 $dest', + u'extension': u'm4a', + }, + u'alac': { + u'command': u'ffmpeg -i $source -y -acodec alac $dest', + u'extension': u'm4a', + }, + u'flac': { + u'command': u'ffmpeg -i $source -y -acodec flac $dest', + u'extension': u'flac', + }, u'mp3': { u'command': u'ffmpeg -i $source -y -aq 2 $dest', u'extension': u'mp3', @@ -250,6 +263,10 @@ class ConvertPlugin(BeetsPlugin): u'command': u'ffmpeg -i $source -y -acodec libvorbis -vn -aq 2 $dest', u'extension': u'ogg', }, + u'wma': { + u'command': u'ffmpeg -i $source -y -acodec wmav2 -vn $dest', + u'extension': u'wma', + }, }, u'max_bitrate': 500, u'embed': True, diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index 3e9c573a1..c7cce6b95 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -48,15 +48,15 @@ The plugin offers several configuration options, all of which live under the or on the command line using the ``-d`` flag. * ``embed`` indicates whether or not to embed album art in converted items. Default: true. -* If you set ``max_bitrate``, all MP3 files with a higher bitrate will be +* If you set ``max_bitrate``, all lossy files with a higher bitrate will be transcoded and those with a lower bitrate will simply be copied. Note that this does not guarantee that all converted files will have a lower - bitrate---that depends on the encoder and its configuration. By default MP3s - will be copied without transcoding and all other formats will be converted. -* ``opts`` are the encoding options that are passed to ``ffmpeg``. Default: - "-aq 2". (Note that "-aq " is equivalent to the LAME option "-V - ".) If you want to specify a bitrate, use "-ab ". Refer to the - `FFmpeg`_ documentation for more details. + bitrate---that depends on the encoder and its configuration. +* ``format`` specify which format preset you would like to use. Default: mp3. +* ``formats`` lets you specify additional formats to convert to. Presets for + AAC, ALAC, FLAC, MP3, Opus, Vorbis and Windows Meda are provided, however + support may vary depending on your ffmpeg library. Each format is defined as + a command and a file extension. * ``auto`` gives you the option to import transcoded versions of your files automatically during the ``import`` command. With this option enabled, the importer will transcode all non-MP3 files over the maximum bitrate before @@ -73,9 +73,26 @@ Here's an example configuration:: convert: embed: false + format: aac max_bitrate: 200 - opts: -aq 4 dest: /home/user/MusicForPhone threads: 4 paths: default: $albumartist/$title + +Here's how formats are configured:: + + convert: + format: mp3_high + formats: + mp3_high: + command: ffmpeg -i $source -y -aq 4 $dest + extension: mp3 + +The ``$source`` and ``$dest`` tokens are automatically replaced with the paths +to each file. Because ``$`` is used to delineate a field reference, you can +use ``$$`` to emit a dollars sign. + +In this example ``-aq `` is equivalent to the LAME option ``-V num``. If +you want to specify a bitrate, use ``-ab ``. Refer to the `FFmpeg`_ +documentation for more details. \ No newline at end of file From 80d060db0708437bff230af154904ac68a28f6ed Mon Sep 17 00:00:00 2001 From: Rowan Lewis Date: Sat, 14 Sep 2013 10:18:36 +1000 Subject: [PATCH 9/9] Added 'quiet' operation mode, prevents start/finish announcements for each file. --- beetsplug/convert.py | 12 ++++++++---- docs/plugins/convert.rst | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 8b887bd9a..d1bfa9ddd 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -63,11 +63,13 @@ def get_file_extension(): def encode(source, dest): - log.info(u'Started encoding {0}'.format(util.displayable_path(source))) - command = get_command() + quiet = config['convert']['quiet'].get() opts = [] + if not quiet: + log.info(u'Started encoding {0}'.format(util.displayable_path(source))) + for arg in command: arg = arg.encode('utf-8') opts.append(Template(arg).substitute({ @@ -86,7 +88,8 @@ def encode(source, dest): util.prune_dirs(os.path.dirname(dest)) return - log.info(u'Finished encoding {0}'.format(util.displayable_path(source))) + if not quiet: + log.info(u'Finished encoding {0}'.format(util.displayable_path(source))) def validate_config(): @@ -269,8 +272,9 @@ class ConvertPlugin(BeetsPlugin): }, }, u'max_bitrate': 500, - u'embed': True, u'auto': False, + u'quiet': False, + u'embed': True, u'paths': {}, }) validate_config() diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index c7cce6b95..7869aa40b 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -61,6 +61,8 @@ The plugin offers several configuration options, all of which live under the automatically during the ``import`` command. With this option enabled, the importer will transcode all non-MP3 files over the maximum bitrate before adding them to your library. +* ``quiet`` mode prevents the plugin from announcing every file it processes. + Default: false. * ``paths`` lets you specify the directory structure and naming scheme for the converted files. Use the same format as the top-level ``paths`` section (see :ref:`path-format-config`). By default, the plugin reuses your top-level