Merge pull request #380 from rowan-lewis/convert-all

Allow the convert plugin to convert to any format, not just mp3.
This commit is contained in:
Adrian Sampson 2013-09-13 19:32:57 -07:00
commit 09d724db3f
2 changed files with 132 additions and 22 deletions

View file

@ -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):

View file

@ -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 <num>" is equivalent to the LAME option "-V
<num>".) If you want to specify a bitrate, use "-ab <bitrate>". 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 <num>`` is equivalent to the LAME option ``-V num``. If
you want to specify a bitrate, use ``-ab <bitrate>``. Refer to the `FFmpeg`_
documentation for more details.