mirror of
https://github.com/beetbox/beets.git
synced 2025-12-07 17:16:07 +01:00
186 lines
6.5 KiB
Python
186 lines
6.5 KiB
Python
# This file is part of beets.
|
|
# Copyright 2012, Jakob Schnitzer.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining
|
|
# a copy of this software and associated documentation files (the
|
|
# "Software"), to deal in the Software without restriction, including
|
|
# without limitation the rights to use, copy, modify, merge, publish,
|
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
# permit persons to whom the Software is furnished to do so, subject to
|
|
# the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
|
|
"""Converts tracks or albums to external directory
|
|
"""
|
|
import logging
|
|
import os
|
|
import sys
|
|
import shutil
|
|
from subprocess import Popen, PIPE
|
|
|
|
import imghdr
|
|
|
|
from beets.plugins import BeetsPlugin
|
|
from beets import ui, library, util, mediafile
|
|
|
|
log = logging.getLogger('beets')
|
|
DEVNULL = open(os.devnull, 'wb')
|
|
conf = {}
|
|
|
|
|
|
def _cpu_count():
|
|
""" Returns the number of CPUs in the system.
|
|
Code was adapted from observing the soundconverter project:
|
|
https://github.com/kassoulet/soundconverter
|
|
"""
|
|
if sys.platform == 'win32':
|
|
try:
|
|
num = int(os.environ['NUMBER_OF_PROCESSORS'])
|
|
except (ValueError, KeyError):
|
|
num = 0
|
|
elif sys.platform == 'darwin':
|
|
try:
|
|
num = int(os.popen('sysctl -n hw.ncpu').read())
|
|
except ValueError:
|
|
num = 0
|
|
else:
|
|
try:
|
|
num = os.sysconf('SC_NPROCESSORS_ONLN')
|
|
except (ValueError, OSError, AttributeError):
|
|
num = 0
|
|
if num >= 1:
|
|
return num
|
|
else:
|
|
return 1
|
|
|
|
|
|
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(util.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()
|
|
|
|
|
|
def encode(source, dest):
|
|
log.info('Started encoding ' + source)
|
|
temp_dest = dest + '~'
|
|
|
|
source_ext = os.path.splitext(source)[1].lower()
|
|
if source_ext == '.flac':
|
|
decode = Popen([conf['flac'], '-c', '-d', '-s', 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'] +
|
|
[source, temp_dest], close_fds=True, stderr=DEVNULL)
|
|
encode.communicate()
|
|
else:
|
|
log.error('Only converting from FLAC or MP3 implemented')
|
|
return
|
|
if encode.returncode != 0:
|
|
# Something went wrong (probably Ctrl+C), remove temporary files
|
|
log.info('Encoding {0} failed. Cleaning up...'.format(source))
|
|
util.remove(temp_dest)
|
|
util.prune_dirs(os.path.dirname(temp_dest))
|
|
return
|
|
shutil.move(temp_dest, dest)
|
|
log.info('Finished encoding ' + source)
|
|
|
|
|
|
def convert_item(lib, dest_dir):
|
|
while True:
|
|
item = yield
|
|
if item.format != 'FLAC' and item.format != 'MP3':
|
|
log.info('Skipping {0} : not supported format'.format(item.path))
|
|
continue
|
|
|
|
dest = os.path.join(dest_dir, lib.destination(item, fragment=True))
|
|
dest = os.path.splitext(dest)[0] + '.mp3'
|
|
|
|
if os.path.exists(dest):
|
|
log.info('Skipping {0} : target file exists'.format(item.path))
|
|
continue
|
|
|
|
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)
|
|
dest_item = library.Item.from_path(dest)
|
|
else:
|
|
encode(item.path, dest)
|
|
dest_item = library.Item.from_path(item.path)
|
|
dest_item.path = dest
|
|
dest_item.write()
|
|
|
|
artpath = lib.get_album(item).artpath
|
|
if artpath and conf['embed']:
|
|
_embed(artpath, [dest_item])
|
|
|
|
|
|
def convert_func(lib, config, opts, args):
|
|
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']
|
|
|
|
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:
|
|
items = (i for a in lib.albums(ui.decargs(args)) for i in a.items())
|
|
else:
|
|
items = lib.items(ui.decargs(args))
|
|
convert = [convert_item(lib, dest) for i in range(threads)]
|
|
pipe = util.pipeline.Pipeline([items, convert])
|
|
pipe.run_parallel()
|
|
|
|
|
|
class ConvertPlugin(BeetsPlugin):
|
|
def configure(self, config):
|
|
conf['dest'] = ui.config_val(config, 'convert', 'dest', None)
|
|
conf['threads'] = ui.config_val(config, 'convert', 'threads',
|
|
_cpu_count())
|
|
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, \
|
|
defaults to maximum availble processors ')
|
|
cmd.parser.add_option('-d', '--dest', action='store',
|
|
help='set the destination directory')
|
|
cmd.func = convert_func
|
|
return [cmd]
|