Exemplo n.º 1
0
def modify_items(lib, mods, query, write, move, album, color, confirm):
    """Modifies matching items according to key=value assignments."""
    # Parse key=value specifications into a dictionary.
    allowed_keys = library.ALBUM_KEYS if album else library.ITEM_KEYS_WRITABLE
    fsets = {}
    for mod in mods:
        key, value = mod.split('=', 1)
        if key not in allowed_keys:
            raise ui.UserError('"%s" is not a valid field' % key)
        fsets[key] = value

    # Get the items to modify.
    items, albums = _do_query(lib, query, album, False)
    objs = albums if album else items

    # Preview change.
    print_('Modifying %i %ss.' % (len(objs), 'album' if album else 'item'))
    for obj in objs:
        # Identify the changed object.
        if album:
            print_(u'* %s - %s' % (obj.albumartist, obj.album))
        else:
            print_(u'* %s - %s' % (obj.artist, obj.title))

        # Show each change.
        for field, value in fsets.iteritems():
            curval = getattr(obj, field)
            _showdiff(field, curval, value, color)

    # Confirm.
    if confirm:
        extra = ' and write tags' if write else ''
        if not ui.input_yn('Really modify%s (Y/n)?' % extra):
            return

    # Apply changes to database.
    for obj in objs:
        for field, value in fsets.iteritems():
            setattr(obj, field, value)

        if move:
            cur_path = obj.item_dir() if album else obj.path
            if lib.directory in ancestry(cur_path): # In library?
                log.debug('moving object %s' % cur_path)
                if album:
                    obj.move()
                else:
                    lib.move(obj)

        # When modifying items, we have to store them to the database.
        if not album:
            lib.store(obj)
    lib.save()

    # Apply tags if requested.
    if write:
        if album:
            items = itertools.chain(*(a.items() for a in albums))
        for item in items:
            item.write()
Exemplo n.º 2
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)
Exemplo n.º 3
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)
Exemplo n.º 4
0
def apply_choices(config):
    """A coroutine for applying changes to albums during the autotag
    process.
    """
    lib = _reopen_lib(config.lib)
    task = None
    while True:    
        task = yield task
        if task.should_skip():
            continue

        items = [i for i in task.items if i] if task.is_album else [task.item]
        # 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.items, task.info)
            else:
                autotag.apply_item_metadata(task.item, task.info)

        # 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.
        replaced_items = defaultdict(list)
        for item in items:
            dup_items = lib.items(library.MatchQuery('path', item.path))
            for dup_item in dup_items:
                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(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(lib, task):
                    duplicate_items += album.items()
            else:
                duplicate_items = _item_duplicate_check(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 lib.directory in util.ancestry(duplicate_path):
                    log.debug(u'deleting replaced duplicate %s' %
                              util.displayable_path(duplicate_path))
                    util.soft_remove(duplicate_path)
                    util.prune_dirs(os.path.dirname(duplicate_path),
                                    lib.directory)

        # Move/copy files.
        task.old_paths = [item.path for item in items]
        for item in items:
            if config.copy:
                # If we're replacing an item, then move rather than
                # copying.
                old_path = item.path
                do_copy = not bool(replaced_items[item])
                lib.move(item, do_copy, task.is_album)
                if not do_copy:
                    # If we moved the item, remove the now-nonexistent
                    # file from old_paths.
                    task.old_paths.remove(old_path)
            if config.write and task.should_write_tags():
                item.write()

        # Add items to library. We consolidate this at the end to avoid
        # locking while we do the copying and tag updates.
        try:
            # Remove old items.
            for replaced in replaced_items.itervalues():
                for item in replaced:
                    lib.remove(item)
            for item in duplicate_items:
                lib.remove(item)

            # Add new ones.
            if task.is_album:
                # Add an album.
                album = lib.add_album(items)
                task.album_id = album.id
            else:
                # Add tracks.
                for item in items:
                    lib.add(item)
        finally:
            lib.save()
Exemplo n.º 5
0
def albums_in_dir(path, ignore=()):
    """Recursively searches the given directory and returns an iterable
    of (path, items) where path is a containing directory and items is
    a list of Items that is probably an album. Specifically, any folder
    containing any media files is an album. Directories and file names
    that match the glob patterns in ``ignore`` are skipped.
    """
    collapse_root = None
    collapse_items = None

    for root, dirs, files in sorted_walk(path, ignore):
        # Get a list of items in the directory.
        items = []
        for filename in files:
            try:
                i = library.Item.from_path(os.path.join(root, filename))
            except mediafile.FileTypeError:
                pass
            except mediafile.UnreadableFileError:
                log.warn('unreadable file: ' + filename)
            else:
                items.append(i)

        # If we're collapsing, test to see whether we should continue to
        # collapse. If so, just add to the collapsed item set;
        # otherwise, end the collapse and continue as normal.
        if collapse_root is not None:
            if collapse_root in ancestry(root):
                # Still collapsing.
                collapse_items += items
                continue
            else:
                # Collapse finished. Yield the collapsed directory and
                # proceed to process the current one.
                if collapse_items:
                    yield collapse_root, collapse_items
                collapse_root = collapse_items = None

        # Does the current directory look like a multi-disc album? If
        # so, begin collapsing here.
        if dirs and not items:  # Must be only directories.
            multidisc = False
            for marker in MULTIDISC_MARKERS:
                pat = MULTIDISC_PAT_FMT % marker
                if all(re.search(pat, dirname, re.I) for dirname in dirs):
                    multidisc = True
                    break

            # This becomes True only when all directories match a
            # pattern for a single marker.
            if multidisc:
                # Start collapsing; continue to the next iteration.
                collapse_root = root
                collapse_items = []
                continue

        # If it's nonempty, yield it.
        if items:
            yield root, items

    # Clear out any unfinished collapse.
    if collapse_root is not None and collapse_items:
        yield collapse_root, collapse_items
Exemplo n.º 6
0
def update_items(lib, query, album, move, color, pretend):
    """For all the items matched by the query, update the library to
    reflect the item's embedded tags.
    """
    items, _ = _do_query(lib, query, album)

    # Walk through the items and pick up their changes.
    affected_albums = set()
    for item in items:
        # Item deleted?
        if not os.path.exists(syspath(item.path)):
            print_(u'X %s - %s' % (item.artist, item.title))
            if not pretend:
                lib.remove(item, True)
            affected_albums.add(item.album_id)
            continue

        # Did the item change since last checked?
        if item.current_mtime() <= item.mtime:
            log.debug(u'skipping %s because mtime is up to date (%i)' %
                      (displayable_path(item.path), item.mtime))
            continue

        # Read new data.
        old_data = dict(item.record)
        item.read()

        # Special-case album artist when it matches track artist. (Hacky
        # but necessary for preserving album-level metadata for non-
        # autotagged imports.)
        if not item.albumartist and \
                old_data['albumartist'] == old_data['artist'] == item.artist:
            item.albumartist = old_data['albumartist']
            item.dirty['albumartist'] = False

        # Get and save metadata changes.
        changes = {}
        for key in library.ITEM_KEYS_META:
            if item.dirty[key]:
                changes[key] = old_data[key], getattr(item, key)
        if changes:
            # Something changed.
            print_(u'* %s - %s' % (item.artist, item.title))
            for key, (oldval, newval) in changes.iteritems():
                _showdiff(key, oldval, newval, color)

            # If we're just pretending, then don't move or save.
            if pretend:
                continue

            # Move the item if it's in the library.
            if move and lib.directory in ancestry(item.path):
                lib.move(item)

            lib.store(item)
            affected_albums.add(item.album_id)
        elif not pretend:
            # The file's mtime was different, but there were no changes
            # to the metadata. Store the new mtime, which is set in the
            # call to read(), so we don't check this again in the
            # future.
            lib.store(item)

    # Skip album changes while pretending.
    if pretend:
        return

    # Modify affected albums to reflect changes in their items.
    for album_id in affected_albums:
        if album_id is None: # Singletons.
            continue
        album = lib.get_album(album_id)
        if not album: # Empty albums have already been removed.
            log.debug('emptied album %i' % album_id)
            continue
        al_items = list(album.items())

        # Update album structure to reflect an item in it.
        for key in library.ALBUM_KEYS_ITEM:
            setattr(album, key, getattr(al_items[0], key))

        # Move album art (and any inconsistent items).
        if move and lib.directory in ancestry(al_items[0].path):
            log.debug('moving album %i' % album_id)
            album.move()

    lib.save()
Exemplo n.º 7
0
def apply_choices(config):
    """A coroutine for applying changes to albums during the autotag
    process.
    """
    lib = _reopen_lib(config.lib)
    task = None
    while True:
        task = yield task
        if task.should_skip():
            continue

        items = [i for i in task.items if i] if task.is_album else [task.item]
        # 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.items, task.info)
            else:
                autotag.apply_item_metadata(task.item, task.info)

        # 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.
        replaced_items = defaultdict(list)
        for item in items:
            dup_items = lib.items(library.MatchQuery('path', item.path))
            for dup_item in dup_items:
                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(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(lib, task):
                    duplicate_items += album.items()
            else:
                duplicate_items = _item_duplicate_check(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 lib.directory in util.ancestry(duplicate_path):
                    log.debug(u'deleting replaced duplicate %s' %
                              util.displayable_path(duplicate_path))
                    util.soft_remove(duplicate_path)
                    util.prune_dirs(os.path.dirname(duplicate_path),
                                    lib.directory)

        # Move/copy files.
        task.old_paths = [item.path for item in items]
        for item in items:
            if config.copy:
                # If we're replacing an item, then move rather than
                # copying.
                old_path = item.path
                do_copy = not bool(replaced_items[item])
                lib.move(item, do_copy, task.is_album)
                if not do_copy:
                    # If we moved the item, remove the now-nonexistent
                    # file from old_paths.
                    task.old_paths.remove(old_path)
            if config.write and task.should_write_tags():
                item.write()

        # Add items to library. We consolidate this at the end to avoid
        # locking while we do the copying and tag updates.
        try:
            # Remove old items.
            for replaced in replaced_items.itervalues():
                for item in replaced:
                    lib.remove(item)
            for item in duplicate_items:
                lib.remove(item)

            # Add new ones.
            if task.is_album:
                # Add an album.
                album = lib.add_album(items)
                task.album_id = album.id
            else:
                # Add tracks.
                for item in items:
                    lib.add(item)
        finally:
            lib.save()
Exemplo n.º 8
0
def albums_in_dir(path, ignore=()):
    """Recursively searches the given directory and returns an iterable
    of (path, items) where path is a containing directory and items is
    a list of Items that is probably an album. Specifically, any folder
    containing any media files is an album. Directories and file names
    that match the glob patterns in ``ignore`` are skipped.
    """
    collapse_root = None
    collapse_items = None

    for root, dirs, files in sorted_walk(path, ignore):
        # Get a list of items in the directory.
        items = []
        for filename in files:
            try:
                i = library.Item.from_path(os.path.join(root, filename))
            except mediafile.FileTypeError:
                pass
            except mediafile.UnreadableFileError:
                log.warn('unreadable file: ' + filename)
            else:
                items.append(i)

        # If we're collapsing, test to see whether we should continue to
        # collapse. If so, just add to the collapsed item set;
        # otherwise, end the collapse and continue as normal.
        if collapse_root is not None:
            if collapse_root in ancestry(root):
                # Still collapsing.
                collapse_items += items
                continue
            else:
                # Collapse finished. Yield the collapsed directory and
                # proceed to process the current one.
                if collapse_items:
                    yield collapse_root, collapse_items
                collapse_root = collapse_items = None

        # Does the current directory look like a multi-disc album? If
        # so, begin collapsing here.
        if dirs and not items: # Must be only directories.
            multidisc = False
            for marker in MULTIDISC_MARKERS:
                pat = MULTIDISC_PAT_FMT % marker
                if all(re.search(pat, dirname, re.I) for dirname in dirs):
                    multidisc = True
                    break

            # This becomes True only when all directories match a
            # pattern for a single marker.
            if multidisc:
                # Start collapsing; continue to the next iteration.
                collapse_root = root
                collapse_items = []
                continue

        # If it's nonempty, yield it.
        if items:
            yield root, items

    # Clear out any unfinished collapse.
    if collapse_root is not None and collapse_items:
        yield collapse_root, collapse_items