def copy_album_art(self, album, dest_dir, path_formats, pretend=False): """Copies the associated cover art of the album. Album must have at least one track. """ if not album or not album.artpath: return album_item = album.items().get() # Album shouldn't be empty. if not album_item: return # 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) # Remove item from the path. dest = os.path.join(*util.components(dest)[:-1]) dest = album.art_destination(album.artpath, item_dir=dest) if album.artpath == dest: return if not pretend: util.mkdirall(dest) if os.path.exists(util.syspath(dest)): self._log.info("Skipping {0} (target file exists)", util.displayable_path(album.artpath)) return if pretend: self._log.info("cp {0} {1}", util.displayable_path(album.artpath), util.displayable_path(dest)) else: self._log.info("Copying cover art to {0}", util.displayable_path(dest)) util.copy(album.artpath, dest)
def move_art(self, copy=False, link=False): """Move or copy any existing album art so that it remains in the same directory as the items. """ old_art = self.artpath if not old_art: return new_art = self.art_destination(old_art) if new_art == old_art: return new_art = util.unique_path(new_art) log.debug(u'moving album art {0} to {1}', util.displayable_path(old_art), util.displayable_path(new_art)) if copy: util.copy(old_art, new_art) elif link: util.link(old_art, new_art) else: util.move(old_art, new_art) self.artpath = new_art # Prune old path when moving. if not copy: util.prune_dirs(os.path.dirname(old_art), self._db.directory)
def truncate(self, source): """Truncates an item to a size less than UPLOAD_MAX_SIZE.""" fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) log.info(u'echonest: truncating {0} to {1}', util.displayable_path(source), util.displayable_path(dest)) opts = [] for arg in TRUNCATE_COMMAND.split(): arg = arg.encode('utf-8') opts.append(Template(arg).substitute(source=source, dest=dest)) # Run the command. try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: log.debug(u'echonest: truncate failed: {0}', exc) util.remove(dest) return log.info(u'echonest: truncate encoding {0}', util.displayable_path(source)) return dest
def write_items(lib, query, pretend): """Write tag information from the database to the respective files in the filesystem. """ items, albums = _do_query(lib, query, False, False) for item in items: # Item deleted? if not os.path.exists(syspath(item.path)): log.info(u'missing file: {0}'.format( util.displayable_path(item.path) )) continue # Get an Item object reflecting the "clean" (on-disk) state. try: clean_item = library.Item.from_path(item.path) except Exception as exc: log.error(u'error reading {0}: {1}'.format( displayable_path(item.path), exc )) continue # Check for and display changes. changed = ui.show_model_changes(item, clean_item, library.ITEM_KEYS_META, always=True) if changed and not pretend: try: item.write() except Exception as exc: log.error(u'could not write {0}: {1}'.format( util.displayable_path(item.path), exc )) continue
def encode(source, dest): quiet = config['convert']['quiet'].get() if not quiet: log.info(u'Started encoding {0}'.format(util.displayable_path(source))) command, _ = get_format() opts = [] for arg in command: opts.append(Template(arg).safe_substitute({ 'source': source, 'dest': dest, })) log.debug(u'convert: executing: {0}'.format( u' '.join(pipes.quote(o.decode('utf8', 'ignore')) for o in opts) )) 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...' .format(util.displayable_path(source))) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) return if not quiet: log.info(u'Finished encoding {0}'.format( util.displayable_path(source)) )
def truncate(self, item): """Truncates an item to a size less than UPLOAD_MAX_SIZE.""" fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) source = item.path log.info(u'echonest: truncating {0} to {1}'.format( util.displayable_path(source), util.displayable_path(dest), )) command = u'ffmpeg -t 300 -i $source -y -acodec copy $dest' opts = [] for arg in command.split(): arg = arg.encode('utf-8') opts.append(Template(arg).substitute(source=source, dest=dest)) # Run the command. try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: log.debug(u'echonest: truncate failed: {0}'.format(exc)) util.remove(dest) return log.info(u'echonest: truncate encoding {0}'.format( util.displayable_path(source)) ) return dest
def embed_item(item, imagepath, maxwidth=None, itempath=None, compare_threshold=0, ifempty=False, as_album=False): """Embed an image into the item's media file. """ if compare_threshold: if not check_art_similarity(item, imagepath, compare_threshold): log.warn(u'Image not similar; skipping.') return if ifempty: art = get_art(item) if not art: pass else: log.debug(u'embedart: media file contained art already {0}'.format( displayable_path(imagepath) )) return if maxwidth and not as_album: imagepath = resize_image(imagepath, maxwidth) try: log.debug(u'embedart: embedding {0}'.format( displayable_path(imagepath) )) item['images'] = [_mediafile_image(imagepath, maxwidth)] except IOError as exc: log.error(u'embedart: could not read image file: {0}'.format(exc)) else: # We don't want to store the image in the database. item.try_write(itempath) del item['images']
def _embed(path, items, maxwidth=0): """Embed an image file, located at `path`, into each item. """ if maxwidth: path = ArtResizer.shared.resize(maxwidth, syspath(path)) data = open(syspath(path), 'rb').read() kindstr = imghdr.what(None, data) if kindstr is None: log.error(u'Could not embed art of unkown type: {0}'.format( displayable_path(path) )) return elif kindstr not in ('jpeg', 'png'): log.error(u'Image type {0} is not allowed as cover art: {1}'.format( kindstr, displayable_path(path) )) return # Add art to each file. 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( displayable_path(item.path), exc )) continue f.art = data f.save()
def check_art_similarity(item, imagepath, compare_threshold): """A boolean indicating if an image is similar to embedded item art. """ with NamedTemporaryFile(delete=True) as f: art = extract(f.name, item) if art: # Converting images to grayscale tends to minimize the weight # of colors in the diff score cmd = 'convert {0} {1} -colorspace gray MIFF:- | ' \ 'compare -metric PHASH - null:'.format(syspath(imagepath), syspath(art)) proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=platform.system() != 'Windows', shell=True) stdout, stderr = proc.communicate() if proc.returncode: if proc.returncode != 1: log.warn(u'embedart: IM phashes compare failed for {0}, \ {1}'.format(displayable_path(imagepath), displayable_path(art))) return phashDiff = float(stderr) else: phashDiff = float(stdout) log.info(u'embedart: compare PHASH score is {0}'.format(phashDiff)) if phashDiff > compare_threshold: return False return True
def _raw_main(args): """A helper function for `main` without top-level exception handling. """ # Temporary: Migrate from 1.0-style configuration. from beets.ui import migrate migrate.automigrate() # Get the default subcommands. from beets.ui.commands import default_commands # Add plugin paths. sys.path += get_plugin_paths() # Load requested plugins. plugins.load_plugins(config["plugins"].as_str_seq()) plugins.send("pluginload") # Construct the root parser. commands = list(default_commands) commands += plugins.commands() commands.append(migrate.migrate_cmd) # Temporary. parser = SubcommandsOptionParser(subcommands=commands) parser.add_option("-l", "--library", dest="library", help="library database file to use") parser.add_option("-d", "--directory", dest="directory", help="destination music directory") parser.add_option("-v", "--verbose", dest="verbose", action="store_true", help="print debugging information") # Parse the command-line! options, subcommand, suboptions, subargs = parser.parse_args(args) config.set_args(options) # Open library file. dbpath = config["library"].as_filename() try: lib = library.Library(dbpath, config["directory"].as_filename(), get_path_formats(), get_replacements()) except sqlite3.OperationalError: raise UserError(u"database file {0} could not be opened".format(util.displayable_path(dbpath))) plugins.send("library_opened", lib=lib) # Configure the logger. if config["verbose"].get(bool): log.setLevel(logging.DEBUG) else: log.setLevel(logging.INFO) log.debug( u"data directory: {0}\n" u"library database: {1}\n" u"library directory: {2}".format( util.displayable_path(config.config_dir()), util.displayable_path(lib.path), util.displayable_path(lib.directory), ) ) # Configure the MusicBrainz API. mb.configure() # Invoke the subcommand. subcommand.func(lib, suboptions, subargs) plugins.send("cli_exit", lib=lib)
def extract(lib, outpath, query): item = lib.items(query).get() if not item: log.error('No item matches query.') return # Extract the art. try: mf = mediafile.MediaFile(syspath(item.path)) except mediafile.UnreadableFileError as exc: log.error(u'Could not extract art from {0}: {1}'.format( displayable_path(item.path), exc )) return art = mf.art if not art: log.error('No album art present in %s - %s.' % (item.artist, item.title)) return # Add an extension to the filename. ext = imghdr.what(None, h=art) if not ext: log.error('Unknown image type.') return outpath += '.' + ext log.info(u'Extracting album art from: {0.artist} - {0.title}\n' u'To: {1}'.format(item, displayable_path(outpath))) with open(syspath(outpath), 'wb') as f: f.write(art)
def on_play(self, status): playlist = self.mpd.playlist() path = playlist.get(status['songid']) if not path: return if is_url(path): self._log.info(u'playing stream {0}', displayable_path(path)) return played, duration = map(int, status['time'].split(':', 1)) remaining = duration - played if self.now_playing and self.now_playing['path'] != path: skipped = self.handle_song_change(self.now_playing) # mpd responds twice on a natural new song start going_to_happen_twice = not skipped else: going_to_happen_twice = False if not going_to_happen_twice: self._log.info(u'playing {0}', displayable_path(path)) self.now_playing = { 'started': time.time(), 'remaining': remaining, 'path': path, 'beets_item': self.get_item(path), } self.update_item(self.now_playing['beets_item'], 'last_played', value=int(time.time()))
def encode(source, dest): log.info(u'Started encoding {0}'.format(util.displayable_path(source))) command = get_command() opts = [] 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...' .format(util.displayable_path(source))) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) return log.info(u'Finished encoding {0}'.format(util.displayable_path(source)))
def migrate_db(replace=False): """Copy the beets library database file to the new location (e.g., from ~/.beetsmusic.blb to ~/.config/beets/library.db). """ _, srcfn = default_paths() destfn = beets.config['library'].as_filename() if not os.path.exists(srcfn) or srcfn == destfn: # Old DB does not exist or we're configured to point to the same # database. Do nothing. return if os.path.exists(destfn): if replace: log.debug(u'moving old database aside: {0}'.format( util.displayable_path(destfn) )) _displace(destfn) else: return log.debug(u'copying database from {0} to {1}'.format( util.displayable_path(srcfn), util.displayable_path(destfn) )) util.copy(srcfn, destfn) return destfn
def check_bad(self, lib, opts, args): for item in lib.items(ui.decargs(args)): # First, check whether the path exists. If not, the user # should probably run `beet update` to cleanup your library. dpath = displayable_path(item.path) self._log.debug("checking path: {}", dpath) if not os.path.exists(item.path): ui.print_("{}: file does not exist".format( ui.colorize('text_error', dpath))) # Run the checker against the file if one is found ext = os.path.splitext(item.path)[1][1:] checker = self.get_checker(ext) if not checker: continue path = item.path if not isinstance(path, unicode): path = item.path.decode(sys.getfilesystemencoding()) status, errors, output = checker(path) if status > 0: ui.print_("{}: checker exited withs status {}" .format(ui.colorize('text_error', dpath), status)) for line in output: ui.print_(" {}".format(displayable_path(line))) elif errors > 0: ui.print_("{}: checker found {} errors or warnings" .format(ui.colorize('text_warning', dpath), errors)) for line in output: ui.print_(" {}".format(displayable_path(line))) else: ui.print_("{}: ok".format(ui.colorize('text_success', dpath)))
def _raw_main(args, lib=None): """A helper function for `main` without top-level exception handling. """ subcommand, suboptions, subargs = _configure(args) if lib is None: # Open library file. dbpath = config["library"].as_filename() try: lib = library.Library(dbpath, config["directory"].as_filename(), get_path_formats(), get_replacements()) except sqlite3.OperationalError: raise UserError(u"database file {0} could not be opened".format(util.displayable_path(dbpath))) plugins.send("library_opened", lib=lib) log.debug( u"data directory: {0}\n" u"library database: {1}\n" u"library directory: {2}".format( util.displayable_path(config.config_dir()), util.displayable_path(lib.path), util.displayable_path(lib.directory), ) ) # Configure the MusicBrainz API. mb.configure() # Invoke the subcommand. subcommand.func(lib, suboptions, subargs) plugins.send("cli_exit", lib=lib)
def convert_item(lib, dest_dir): while True: item = yield 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(u'Skipping {0} (target file exists)'.format( util.displayable_path(item.path) )) continue # Ensure that only one thread tries to create directories at a # time. (The existence check is not atomic with the directory # creation inside this function.) with _fs_lock: util.mkdirall(dest) maxbr = config['convert']['max_bitrate'].get(int) if item.format == 'MP3' and item.bitrate < 1000 * maxbr: log.info(u'Copying {0}'.format(util.displayable_path(item.path))) util.copy(item.path, dest) else: encode(item.path, dest) item.path = dest item.write() if config['convert']['embed']: album = lib.get_album(item) if album: artpath = album.artpath if artpath: _embed(artpath, [item])
def convert(self, source): """Converts an item in an unsupported media format to ogg. Config pending. This is stolen from Jakob Schnitzers convert plugin. """ fd, dest = tempfile.mkstemp(b'.ogg') os.close(fd) self._log.info(u'encoding {0} to {1}', util.displayable_path(source), util.displayable_path(dest)) opts = [] for arg in CONVERT_COMMAND.split(): arg = arg.encode('utf-8') opts.append(Template(arg).substitute(source=source, dest=dest)) # Run the command. try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: self._log.debug(u'encode failed: {0}', exc) util.remove(dest) return self._log.info(u'finished encoding {0}', util.displayable_path(source)) return dest
def art_in_path(path): """Look for album art files in a specified directory.""" if not os.path.isdir(path): return # Find all files that look like images in the directory. images = [] for fn in os.listdir(path): for ext in IMAGE_EXTENSIONS: if fn.lower().endswith('.' + ext): images.append(fn) # Look for "preferred" filenames. for fn in images: for name in COVER_NAMES: if fn.lower().startswith(name): log.debug(u'fetchart: using well-named art file {0}'.format( util.displayable_path(fn) )) return os.path.join(path, fn) # Fall back to any image in the folder. if images: log.debug(u'fetchart: using fallback art file {0}'.format( util.displayable_path(images[0]) )) return os.path.join(path, images[0])
def print_data(data, item=None, fmt=None): """Print, with optional formatting, the fields of a single element. If no format string `fmt` is passed, the entries on `data` are printed one in each line, with the format 'field: value'. If `fmt` is not `None`, the `item` is printed according to `fmt`, using the `Item.__format__` machinery. """ if fmt: # use fmt specified by the user ui.print_(format(item, fmt)) return path = displayable_path(item.path) if item else None formatted = {} for key, value in data.iteritems(): if isinstance(value, list): formatted[key] = u'; '.join(value) if value is not None: formatted[key] = value if len(formatted) == 0: return maxwidth = max(len(key) for key in formatted) lineformat = u'{{0:>{0}}}: {{1}}'.format(maxwidth) if path: ui.print_(displayable_path(path)) for field in sorted(formatted): value = formatted[field] if isinstance(value, list): value = u'; '.join(value) ui.print_(lineformat.format(field, value))
def art_in_path(path, cover_names, cautious): """Look for album art files in a specified directory.""" if not os.path.isdir(path): return # Find all files that look like images in the directory. images = [] for fn in os.listdir(path): for ext in IMAGE_EXTENSIONS: if fn.lower().endswith('.' + ext): images.append(fn) # Look for "preferred" filenames. cover_pat = r"(\b|_)({0})(\b|_)".format('|'.join(cover_names)) for fn in images: if re.search(cover_pat, os.path.splitext(fn)[0], re.I): log.debug(u'fetchart: using well-named art file {0}'.format( util.displayable_path(fn) )) return os.path.join(path, fn) # Fall back to any image in the folder. if images and not cautious: log.debug(u'fetchart: using fallback art file {0}'.format( util.displayable_path(images[0]) )) return os.path.join(path, images[0])
def on_play(self, status): playlist = self.mpd.playlist() path = playlist.get(status["songid"]) if not path: return if is_url(path): log.info(u"mpdstats: playing stream {0}".format(displayable_path(path))) return played, duration = map(int, status["time"].split(":", 1)) remaining = duration - played if self.now_playing and self.now_playing["path"] != path: self.handle_song_change(self.now_playing) log.info(u"mpdstats: playing {0}".format(displayable_path(path))) self.now_playing = { "started": time.time(), "remaining": remaining, "path": path, "beets_item": self.get_item(path), }
def _configure(options): """Amend the global configuration object with command line options. """ # Add any additional config files specified with --config. This # special handling lets specified plugins get loaded before we # finish parsing the command line. if getattr(options, 'config', None) is not None: config_path = options.config del options.config config.set_file(config_path) config.set_args(options) # Configure the logger. if config['verbose'].get(bool): log.setLevel(logging.DEBUG) else: log.setLevel(logging.INFO) config_path = config.user_config_path() if os.path.isfile(config_path): log.debug('user configuration: {0}'.format( util.displayable_path(config_path))) else: log.debug('no user configuration found at {0}'.format( util.displayable_path(config_path))) log.debug(u'data directory: {0}' .format(util.displayable_path(config.config_dir()))) return config
def get(self, path, cover_names, cautious): """Look for album art files in a specified directory. """ if not os.path.isdir(path): return # Find all files that look like images in the directory. images = [] for fn in os.listdir(path): for ext in IMAGE_EXTENSIONS: if fn.lower().endswith(b'.' + ext.encode('utf8')) and \ os.path.isfile(os.path.join(path, fn)): images.append(fn) # Look for "preferred" filenames. images = sorted(images, key=lambda x: self.filename_priority(x, cover_names)) cover_pat = br"(\b|_)({0})(\b|_)".format(b'|'.join(cover_names)) for fn in images: if re.search(cover_pat, os.path.splitext(fn)[0], re.I): self._log.debug(u'using well-named art file {0}', util.displayable_path(fn)) return os.path.join(path, fn) # Fall back to any image in the folder. if images and not cautious: self._log.debug(u'using fallback art file {0}', util.displayable_path(images[0])) return os.path.join(path, images[0])
def fingerprint_item(log, item, write=False): """Get the fingerprint for an Item. If the item already has a fingerprint, it is not regenerated. If fingerprint generation fails, return None. If the items are associated with a library, they are saved to the database. If `write` is set, then the new fingerprints are also written to files' metadata. """ # Get a fingerprint and length for this track. if not item.length: log.info(u'{0}: no duration available', util.displayable_path(item.path)) elif item.acoustid_fingerprint: if write: log.info(u'{0}: fingerprint exists, skipping', util.displayable_path(item.path)) else: log.info(u'{0}: using existing fingerprint', util.displayable_path(item.path)) return item.acoustid_fingerprint else: log.info(u'{0}: fingerprinting', util.displayable_path(item.path)) try: _, fp = acoustid.fingerprint_file(item.path) item.acoustid_fingerprint = fp if write: log.info(u'{0}: writing fingerprint', util.displayable_path(item.path)) item.try_write() if item._db: item.store() return item.acoustid_fingerprint except acoustid.FingerprintGenerationError as exc: log.info(u'fingerprint generation failed: {0}', exc)
def convert(self, item): """Converts an item in an unsupported media format to ogg. Config pending. This is stolen from Jakob Schnitzers convert plugin. """ fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) source = item.path log.info(u'echonest: encoding {0} to {1}'.format( util.displayable_path(source), util.displayable_path(dest), )) # Build up the FFmpeg command line. # FIXME: use avconv? command = u'ffmpeg -i $source -y -acodec libvorbis -vn -aq 2 $dest' opts = [] for arg in command.split(): arg = arg.encode('utf-8') opts.append(Template(arg).substitute(source=source, dest=dest)) # Run the command. try: subprocess.check_call(opts, close_fds=True, stderr=DEVNULL) except (OSError, subprocess.CalledProcessError) as exc: log.debug(u'echonest: encode failed: {0}'.format(exc)) util.remove(dest) return log.info(u'echonest: finished encoding {0}'.format( util.displayable_path(source)) ) return dest
def _configure(args): """Parse the command line, load configuration files (including loading any indicated plugins), and return the invoked subcomand, the subcommand options, and the subcommand arguments. """ # Temporary: Migrate from 1.0-style configuration. from beets.ui import migrate migrate.automigrate() # Get the default subcommands. from beets.ui.commands import default_commands # Construct the root parser. parser = SubcommandsOptionParser() parser.add_option('-l', '--library', dest='library', help='library database file to use') parser.add_option('-d', '--directory', dest='directory', help="destination music directory") parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='print debugging information') parser.add_option('-c', '--config', dest='config', help='path to configuration file') # Parse the command-line! options, subargs = parser.parse_global_options(args) # Add any additional config files specified with --config. This # special handling lets specified plugins get loaded before we # finish parsing the command line. if getattr(options, 'config', None) is not None: config_path = options.config del options.config config.set_file(config_path) config.set_args(options) # Configure the logger. if config['verbose'].get(bool): log.setLevel(logging.DEBUG) else: log.setLevel(logging.INFO) config_path = config.user_config_path() if os.path.isfile(config_path): log.debug('user configuration: {0}'.format( util.displayable_path(config_path))) else: log.debug('no user configuration found at {0}'.format( util.displayable_path(config_path))) # Add builtin subcommands parser.add_subcommand(*default_commands) parser.add_subcommand(migrate.migrate_cmd) # Now add the plugin commands to the parser. _load_plugins() for cmd in plugins.commands(): parser.add_subcommand(cmd) # Parse the remainder of the command line with loaded plugins. return parser.parse_subcommand(subargs)
def on_play(self, status): playlist = self.mpd.playlist() path = playlist.get(status['songid']) if not path: return if is_url(path): log.info(u'mpdstats: playing stream {0}'.format( displayable_path(path) )) return played, duration = map(int, status['time'].split(':', 1)) remaining = duration - played if self.now_playing and self.now_playing['path'] != path: self.handle_song_change(self.now_playing) log.info(u'mpdstats: playing {0}'.format( displayable_path(path) )) self.now_playing = { 'started': time.time(), 'remaining': remaining, 'path': path, 'beets_item': self.get_item(path), }
def _checksum(self, item, prog): """Run external `prog` on file path associated with `item`, cache output as flexattr on a key that is the name of the program, and return the key, checksum tuple. """ args = [p.format(file=item.path) for p in shlex.split(prog)] key = args[0] checksum = getattr(item, key, False) if not checksum: self._log.debug(u'key {0} on item {1} not cached:' u'computing checksum', key, displayable_path(item.path)) try: checksum = command_output(args) setattr(item, key, checksum) item.store() self._log.debug(u'computed checksum for {0} using {1}', item.title, key) except subprocess.CalledProcessError as e: self._log.debug(u'failed to checksum {0}: {1}', displayable_path(item.path), e) else: self._log.debug(u'key {0} on item {1} cached:' u'not computing checksum', key, displayable_path(item.path)) return key, checksum
def im_resize(maxwidth, path_in, path_out=None): """Resize using ImageMagick's ``convert`` tool. Return the output path of resized image. """ path_out = path_out or temp_file_for(path_in) log.debug( u"artresizer: ImageMagick resizing {0} to {1}", util.displayable_path(path_in), util.displayable_path(path_out) ) # "-resize widthxheight>" shrinks images with dimension(s) larger # than the corresponding width and/or height dimension(s). The > # "only shrink" flag is prefixed by ^ escape char for Windows # compatibility. try: util.command_output( [ b"convert", util.syspath(path_in, prefix=False), b"-resize", b"{0}x^>".format(maxwidth), util.syspath(path_out, prefix=False), ] ) except subprocess.CalledProcessError: log.warn(u"artresizer: IM convert failed for {0}", util.displayable_path(path_in)) return path_in return path_out
def _convert(item): dest = self.destination(item) with fs_lock: util.mkdirall(dest) if self.should_transcode(item): self._encode(self.convert_cmd, item.path, dest) # Don't rely on the converter to write correct/complete tags. item.write(path=dest) else: self._log.debug(u'copying {0}'.format(displayable_path(dest))) util.copy(item.path, dest, replace=True) if self._embed: self.sync_art(item, dest) return item, dest
def _group_by(self, objs, keys, strict): """Return a dictionary with keys arbitrary concatenations of attributes and values lists of objects (Albums or Items) with those keys. If strict, all attributes must be defined for a duplicate match. """ import collections counts = collections.defaultdict(list) for obj in objs: values = [getattr(obj, k, None) for k in keys] values = [v for v in values if v not in (None, '')] if strict and len(values) < len(keys): self._log.debug(u'some keys {0} on item {1} are null or empty:' u' skipping', keys, displayable_path(obj.path)) elif (not strict and not len(values)): self._log.debug(u'all keys {0} on item {1} are null or empty:' u' skipping', keys, displayable_path(obj.path)) else: key = tuple(values) counts[key].append(obj) return counts
def move_art(self, copy=False): """Move or copy any existing album art so that it remains in the same directory as the items. """ old_art = self.artpath if not old_art: return new_art = self.art_destination(old_art) if new_art == old_art: return new_art = util.unique_path(new_art) log.debug(u'moving album art {0} to {1}'.format( util.displayable_path(old_art), util.displayable_path(new_art))) if copy: util.copy(old_art, new_art) else: util.move(old_art, new_art) self.artpath = new_art # Prune old path when moving. if not copy: util.prune_dirs(os.path.dirname(old_art), self._db.directory)
def _checksum(item, prog): """Run external `prog` on file path associated with `item`, cache output as flexattr on a key that is the name of the program, and return the key, checksum tuple. """ args = shlex.split(prog.format(file=item.path)) key = args[0] checksum = getattr(item, key, False) if not checksum: log.debug('%s: key %s on item %s not cached: computing checksum', PLUGIN, key, displayable_path(item.path)) try: checksum = command_output(args) setattr(item, key, checksum) item.store() log.debug('%s: computed checksum for %s using %s', PLUGIN, item.title, key) except Exception as e: log.debug('%s: failed to checksum %s: %s', PLUGIN, displayable_path(item.path), e) else: log.debug('%s: key %s on item %s cached: not computing checksum', PLUGIN, key, displayable_path(item.path)) return key, checksum
def info(paths): # Set up fields to output. fields = [] for name, _, _, mffield in library.ITEM_FIELDS: if mffield: fields.append(name) # Line format. other_fields = ['album art'] maxwidth = max(len(name) for name in fields + other_fields) lineformat = u'{{0:>{0}}}: {{1}}'.format(maxwidth) first = True for path in paths: if not first: ui.print_() path = util.normpath(path) if not os.path.isfile(path): ui.print_(u'not a file: {0}'.format(util.displayable_path(path))) continue ui.print_(path) try: mf = mediafile.MediaFile(path) except mediafile.UnreadableFileError: ui.print_('cannot read file: {0}'.format( util.displayable_path(path))) continue # Basic fields. for name in fields: ui.print_(lineformat.format(name, getattr(mf, name))) # Extra stuff. ui.print_(lineformat.format('album art', mf.art is not None)) first = False
def _checksum(item, prog, log): """Run external `prog` on file path associated with `item`, cache output as flexattr on a key that is the name of the program, and return the key, checksum tuple. """ args = [p.format(file=item.path) for p in shlex.split(prog)] key = args[0] checksum = getattr(item, key, False) if not checksum: log.debug(u'{0}: key {1} on item {2} not cached: computing checksum', PLUGIN, key, displayable_path(item.path)) try: checksum = command_output(args) setattr(item, key, checksum) item.store() log.debug(u'{0}: computed checksum for {1} using {2}', PLUGIN, item.title, key) except subprocess.CalledProcessError as e: log.debug(u'{0}: failed to checksum {1}: {2}', PLUGIN, displayable_path(item.path), e) else: log.debug(u'{0}: key {1} on item {2} cached: not computing checksum', PLUGIN, key, displayable_path(item.path)) return key, checksum
def encode(source, dest): """Encode ``source`` to ``dest`` using the command from ``get_format()``. Raises an ``ui.UserError`` if the command was not found and a ``subprocess.CalledProcessError`` if the command exited with a non-zero status code. """ quiet = config['convert']['quiet'].get() if not quiet: log.info(u'Started encoding {0}'.format(util.displayable_path(source))) command, _ = get_format() command = Template(command).safe_substitute({ 'source': pipes.quote(source), 'dest': pipes.quote(dest), }) log.debug(u'convert: executing: {0}'.format( util.displayable_path(command))) try: util.command_output(command, shell=True) except subprocess.CalledProcessError: # Something went wrong (probably Ctrl+C), remove temporary files log.info(u'Encoding {0} failed. Cleaning up...'.format( util.displayable_path(source))) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) raise except OSError as exc: raise ui.UserError(u'convert: could invoke ffmpeg: {0}'.format(exc)) if not quiet: log.info(u'Finished encoding {0}'.format( util.displayable_path(source)))
def migrate_db(replace=False): """Copy the beets library database file to the new location (e.g., from ~/.beetsmusic.blb to ~/.config/beets/library.db). """ _, srcfn = default_paths() destfn = beets.config['library'].as_filename() if not os.path.exists(srcfn) or srcfn == destfn: # Old DB does not exist or we're configured to point to the same # database. Do nothing. return if os.path.exists(destfn): if replace: log.debug(u'moving old database aside: {0}'.format( util.displayable_path(destfn))) _displace(destfn) else: return log.debug(u'copying database from {0} to {1}'.format( util.displayable_path(srcfn), util.displayable_path(destfn))) util.copy(srcfn, destfn) return destfn
def run_command(self, cmd): self._log.debug(u"running command: {}", displayable_path(list2cmdline(cmd))) try: output = check_output(cmd, stderr=STDOUT) errors = 0 status = 0 except CalledProcessError as e: output = e.output errors = 1 status = e.returncode except OSError as e: raise CheckerCommandException(cmd, e) output = output.decode(sys.getdefaultencoding(), 'replace') return status, errors, [line for line in output.split("\n") if line]
def im_resize(maxwidth, path_in, path_out=None, quality=0, max_filesize=0): """Resize using ImageMagick. Use the ``magick`` program or ``convert`` on older versions. Return the output path of resized image. """ path_out = path_out or temp_file_for(path_in) log.debug(u'artresizer: ImageMagick resizing {0} to {1}', util.displayable_path(path_in), util.displayable_path(path_out)) # "-resize WIDTHx>" shrinks images with the width larger # than the given width while maintaining the aspect ratio # with regards to the height. cmd = ArtResizer.shared.im_convert_cmd + [ util.syspath(path_in, prefix=False), '-resize', '{0}x>'.format(maxwidth), ] if quality > 0: cmd += ['-quality', '{0}'.format(quality)] # "-define jpeg:extent=SIZEb" sets the target filesize for imagemagick to # SIZE in bytes. if max_filesize > 0: cmd += ['-define', 'jpeg:extent={0}b'.format(max_filesize)] cmd.append(util.syspath(path_out, prefix=False)) try: util.command_output(cmd) except subprocess.CalledProcessError: log.warning(u'artresizer: IM convert failed for {0}', util.displayable_path(path_in)) return path_in return path_out
def assert_permissions(path, permission, log): """Check whether the file's permissions are as expected, otherwise, log a warning message. Return a boolean indicating the match, like `check_permissions`. """ if not check_permissions(util.syspath(path), permission): log.warning( u'could not set permissions on {}', util.displayable_path(path), ) log.debug( u'set permissions to {}, but permissions are now {}', permission, os.stat(util.syspath(path)).st_mode & 0o777, )
def clear(self, lib, query): id3v23 = config['id3v23'].get(bool) items = lib.items(query) self._log.info(u'Clearing album art from {0} items', len(items)) for item in items: self._log.debug(u'Clearing art for {0}', item) try: mf = mediafile.MediaFile(syspath(item.path), id3v23) except mediafile.UnreadableFileError as exc: self._log.warning(u'Could not read file {0}: {1}', displayable_path(item.path), exc) else: del mf.art mf.save()
def _load_plugins(config): """Load the plugins specified in the configuration. """ paths = config['pluginpath'].get(confit.StrSeq(split=False)) paths = map(util.normpath, paths) log.debug('plugin paths: {0}', util.displayable_path(paths)) import beetsplug beetsplug.__path__ = paths + beetsplug.__path__ # For backwards compatibility. sys.path += paths plugins.load_plugins(config['plugins'].as_str_seq()) plugins.send("pluginload") return plugins
def read_items(paths): """Return a list of items created from each path. If an item could not be read it skips the item and logs an error. """ # TODO remove this method. Should be handled in ImportTask creation. items = [] for path in paths: try: items.append(library.Item.from_path(path)) except library.ReadError as exc: if isinstance(exc.reason, mediafile.FileTypeError): # Silently ignore non-music files. pass elif isinstance(exc.reason, mediafile.UnreadableFileError): log.warn(u'unreadable file: {0}'.format( displayable_path(path)) ) else: log.error(u'error reading {0}: {1}'.format( displayable_path(path), exc, )) return items
def im_resize(maxwidth, path_in, path_out=None): """Resize using ImageMagick's ``convert`` tool. Return the output path of resized image. """ path_out = path_out or temp_file_for(path_in) log.debug(u'artresizer: ImageMagick resizing {0} to {1}', util.displayable_path(path_in), util.displayable_path(path_out)) # "-resize WIDTHx>" shrinks images with the width larger # than the given width while maintaining the aspect ratio # with regards to the height. try: util.command_output([ 'convert', util.syspath(path_in, prefix=False), '-resize', '{0}x>'.format(maxwidth), util.syspath(path_out, prefix=False), ]) except subprocess.CalledProcessError: log.warning(u'artresizer: IM convert failed for {0}', util.displayable_path(path_in)) return path_in return path_out
def choose_match(self, task): """Given an initial autotagging of items, go through an interactive dance with the user to ask for a choice of metadata. Returns an AlbumMatch object, ASIS, or SKIP. """ # Show what we're tagging. print_() print_(displayable_path(task.paths, u'\n')) # Take immediate action if appropriate. action = _summary_judment(task.rec) if action == importer.action.APPLY: match = task.candidates[0] show_change(task.cur_artist, task.cur_album, match) return match elif action is not None: return action # Loop until we have a choice. candidates, rec = task.candidates, task.rec while True: # Ask for a choice from the user. choice = choose_candidate(candidates, False, rec, task.cur_artist, task.cur_album, itemcount=len(task.items)) # Choose which tags to use. if choice in (importer.action.SKIP, importer.action.ASIS, importer.action.TRACKS): # Pass selection to main control flow. return choice elif choice is importer.action.MANUAL: # Try again with manual search terms. search_artist, search_album = manual_search(False) _, _, candidates, rec = autotag.tag_album( task.items, search_artist, search_album ) elif choice is importer.action.MANUAL_ID: # Try a manually-entered ID. search_id = manual_id(False) if search_id: _, _, candidates, rec = autotag.tag_album( task.items, search_id=search_id ) else: # We have a candidate! Finish tagging. Here, choice is an # AlbumMatch object. assert isinstance(choice, autotag.AlbumMatch) return choice
def check_item(self, item): # First, check whether the path exists. If not, the user # should probably run `beet update` to cleanup your library. dpath = displayable_path(item.path) self._log.debug(u"checking path: {}", dpath) if not os.path.exists(item.path): ui.print_(u"{}: file does not exist".format( ui.colorize('text_error', dpath))) # Run the checker against the file if one is found ext = os.path.splitext(item.path)[1][1:].decode('utf8', 'ignore') checker = self.get_checker(ext) if not checker: self._log.error(u"no checker specified in the config for {}", ext) return [] path = item.path if not isinstance(path, six.text_type): path = item.path.decode(sys.getfilesystemencoding()) try: status, errors, output = checker(path) except CheckerCommandException as e: if e.errno == errno.ENOENT: self._log.error( u"command not found: {} when validating file: {}", e.checker, e.path) else: self._log.error(u"error invoking {}: {}", e.checker, e.msg) return [] error_lines = [] if status > 0: error_lines.append(u"{}: checker exited with status {}".format( ui.colorize('text_error', dpath), status)) for line in output: error_lines.append(u" {}".format(line)) elif errors > 0: error_lines.append( u"{}: checker found {} errors or warnings".format( ui.colorize('text_warning', dpath), errors)) for line in output: error_lines.append(u" {}".format(line)) elif self.verbose: error_lines.append(u"{}: ok".format( ui.colorize('text_success', dpath))) return error_lines
def show_progress(session): """This stage replaces the initial_lookup and user_query stages when the importer is run without autotagging. It displays the album name and artist as the files are added. """ task = None while True: task = yield task if task.sentinel: continue log.info(displayable_path(task.paths)) # Behave as if ASIS were selected. task.set_null_candidates() task.set_choice(action.ASIS)
def initial_lookup(session): """A coroutine for performing the initial MusicBrainz lookup for an album. It accepts lists of Items and yields (items, cur_artist, cur_album, candidates, rec) tuples. If no match is found, all of the yielded parameters (except items) are None. """ task = None while True: task = yield task if task.sentinel: continue plugins.send('import_task_start', session=session, task=task) log.debug('Looking up: %s' % displayable_path(task.paths)) task.set_candidates(*autotag.tag_album(task.items))
def remove_replaced(self, lib): """Removes all the items from the library that have the same path as an item from this task. Records the replaced items in the `replaced_items` dictionary """ self.replaced_items = defaultdict(list) for item in self.imported_items(): dup_items = lib.items(dbcore.query.BytesQuery('path', item.path)) self.replaced_items[item] = dup_items for dup_item in dup_items: log.debug('replacing item %i: %s' % (dup_item.id, displayable_path(item.path))) dup_item.remove() log.debug('%i of %i items replaced' % (len(self.replaced_items), len(self.imported_items())))
def ask_resume(self, toppath): """If import of `toppath` was aborted in an earlier session, ask user if she wants to resume the import. Determines the return value of `is_resuming(toppath)`. """ if self.want_resume and has_progress(toppath): # Either accept immediately or prompt for input to decide. if self.want_resume is True or \ self.should_resume(toppath): log.warn(u'Resuming interrupted import of {0}'.format( util.displayable_path(toppath))) self._is_resuming[toppath] = True else: # Clear progress; we're starting from the top. progress_reset(toppath)
def scrub_func(lib, opts, args): # This is a little bit hacky, but we set a global flag to # avoid autoscrubbing when we're also explicitly scrubbing. global scrubbing scrubbing = True # Walk through matching files and remove tags. for item in lib.items(ui.decargs(args)): log.info(u'scrubbing: %s' % util.displayable_path(item.path)) _scrub(item.path) if opts.write: log.debug(u'writing new tags after scrub') item.write() scrubbing = False
def set_titles_no_junk(task, session): items = task.items if task.is_album else [task.item] for item in items: if item.title: continue item_file_path = Path(displayable_path(item.path)) youtube_title = frompath.get_title(item_file_path) album_name = frompath.get_album_name(item_file_path) artist_name = frompath.get_artist_name(item_file_path) artist_album_junk = [ '(?i)(?P<junk>\\({0}\\))'.format(re.escape(album_name)), '(?i)(?P<junk>\\(?{0}\\)?)'.format(re.escape(artist_name)) ] item.title = remove_junk(youtube_title, artist_album_junk, YOUTUBE_TITLE_JUNK)
def get_extractor_data(self, item): with tempfile.NamedTemporaryFile() as tmpfile: tmpfile.close() args = [self.extractor, util.syspath(item.path), tmpfile.name] try: util.command_output(args) except subprocess.CalledProcessError as exc: self._log.warning(u'{} "{}" "{}" exited with status {}', self.extractor, util.displayable_path(item.path), tmpfile.name, exc.returncode) return with open(tmpfile.name, 'rb') as tmp_file: return json.load(tmp_file)
def _group_by(objs, keys, log): """Return a dictionary with keys arbitrary concatenations of attributes and values lists of objects (Albums or Items) with those keys. """ import collections counts = collections.defaultdict(list) for obj in objs: values = [getattr(obj, k, None) for k in keys] values = [v for v in values if v not in (None, '')] if values: key = '\001'.join(values) counts[key].append(obj) else: log.debug(u'{0}: all keys {1} on item {2} are null: skipping', PLUGIN, keys, displayable_path(obj.path)) return counts
def find_key(self, items, write=False): overwrite = self.config['overwrite'].get(bool) command = [self.config['bin'].as_str()] # The KeyFinder GUI program needs the -f flag before the path. # keyfinder-cli is similar, but just wants the path with no flag. if 'keyfinder-cli' not in os.path.basename(command[0]).lower(): command.append('-f') for item in items: if item['initial_key'] and not overwrite: continue try: output = util.command_output(command + [util.syspath(item.path)]).stdout except (subprocess.CalledProcessError, OSError) as exc: self._log.error('execution failed: {0}', exc) continue except UnicodeEncodeError: # Workaround for Python 2 Windows bug. # https://bugs.python.org/issue1759845 self._log.error('execution failed for Unicode path: {0!r}', item.path) continue try: key_raw = output.rsplit(None, 1)[-1] except IndexError: # Sometimes keyfinder-cli returns 0 but with no key, usually # when the file is silent or corrupt, so we log and skip. self._log.error('no key returned for path: {0}', item.path) continue try: key = util.text_string(key_raw) except UnicodeDecodeError: self._log.error('output is invalid UTF-8') continue item['initial_key'] = key self._log.info('added computed initial key {0} for {1}', key, util.displayable_path(item.path)) if write: item.try_write() item.store()
def test_import_global(self): config['filefilter']['path'] = '.*track_1.*\.mp3' self.__run([ 'Album: %s' % displayable_path(self.artist_path), ' %s' % displayable_path(self.artist_paths[0]), 'Album: %s' % displayable_path(self.misc_path), ' %s' % displayable_path(self.misc_paths[0]), ]) self.__run([ 'Singleton: %s' % displayable_path(self.artist_paths[0]), 'Singleton: %s' % displayable_path(self.misc_paths[0]) ], singletons=True)
def run(self, lib, opts, args): file_path = opts.output file_mode = 'a' if opts.append else 'w' file_format = opts.format or self.config['default_format'].get(str) file_format_is_line_based = (file_format == 'jsonlines') format_options = self.config[file_format]['formatting'].get(dict) export_format = ExportFormat.factory(file_type=file_format, **{ 'file_path': file_path, 'file_mode': file_mode }) if opts.library or opts.album: data_collector = library_data else: data_collector = tag_data included_keys = [] for keys in opts.included_keys: included_keys.extend(keys.split(',')) items = [] for data_emitter in data_collector( lib, ui.decargs(args), album=opts.album, ): try: data, item = data_emitter(included_keys or '*') except (mediafile.UnreadableFileError, OSError) as ex: self._log.error('cannot read file: {0}', ex) continue for key, value in data.items(): if isinstance(value, bytes): data[key] = util.displayable_path(value) if file_format_is_line_based: export_format.export(data, **format_options) else: items += [data] if not file_format_is_line_based: export_format.export(items, **format_options)
def update_item(self, item, attribute, value=None, increment=None): """Update the beets item. Set attribute to value or increment the value of attribute. If the increment argument is used the value is cast to the corresponding type. """ if item is None: return if increment is not None: item.load() value = type(increment)(item.get(attribute, 0)) + increment if value is not None: item[attribute] = value item.store() self._log.debug(u'updated: {0} = {1} [{2}]', attribute, item[attribute], displayable_path(item.path))