mirror of
https://github.com/rembo10/headphones.git
synced 2026-07-20 16:03:59 +01:00
update Beets
This commit is contained in:
+461
-233
@@ -14,33 +14,33 @@
|
||||
|
||||
"""Converts tracks or albums to external directory
|
||||
"""
|
||||
from beets.util import par_map, decode_commandline_path, arg_encoding
|
||||
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
import shlex
|
||||
import threading
|
||||
from string import Template
|
||||
|
||||
from beets import ui, util, plugins, config
|
||||
from confuse import ConfigTypeError, Optional
|
||||
|
||||
from beets import art, config, plugins, ui, util
|
||||
from beets.library import Item, parse_query_string
|
||||
from beets.plugins import BeetsPlugin
|
||||
from confuse import ConfigTypeError
|
||||
from beets import art
|
||||
from beets.util import arg_encoding, par_map
|
||||
from beets.util.artresizer import ArtResizer
|
||||
from beets.library import parse_query_string
|
||||
from beets.library import Item
|
||||
from beets.util.m3u import M3UFile
|
||||
|
||||
_fs_lock = threading.Lock()
|
||||
_temp_files = [] # Keep track of temporary transcoded files for deletion.
|
||||
|
||||
# Some convenient alternate names for formats.
|
||||
ALIASES = {
|
||||
'wma': 'windows media',
|
||||
'vorbis': 'ogg',
|
||||
"windows media": "wma",
|
||||
"vorbis": "ogg",
|
||||
}
|
||||
|
||||
LOSSLESS_FORMATS = ['ape', 'flac', 'alac', 'wav', 'aiff']
|
||||
LOSSLESS_FORMATS = ["ape", "flac", "alac", "wave", "aiff"]
|
||||
|
||||
|
||||
def replace_ext(path, ext):
|
||||
@@ -48,140 +48,217 @@ def replace_ext(path, ext):
|
||||
|
||||
The new extension must not contain a leading dot.
|
||||
"""
|
||||
ext_dot = b'.' + ext
|
||||
ext_dot = b"." + ext
|
||||
return os.path.splitext(path)[0] + ext_dot
|
||||
|
||||
|
||||
def get_format(fmt=None):
|
||||
"""Return the command template and the extension from the config.
|
||||
"""
|
||||
"""Return the command template and the extension from the config."""
|
||||
if not fmt:
|
||||
fmt = config['convert']['format'].as_str().lower()
|
||||
fmt = config["convert"]["format"].as_str().lower()
|
||||
fmt = ALIASES.get(fmt, fmt)
|
||||
|
||||
try:
|
||||
format_info = config['convert']['formats'][fmt].get(dict)
|
||||
command = format_info['command']
|
||||
extension = format_info.get('extension', fmt)
|
||||
format_info = config["convert"]["formats"][fmt].get(dict)
|
||||
command = format_info["command"]
|
||||
extension = format_info.get("extension", fmt)
|
||||
except KeyError:
|
||||
raise ui.UserError(
|
||||
'convert: format {} needs the "command" field'
|
||||
.format(fmt)
|
||||
'convert: format {} needs the "command" field'.format(fmt)
|
||||
)
|
||||
except ConfigTypeError:
|
||||
command = config['convert']['formats'][fmt].get(str)
|
||||
command = config["convert"]["formats"][fmt].get(str)
|
||||
extension = fmt
|
||||
|
||||
# Convenience and backwards-compatibility shortcuts.
|
||||
keys = config['convert'].keys()
|
||||
if 'command' in keys:
|
||||
command = config['convert']['command'].as_str()
|
||||
elif 'opts' in keys:
|
||||
keys = config["convert"].keys()
|
||||
if "command" in keys:
|
||||
command = config["convert"]["command"].as_str()
|
||||
elif "opts" in keys:
|
||||
# Undocumented option for backwards compatibility with < 1.3.1.
|
||||
command = 'ffmpeg -i $source -y {} $dest'.format(
|
||||
config['convert']['opts'].as_str()
|
||||
command = "ffmpeg -i $source -y {} $dest".format(
|
||||
config["convert"]["opts"].as_str()
|
||||
)
|
||||
if 'extension' in keys:
|
||||
extension = config['convert']['extension'].as_str()
|
||||
if "extension" in keys:
|
||||
extension = config["convert"]["extension"].as_str()
|
||||
|
||||
return (command.encode('utf-8'), extension.encode('utf-8'))
|
||||
return (command.encode("utf-8"), extension.encode("utf-8"))
|
||||
|
||||
|
||||
def should_transcode(item, fmt):
|
||||
"""Determine whether the item should be transcoded as part of
|
||||
conversion (i.e., its bitrate is high or it has the wrong format).
|
||||
"""
|
||||
no_convert_queries = config['convert']['no_convert'].as_str_seq()
|
||||
no_convert_queries = config["convert"]["no_convert"].as_str_seq()
|
||||
if no_convert_queries:
|
||||
for query_string in no_convert_queries:
|
||||
query, _ = parse_query_string(query_string, Item)
|
||||
if query.match(item):
|
||||
return False
|
||||
if config['convert']['never_convert_lossy_files'] and \
|
||||
not (item.format.lower() in LOSSLESS_FORMATS):
|
||||
if config["convert"]["never_convert_lossy_files"] and not (
|
||||
item.format.lower() in LOSSLESS_FORMATS
|
||||
):
|
||||
return False
|
||||
maxbr = config['convert']['max_bitrate'].get(int)
|
||||
return fmt.lower() != item.format.lower() or \
|
||||
item.bitrate >= 1000 * maxbr
|
||||
maxbr = config["convert"]["max_bitrate"].get(Optional(int))
|
||||
if maxbr is not None and item.bitrate >= 1000 * maxbr:
|
||||
return True
|
||||
return fmt.lower() != item.format.lower()
|
||||
|
||||
|
||||
class ConvertPlugin(BeetsPlugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.config.add({
|
||||
'dest': None,
|
||||
'pretend': False,
|
||||
'link': False,
|
||||
'hardlink': False,
|
||||
'threads': util.cpu_count(),
|
||||
'format': 'mp3',
|
||||
'id3v23': 'inherit',
|
||||
'formats': {
|
||||
'aac': {
|
||||
'command': 'ffmpeg -i $source -y -vn -acodec aac '
|
||||
'-aq 1 $dest',
|
||||
'extension': 'm4a',
|
||||
self.config.add(
|
||||
{
|
||||
"dest": None,
|
||||
"pretend": False,
|
||||
"link": False,
|
||||
"hardlink": False,
|
||||
"threads": os.cpu_count(),
|
||||
"format": "mp3",
|
||||
"id3v23": "inherit",
|
||||
"formats": {
|
||||
"aac": {
|
||||
"command": "ffmpeg -i $source -y -vn -acodec aac "
|
||||
"-aq 1 $dest",
|
||||
"extension": "m4a",
|
||||
},
|
||||
"alac": {
|
||||
"command": "ffmpeg -i $source -y -vn -acodec alac $dest",
|
||||
"extension": "m4a",
|
||||
},
|
||||
"flac": "ffmpeg -i $source -y -vn -acodec flac $dest",
|
||||
"mp3": "ffmpeg -i $source -y -vn -aq 2 $dest",
|
||||
"opus": "ffmpeg -i $source -y -vn -acodec libopus -ab 96k $dest",
|
||||
"ogg": "ffmpeg -i $source -y -vn -acodec libvorbis -aq 3 $dest",
|
||||
"wma": "ffmpeg -i $source -y -vn -acodec wmav2 -vn $dest",
|
||||
},
|
||||
'alac': {
|
||||
'command': 'ffmpeg -i $source -y -vn -acodec alac $dest',
|
||||
'extension': 'm4a',
|
||||
},
|
||||
'flac': 'ffmpeg -i $source -y -vn -acodec flac $dest',
|
||||
'mp3': 'ffmpeg -i $source -y -vn -aq 2 $dest',
|
||||
'opus':
|
||||
'ffmpeg -i $source -y -vn -acodec libopus -ab 96k $dest',
|
||||
'ogg':
|
||||
'ffmpeg -i $source -y -vn -acodec libvorbis -aq 3 $dest',
|
||||
'wma':
|
||||
'ffmpeg -i $source -y -vn -acodec wmav2 -vn $dest',
|
||||
},
|
||||
'max_bitrate': 500,
|
||||
'auto': False,
|
||||
'tmpdir': None,
|
||||
'quiet': False,
|
||||
'embed': True,
|
||||
'paths': {},
|
||||
'no_convert': '',
|
||||
'never_convert_lossy_files': False,
|
||||
'copy_album_art': False,
|
||||
'album_art_maxwidth': 0,
|
||||
'delete_originals': False,
|
||||
})
|
||||
self.early_import_stages = [self.auto_convert]
|
||||
"max_bitrate": None,
|
||||
"auto": False,
|
||||
"auto_keep": False,
|
||||
"tmpdir": None,
|
||||
"quiet": False,
|
||||
"embed": True,
|
||||
"paths": {},
|
||||
"no_convert": "",
|
||||
"never_convert_lossy_files": False,
|
||||
"copy_album_art": False,
|
||||
"album_art_maxwidth": 0,
|
||||
"delete_originals": False,
|
||||
"playlist": None,
|
||||
}
|
||||
)
|
||||
self.early_import_stages = [self.auto_convert, self.auto_convert_keep]
|
||||
|
||||
self.register_listener('import_task_files', self._cleanup)
|
||||
self.register_listener("import_task_files", self._cleanup)
|
||||
|
||||
def commands(self):
|
||||
cmd = ui.Subcommand('convert', help='convert to external location')
|
||||
cmd.parser.add_option('-p', '--pretend', action='store_true',
|
||||
help='show actions but do nothing')
|
||||
cmd.parser.add_option('-t', '--threads', action='store', type='int',
|
||||
help='change the number of threads, \
|
||||
defaults to maximum available processors')
|
||||
cmd.parser.add_option('-k', '--keep-new', action='store_true',
|
||||
dest='keep_new', help='keep only the converted \
|
||||
and move the old files')
|
||||
cmd.parser.add_option('-d', '--dest', action='store',
|
||||
help='set the destination directory')
|
||||
cmd.parser.add_option('-f', '--format', action='store', dest='format',
|
||||
help='set the target format of the tracks')
|
||||
cmd.parser.add_option('-y', '--yes', action='store_true', dest='yes',
|
||||
help='do not ask for confirmation')
|
||||
cmd.parser.add_option('-l', '--link', action='store_true', dest='link',
|
||||
help='symlink files that do not \
|
||||
need transcoding.')
|
||||
cmd.parser.add_option('-H', '--hardlink', action='store_true',
|
||||
dest='hardlink',
|
||||
help='hardlink files that do not \
|
||||
need transcoding. Overrides --link.')
|
||||
cmd = ui.Subcommand("convert", help="convert to external location")
|
||||
cmd.parser.add_option(
|
||||
"-p",
|
||||
"--pretend",
|
||||
action="store_true",
|
||||
help="show actions but do nothing",
|
||||
)
|
||||
cmd.parser.add_option(
|
||||
"-t",
|
||||
"--threads",
|
||||
action="store",
|
||||
type="int",
|
||||
help="change the number of threads, \
|
||||
defaults to maximum available processors",
|
||||
)
|
||||
cmd.parser.add_option(
|
||||
"-k",
|
||||
"--keep-new",
|
||||
action="store_true",
|
||||
dest="keep_new",
|
||||
help="keep only the converted \
|
||||
and move the old files",
|
||||
)
|
||||
cmd.parser.add_option(
|
||||
"-d", "--dest", action="store", help="set the destination directory"
|
||||
)
|
||||
cmd.parser.add_option(
|
||||
"-f",
|
||||
"--format",
|
||||
action="store",
|
||||
dest="format",
|
||||
help="set the target format of the tracks",
|
||||
)
|
||||
cmd.parser.add_option(
|
||||
"-y",
|
||||
"--yes",
|
||||
action="store_true",
|
||||
dest="yes",
|
||||
help="do not ask for confirmation",
|
||||
)
|
||||
cmd.parser.add_option(
|
||||
"-l",
|
||||
"--link",
|
||||
action="store_true",
|
||||
dest="link",
|
||||
help="symlink files that do not \
|
||||
need transcoding.",
|
||||
)
|
||||
cmd.parser.add_option(
|
||||
"-H",
|
||||
"--hardlink",
|
||||
action="store_true",
|
||||
dest="hardlink",
|
||||
help="hardlink files that do not \
|
||||
need transcoding. Overrides --link.",
|
||||
)
|
||||
cmd.parser.add_option(
|
||||
"-m",
|
||||
"--playlist",
|
||||
action="store",
|
||||
help="""create an m3u8 playlist file containing
|
||||
the converted files. The playlist file will be
|
||||
saved below the destination directory, thus
|
||||
PLAYLIST could be a file name or a relative path.
|
||||
To ensure a working playlist when transferred to
|
||||
a different computer, or opened from an external
|
||||
drive, relative paths pointing to media files
|
||||
will be used.""",
|
||||
)
|
||||
cmd.parser.add_album_option()
|
||||
cmd.func = self.convert_func
|
||||
return [cmd]
|
||||
|
||||
def auto_convert(self, config, task):
|
||||
if self.config['auto']:
|
||||
par_map(lambda item: self.convert_on_import(config.lib, item),
|
||||
task.imported_items())
|
||||
if self.config["auto"]:
|
||||
par_map(
|
||||
lambda item: self.convert_on_import(config.lib, item),
|
||||
task.imported_items(),
|
||||
)
|
||||
|
||||
def auto_convert_keep(self, config, task):
|
||||
if self.config["auto_keep"]:
|
||||
empty_opts = self.commands()[0].parser.get_default_values()
|
||||
(
|
||||
dest,
|
||||
threads,
|
||||
path_formats,
|
||||
fmt,
|
||||
pretend,
|
||||
hardlink,
|
||||
link,
|
||||
playlist,
|
||||
) = self._get_opts_and_config(empty_opts)
|
||||
|
||||
items = task.imported_items()
|
||||
self._parallel_convert(
|
||||
dest,
|
||||
False,
|
||||
path_formats,
|
||||
fmt,
|
||||
pretend,
|
||||
link,
|
||||
hardlink,
|
||||
threads,
|
||||
items,
|
||||
)
|
||||
|
||||
# Utilities converted from functions to methods on logging overhaul
|
||||
|
||||
@@ -196,55 +273,70 @@ class ConvertPlugin(BeetsPlugin):
|
||||
assert isinstance(source, bytes)
|
||||
assert isinstance(dest, bytes)
|
||||
|
||||
quiet = self.config['quiet'].get(bool)
|
||||
quiet = self.config["quiet"].get(bool)
|
||||
|
||||
if not quiet and not pretend:
|
||||
self._log.info('Encoding {0}', util.displayable_path(source))
|
||||
self._log.info("Encoding {0}", util.displayable_path(source))
|
||||
|
||||
command = command.decode(arg_encoding(), 'surrogateescape')
|
||||
source = decode_commandline_path(source)
|
||||
dest = decode_commandline_path(dest)
|
||||
command = command.decode(arg_encoding(), "surrogateescape")
|
||||
source = os.fsdecode(source)
|
||||
dest = os.fsdecode(dest)
|
||||
|
||||
# Substitute $source and $dest in the argument list.
|
||||
args = shlex.split(command)
|
||||
encode_cmd = []
|
||||
for i, arg in enumerate(args):
|
||||
args[i] = Template(arg).safe_substitute({
|
||||
'source': source,
|
||||
'dest': dest,
|
||||
})
|
||||
args[i] = Template(arg).safe_substitute(
|
||||
{
|
||||
"source": source,
|
||||
"dest": dest,
|
||||
}
|
||||
)
|
||||
encode_cmd.append(args[i].encode(util.arg_encoding()))
|
||||
|
||||
if pretend:
|
||||
self._log.info('{0}', ' '.join(ui.decargs(args)))
|
||||
self._log.info("{0}", " ".join(ui.decargs(args)))
|
||||
return
|
||||
|
||||
try:
|
||||
util.command_output(encode_cmd)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
# Something went wrong (probably Ctrl+C), remove temporary files
|
||||
self._log.info('Encoding {0} failed. Cleaning up...',
|
||||
util.displayable_path(source))
|
||||
self._log.debug('Command {0} exited with status {1}: {2}',
|
||||
args,
|
||||
exc.returncode,
|
||||
exc.output)
|
||||
self._log.info(
|
||||
"Encoding {0} failed. Cleaning up...",
|
||||
util.displayable_path(source),
|
||||
)
|
||||
self._log.debug(
|
||||
"Command {0} exited with status {1}: {2}",
|
||||
args,
|
||||
exc.returncode,
|
||||
exc.output,
|
||||
)
|
||||
util.remove(dest)
|
||||
util.prune_dirs(os.path.dirname(dest))
|
||||
raise
|
||||
except OSError as exc:
|
||||
raise ui.UserError(
|
||||
"convert: couldn't invoke '{}': {}".format(
|
||||
' '.join(ui.decargs(args)), exc
|
||||
" ".join(ui.decargs(args)), exc
|
||||
)
|
||||
)
|
||||
|
||||
if not quiet and not pretend:
|
||||
self._log.info('Finished encoding {0}',
|
||||
util.displayable_path(source))
|
||||
self._log.info(
|
||||
"Finished encoding {0}", util.displayable_path(source)
|
||||
)
|
||||
|
||||
def convert_item(self, dest_dir, keep_new, path_formats, fmt,
|
||||
pretend=False, link=False, hardlink=False):
|
||||
def convert_item(
|
||||
self,
|
||||
dest_dir,
|
||||
keep_new,
|
||||
path_formats,
|
||||
fmt,
|
||||
pretend=False,
|
||||
link=False,
|
||||
hardlink=False,
|
||||
):
|
||||
"""A pipeline thread that converts `Item` objects from a
|
||||
library.
|
||||
"""
|
||||
@@ -252,8 +344,7 @@ class ConvertPlugin(BeetsPlugin):
|
||||
item, original, converted = None, None, None
|
||||
while True:
|
||||
item = yield (item, original, converted)
|
||||
dest = item.destination(basedir=dest_dir,
|
||||
path_formats=path_formats)
|
||||
dest = item.destination(basedir=dest_dir, path_formats=path_formats)
|
||||
|
||||
# When keeping the new file in the library, we first move the
|
||||
# current (pristine) file to the destination. We'll then copy it
|
||||
@@ -277,18 +368,23 @@ class ConvertPlugin(BeetsPlugin):
|
||||
util.mkdirall(dest)
|
||||
|
||||
if os.path.exists(util.syspath(dest)):
|
||||
self._log.info('Skipping {0} (target file exists)',
|
||||
util.displayable_path(item.path))
|
||||
self._log.info(
|
||||
"Skipping {0} (target file exists)",
|
||||
util.displayable_path(item.path),
|
||||
)
|
||||
continue
|
||||
|
||||
if keep_new:
|
||||
if pretend:
|
||||
self._log.info('mv {0} {1}',
|
||||
util.displayable_path(item.path),
|
||||
util.displayable_path(original))
|
||||
self._log.info(
|
||||
"mv {0} {1}",
|
||||
util.displayable_path(item.path),
|
||||
util.displayable_path(original),
|
||||
)
|
||||
else:
|
||||
self._log.info('Moving to {0}',
|
||||
util.displayable_path(original))
|
||||
self._log.info(
|
||||
"Moving to {0}", util.displayable_path(original)
|
||||
)
|
||||
util.move(item.path, original)
|
||||
|
||||
if should_transcode(item, fmt):
|
||||
@@ -300,20 +396,25 @@ class ConvertPlugin(BeetsPlugin):
|
||||
else:
|
||||
linked = link or hardlink
|
||||
if pretend:
|
||||
msg = 'ln' if hardlink else ('ln -s' if link else 'cp')
|
||||
msg = "ln" if hardlink else ("ln -s" if link else "cp")
|
||||
|
||||
self._log.info('{2} {0} {1}',
|
||||
util.displayable_path(original),
|
||||
util.displayable_path(converted),
|
||||
msg)
|
||||
self._log.info(
|
||||
"{2} {0} {1}",
|
||||
util.displayable_path(original),
|
||||
util.displayable_path(converted),
|
||||
msg,
|
||||
)
|
||||
else:
|
||||
# No transcoding necessary.
|
||||
msg = 'Hardlinking' if hardlink \
|
||||
else ('Linking' if link else 'Copying')
|
||||
msg = (
|
||||
"Hardlinking"
|
||||
if hardlink
|
||||
else ("Linking" if link else "Copying")
|
||||
)
|
||||
|
||||
self._log.info('{1} {0}',
|
||||
util.displayable_path(item.path),
|
||||
msg)
|
||||
self._log.info(
|
||||
"{1} {0}", util.displayable_path(item.path), msg
|
||||
)
|
||||
|
||||
if hardlink:
|
||||
util.hardlink(original, converted)
|
||||
@@ -325,8 +426,8 @@ class ConvertPlugin(BeetsPlugin):
|
||||
if pretend:
|
||||
continue
|
||||
|
||||
id3v23 = self.config['id3v23'].as_choice([True, False, 'inherit'])
|
||||
if id3v23 == 'inherit':
|
||||
id3v23 = self.config["id3v23"].as_choice([True, False, "inherit"])
|
||||
if id3v23 == "inherit":
|
||||
id3v23 = None
|
||||
|
||||
# Write tags from the database to the converted file.
|
||||
@@ -339,23 +440,41 @@ class ConvertPlugin(BeetsPlugin):
|
||||
item.read()
|
||||
item.store() # Store new path and audio data.
|
||||
|
||||
if self.config['embed'] and not linked:
|
||||
if self.config["embed"] and not linked:
|
||||
album = item._cached_album
|
||||
if album and album.artpath:
|
||||
self._log.debug('embedding album art from {}',
|
||||
util.displayable_path(album.artpath))
|
||||
art.embed_item(self._log, item, album.artpath,
|
||||
itempath=converted, id3v23=id3v23)
|
||||
maxwidth = self._get_art_resize(album.artpath)
|
||||
self._log.debug(
|
||||
"embedding album art from {}",
|
||||
util.displayable_path(album.artpath),
|
||||
)
|
||||
art.embed_item(
|
||||
self._log,
|
||||
item,
|
||||
album.artpath,
|
||||
maxwidth,
|
||||
itempath=converted,
|
||||
id3v23=id3v23,
|
||||
)
|
||||
|
||||
if keep_new:
|
||||
plugins.send('after_convert', item=item,
|
||||
dest=dest, keepnew=True)
|
||||
plugins.send(
|
||||
"after_convert", item=item, dest=dest, keepnew=True
|
||||
)
|
||||
else:
|
||||
plugins.send('after_convert', item=item,
|
||||
dest=converted, keepnew=False)
|
||||
plugins.send(
|
||||
"after_convert", item=item, dest=converted, keepnew=False
|
||||
)
|
||||
|
||||
def copy_album_art(self, album, dest_dir, path_formats, pretend=False,
|
||||
link=False, hardlink=False):
|
||||
def copy_album_art(
|
||||
self,
|
||||
album,
|
||||
dest_dir,
|
||||
path_formats,
|
||||
pretend=False,
|
||||
link=False,
|
||||
hardlink=False,
|
||||
):
|
||||
"""Copies or converts the associated cover art of the album. Album must
|
||||
have at least one track.
|
||||
"""
|
||||
@@ -369,8 +488,9 @@ class ConvertPlugin(BeetsPlugin):
|
||||
|
||||
# Get the destination of the first item (track) of the album, we use
|
||||
# this function to format the path accordingly to path_formats.
|
||||
dest = album_item.destination(basedir=dest_dir,
|
||||
path_formats=path_formats)
|
||||
dest = album_item.destination(
|
||||
basedir=dest_dir, path_formats=path_formats
|
||||
)
|
||||
|
||||
# Remove item from the path.
|
||||
dest = os.path.join(*util.components(dest)[:-1])
|
||||
@@ -383,46 +503,47 @@ class ConvertPlugin(BeetsPlugin):
|
||||
util.mkdirall(dest)
|
||||
|
||||
if os.path.exists(util.syspath(dest)):
|
||||
self._log.info('Skipping {0} (target file exists)',
|
||||
util.displayable_path(album.artpath))
|
||||
self._log.info(
|
||||
"Skipping {0} (target file exists)",
|
||||
util.displayable_path(album.artpath),
|
||||
)
|
||||
return
|
||||
|
||||
# Decide whether we need to resize the cover-art image.
|
||||
resize = False
|
||||
maxwidth = None
|
||||
if self.config['album_art_maxwidth']:
|
||||
maxwidth = self.config['album_art_maxwidth'].get(int)
|
||||
size = ArtResizer.shared.get_size(album.artpath)
|
||||
self._log.debug('image size: {}', size)
|
||||
if size:
|
||||
resize = size[0] > maxwidth
|
||||
else:
|
||||
self._log.warning('Could not get size of image (please see '
|
||||
'documentation for dependencies).')
|
||||
maxwidth = self._get_art_resize(album.artpath)
|
||||
|
||||
# Either copy or resize (while copying) the image.
|
||||
if resize:
|
||||
self._log.info('Resizing cover art from {0} to {1}',
|
||||
util.displayable_path(album.artpath),
|
||||
util.displayable_path(dest))
|
||||
if maxwidth is not None:
|
||||
self._log.info(
|
||||
"Resizing cover art from {0} to {1}",
|
||||
util.displayable_path(album.artpath),
|
||||
util.displayable_path(dest),
|
||||
)
|
||||
if not pretend:
|
||||
ArtResizer.shared.resize(maxwidth, album.artpath, dest)
|
||||
else:
|
||||
if pretend:
|
||||
msg = 'ln' if hardlink else ('ln -s' if link else 'cp')
|
||||
msg = "ln" if hardlink else ("ln -s" if link else "cp")
|
||||
|
||||
self._log.info('{2} {0} {1}',
|
||||
util.displayable_path(album.artpath),
|
||||
util.displayable_path(dest),
|
||||
msg)
|
||||
self._log.info(
|
||||
"{2} {0} {1}",
|
||||
util.displayable_path(album.artpath),
|
||||
util.displayable_path(dest),
|
||||
msg,
|
||||
)
|
||||
else:
|
||||
msg = 'Hardlinking' if hardlink \
|
||||
else ('Linking' if link else 'Copying')
|
||||
msg = (
|
||||
"Hardlinking"
|
||||
if hardlink
|
||||
else ("Linking" if link else "Copying")
|
||||
)
|
||||
|
||||
self._log.info('{2} cover art from {0} to {1}',
|
||||
util.displayable_path(album.artpath),
|
||||
util.displayable_path(dest),
|
||||
msg)
|
||||
self._log.info(
|
||||
"{2} cover art from {0} to {1}",
|
||||
util.displayable_path(album.artpath),
|
||||
util.displayable_path(dest),
|
||||
msg,
|
||||
)
|
||||
if hardlink:
|
||||
util.hardlink(album.artpath, dest)
|
||||
elif link:
|
||||
@@ -431,79 +552,92 @@ class ConvertPlugin(BeetsPlugin):
|
||||
util.copy(album.artpath, dest)
|
||||
|
||||
def convert_func(self, lib, opts, args):
|
||||
dest = opts.dest or self.config['dest'].get()
|
||||
if not dest:
|
||||
raise ui.UserError('no convert destination set')
|
||||
dest = util.bytestring_path(dest)
|
||||
|
||||
threads = opts.threads or self.config['threads'].get(int)
|
||||
|
||||
path_formats = ui.get_path_formats(self.config['paths'] or None)
|
||||
|
||||
fmt = opts.format or self.config['format'].as_str().lower()
|
||||
|
||||
if opts.pretend is not None:
|
||||
pretend = opts.pretend
|
||||
else:
|
||||
pretend = self.config['pretend'].get(bool)
|
||||
|
||||
if opts.hardlink is not None:
|
||||
hardlink = opts.hardlink
|
||||
link = False
|
||||
elif opts.link is not None:
|
||||
hardlink = False
|
||||
link = opts.link
|
||||
else:
|
||||
hardlink = self.config['hardlink'].get(bool)
|
||||
link = self.config['link'].get(bool)
|
||||
(
|
||||
dest,
|
||||
threads,
|
||||
path_formats,
|
||||
fmt,
|
||||
pretend,
|
||||
hardlink,
|
||||
link,
|
||||
playlist,
|
||||
) = self._get_opts_and_config(opts)
|
||||
|
||||
if opts.album:
|
||||
albums = lib.albums(ui.decargs(args))
|
||||
items = [i for a in albums for i in a.items()]
|
||||
if not pretend:
|
||||
for a in albums:
|
||||
ui.print_(format(a, ''))
|
||||
ui.print_(format(a, ""))
|
||||
else:
|
||||
items = list(lib.items(ui.decargs(args)))
|
||||
if not pretend:
|
||||
for i in items:
|
||||
ui.print_(format(i, ''))
|
||||
ui.print_(format(i, ""))
|
||||
|
||||
if not items:
|
||||
self._log.error('Empty query result.')
|
||||
self._log.error("Empty query result.")
|
||||
return
|
||||
if not (pretend or opts.yes or ui.input_yn("Convert? (Y/n)")):
|
||||
return
|
||||
|
||||
if opts.album and self.config['copy_album_art']:
|
||||
if opts.album and self.config["copy_album_art"]:
|
||||
for album in albums:
|
||||
self.copy_album_art(album, dest, path_formats, pretend,
|
||||
link, hardlink)
|
||||
self.copy_album_art(
|
||||
album, dest, path_formats, pretend, link, hardlink
|
||||
)
|
||||
|
||||
convert = [self.convert_item(dest,
|
||||
opts.keep_new,
|
||||
path_formats,
|
||||
fmt,
|
||||
pretend,
|
||||
link,
|
||||
hardlink)
|
||||
for _ in range(threads)]
|
||||
pipe = util.pipeline.Pipeline([iter(items), convert])
|
||||
pipe.run_parallel()
|
||||
self._parallel_convert(
|
||||
dest,
|
||||
opts.keep_new,
|
||||
path_formats,
|
||||
fmt,
|
||||
pretend,
|
||||
link,
|
||||
hardlink,
|
||||
threads,
|
||||
items,
|
||||
)
|
||||
|
||||
if playlist:
|
||||
# Playlist paths are understood as relative to the dest directory.
|
||||
pl_normpath = util.normpath(playlist)
|
||||
pl_dir = os.path.dirname(pl_normpath)
|
||||
self._log.info("Creating playlist file {0}", pl_normpath)
|
||||
# Generates a list of paths to media files, ensures the paths are
|
||||
# relative to the playlist's location and translates the unicode
|
||||
# strings we get from item.destination to bytes.
|
||||
items_paths = [
|
||||
os.path.relpath(
|
||||
util.bytestring_path(
|
||||
item.destination(
|
||||
basedir=dest,
|
||||
path_formats=path_formats,
|
||||
fragment=False,
|
||||
)
|
||||
),
|
||||
pl_dir,
|
||||
)
|
||||
for item in items
|
||||
]
|
||||
if not pretend:
|
||||
m3ufile = M3UFile(playlist)
|
||||
m3ufile.set_contents(items_paths)
|
||||
m3ufile.write()
|
||||
|
||||
def convert_on_import(self, lib, item):
|
||||
"""Transcode a file automatically after it is imported into the
|
||||
library.
|
||||
"""
|
||||
fmt = self.config['format'].as_str().lower()
|
||||
fmt = self.config["format"].as_str().lower()
|
||||
if should_transcode(item, fmt):
|
||||
command, ext = get_format()
|
||||
|
||||
# Create a temporary file for the conversion.
|
||||
tmpdir = self.config['tmpdir'].get()
|
||||
tmpdir = self.config["tmpdir"].get()
|
||||
if tmpdir:
|
||||
tmpdir = util.py3_path(util.bytestring_path(tmpdir))
|
||||
fd, dest = tempfile.mkstemp(util.py3_path(b'.' + ext), dir=tmpdir)
|
||||
tmpdir = os.fsdecode(util.bytestring_path(tmpdir))
|
||||
fd, dest = tempfile.mkstemp(os.fsdecode(b"." + ext), dir=tmpdir)
|
||||
os.close(fd)
|
||||
dest = util.bytestring_path(dest)
|
||||
_temp_files.append(dest) # Delete the transcode later.
|
||||
@@ -522,13 +656,107 @@ class ConvertPlugin(BeetsPlugin):
|
||||
item.read() # Load new audio information data.
|
||||
item.store()
|
||||
|
||||
if self.config['delete_originals']:
|
||||
self._log.info('Removing original file {0}', source_path)
|
||||
if self.config["delete_originals"]:
|
||||
self._log.log(
|
||||
logging.DEBUG if self.config["quiet"] else logging.INFO,
|
||||
"Removing original file {0}",
|
||||
source_path,
|
||||
)
|
||||
util.remove(source_path, False)
|
||||
|
||||
def _get_art_resize(self, artpath):
|
||||
"""For a given piece of album art, determine whether or not it needs
|
||||
to be resized according to the user's settings. If so, returns the
|
||||
new size. If not, returns None.
|
||||
"""
|
||||
newwidth = None
|
||||
if self.config["album_art_maxwidth"]:
|
||||
maxwidth = self.config["album_art_maxwidth"].get(int)
|
||||
size = ArtResizer.shared.get_size(artpath)
|
||||
self._log.debug("image size: {}", size)
|
||||
if size:
|
||||
if size[0] > maxwidth:
|
||||
newwidth = maxwidth
|
||||
else:
|
||||
self._log.warning(
|
||||
"Could not get size of image (please see "
|
||||
"documentation for dependencies)."
|
||||
)
|
||||
return newwidth
|
||||
|
||||
def _cleanup(self, task, session):
|
||||
for path in task.old_paths:
|
||||
if path in _temp_files:
|
||||
if os.path.isfile(path):
|
||||
if os.path.isfile(util.syspath(path)):
|
||||
util.remove(path)
|
||||
_temp_files.remove(path)
|
||||
|
||||
def _get_opts_and_config(self, opts):
|
||||
"""Returns parameters needed for convert function.
|
||||
Get parameters from command line if available,
|
||||
default to config if not available.
|
||||
"""
|
||||
dest = opts.dest or self.config["dest"].get()
|
||||
if not dest:
|
||||
raise ui.UserError("no convert destination set")
|
||||
dest = util.bytestring_path(dest)
|
||||
|
||||
threads = opts.threads or self.config["threads"].get(int)
|
||||
|
||||
path_formats = ui.get_path_formats(self.config["paths"] or None)
|
||||
|
||||
fmt = opts.format or self.config["format"].as_str().lower()
|
||||
|
||||
playlist = opts.playlist or self.config["playlist"].get()
|
||||
if playlist is not None:
|
||||
playlist = os.path.join(dest, util.bytestring_path(playlist))
|
||||
|
||||
if opts.pretend is not None:
|
||||
pretend = opts.pretend
|
||||
else:
|
||||
pretend = self.config["pretend"].get(bool)
|
||||
|
||||
if opts.hardlink is not None:
|
||||
hardlink = opts.hardlink
|
||||
link = False
|
||||
elif opts.link is not None:
|
||||
hardlink = False
|
||||
link = opts.link
|
||||
else:
|
||||
hardlink = self.config["hardlink"].get(bool)
|
||||
link = self.config["link"].get(bool)
|
||||
|
||||
return (
|
||||
dest,
|
||||
threads,
|
||||
path_formats,
|
||||
fmt,
|
||||
pretend,
|
||||
hardlink,
|
||||
link,
|
||||
playlist,
|
||||
)
|
||||
|
||||
def _parallel_convert(
|
||||
self,
|
||||
dest,
|
||||
keep_new,
|
||||
path_formats,
|
||||
fmt,
|
||||
pretend,
|
||||
link,
|
||||
hardlink,
|
||||
threads,
|
||||
items,
|
||||
):
|
||||
"""Run the convert_item function for every items on as many thread as
|
||||
defined in threads
|
||||
"""
|
||||
convert = [
|
||||
self.convert_item(
|
||||
dest, keep_new, path_formats, fmt, pretend, link, hardlink
|
||||
)
|
||||
for _ in range(threads)
|
||||
]
|
||||
pipe = util.pipeline.Pipeline([iter(items), convert])
|
||||
pipe.run_parallel()
|
||||
|
||||
Reference in New Issue
Block a user