mirror of
https://github.com/beetbox/beets.git
synced 2025-12-13 20:13:09 +01:00
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:
commit
09d724db3f
2 changed files with 132 additions and 22 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Loading…
Reference in a new issue