示例#1
0
 def save(self, event=True):
     """Writes the library to disk (completing an sqlite
     transaction).
     """
     self.conn.commit()
     if event:
         plugins.send("save", lib=self)
示例#2
0
 def save(self, event=True):
     """Writes the library to disk (completing an sqlite
     transaction).
     """
     self.conn.commit()
     if event:
         plugins.send('save', lib=self)
示例#3
0
def finalize(config):
    """A coroutine that finishes up importer tasks. In particular, the
    coroutine sends plugin events, deletes old files, and saves
    progress. This is a "terminal" coroutine (it yields None).
    """
    lib = _reopen_lib(config.lib)
    while True:
        task = yield
        if task.should_skip():
            if config.resume is not False:
                task.save_progress()
            continue

        items = task.items if task.is_album else [task.item]

        # Announce that we've added an album.
        if task.is_album:
            album = lib.get_album(task.album_id)
            plugins.send('album_imported', lib=lib, album=album)
        else:
            for item in items:
                plugins.send('item_imported', lib=lib, item=item)

        # Finally, delete old files.
        if config.copy and config.delete:
            new_paths = [os.path.realpath(item.path) for item in items]
            for old_path in task.old_paths:
                # Only delete files that were actually moved.
                if old_path not in new_paths:
                    os.remove(syspath(old_path))

        # Update progress.
        if config.resume is not False:
            task.save_progress()
示例#4
0
    def write(self):
        """Writes the item's metadata to the associated file.
        """
        f = MediaFile(syspath(self.path))
        plugins.send('write', item=self, mf=f)
        for key in ITEM_KEYS_WRITABLE:
            setattr(f, key, getattr(self, key))
        f.save()

        # The file has a new mtime.
        self.mtime = self.current_mtime()
示例#5
0
    def write(self):
        """Writes the item's metadata to the associated file.
        """
        f = MediaFile(syspath(self.path))
        plugins.send("write", item=self, mf=f)
        for key in ITEM_KEYS_WRITABLE:
            setattr(f, key, getattr(self, key))
        f.save()

        # The file has a new mtime.
        self.mtime = self.current_mtime()
示例#6
0
def item_lookup(config):
    """A coroutine used to perform the initial MusicBrainz lookup for
    an item task.
    """
    task = None
    while True:
        task = yield task
        if task.sentinel:
            continue

        plugins.send('import_task_start', task=task, config=config)

        task.set_item_candidates(*autotag.tag_item(task.item, config.timid))
示例#7
0
def manipulate_files(config):
    """A coroutine (pipeline stage) that performs necessary file
    manipulations *after* items have been added to the library.
    """
    task = None
    while True:
        task = yield task
        if task.should_skip():
            continue

        # Move/copy files.
        items = task.imported_items()
        task.old_paths = [item.path for item in items]  # For deletion.
        for item in items:
            if config.move:
                # Just move the file.
                old_path = item.path
                config.lib.move(item, False)
                task.prune(old_path)
            elif config.copy:
                # If it's a reimport, move in-library files and copy
                # out-of-library files. Otherwise, copy and keep track
                # of the old path.
                old_path = item.path
                if task.replaced_items[item]:
                    # This is a reimport. Move in-library files and copy
                    # out-of-library files.
                    if config.lib.directory in util.ancestry(old_path):
                        config.lib.move(item, False)
                        # We moved the item, so remove the
                        # now-nonexistent file from old_paths.
                        task.old_paths.remove(old_path)
                    else:
                        config.lib.move(item, True)
                else:
                    # A normal import. Just copy files and keep track of
                    # old paths.
                    config.lib.move(item, True)

            if config.write and task.should_write_tags():
                item.write()

        # Save new paths.
        with config.lib.transaction():
            for item in items:
                config.lib.store(item)

        # Plugin event.
        plugins.send('import_task_files', config=config, task=task)
示例#8
0
def user_query(config):
    """A coroutine for interfacing with the user about the tagging
    process. lib is the Library to import into and logfile may be
    a file-like object for logging the import process. The coroutine
    accepts and yields ImportTask objects.
    """
    recent = set()
    task = None
    while True:
        task = yield task
        if task.sentinel:
            continue

        # Ask the user for a choice.
        choice = config.choose_match_func(task, config)
        task.set_choice(choice)
        log_choice(config, task)
        plugins.send('import_task_choice', task=task, config=config)

        # As-tracks: transition to singleton workflow.
        if choice is action.TRACKS:
            # Set up a little pipeline for dealing with the singletons.
            item_tasks = []
            def emitter():
                for item in task.items:
                    yield ImportTask.item_task(item)
                yield ImportTask.progress_sentinel(task.toppath, task.path)
            def collector():
                while True:
                    item_task = yield
                    item_tasks.append(item_task)
            ipl = pipeline.Pipeline((emitter(), item_lookup(config),
                                     item_query(config), collector()))
            ipl.run_sequential()
            task = pipeline.multiple(item_tasks)
            continue

        # Check for duplicates if we have a match (or ASIS).
        if task.choice_flag in (action.ASIS, action.APPLY):
            ident = task.chosen_ident()
            # The "recent" set keeps track of identifiers for recently
            # imported albums -- those that haven't reached the database
            # yet.
            if ident in recent or _duplicate_check(config.lib, task):
                config.resolve_duplicate_func(task, config)
                log_choice(config, task, True)
            recent.add(ident)
示例#9
0
def finalize(config):
    """A coroutine that finishes up importer tasks. In particular, the
    coroutine sends plugin events, deletes old files, and saves
    progress. This is a "terminal" coroutine (it yields None).
    """
    lib = _reopen_lib(config.lib)
    while True:
        task = yield
        if task.should_skip():
            if config.resume is not False:
                task.save_progress()
            if config.incremental:
                task.save_history()
            continue

        items = [i for i in task.items if i] if task.is_album else [task.item]

        # Announce that we've added an album.
        if task.is_album:
            album = lib.get_album(task.album_id)
            plugins.send('album_imported', lib=lib, album=album, config=config)
        else:
            for item in items:
                plugins.send('item_imported',
                             lib=lib,
                             item=item,
                             config=config)

        # Finally, delete old files.
        if config.copy and config.delete:
            new_paths = [os.path.realpath(item.path) for item in items]
            for old_path in task.old_paths:
                # Only delete files that were actually copied.
                if old_path not in new_paths:
                    os.remove(syspath(old_path))
                    # Clean up directory if it is emptied.
                    if task.toppath:
                        util.prune_dirs(os.path.dirname(old_path),
                                        task.toppath)

        # Update progress.
        if config.resume is not False:
            task.save_progress()
        if config.incremental:
            task.save_history()
示例#10
0
def initial_lookup(config):
    """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', task=task, config=config)

        log.debug('Looking up: %s' % task.path)
        try:
            task.set_candidates(*autotag.tag_album(task.items, config.timid))
        except autotag.AutotagError:
            task.set_null_candidates()
示例#11
0
def item_query(config):
    """A coroutine that queries the user for input on single-item
    lookups.
    """
    task = None
    recent = set()
    while True:
        task = yield task
        if task.sentinel:
            continue

        choice = config.choose_item_func(task, config)
        task.set_choice(choice)
        log_choice(config, task)
        plugins.send('import_task_choice', task=task, config=config)

        # Duplicate check.
        if task.choice_flag in (action.ASIS, action.APPLY):
            ident = task.chosen_ident()
            if ident in recent or _item_duplicate_check(config.lib, task):
                config.resolve_duplicate_func(task, config)
                log_choice(config, task, True)
            recent.add(ident)
示例#12
0
def main(args=None, configfh=None):
    """Run the main command-line interface for beets."""
    # Get the default subcommands.
    from beets.ui.commands import default_commands

    # Read defaults from config file.
    config = ConfigParser.SafeConfigParser()
    if configfh:
        configpath = None
    elif CONFIG_PATH_VAR in os.environ:
        configpath = os.path.expanduser(os.environ[CONFIG_PATH_VAR])
    else:
        configpath = DEFAULT_CONFIG_FILE
    if configpath:
        configpath = util.syspath(configpath)
        if os.path.exists(util.syspath(configpath)):
            configfh = open(configpath)
        else:
            configfh = None
    if configfh:
        config.readfp(configfh)

    # Add plugin paths.
    plugpaths = config_val(config, 'beets', 'pluginpath', '')
    for plugpath in plugpaths.split(':'):
        sys.path.append(os.path.expanduser(plugpath))
    # Load requested plugins.
    plugnames = config_val(config, 'beets', 'plugins', '')
    plugins.load_plugins(plugnames.split())
    plugins.load_listeners()
    plugins.send("pluginload")
    plugins.configure(config)

    # Construct the root parser.
    commands = list(default_commands)
    commands += plugins.commands()
    parser = SubcommandsOptionParser(subcommands=commands)
    parser.add_option('-l', '--library', dest='libpath',
                      help='library database file to use')
    parser.add_option('-d', '--directory', dest='directory',
                      help="destination music directory")
    parser.add_option('-p', '--pathformat', dest='path_format',
                      help="destination path format string")
    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)
    
    # Open library file.
    libpath = options.libpath or \
        config_val(config, 'beets', 'library', DEFAULT_LIBRARY)
    directory = options.directory or \
        config_val(config, 'beets', 'directory', DEFAULT_DIRECTORY)
    legacy_path_format = config_val(config, 'beets', 'path_format', None)
    if options.path_format:
        # If given, -p overrides all path format settings
        path_formats = {'default': options.path_format}
    else:
        if legacy_path_format:
            # Old path formats override the default values.
            path_formats = {'default': legacy_path_format}
        else:
            # If no legacy path format, use the defaults instead.
            path_formats = DEFAULT_PATH_FORMATS
        if config.has_section('paths'):
            path_formats.update(config.items('paths'))
    art_filename = \
        config_val(config, 'beets', 'art_filename', DEFAULT_ART_FILENAME)
    db_path = os.path.expanduser(libpath)
    try:
        lib = library.Library(db_path,
                              directory,
                              path_formats,
                              art_filename)
    except sqlite3.OperationalError:
        raise UserError("database file %s could not be opened" % db_path)
    
    # Configure the logger.
    log = logging.getLogger('beets')
    if options.verbose:
        log.setLevel(logging.DEBUG)
    else:
        log.setLevel(logging.INFO)
    
    # Invoke the subcommand.
    try:
        subcommand.func(lib, config, suboptions, subargs)
    except UserError, exc:
        message = exc.args[0] if exc.args else None
        subcommand.parser.error(message)
示例#13
0
def main(args=None, configfh=None):
    """Run the main command-line interface for beets."""
    # Get the default subcommands.
    from lib.beets.ui.commands import default_commands

    # Get default file paths.
    default_config, default_libpath, default_dir = default_paths()

    # Read defaults from config file.
    config = ConfigParser.SafeConfigParser()
    if configfh:
        configpath = None
    elif CONFIG_PATH_VAR in os.environ:
        configpath = os.path.expanduser(os.environ[CONFIG_PATH_VAR])
    else:
        configpath = default_config
    if configpath:
        configpath = util.syspath(configpath)
        if os.path.exists(util.syspath(configpath)):
            configfh = codecs.open(configpath, 'r', encoding='utf-8')
        else:
            configfh = None
    if configfh:
        config.readfp(configfh)

    # Add plugin paths.
    plugpaths = config_val(config, 'beets', 'pluginpath', '')
    for plugpath in plugpaths.split(':'):
        sys.path.append(os.path.expanduser(plugpath))
    # Load requested plugins.
    plugnames = config_val(config, 'beets', 'plugins', '')
    plugins.load_plugins(plugnames.split())
    plugins.send("pluginload")
    plugins.configure(config)

    # Construct the root parser.
    commands = list(default_commands)
    commands += plugins.commands()
    parser = SubcommandsOptionParser(subcommands=commands)
    parser.add_option('-l',
                      '--library',
                      dest='libpath',
                      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)

    # Open library file.
    libpath = options.libpath or \
        config_val(config, 'beets', 'library', default_libpath)
    directory = options.directory or \
        config_val(config, 'beets', 'directory', default_dir)
    path_formats = _get_path_formats(config)
    art_filename = \
        config_val(config, 'beets', 'art_filename', DEFAULT_ART_FILENAME)
    lib_timeout = config_val(config, 'beets', 'timeout', DEFAULT_TIMEOUT)
    replacements = _get_replacements(config)
    try:
        lib_timeout = float(lib_timeout)
    except ValueError:
        lib_timeout = DEFAULT_TIMEOUT
    db_path = os.path.expanduser(libpath)
    try:
        lib = library.Library(db_path, directory, path_formats, art_filename,
                              lib_timeout, replacements)
    except sqlite3.OperationalError:
        raise UserError("database file %s could not be opened" % db_path)
    plugins.send("library_opened", lib=lib)

    # Configure the logger.
    log = logging.getLogger('beets')
    if options.verbose:
        log.setLevel(logging.DEBUG)
    else:
        log.setLevel(logging.INFO)
    log.debug(u'config file: %s' % util.displayable_path(configpath))
    log.debug(u'library database: %s' % util.displayable_path(lib.path))
    log.debug(u'library directory: %s' % util.displayable_path(lib.directory))

    # Invoke the subcommand.
    try:
        subcommand.func(lib, config, suboptions, subargs)
    except UserError as exc:
        message = exc.args[0] if exc.args else None
        subcommand.parser.error(message)
    except util.HumanReadableException as exc:
        exc.log(log)
        sys.exit(1)
    except IOError as exc:
        if exc.errno == errno.EPIPE:
            # "Broken pipe". End silently.
            pass
        else:
            raise
示例#14
0
def import_files(lib, paths, copy, write, autot, logpath, art, threaded,
                 color, delete, quiet, resume, quiet_fallback, singletons,
                 timid, query, incremental, ignore):
    """Import the files in the given list of paths, tagging each leaf
    directory as an album. If copy, then the files are copied into
    the library folder. If write, then new metadata is written to the
    files themselves. If not autot, then just import the files
    without attempting to tag. If logpath is provided, then untaggable
    albums will be logged there. If art, then attempt to download
    cover art for each album. If threaded, then accelerate autotagging
    imports by running them in multiple threads. If color, then
    ANSI-colorize some terminal output. If delete, then old files are
    deleted when they are copied. If quiet, then the user is
    never prompted for input; instead, the tagger just skips anything
    it is not confident about. resume indicates whether interrupted
    imports can be resumed and is either a boolean or None.
    quiet_fallback should be either ASIS or SKIP and indicates what
    should happen in quiet mode when the recommendation is not strong.
    """
    # Check the user-specified directories.
    for path in paths:
        if not singletons and not os.path.isdir(syspath(path)):
            raise ui.UserError('not a directory: ' + path)
        elif singletons and not os.path.exists(syspath(path)):
            raise ui.UserError('no such file: ' + path)

    # Check parameter consistency.
    if quiet and timid:
        raise ui.UserError("can't be both quiet and timid")

    # Open the log.
    if logpath:
        logpath = normpath(logpath)
        try:
            logfile = open(syspath(logpath), 'a')
        except IOError:
            raise ui.UserError(u"could not open log file for writing: %s" %
                               displayable_path(logpath))
        print >>logfile, 'import started', time.asctime()
    else:
        logfile = None

    # Never ask for input in quiet mode.
    if resume is None and quiet:
        resume = False

    try:
        # Perform the import.
        importer.run_import(
            lib = lib,
            paths = paths,
            resume = resume,
            logfile = logfile,
            color = color,
            quiet = quiet,
            quiet_fallback = quiet_fallback,
            copy = copy,
            write = write,
            art = art,
            delete = delete,
            threaded = threaded,
            autot = autot,
            choose_match_func = choose_match,
            should_resume_func = should_resume,
            singletons = singletons,
            timid = timid,
            choose_item_func = choose_item,
            query = query,
            incremental = incremental,
            ignore = ignore,
            resolve_duplicate_func = resolve_duplicate,
        )
    
    finally:
        # If we were logging, close the file.
        if logfile:
            print >>logfile, ''
            logfile.close()

    # Emit event.
    plugins.send('import', lib=lib, paths=paths)
示例#15
0
def apply_choices(config):
    """A coroutine for applying changes to albums and singletons during
    the autotag process.
    """
    task = None
    while True:
        task = yield task
        if task.should_skip():
            continue

        items = task.imported_items()
        # Clear IDs in case the items are being re-tagged.
        for item in items:
            item.id = None
            item.album_id = None

        # Change metadata.
        if task.should_write_tags():
            if task.is_album:
                autotag.apply_metadata(
                    task.match.info, task.match.mapping,
                    per_disc_numbering=config.per_disc_numbering
                )
            else:
                autotag.apply_item_metadata(task.item, task.match.info)
            plugins.send('import_task_apply', config=config, task=task)

        # Infer album-level fields.
        if task.is_album:
            _infer_album_fields(task)

        # Find existing item entries that these are replacing (for
        # re-imports). Old album structures are automatically cleaned up
        # when the last item is removed.
        task.replaced_items = defaultdict(list)
        for item in items:
            dup_items = config.lib.items(library.MatchQuery('path', item.path))
            for dup_item in dup_items:
                task.replaced_items[item].append(dup_item)
                log.debug('replacing item %i: %s' %
                          (dup_item.id, displayable_path(item.path)))
        log.debug('%i of %i items replaced' % (len(task.replaced_items),
                                               len(items)))

        # Find old items that should be replaced as part of a duplicate
        # resolution.
        duplicate_items = []
        if task.remove_duplicates:
            if task.is_album:
                for album in _duplicate_check(config.lib, task):
                    duplicate_items += album.items()
            else:
                duplicate_items = _item_duplicate_check(config.lib, task)
            log.debug('removing %i old duplicated items' %
                      len(duplicate_items))

            # Delete duplicate files that are located inside the library
            # directory.
            for duplicate_path in [i.path for i in duplicate_items]:
                if config.lib.directory in util.ancestry(duplicate_path):
                    log.debug(u'deleting replaced duplicate %s' %
                              util.displayable_path(duplicate_path))
                    util.remove(duplicate_path)
                    util.prune_dirs(os.path.dirname(duplicate_path),
                                    config.lib.directory)

        # Add items -- before path changes -- to the library. We add the
        # items now (rather than at the end) so that album structures
        # are in place before calls to destination().
        with config.lib.transaction():
            # Remove old items.
            for replaced in task.replaced_items.itervalues():
                for item in replaced:
                    config.lib.remove(item)
            for item in duplicate_items:
                config.lib.remove(item)

            # Add new ones.
            if task.is_album:
                # Add an album.
                album = config.lib.add_album(items)
                task.album_id = album.id
            else:
                # Add tracks.
                for item in items:
                    config.lib.add(item)
示例#16
0
def main(args=None, configfh=None):
    """Run the main command-line interface for beets."""
    # Get the default subcommands.
    from beets.ui.commands import default_commands

    # Read defaults from config file.
    config = ConfigParser.SafeConfigParser()
    if configfh:
        configpath = None
    elif CONFIG_PATH_VAR in os.environ:
        configpath = os.path.expanduser(os.environ[CONFIG_PATH_VAR])
    else:
        configpath = DEFAULT_CONFIG_FILE
    if configpath:
        configpath = util.syspath(configpath)
        if os.path.exists(util.syspath(configpath)):
            configfh = open(configpath)
        else:
            configfh = None
    if configfh:
        config.readfp(configfh)

    # Add plugin paths.
    plugpaths = config_val(config, 'beets', 'pluginpath', '')
    for plugpath in plugpaths.split(':'):
        sys.path.append(os.path.expanduser(plugpath))
    # Load requested plugins.
    plugnames = config_val(config, 'beets', 'plugins', '')
    plugins.load_plugins(plugnames.split())
    plugins.load_listeners()
    plugins.send("pluginload")
    plugins.configure(config)

    # Construct the root parser.
    commands = list(default_commands)
    commands += plugins.commands()
    parser = SubcommandsOptionParser(subcommands=commands)
    parser.add_option('-l',
                      '--library',
                      dest='libpath',
                      help='library database file to use')
    parser.add_option('-d',
                      '--directory',
                      dest='directory',
                      help="destination music directory")
    parser.add_option('-p',
                      '--pathformat',
                      dest='path_format',
                      help="destination path format string")
    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)

    # Open library file.
    libpath = options.libpath or \
        config_val(config, 'beets', 'library', DEFAULT_LIBRARY)
    directory = options.directory or \
        config_val(config, 'beets', 'directory', DEFAULT_DIRECTORY)
    legacy_path_format = config_val(config, 'beets', 'path_format', None)
    if options.path_format:
        # If given, -p overrides all path format settings
        path_formats = {'default': options.path_format}
    else:
        if legacy_path_format:
            # Old path formats override the default values.
            path_formats = {'default': legacy_path_format}
        else:
            # If no legacy path format, use the defaults instead.
            path_formats = DEFAULT_PATH_FORMATS
        if config.has_section('paths'):
            path_formats.update(config.items('paths'))
    art_filename = \
        config_val(config, 'beets', 'art_filename', DEFAULT_ART_FILENAME)
    db_path = os.path.expanduser(libpath)
    try:
        lib = library.Library(db_path, directory, path_formats, art_filename)
    except sqlite3.OperationalError:
        raise UserError("database file %s could not be opened" % db_path)

    # Configure the logger.
    log = logging.getLogger('beets')
    if options.verbose:
        log.setLevel(logging.DEBUG)
    else:
        log.setLevel(logging.INFO)

    # Invoke the subcommand.
    try:
        subcommand.func(lib, config, suboptions, subargs)
    except UserError, exc:
        message = exc.args[0] if exc.args else None
        subcommand.parser.error(message)
示例#17
0
def main(args=None, configfh=None):
    """Run the main command-line interface for beets."""
    # Get the default subcommands.
    from lib.beets.ui.commands import default_commands

    # Get default file paths.
    default_config, default_libpath, default_dir = default_paths()

    # Read defaults from config file.
    config = ConfigParser.SafeConfigParser()
    if configfh:
        configpath = None
    elif CONFIG_PATH_VAR in os.environ:
        configpath = os.path.expanduser(os.environ[CONFIG_PATH_VAR])
    else:
        configpath = default_config
    if configpath:
        configpath = util.syspath(configpath)
        if os.path.exists(util.syspath(configpath)):
            configfh = open(configpath)
        else:
            configfh = None
    if configfh:
        config.readfp(configfh)

    # Add plugin paths.
    plugpaths = config_val(config, 'beets', 'pluginpath', '')
    for plugpath in plugpaths.split(':'):
        sys.path.append(os.path.expanduser(plugpath))
    # Load requested plugins.
    plugnames = config_val(config, 'beets', 'plugins', '')
    plugins.load_plugins(plugnames.split())
    plugins.load_listeners()
    plugins.send("pluginload")
    plugins.configure(config)

    # Construct the root parser.
    commands = list(default_commands)
    commands += plugins.commands()
    parser = SubcommandsOptionParser(subcommands=commands)
    parser.add_option('-l', '--library', dest='libpath',
                      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)
    
    # Open library file.
    libpath = options.libpath or \
        config_val(config, 'beets', 'library', default_libpath)
    directory = options.directory or \
        config_val(config, 'beets', 'directory', default_dir)
    path_formats = _get_path_formats(config)
    art_filename = \
        config_val(config, 'beets', 'art_filename', DEFAULT_ART_FILENAME)
    lib_timeout = config_val(config, 'beets', 'timeout', DEFAULT_TIMEOUT)
    replacements = _get_replacements(config)
    try:
        lib_timeout = float(lib_timeout)
    except ValueError:
        lib_timeout = DEFAULT_TIMEOUT
    db_path = os.path.expanduser(libpath)
    try:
        lib = library.Library(db_path,
                              directory,
                              path_formats,
                              art_filename,
                              lib_timeout,
                              replacements)
    except sqlite3.OperationalError:
        raise UserError("database file %s could not be opened" % db_path)
    
    # Configure the logger.
    log = logging.getLogger('beets')
    if options.verbose:
        log.setLevel(logging.DEBUG)
    else:
        log.setLevel(logging.INFO)
    log.debug(u'config file: %s' % util.displayable_path(configpath))
    log.debug(u'library database: %s' % util.displayable_path(lib.path))
    log.debug(u'library directory: %s' % util.displayable_path(lib.directory))
    
    # Invoke the subcommand.
    try:
        subcommand.func(lib, config, suboptions, subargs)
    except UserError, exc:
        message = exc.args[0] if exc.args else None
        subcommand.parser.error(message)