Merge branch 'convert'

This commit is contained in:
Jakob Schnitzer 2012-10-08 12:49:04 +02:00
commit b05fc8ef4f
3 changed files with 205 additions and 0 deletions

149
beetsplug/convert.py Normal file
View file

@ -0,0 +1,149 @@
"""Converts tracks or albums to external directory
"""
import logging
import os
import threading
import shutil
from subprocess import Popen, PIPE
import imghdr
from beets.plugins import BeetsPlugin
from beets import ui, library, util, mediafile
from beets.util.functemplate import Template
log = logging.getLogger('beets')
DEVNULL = open(os.devnull, 'wb')
conf = {}
def _embed(path, items):
"""Embed an image file, located at `path`, into each item.
"""
data = open(util.syspath(path), 'rb').read()
kindstr = imghdr.what(None, data)
if kindstr not in ('jpeg', 'png'):
log.error('A file of type %s is not allowed as coverart.' % kindstr)
return
log.debug('Embedding album art.')
for item in items:
try:
f = mediafile.MediaFile(syspath(item.path))
except mediafile.UnreadableFileError as exc:
log.warn('Could not embed art in {0}: {1}'.format(
repr(item.path), exc
))
continue
f.art = data
f.save()
class encodeThread(threading.Thread):
def __init__(self, source, dest, artpath):
threading.Thread.__init__(self)
self.source = source
self.dest = dest
self.artpath = artpath
def run(self):
sema.acquire()
self.encode()
sema.release()
dest_item = library.Item.from_path(self.source)
dest_item.path = self.dest
dest_item.write()
if self.artpath and conf['embed']:
_embed(self.artpath,[dest_item])
def encode(self):
log.info('Started encoding '+ self.source)
temp_dest = self.dest + '~'
source_ext = os.path.splitext(self.source)[1].lower()
if source_ext == '.flac':
decode = Popen([conf['flac'], '-c', '-d', '-s', self.source],
stdout=PIPE)
encode = Popen([conf['lame']] + conf['opts'] + ['-', temp_dest],
stdin=decode.stdout, stderr=DEVNULL)
decode.stdout.close()
encode.communicate()
elif source_ext == '.mp3':
encode = Popen([conf['lame']] + conf['opts'] + ['--mp3input'] +
[self.source, temp_dest], close_fds=True, stderr=DEVNULL)
encode.communicate()
else:
log.error('Only converting from FLAC or MP3 implemented')
return
shutil.move(temp_dest, self.dest)
log.info('Finished encoding '+ self.source)
def convert_item(lib, item, dest_dir, artpath):
if item.format != 'FLAC' and item.format != 'MP3':
log.info('Skipping {0} : not supported format'.format(item.path))
return
dest = os.path.join(dest_dir,lib.destination(item, fragment = True))
dest = os.path.splitext(dest)[0] + '.mp3'
if not os.path.exists(dest):
util.mkdirall(dest)
if item.format == 'MP3' and item.bitrate < 1000*conf['max_bitrate']:
log.info('Copying {0}'.format(item.path))
shutil.copy(item.path, dest)
if artpath and conf['embed']:
_embed(artpath,[library.Item.from_path(dest)])
else:
thread = encodeThread(item.path, dest, artpath)
thread.start()
else:
log.info('Skipping {0} : target file exists'.format(item.path))
def convert_func(lib, config, opts, args):
global sema
dest = opts.dest if opts.dest is not None else conf['dest']
if not dest:
log.error('No destination set')
return
threads = opts.threads if opts.threads is not None else conf['threads']
sema = threading.BoundedSemaphore(threads)
fmt = '$albumartist - $album' if opts.album \
else '$artist - $album - $title'
ui.commands.list_items(lib, ui.decargs(args), opts.album, False, fmt)
if not ui.input_yn("Convert? (Y/n)"):
return
if opts.album:
for album in lib.albums(ui.decargs(args)):
for item in album.items():
convert_item(lib, item, dest, album.artpath)
else:
for item in lib.items(ui.decargs(args)):
convert_item(lib, item, dest, lib.get_album(item).artpath)
class ConvertPlugin(BeetsPlugin):
def configure(self, config):
conf['dest'] = ui.config_val(config, 'convert', 'dest', None)
conf['threads'] = ui.config_val(config, 'convert', 'threads', 2)
conf['flac'] = ui.config_val(config, 'convert', 'flac', 'flac')
conf['lame'] = ui.config_val(config, 'convert', 'lame', 'lame')
conf['opts'] = ui.config_val(config, 'convert',
'opts', '-V2').split(' ')
conf['max_bitrate'] = int(ui.config_val(config, 'convert',
'max_bitrate','500'))
conf['embed'] = ui.config_val(config, 'convert', 'embed', True,
vtype = bool)
def commands(self):
cmd = ui.Subcommand('convert', help='convert to external location')
cmd.parser.add_option('-a', '--album', action='store_true',
help='choose albums instead of tracks')
cmd.parser.add_option('-t', '--threads', action='store', type='int',
help='change the number of threads (default 2)')
cmd.parser.add_option('-d', '--dest', action='store',
help='set the destination directory')
cmd.func = convert_func
return [cmd]

54
docs/plugins/convert.rst Normal file
View file

@ -0,0 +1,54 @@
Convert Plugin
==============
The ``convert`` plugin lets you convert parts of your collection to a directory
of your choice. Currently only converting from MP3 or FLAC to MP3 is supported.
It will skip files that are already present in the target directory. It uses
the same directory structure as your library.
Installation
------------
This plugin requires ``flac`` and ``lame``. If thoses executables are in your
path, they will be found automatically by the plugin, otherwise you have to set
their respective config options. Of course you will have to enable the plugin
as well (see :doc:`/plugins/index`)::
[convert]
flac:/usr/bin/flac
lame:/usr/bin/lame
Usage
-----
To convert a part of your collection simply run ``beet convert QUERY``. This
will display all items matching ``QUERY`` and ask you for confirmation before
starting the conversion. The ``-a`` (or ``--album``) option causes the command
to match albums instead of tracks.
The ``-t`` (``--threads``) and ``-d`` (``--dest``) options allow you to specify
or overwrite the respective configuration options.
Configuration
-------------
This plugin offers a couple of configuration options: If you want to disable
that album art is embedded in your converted items (enabled by default), you
will have to set the ``embed`` option to false. If you set ``max_bitrate``, all
MP3 files with a higher bitrate will be converted and thoses with a lower
bitrate will simply be copied. Be aware that this doesn't mean that your
converted files will have a lower bitrate since that depends on the specified
encoding options. By default only FLAC files will be converted (and all MP3s
will be copied). ``opts`` are the encoding options that are passed to ``lame``
(defaults to "-V2"). Please refer to the ``lame`` docs for possible options.
The ``dest`` sets the directory the files will be converted (or copied) to.
Finally ``threads`` lets you determine the number of threads to use for
encoding (default: 2). An example configuration::
[convert]
embed:false
max_bitrate:200
opts:-V4
dest:/home/user/MusicForPhone
threads:4

View file

@ -54,6 +54,7 @@ disabled by default, but you can turn them on as described above.
fuzzy_search
zero
ihate
convert
Autotagger Extensions
''''''''''''''''''''''
@ -96,6 +97,7 @@ Miscellaneous
* :doc:`ihate`: Skip by defined patterns things you hate during import process.
* :doc:`bpd`: A music player for your beets library that emulates `MPD`_ and is
compatible with `MPD clients`_.
* :doc:`convert`: Converts parts of your collection to an external directory
.. _MPD: http://mpd.wikia.com/
.. _MPD clients: http://mpd.wikia.com/wiki/Clients