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()
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
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()
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)
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()
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
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()
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()
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()
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)
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)
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)
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)
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)
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()
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)
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()
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()
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)
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()
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)
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
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()
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)
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()
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
def test_ancestry_works_on_dir(self): p = '/a/b/c/' a = ['/', '/a', '/a/b', '/a/b/c'] self.assertEqual(util.ancestry(p), a)
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
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()
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()
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)
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
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()
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)
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()
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()
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
def test_ancestry_works_on_relative(self): p = 'a/b/c' a = ['a', 'a/b'] self.assertEqual(util.ancestry(p), a)
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()
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()
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()
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:]