Ejemplo n.º 1
0
def update_items(lib, query, album, move, color):
    """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))
            lib.remove(item, True)
            affected_albums.add(item.album_id)
            continue

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

        # 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)

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

            lib.store(item)
            affected_albums.add(item.album_id)

    # 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()
Ejemplo n.º 2
0
def _print_and_apply_changes(lib, item, old_data, move, pretend, write):
    """Apply changes to an Item and preview them in the console. Return
    a boolean indicating whether any changes were made.
    """
    changes = {}
    for key in library.ITEM_KEYS_META:
        if key in item._dirty:
            changes[key] = old_data[key], getattr(item, key)
    if not changes:
        return False

    # Something changed.
    ui.print_obj(item, lib)
    for key, (oldval, newval) in changes.iteritems():
        ui.commands._showdiff(key, oldval, newval)

    # If we're just pretending, then don't move or save.
    if not pretend:
        # Move the item if it's in the library.
        if move and lib.directory in util.ancestry(item.path):
            item.move(with_album=False)

        if write:
            try:
                item.write()
            except Exception as exc:
                log.error(u'could not sync {0}: {1}'.format(
                    util.displayable_path(item.path), exc))
                return False
        item.store()

    return True
Ejemplo n.º 3
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()
Ejemplo n.º 4
0
    def manipulate_files(self,
                         move=False,
                         copy=False,
                         write=False,
                         link=False,
                         session=None):
        items = self.imported_items()
        # Save the original paths of all items for deletion and pruning
        # in the next step (finalization).
        self.old_paths = [item.path for item in items]
        for item in items:
            if move or copy or link:
                # In copy and link modes, treat re-imports specially:
                # move in-library files. (Out-of-library files are
                # copied/moved as usual).
                old_path = item.path
                if (copy or link) and self.replaced_items[item] and \
                   session.lib.directory in util.ancestry(old_path):
                    item.move()
                    # We moved the item, so remove the
                    # now-nonexistent file from old_paths.
                    self.old_paths.remove(old_path)
                else:
                    # A normal import. Just copy files and keep track of
                    # old paths.
                    item.move(copy, link)

            if write and self.apply:
                item.try_write()

        with session.lib.transaction():
            for item in self.imported_items():
                item.store()

        plugins.send('import_task_files', session=session, task=self)
Ejemplo n.º 5
0
def apply_item_changes(lib, item, move, pretend, write):
    """Store, move, and write the item according to the arguments.

    :param lib: beets library.
    :type lib: beets.library.Library
    :param item: Item whose changes to apply.
    :type item: beets.library.Item
    :param move: Move the item if it's in the library.
    :type move: bool
    :param pretend: Return without moving, writing, or storing the item's
        metadata.
    :type pretend: bool
    :param write: Write the item's metadata to its media file.
    :type write: bool
    """
    if pretend:
        return

    from beets import util

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

    if write:
        item.try_write()

    item.store()
Ejemplo n.º 6
0
    def manipulate_files(self, move=False, copy=False, write=False,
                         link=False, session=None):
        items = self.imported_items()
        # Save the original paths of all items for deletion and pruning
        # in the next step (finalization).
        self.old_paths = [item.path for item in items]
        for item in items:
            if move or copy or link:
                # In copy and link modes, treat re-imports specially:
                # move in-library files. (Out-of-library files are
                # copied/moved as usual).
                old_path = item.path
                if (copy or link) and self.replaced_items[item] and \
                   session.lib.directory in util.ancestry(old_path):
                    item.move()
                    # We moved the item, so remove the
                    # now-nonexistent file from old_paths.
                    self.old_paths.remove(old_path)
                else:
                    # A normal import. Just copy files and keep track of
                    # old paths.
                    item.move(copy, link)

            if write and self.apply:
                item.try_write()

        with session.lib.transaction():
            for item in self.imported_items():
                item.store()

        plugins.send('import_task_files', session=session, task=self)
Ejemplo n.º 7
0
def _print_and_apply_changes(lib, item, move, pretend, write):
    """Apply changes to an Item and preview them in the console. Return
    a boolean indicating whether any changes were made.
    """
    changes = {}
    for key in library.ITEM_KEYS_META:
        if item.dirty[key]:
            changes[key] = item.old_data[key], getattr(item, key)
    if not changes:
        return False

    # Something changed.
    ui.print_obj(item, lib)
    for key, (oldval, newval) in changes.iteritems():
        ui.commands._showdiff(key, oldval, newval)

    # If we're just pretending, then don't move or save.
    if not pretend:
        # Move the item if it's in the library.
        if move and lib.directory in util.ancestry(item.path):
            lib.move(item, with_album=False)

        if write:
            try:
                item.write()
            except Exception as exc:
                log.error(u'could not sync {0}: {1}'.format(
                    util.displayable_path(item.path), exc))
                return False
        lib.store(item)

    return True
Ejemplo n.º 8
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.
    with lib.transaction():
        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)

    # Apply tags if requested.
    if write:
        if album:
            items = itertools.chain(*(a.items() for a in albums))
        for item in items:
            item.write()
Ejemplo n.º 9
0
def modify_items(lib, mods, query, write, move, album, confirm):
    """Modifies matching items according to key=value assignments."""
    # Parse key=value specifications into a dictionary.
    model_cls = library.Album if album else library.Item
    fsets = {}
    for mod in mods:
        key, value = mod.split('=', 1)
        fsets[key] = model_cls._parse(key, value)

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

    # Preview change and collect modified objects.
    print_('Modifying %i %ss.' % (len(objs), 'album' if album else 'item'))
    changed = set()
    for obj in objs:
        # Identify the changed object.
        ui.print_obj(obj, lib)

        # Show each change.
        for field, value in fsets.iteritems():
            if _showdiff(field, obj._get_formatted(field),
                         obj._format(field, value)):
                changed.add(obj)

    # Still something to do?
    if not changed:
        print_('No changes to make.')
        return

    # Confirm action.
    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.
    with lib.transaction():
        for obj in changed:
            for field, value in fsets.iteritems():
                obj[field] = value

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

            obj.store()

    # Apply tags if requested.
    if write:
        if album:
            changed_items = itertools.chain(*(a.items() for a in changed))
        else:
            changed_items = changed
        for item in changed_items:
            item.write()
Ejemplo n.º 10
0
    def albums(self, lib, query, move, pretend, write):
        """Retrieve and apply info from the autotagger for albums matched by
        query and their items.
        """
        # Process matching albums.
        for album in lib.albums(query):
            # Do we have a valid Beatport album?
            items = self.get_album_tracks(album)
            if not items:
                continue

            # Get the Beatport album information.
            albuminfo = self.beatport_plugin.album_for_id(album.mb_albumid)
            if not albuminfo:
                self._log.info(
                    'Release ID {} not found for album {}',
                    album.mb_albumid,
                    album,
                )
                continue

            beatport_trackid_to_trackinfo = {
                track.track_id: track
                for track in albuminfo.tracks
            }
            library_trackid_to_item = {
                int(item.mb_trackid): item
                for item in items
            }
            item_to_trackinfo = {
                item: beatport_trackid_to_trackinfo[track_id]
                for track_id, item in library_trackid_to_item.items()
            }

            self._log.info('applying changes to {}', album)
            with lib.transaction():
                autotag.apply_metadata(albuminfo, item_to_trackinfo)
                changed = False
                # Find any changed item to apply Beatport changes to album.
                any_changed_item = items[0]
                for item in items:
                    item_changed = ui.show_model_changes(item)
                    changed |= item_changed
                    if item_changed:
                        any_changed_item = item
                        apply_item_changes(lib, item, move, pretend, write)

                if pretend or not changed:
                    continue

                # Update album structure to reflect an item in it.
                for key in library.Album.item_keys:
                    album[key] = any_changed_item[key]
                album.store()

                # Move album art (and any inconsistent items).
                if move and lib.directory in util.ancestry(items[0].path):
                    self._log.debug('moving album {}', album)
                    album.move()
Ejemplo n.º 11
0
def modify_items(lib, mods, dels, query, write, move, album, confirm):
    """Modifies matching items according to key=value assignments."""
    # Parse key=value specifications into a dictionary.
    model_cls = library.Album if album else library.Item
    fsets = {}
    for mod in mods:
        key, value = mod.split('=', 1)
        fsets[key] = model_cls._parse(key, value)

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

    # Apply changes *temporarily*, preview them, and collect modified
    # objects.
    print_('Modifying %i %ss.' % (len(objs), 'album' if album else 'item'))
    changed = set()
    for obj in objs:
        for field, value in fsets.iteritems():
            obj[field] = value
        for field in dels:
            del obj[field]
        if ui.show_model_changes(obj):
            changed.add(obj)

    # Still something to do?
    if not changed:
        print_('No changes to make.')
        return

    # Confirm action.
    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.
    with lib.transaction():
        for obj in changed:
            if move:
                cur_path = obj.path
                if lib.directory in ancestry(cur_path): # In library?
                    log.debug('moving object %s' % cur_path)
                    obj.move()

            obj.store()

    # Apply tags if requested.
    if write:
        if album:
            changed_items = itertools.chain(*(a.items() for a in changed))
        else:
            changed_items = changed
        for item in changed_items:
            try:
                item.write()
            except library.FileOperationError as exc:
                log.error(exc)
Ejemplo n.º 12
0
def modify_items(lib, mods, dels, query, write, move, album, confirm):
    """Modifies matching items according to key=value assignments."""
    # Parse key=value specifications into a dictionary.
    model_cls = library.Album if album else library.Item
    fsets = {}
    for mod in mods:
        key, value = mod.split('=', 1)
        fsets[key] = model_cls._parse(key, value)

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

    # Apply changes *temporarily*, preview them, and collect modified
    # objects.
    print_('Modifying %i %ss.' % (len(objs), 'album' if album else 'item'))
    changed = set()
    for obj in objs:
        for field, value in fsets.iteritems():
            obj[field] = value
        for field in dels:
            del obj[field]
        if ui.show_model_changes(obj):
            changed.add(obj)

    # Still something to do?
    if not changed:
        print_('No changes to make.')
        return

    # Confirm action.
    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.
    with lib.transaction():
        for obj in changed:
            if move:
                cur_path = obj.path
                if lib.directory in ancestry(cur_path): # In library?
                    log.debug('moving object %s' % cur_path)
                    obj.move()

            obj.store()

    # Apply tags if requested.
    if write:
        if album:
            changed_items = itertools.chain(*(a.items() for a in changed))
        else:
            changed_items = changed
        for item in changed_items:
            try:
                item.write()
            except library.FileOperationError as exc:
                log.error(exc)
Ejemplo n.º 13
0
def manipulate_files(session):
    """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

        # Remove duplicate files marked for deletion.
        if task.remove_duplicates:
            for duplicate_path in task.duplicate_paths:
                log.debug(u'deleting replaced duplicate %s' %
                          util.displayable_path(duplicate_path))
                util.remove(duplicate_path)
                util.prune_dirs(os.path.dirname(duplicate_path),
                                session.lib.directory)

        # Move/copy/write files.
        items = task.imported_items()
        # Save the original paths of all items for deletion and pruning
        # in the next step (finalization).
        task.old_paths = [item.path for item in items]
        for item in items:
            if config['import']['move']:
                # Just move the file.
                item.move(False)
            elif config['import']['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 session.lib.directory in util.ancestry(old_path):
                        item.move(False)
                        # We moved the item, so remove the
                        # now-nonexistent file from old_paths.
                        task.old_paths.remove(old_path)
                    else:
                        item.move(True)
                else:
                    # A normal import. Just copy files and keep track of
                    # old paths.
                    item.move(True)

            if config['import']['write'] and task.should_write_tags():
                item.try_write()

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

        # Plugin event.
        plugins.send('import_task_files', session=session, task=task)
Ejemplo n.º 14
0
def manipulate_files(session):
    """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

        # Remove duplicate files marked for deletion.
        if task.remove_duplicates:
            for duplicate_path in task.duplicate_paths:
                log.debug(u'deleting replaced duplicate %s' %
                          util.displayable_path(duplicate_path))
                util.remove(duplicate_path)
                util.prune_dirs(os.path.dirname(duplicate_path),
                                session.lib.directory)

        # Move/copy/write files.
        items = task.imported_items()
        # Save the original paths of all items for deletion and pruning
        # in the next step (finalization).
        task.old_paths = [item.path for item in items]
        for item in items:
            if config['import']['move']:
                # Just move the file.
                item.move(False)
            elif config['import']['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 session.lib.directory in util.ancestry(old_path):
                        item.move(False)
                        # We moved the item, so remove the
                        # now-nonexistent file from old_paths.
                        task.old_paths.remove(old_path)
                    else:
                        item.move(True)
                else:
                    # A normal import. Just copy files and keep track of
                    # old paths.
                    item.move(True)

            if config['import']['write'] and task.should_write_tags():
                item.write()

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

        # Plugin event.
        plugins.send('import_task_files', session=session, task=task)
Ejemplo n.º 15
0
 def remove_duplicates(self, lib):
     duplicate_items = self.duplicate_items(lib)
     log.debug("removing %i old duplicated items" % len(duplicate_items))
     for item in duplicate_items:
         item.remove()
         if lib.directory in util.ancestry(item.path):
             log.debug(u"deleting duplicate %s" % util.displayable_path(item.path))
             util.remove(item.path)
             util.prune_dirs(os.path.dirname(item.path), lib.directory)
Ejemplo n.º 16
0
 def remove_duplicates(self, lib):
     duplicate_items = self.duplicate_items(lib)
     log.debug('removing %i old duplicated items' % len(duplicate_items))
     for item in duplicate_items:
         item.remove()
         if lib.directory in util.ancestry(item.path):
             log.debug(u'deleting duplicate %s' %
                       util.displayable_path(item.path))
             util.remove(item.path)
             util.prune_dirs(os.path.dirname(item.path), lib.directory)
Ejemplo n.º 17
0
def apply_item_changes(lib, item, move, pretend, write):
    """Store, move and write the item according to the arguments.
    """
    if not pretend:
        # Move the item if it's in the library.
        if move and lib.directory in util.ancestry(item.path):
            item.move(with_album=False)

        if write:
            item.try_write()
        item.store()
Ejemplo n.º 18
0
def apply_item_changes(lib, item, move, pretend, write):
    """Store, move and write the item according to the arguments.
    """
    if not pretend:
        # Move the item if it's in the library.
        if move and lib.directory in util.ancestry(item.path):
            item.move(with_album=False)

        if write:
            item.try_write()
        item.store()
Ejemplo n.º 19
0
 def remove_duplicates(self, lib):
     duplicate_items = self.duplicate_items(lib)
     log.debug(u'removing {0} old duplicated items'
               .format(len(duplicate_items)))
     for item in duplicate_items:
         item.remove()
         if lib.directory in util.ancestry(item.path):
             log.debug(u'deleting duplicate {0}'
                       .format(util.displayable_path(item.path)))
             util.remove(item.path)
             util.prune_dirs(os.path.dirname(item.path),
                             lib.directory)
Ejemplo n.º 20
0
def modify_items(lib, mods, query, write, move, album, confirm):
    """Modifies matching items according to key=value assignments."""
    # Parse key=value specifications into a dictionary.
    fsets = {}
    for mod in mods:
        key, value = mod.split('=', 1)
        fsets[key] = _convert_type(key, value, album)

    # 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.
        ui.print_obj(obj, lib)

        # Show each change.
        for field, value in fsets.iteritems():
            _showdiff(field, obj.get(field), value)

    # 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.
    with lib.transaction():
        for obj in objs:
            for field, value in fsets.iteritems():
                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)

            obj.store()

    # Apply tags if requested.
    if write:
        if album:
            items = itertools.chain(*(a.items() for a in albums))
        for item in items:
            item.write()
Ejemplo n.º 21
0
Archivo: mbsync.py Proyecto: 241n/beets
def mbsync_albums(lib, query, move, pretend, write):
    """Retrieve and apply info from the autotagger for albums matched by
    query and their items.
    """
    # Process matching albums.
    for a in lib.albums(query):
        if not a.mb_albumid:
            log.info(u'Skipping album {0}: has no mb_albumid'.format(a.id))
            continue

        items = list(a.items())

        # Get the MusicBrainz album information.
        album_info = hooks.album_for_mbid(a.mb_albumid)
        if not album_info:
            log.info(u'Release ID not found: {0}'.format(a.mb_albumid))
            continue

        # Construct a track mapping according to MBIDs. This should work
        # for albums that have missing or extra tracks.
        mapping = {}
        for item in items:
            for track_info in album_info.tracks:
                if item.mb_trackid == track_info.track_id:
                    mapping[item] = track_info
                    break

        # Apply.
        with lib.transaction():
            autotag.apply_metadata(album_info, mapping)
            changed = False
            for item in items:
                item_changed = ui.show_model_changes(item)
                changed |= item_changed
                if item_changed:
                    apply_item_changes(lib, item, move, pretend, write)

            if not changed:
                # No change to any item.
                continue

            if not pretend:
                # Update album structure to reflect an item in it.
                for key in library.Album.item_keys:
                    a[key] = items[0][key]
                a.store()

                # Move album art (and any inconsistent items).
                if move and lib.directory in util.ancestry(items[0].path):
                    log.debug(u'moving album {0}'.format(a.id))
                    a.move()
Ejemplo n.º 22
0
def mbsync_albums(lib, query, move, pretend, write):
    """Retrieve and apply info from the autotagger for albums matched by
    query and their items.
    """
    # Process matching albums.
    for a in lib.albums(query):
        if not a.mb_albumid:
            log.info(u'Skipping album {0}: has no mb_albumid'.format(a.id))
            continue

        items = list(a.items())

        # Get the MusicBrainz album information.
        album_info = hooks.album_for_mbid(a.mb_albumid)
        if not album_info:
            log.info(u'Release ID not found: {0}'.format(a.mb_albumid))
            continue

        # Construct a track mapping according to MBIDs. This should work
        # for albums that have missing or extra tracks.
        mapping = {}
        for item in items:
            for track_info in album_info.tracks:
                if item.mb_trackid == track_info.track_id:
                    mapping[item] = track_info
                    break

        # Apply.
        with lib.transaction():
            autotag.apply_metadata(album_info, mapping)
            changed = False
            for item in items:
                item_changed = ui.show_model_changes(item)
                changed |= item_changed
                if item_changed:
                    apply_item_changes(lib, item, move, pretend, write)

            if not changed:
                # No change to any item.
                continue

            if not pretend:
                # Update album structure to reflect an item in it.
                for key in library.Album.item_keys:
                    a[key] = items[0][key]
                a.store()

                # Move album art (and any inconsistent items).
                if move and lib.directory in util.ancestry(items[0].path):
                    log.debug(u'moving album {0}'.format(a.id))
                    a.move()
Ejemplo n.º 23
0
def modify_items(lib, mods, query, write, move, album, confirm):
    """Modifies matching items according to key=value assignments."""
    # Parse key=value specifications into a dictionary.
    fsets = {}
    for mod in mods:
        key, value = mod.split('=', 1)
        fsets[key] = _convert_type(key, value, album)

    # 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.
        ui.print_obj(obj, lib)

        # Show each change.
        for field, value in fsets.iteritems():
            _showdiff(field, obj.get(field), value)

    # 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.
    with lib.transaction():
        for obj in objs:
            for field, value in fsets.iteritems():
                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)

            obj.store()

    # Apply tags if requested.
    if write:
        if album:
            items = itertools.chain(*(a.items() for a in albums))
        for item in items:
            item.write()
Ejemplo n.º 24
0
def manipulate_files(config):
    """A coroutine (pipeline stage) that performs necessary file
    manipulations *after* items have been added to the library.
    """
    lib = _reopen_lib(config.lib)
    task = None
    while True:
        task = yield task
        if task.should_skip():
            continue

        # Move/copy files.
        items = task.all_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
                lib.move(item, False)
                # Clean up empty parent directory.
                if task.toppath:
                    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 lib.directory in util.ancestry(old_path):
                        lib.move(item, False)
                        # We moved the item, so remove the
                        # now-nonexistent file from old_paths.
                        task.old_paths.remove(old_path)
                    else:
                        lib.move(item, True)
                else:
                    # A normal import. Just copy files and keep track of
                    # old paths.
                    lib.move(item, True)

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

        # Save new paths.
        with lib.transaction():
            for item in items:
                lib.store(item)
Ejemplo n.º 25
0
def mbsync_albums(lib, query, move, pretend, write):
    """Synchronize matching albums.
    """
    # Process matching albums.
    for a in lib.albums(query):
        if not a.mb_albumid:
            log.info(u'Skipping album {0}: has no mb_albumid'.format(a.id))
            continue

        items = list(a.items())
        for item in items:
            item.old_data = dict(item.record)

        # Get the MusicBrainz album information.
        matches = hooks._album_for_id(a.mb_albumid)
        if not matches:
            log.info(u'Release ID not found: {0}'.format(a.mb_albumid))
            continue
        album_info = matches[0]

        # Construct a track mapping according to MBIDs. This should work
        # for albums that have missing or extra tracks.
        mapping = {}
        for item in items:
            for track_info in album_info.tracks:
                if item.mb_trackid == track_info.track_id:
                    mapping[item] = track_info
                    break

        # Apply.
        with lib.transaction():
            autotag.apply_metadata(album_info, mapping)
            changed = False
            for item in items:
                changed = _print_and_apply_changes(lib, item, move, pretend,
                                                   write) or changed
            if not changed:
                # No change to any item.
                continue

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

                # Move album art (and any inconsistent items).
                if move and lib.directory in util.ancestry(items[0].path):
                    log.debug(u'moving album {0}'.format(a.id))
                    a.move()
Ejemplo n.º 26
0
def manipulate_files(session):
    """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/write files.
        items = task.imported_items()
        task.old_paths = [item.path for item in items]  # For deletion.
        for item in items:
            if config['import']['move']:
                # Just move the file.
                old_path = item.path
                session.lib.move(item, False)
                task.prune(old_path)
            elif config['import']['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 session.lib.directory in util.ancestry(old_path):
                        session.lib.move(item, False)
                        # We moved the item, so remove the
                        # now-nonexistent file from old_paths.
                        task.old_paths.remove(old_path)
                    else:
                        session.lib.move(item, True)
                else:
                    # A normal import. Just copy files and keep track of
                    # old paths.
                    session.lib.move(item, True)

            if config['import']['write'] and task.should_write_tags():
                item.write()

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

        # Plugin event.
        plugins.send('import_task_files', session=session, task=task)
Ejemplo n.º 27
0
def manipulate_files(session):
    """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/write files.
        items = task.imported_items()
        task.old_paths = [item.path for item in items]  # For deletion.
        for item in items:
            if config['import']['move']:
                # Just move the file.
                old_path = item.path
                session.lib.move(item, False)
                task.prune(old_path)
            elif config['import']['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 session.lib.directory in util.ancestry(old_path):
                        session.lib.move(item, False)
                        # We moved the item, so remove the
                        # now-nonexistent file from old_paths.
                        task.old_paths.remove(old_path)
                    else:
                        session.lib.move(item, True)
                else:
                    # A normal import. Just copy files and keep track of
                    # old paths.
                    session.lib.move(item, True)

            if config['import']['write'] and task.should_write_tags():
                item.write()

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

        # Plugin event.
        plugins.send('import_task_files', session=session, task=task)
Ejemplo n.º 28
0
def mbsync_albums(lib, query, move, pretend, write):
    """Synchronize matching albums.
    """
    # Process matching albums.
    for a in lib.albums(query):
        if not a.mb_albumid:
            log.info(u'Skipping album {0}: has no mb_albumid'.format(a.id))
            continue

        items = list(a.items())
        for item in items:
            item.old_data = dict(item.record)

        # Get the MusicBrainz album information.
        matches = hooks._album_for_id(a.mb_albumid)
        if not matches:
            log.info(u'Release ID not found: {0}'.format(a.mb_albumid))
            continue
        album_info = matches[0]

        # Construct a track mapping according to MBIDs. This should work
        # for albums that have missing or extra tracks.
        mapping = {}
        for item in items:
            for track_info in album_info.tracks:
                if item.mb_trackid == track_info.track_id:
                    mapping[item] = track_info
                    break

        # Apply.
        with lib.transaction():
            autotag.apply_metadata(album_info, mapping)
            changed = False
            for item in items:
                changed = _print_and_apply_changes(lib, item, move, pretend,
                    write) or changed
            if not changed:
                # No change to any item.
                continue

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

                # Move album art (and any inconsistent items).
                if move and lib.directory in util.ancestry(items[0].path):
                    log.debug(u'moving album {0}'.format(a.id))
                    a.move()
Ejemplo n.º 29
0
def _print_and_apply_changes(lib, item, old_data, move, pretend, write):
    """Apply changes to an Item and preview them in the console. Return
    a boolean indicating whether any changes were made.
    """
    changed = ui.show_model_changes(item)
    if not changed:
        return False

    # If we're just pretending, then don't move or save.
    if not pretend:
        # Move the item if it's in the library.
        if move and lib.directory in util.ancestry(item.path):
            item.move(with_album=False)

        if write and not item.try_write():
            return False
        item.store()

    return True
Ejemplo n.º 30
0
    def try_sync(self, write, move, with_album=True):
        """Synchronize the item with the database and, possibly, updates its
        tags on disk and its path (by moving the file).

        `write` indicates whether to write new tags into the file. Similarly,
        `move` controls whether the path should be updated. In the
        latter case, files are *only* moved when they are inside their
        library's directory (if any).

        Similar to calling :meth:`write`, :meth:`move`, and :meth:`store`
        (conditionally).
        """
        if write:
            self.try_write()
        if move:
            # Check whether this file is inside the library directory.
            if self._db and self._db.directory in util.ancestry(self.path):
                log.debug(u"moving {0} to synchronize path", util.displayable_path(self.path))
                self.move(with_album=with_album)
        self.store()
Ejemplo n.º 31
0
    def manipulate_files(self,
                         move=False,
                         copy=False,
                         write=False,
                         session=None):
        items = self.imported_items()
        # Save the original paths of all items for deletion and pruning
        # in the next step (finalization).
        self.old_paths = [item.path for item in items]
        for item in items:
            if session.config['move']:
                # Just move the file.
                item.move(False)
            elif session.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 self.replaced_items[item]:
                    # This is a reimport. Move in-library files and copy
                    # out-of-library files.
                    if session.lib.directory in util.ancestry(old_path):
                        item.move(False)
                        # We moved the item, so remove the
                        # now-nonexistent file from old_paths.
                        self.old_paths.remove(old_path)
                    else:
                        item.move(True)
                else:
                    # A normal import. Just copy files and keep track of
                    # old paths.
                    item.move(True)

            if session.config['write'] and self.apply:
                item.try_write()

        with session.lib.transaction():
            for item in self.imported_items():
                item.store()

        plugins.send('import_task_files', session=session, task=self)
Ejemplo n.º 32
0
    def try_sync(self, write, move, with_album=True):
        """Synchronize the item with the database and, possibly, updates its
        tags on disk and its path (by moving the file).

        `write` indicates whether to write new tags into the file. Similarly,
        `move` controls whether the path should be updated. In the
        latter case, files are *only* moved when they are inside their
        library's directory (if any).

        Similar to calling :meth:`write`, :meth:`move`, and :meth:`store`
        (conditionally).
        """
        if write:
            self.try_write()
        if move:
            # Check whether this file is inside the library directory.
            if self._db and self._db.directory in util.ancestry(self.path):
                log.debug(u'moving {0} to synchronize path',
                          util.displayable_path(self.path))
                self.move(with_album=with_album)
        self.store()
Ejemplo n.º 33
0
    def manipulate_files(self, move=False, copy=False, write=False,
                         session=None):
        items = self.imported_items()
        # Save the original paths of all items for deletion and pruning
        # in the next step (finalization).
        self.old_paths = [item.path for item in items]
        for item in items:
            if session.config['move']:
                # Just move the file.
                item.move(False)
            elif session.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 self.replaced_items[item]:
                    # This is a reimport. Move in-library files and copy
                    # out-of-library files.
                    if session.lib.directory in util.ancestry(old_path):
                        item.move(False)
                        # We moved the item, so remove the
                        # now-nonexistent file from old_paths.
                        self.old_paths.remove(old_path)
                    else:
                        item.move(True)
                else:
                    # A normal import. Just copy files and keep track of
                    # old paths.
                    item.move(True)

            if session.config['write'] and self.apply:
                item.try_write()

        with session.lib.transaction():
            for item in self.imported_items():
                item.store()

        plugins.send('import_task_files', session=session, task=self)
Ejemplo n.º 34
0
def _print_and_apply_changes(lib, item, old_data, move, pretend, write):
    """Apply changes to an Item and preview them in the console. Return
    a boolean indicating whether any changes were made.
    """
    changed = ui.show_model_changes(item)
    if not changed:
        return False

    # If we're just pretending, then don't move or save.
    if not pretend:
        # Move the item if it's in the library.
        if move and lib.directory in util.ancestry(item.path):
            item.move(with_album=False)

        if write:
            try:
                item.write()
            except Exception as exc:
                log.error(u'could not sync {0}: {1}'.format(
                    util.displayable_path(item.path), exc))
                return False
        item.store()

    return True
Ejemplo n.º 35
0
 def test_ancestry_works_on_dir(self):
     p = '/a/b/c/'
     a = ['/', '/a', '/a/b', '/a/b/c']
     self.assertEqual(util.ancestry(p), a)
Ejemplo n.º 36
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
Ejemplo n.º 37
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()
Ejemplo n.º 38
0
def update_items(lib, query, album, move, pretend):
    """For all the items matched by the query, update the library to
    reflect the item's embedded tags.
    """
    with lib.transaction():
        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)):
                ui.print_obj(item, lib)
                ui.print_(ui.colorize('red', u'  deleted'))
                if not pretend:
                    item.remove(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.
            try:
                item.read()
            except Exception as exc:
                log.error(u'error reading {0}: {1}'.format(
                    displayable_path(item.path), exc))
                continue

            # 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:
                old_item = lib.get_item(item.id)
                if old_item.albumartist == old_item.artist == item.artist:
                    item.albumartist = old_item.albumartist
                    item._dirty.discard('albumartist')

            # Check for and display changes.
            changed = ui.show_model_changes(item,
                                            fields=library.ITEM_KEYS_META)

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

                    item.store()
                    affected_albums.add(item.album_id)
                else:
                    # 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.
                    item.store()

        # 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
            first_item = album.items().get()

            # Update album structure to reflect an item in it.
            for key in library.ALBUM_KEYS_ITEM:
                album[key] = first_item[key]
            album.store()

            # Move album art (and any inconsistent items).
            if move and lib.directory in ancestry(first_item.path):
                log.debug('moving album %i' % album_id)
                album.move()
Ejemplo n.º 39
0
def apply_choices(session):
    """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
                )
            else:
                autotag.apply_item_metadata(task.item, task.match.info)
            plugins.send('import_task_apply', session=session, 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 = session.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(session.lib, task):
                    duplicate_items += album.items()
            else:
                duplicate_items = _item_duplicate_check(session.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 session.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),
                                    session.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 session.lib.transaction():
            # Remove old items.
            for replaced in task.replaced_items.itervalues():
                for item in replaced:
                    session.lib.remove(item)
            for item in duplicate_items:
                session.lib.remove(item)

            # Add new ones.
            if task.is_album:
                # Add an album.
                album = session.lib.add_album(items)
                task.album_id = album.id
            else:
                # Add tracks.
                for item in items:
                    session.lib.add(item)
Ejemplo n.º 40
0
def albums_in_dir(path):
    """Recursively searches the given directory and returns an iterable
    of (paths, items) where paths is a list of directories and items is
    a list of Items that is probably an album. Specifically, any folder
    containing any media files is an album.
    """
    collapse_pat = collapse_paths = collapse_items = None
    ignore = config['ignore'].as_str_seq()

    for root, dirs, files in sorted_walk(path, ignore=ignore, logger=log):
        items = [os.path.join(root, f) for f in files]
        # If we're currently collapsing the constituent directories in a
        # multi-disc album, check whether we should continue collapsing
        # and add the current directory. If so, just add the directory
        # and move on to the next directory. If not, stop collapsing.
        if collapse_paths:
            if (not collapse_pat and collapse_paths[0] in ancestry(root)) or \
                    (collapse_pat and
                     collapse_pat.match(os.path.basename(root))):
                # Still collapsing.
                collapse_paths.append(root)
                collapse_items += items
                continue
            else:
                # Collapse finished. Yield the collapsed directory and
                # proceed to process the current one.
                if collapse_items:
                    yield collapse_paths, collapse_items
                collapse_pat = collapse_paths = collapse_items = None

        # Check whether this directory looks like the *first* directory
        # in a multi-disc sequence. There are two indicators: the file
        # is named like part of a multi-disc sequence (e.g., "Title Disc
        # 1") or it contains no items but only directories that are
        # named in this way.
        start_collapsing = False
        for marker in MULTIDISC_MARKERS:
            marker_pat = re.compile(MULTIDISC_PAT_FMT % marker, re.I)
            match = marker_pat.match(os.path.basename(root))

            # Is this directory the root of a nested multi-disc album?
            if dirs and not items:
                # Check whether all subdirectories have the same prefix.
                start_collapsing = True
                subdir_pat = None
                for subdir in dirs:
                    # The first directory dictates the pattern for
                    # the remaining directories.
                    if not subdir_pat:
                        match = marker_pat.match(subdir)
                        if match:
                            subdir_pat = re.compile(
                                r'^%s\d' % re.escape(match.group(1)), re.I)
                        else:
                            start_collapsing = False
                            break

                    # Subsequent directories must match the pattern.
                    elif not subdir_pat.match(subdir):
                        start_collapsing = False
                        break

                # If all subdirectories match, don't check other
                # markers.
                if start_collapsing:
                    break

            # Is this directory the first in a flattened multi-disc album?
            elif match:
                start_collapsing = True
                # Set the current pattern to match directories with the same
                # prefix as this one, followed by a digit.
                collapse_pat = re.compile(r'^%s\d' % re.escape(match.group(1)),
                                          re.I)
                break

        # If either of the above heuristics indicated that this is the
        # beginning of a multi-disc album, initialize the collapsed
        # directory and item lists and check the next directory.
        if start_collapsing:
            # Start collapsing; continue to the next iteration.
            collapse_paths = [root]
            collapse_items = items
            continue

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

    # Clear out any unfinished collapse.
    if collapse_paths and collapse_items:
        yield collapse_paths, collapse_items
Ejemplo n.º 41
0
    def albums(self, lib, query, move, pretend, write):
        """Retrieve and apply info from the autotagger for albums matched by
        query and their items.
        """
        # Process matching albums.
        for a in lib.albums(query):
            album_formatted = format(a)
            if not a.mb_albumid:
                self._log.info(u'Skipping album with no mb_albumid: {0}',
                               album_formatted)
                continue

            items = list(a.items())

            # Get the MusicBrainz album information.
            album_info = hooks.album_for_mbid(a.mb_albumid)
            if not album_info:
                self._log.info(u'Release ID {0} not found for album {1}',
                               a.mb_albumid,
                               album_formatted)
                continue

            # Map release track and recording MBIDs to their information.
            # Recordings can appear multiple times on a release, so each MBID
            # maps to a list of TrackInfo objects.
            releasetrack_index = dict()
            track_index = defaultdict(list)
            for track_info in album_info.tracks:
                releasetrack_index[track_info.release_track_id] = track_info
                track_index[track_info.track_id].append(track_info)

            # Construct a track mapping according to MBIDs (release track MBIDs
            # first, if available, and recording MBIDs otherwise). This should
            # work for albums that have missing or extra tracks.
            mapping = {}
            for item in items:
                if item.mb_releasetrackid and \
                        item.mb_releasetrackid in releasetrack_index:
                    mapping[item] = releasetrack_index[item.mb_releasetrackid]
                else:
                    candidates = track_index[item.mb_trackid]
                    if len(candidates) == 1:
                        mapping[item] = candidates[0]
                    else:
                        # If there are multiple copies of a recording, they are
                        # disambiguated using their disc and track number.
                        for c in candidates:
                            if (c.medium_index == item.track and
                                    c.medium == item.disc):
                                mapping[item] = c
                                break

            # Apply.
            self._log.debug(u'applying changes to {}', album_formatted)
            with lib.transaction():
                autotag.apply_metadata(album_info, mapping)
                changed = False
                # Find any changed item to apply MusicBrainz changes to album.
                any_changed_item = items[0]
                for item in items:
                    item_changed = ui.show_model_changes(item)
                    changed |= item_changed
                    if item_changed:
                        any_changed_item = item
                        apply_item_changes(lib, item, move, pretend, write)

                if not changed:
                    # No change to any item.
                    continue

                if not pretend:
                    # Update album structure to reflect an item in it.
                    for key in library.Album.item_keys:
                        a[key] = any_changed_item[key]
                    a.store()

                    # Move album art (and any inconsistent items).
                    if move and lib.directory in util.ancestry(items[0].path):
                        self._log.debug(u'moving album {0}', album_formatted)
                        a.move()
Ejemplo n.º 42
0
def apply_choices(session):
    """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)
            else:
                autotag.apply_item_metadata(task.item, task.match.info)
            plugins.send('import_task_apply', session=session, 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 = session.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(session.lib, task):
                    duplicate_items += album.items()
            else:
                duplicate_items = _item_duplicate_check(session.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 session.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),
                                    session.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 session.lib.transaction():
            # Remove old items.
            for replaced in task.replaced_items.itervalues():
                for item in replaced:
                    session.lib.remove(item)
            for item in duplicate_items:
                session.lib.remove(item)

            # Add new ones.
            if task.is_album:
                # Add an album.
                album = session.lib.add_album(items)
                task.album_id = album.id
            else:
                # Add tracks.
                for item in items:
                    session.lib.add(item)
Ejemplo n.º 43
0
    def albums(self, lib, query, move, pretend, write):
        """Retrieve and apply info from the autotagger for albums matched by
        query and their items.
        """
        # Process matching albums.
        for a in lib.albums(query):
            album_formatted = format(a)
            if not a.mb_albumid:
                self._log.info('Skipping album with no mb_albumid: {0}',
                               album_formatted)
                continue

            items = list(a.items())

            # Do we have a valid MusicBrainz album ID?
            if not re.match(MBID_REGEX, a.mb_albumid):
                self._log.info('Skipping album with invalid mb_albumid: {0}',
                               album_formatted)
                continue

            # Get the MusicBrainz album information.
            album_info = hooks.album_for_mbid(a.mb_albumid)
            if not album_info:
                self._log.info('Release ID {0} not found for album {1}',
                               a.mb_albumid, album_formatted)
                continue

            # Map release track and recording MBIDs to their information.
            # Recordings can appear multiple times on a release, so each MBID
            # maps to a list of TrackInfo objects.
            releasetrack_index = {}
            track_index = defaultdict(list)
            for track_info in album_info.tracks:
                releasetrack_index[track_info.release_track_id] = track_info
                track_index[track_info.track_id].append(track_info)

            # Construct a track mapping according to MBIDs (release track MBIDs
            # first, if available, and recording MBIDs otherwise). This should
            # work for albums that have missing or extra tracks.
            mapping = {}
            for item in items:
                if item.mb_releasetrackid and \
                        item.mb_releasetrackid in releasetrack_index:
                    mapping[item] = releasetrack_index[item.mb_releasetrackid]
                else:
                    candidates = track_index[item.mb_trackid]
                    if len(candidates) == 1:
                        mapping[item] = candidates[0]
                    else:
                        # If there are multiple copies of a recording, they are
                        # disambiguated using their disc and track number.
                        for c in candidates:
                            if (c.medium_index == item.track
                                    and c.medium == item.disc):
                                mapping[item] = c
                                break

            # Apply.
            self._log.debug('applying changes to {}', album_formatted)
            with lib.transaction():
                autotag.apply_metadata(album_info, mapping)
                changed = False
                # Find any changed item to apply MusicBrainz changes to album.
                any_changed_item = items[0]
                for item in items:
                    item_changed = ui.show_model_changes(item)
                    changed |= item_changed
                    if item_changed:
                        any_changed_item = item
                        apply_item_changes(lib, item, move, pretend, write)

                if not changed:
                    # No change to any item.
                    continue

                if not pretend:
                    # Update album structure to reflect an item in it.
                    for key in library.Album.item_keys:
                        a[key] = any_changed_item[key]
                    a.store()

                    # Move album art (and any inconsistent items).
                    if move and lib.directory in util.ancestry(items[0].path):
                        self._log.debug('moving album {0}', album_formatted)
                        a.move()
Ejemplo n.º 44
0
def mbsync_albums(lib, query, move, pretend, write):
    """Retrieve and apply info from the autotagger for albums matched by
    query and their items.
    """
    # Process matching albums.
    for a in lib.albums(query):
        if not a.mb_albumid:
            log.info(u'Skipping album {0}: has no mb_albumid', a.id)
            continue

        items = list(a.items())

        # Get the MusicBrainz album information.
        album_info = hooks.album_for_mbid(a.mb_albumid)
        if not album_info:
            log.info(u'Release ID not found: {0}', a.mb_albumid)
            continue

        # Map recording MBIDs to their information. Recordings can appear
        # multiple times on a release, so each MBID maps to a list of TrackInfo
        # objects.
        track_index = defaultdict(list)
        for track_info in album_info.tracks:
            track_index[track_info.track_id].append(track_info)

        # Construct a track mapping according to MBIDs. This should work
        # for albums that have missing or extra tracks. If there are multiple
        # copies of a recording, they are disambiguated using their disc and
        # track number.
        mapping = {}
        for item in items:
            candidates = track_index[item.mb_trackid]
            if len(candidates) == 1:
                mapping[item] = candidates[0]
            else:
                for c in candidates:
                    if c.medium_index == item.track and c.medium == item.disc:
                        mapping[item] = c
                        break

        # Apply.
        with lib.transaction():
            autotag.apply_metadata(album_info, mapping)
            changed = False
            for item in items:
                item_changed = ui.show_model_changes(item)
                changed |= item_changed
                if item_changed:
                    apply_item_changes(lib, item, move, pretend, write)

            if not changed:
                # No change to any item.
                continue

            if not pretend:
                # Update album structure to reflect an item in it.
                for key in library.Album.item_keys:
                    a[key] = items[0][key]
                a.store()

                # Move album art (and any inconsistent items).
                if move and lib.directory in util.ancestry(items[0].path):
                    log.debug(u'moving album {0}', a.id)
                    a.move()
Ejemplo n.º 45
0
def update_items(lib, query, album, move, pretend):
    """For all the items matched by the query, update the library to
    reflect the item's embedded tags.
    """
    with lib.transaction():
        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)):
                ui.print_obj(item, lib)
                ui.print_(ui.colorize('red', u'  deleted'))
                if not pretend:
                    item.remove(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.
            try:
                item.read()
            except Exception as exc:
                log.error(u'error reading {0}: {1}'.format(
                    displayable_path(item.path), exc))
                continue

            # 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:
                old_item = lib.get_item(item.id)
                if old_item.albumartist == old_item.artist == item.artist:
                    item.albumartist = old_item.albumartist
                    item._dirty.discard('albumartist')

            # Check for and display changes.
            changed = ui.show_model_changes(item,
                                            fields=library.ITEM_KEYS_META)

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

                    item.store()
                    affected_albums.add(item.album_id)
                else:
                    # 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.
                    item.store()

        # 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
            first_item = album.items().get()

            # Update album structure to reflect an item in it.
            for key in library.ALBUM_KEYS_ITEM:
                album[key] = first_item[key]
            album.store()

            # Move album art (and any inconsistent items).
            if move and lib.directory in ancestry(first_item.path):
                log.debug('moving album %i' % album_id)
                album.move()
Ejemplo n.º 46
0
 def test_ancestry_works_on_dir(self):
     p = '/a/b/c/'
     a = ['/', '/a', '/a/b', '/a/b/c']
     self.assertEqual(util.ancestry(p), a)
Ejemplo n.º 47
0
def albums_in_dir(path):
    """Recursively searches the given directory and returns an iterable
    of (paths, items) where paths is a list of directories and items is
    a list of Items that is probably an album. Specifically, any folder
    containing any media files is an album.
    """
    collapse_pat = collapse_paths = collapse_items = None
    ignore = config['ignore'].as_str_seq()

    for root, dirs, files in sorted_walk(path, ignore=ignore, logger=log):
        items = [os.path.join(root, f) for f in files]
        # If we're currently collapsing the constituent directories in a
        # multi-disc album, check whether we should continue collapsing
        # and add the current directory. If so, just add the directory
        # and move on to the next directory. If not, stop collapsing.
        if collapse_paths:
            if (not collapse_pat and collapse_paths[0] in ancestry(root)) or \
                    (collapse_pat and
                     collapse_pat.match(os.path.basename(root))):
                # Still collapsing.
                collapse_paths.append(root)
                collapse_items += items
                continue
            else:
                # Collapse finished. Yield the collapsed directory and
                # proceed to process the current one.
                if collapse_items:
                    yield collapse_paths, collapse_items
                collapse_pat = collapse_paths = collapse_items = None

        # Check whether this directory looks like the *first* directory
        # in a multi-disc sequence. There are two indicators: the file
        # is named like part of a multi-disc sequence (e.g., "Title Disc
        # 1") or it contains no items but only directories that are
        # named in this way.
        start_collapsing = False
        for marker in MULTIDISC_MARKERS:
            marker_pat = re.compile(MULTIDISC_PAT_FMT % marker, re.I)
            match = marker_pat.match(os.path.basename(root))

            # Is this directory the root of a nested multi-disc album?
            if dirs and not items:
                # Check whether all subdirectories have the same prefix.
                start_collapsing = True
                subdir_pat = None
                for subdir in dirs:
                    # The first directory dictates the pattern for
                    # the remaining directories.
                    if not subdir_pat:
                        match = marker_pat.match(subdir)
                        if match:
                            subdir_pat = re.compile(
                                r'^%s\d' % re.escape(match.group(1)), re.I
                            )
                        else:
                            start_collapsing = False
                            break

                    # Subsequent directories must match the pattern.
                    elif not subdir_pat.match(subdir):
                        start_collapsing = False
                        break

                # If all subdirectories match, don't check other
                # markers.
                if start_collapsing:
                    break

            # Is this directory the first in a flattened multi-disc album?
            elif match:
                start_collapsing = True
                # Set the current pattern to match directories with the same
                # prefix as this one, followed by a digit.
                collapse_pat = re.compile(
                    r'^%s\d' % re.escape(match.group(1)), re.I
                )
                break

        # If either of the above heuristics indicated that this is the
        # beginning of a multi-disc album, initialize the collapsed
        # directory and item lists and check the next directory.
        if start_collapsing:
            # Start collapsing; continue to the next iteration.
            collapse_paths = [root]
            collapse_items = items
            continue

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

    # Clear out any unfinished collapse.
    if collapse_paths and collapse_items:
        yield collapse_paths, collapse_items
Ejemplo n.º 48
0
 def test_ancestry_works_on_relative(self):
     p = 'a/b/c'
     a = ['a', 'a/b']
     self.assertEqual(util.ancestry(p), a)
Ejemplo n.º 49
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
Ejemplo n.º 50
0
 def test_ancestry_works_on_relative(self):
     p = 'a/b/c'
     a = ['a', 'a/b']
     self.assertEqual(util.ancestry(p), a)
Ejemplo n.º 51
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 = task.all_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.items, task.info)
            else:
                autotag.apply_item_metadata(task.item, task.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.
        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)

        # 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().
        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()

        # Move/copy files.
        task.old_paths = [item.path for item in items]  # For deletion.
        for item in items:
            if config.copy or config.move:
                if config.move:
                    # Just move the file.
                    lib.move(item, False)
                else:
                    # If it's a reimport, move the file. Otherwise, copy
                    # and keep track of the old path.
                    old_path = item.path
                    do_copy = not bool(replaced_items[item])
                    lib.move(item, do_copy)
                    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()

        # Save new paths.
        try:
            for item in items:
                lib.store(item)
        finally:
            lib.save()
Ejemplo n.º 52
0
def mbsync_albums(lib, query, move, pretend, write):
    """Retrieve and apply info from the autotagger for albums matched by
    query and their items.
    """
    # Process matching albums.
    for a in lib.albums(query):
        if not a.mb_albumid:
            log.info(u'Skipping album {0}: has no mb_albumid'.format(a.id))
            continue

        items = list(a.items())

        # Get the MusicBrainz album information.
        album_info = hooks.album_for_mbid(a.mb_albumid)
        if not album_info:
            log.info(u'Release ID not found: {0}'.format(a.mb_albumid))
            continue

        # Map recording MBIDs to their information. Recordings can appear
        # multiple times on a release, so each MBID maps to a list of TrackInfo
        # objects.
        track_index = defaultdict(list)
        for track_info in album_info.tracks:
            track_index[track_info.track_id].append(track_info)

        # Construct a track mapping according to MBIDs. This should work
        # for albums that have missing or extra tracks. If there are multiple
        # copies of a recording, they are disambiguated using their disc and
        # track number.
        mapping = {}
        for item in items:
            candidates = track_index[item.mb_trackid]
            if len(candidates) == 1:
                mapping[item] = candidates[0]
            else:
                for c in candidates:
                    if c.medium_index == item.track and c.medium == item.disc:
                        mapping[item] = c
                        break

        # Apply.
        with lib.transaction():
            autotag.apply_metadata(album_info, mapping)
            changed = False
            for item in items:
                item_changed = ui.show_model_changes(item)
                changed |= item_changed
                if item_changed:
                    apply_item_changes(lib, item, move, pretend, write)

            if not changed:
                # No change to any item.
                continue

            if not pretend:
                # Update album structure to reflect an item in it.
                for key in library.Album.item_keys:
                    a[key] = items[0][key]
                a.store()

                # Move album art (and any inconsistent items).
                if move and lib.directory in util.ancestry(items[0].path):
                    log.debug(u'moving album {0}'.format(a.id))
                    a.move()
Ejemplo n.º 53
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.
    """
    with lib.transaction():
        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()
Ejemplo n.º 54
0
def dirs_in_library(library, item):
    """Creates a list of ancestor directories in the beets library path.
    """
    return [ancestor
            for ancestor in ancestry(item)
            if ancestor.startswith(library)][1:]