diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 6beaabbf9..d1bfa9ddd 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,18 +42,44 @@ 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] + 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 = get_command() + quiet = config['convert']['quiet'].get() + opts = [] - 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) + 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({ + 'source': source, + 'dest': dest + })) + + encode = Popen(opts, 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...' @@ -60,7 +87,37 @@ 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))) + + if not quiet: + 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'] + + try: + formats[format].get() + except: + raise ui.UserError( + 'Format {0} does not appear to exist, please check your convert plugin configuration.'.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) + ) + + 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): @@ -68,7 +125,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 != 'MP3' 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): @@ -106,7 +164,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] + get_file_extension() encode(dest, item.path) else: encode(item.path, dest) @@ -135,7 +193,7 @@ def convert_on_import(lib, item): library. """ if should_transcode(item): - fd, dest = tempfile.mkstemp('.mp3') + fd, dest = tempfile.mkstemp(get_file_extension()) os.close(fd) _temp_files.append(dest) # Delete the transcode later. encode(item.path, dest) @@ -148,8 +206,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) @@ -180,13 +240,44 @@ class ConvertPlugin(BeetsPlugin): self.config.add({ u'dest': None, u'threads': util.cpu_count(), - u'ffmpeg': u'ffmpeg', - u'opts': u'-aq 2', + 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', + }, + u'opus': { + u'command': u'ffmpeg -i $source -y -acodec libopus -vn -ab 96k $dest', + u'extension': u'opus', + }, + u'vorbis': { + 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, u'auto': False, + u'quiet': False, + u'embed': True, u'paths': {}, }) + validate_config() self.import_stages = [self.auto_convert] def commands(self): diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index 3e9c573a1..7869aa40b 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -48,19 +48,21 @@ 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 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 @@ -73,9 +75,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