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(config): """A coroutine (pipeline stage) that performs necessary file manipulations *after* items have been added to the library. """ task = None while True: task = yield task if task.should_skip(): continue # Move/copy files. items = task.imported_items() task.old_paths = [item.path for item in items] # For deletion. for item in items: if config.move: # Just move the file. old_path = item.path config.lib.move(item, False) task.prune(old_path) elif config.copy: # If it's a reimport, move in-library files and copy # out-of-library files. Otherwise, copy and keep track # of the old path. old_path = item.path if task.replaced_items[item]: # This is a reimport. Move in-library files and copy # out-of-library files. if config.lib.directory in util.ancestry(old_path): config.lib.move(item, False) # We moved the item, so remove the # now-nonexistent file from old_paths. task.old_paths.remove(old_path) else: config.lib.move(item, True) else: # A normal import. Just copy files and keep track of # old paths. config.lib.move(item, True) if config.write and task.should_write_tags(): item.write() # Save new paths. with config.lib.transaction(): for item in items: config.lib.store(item) # Plugin event. plugins.send('import_task_files', config=config, task=task)
def apply_choices(config): """A coroutine for applying changes to albums and singletons during the autotag process. """ task = None while True: task = yield task if task.should_skip(): continue items = task.imported_items() # Clear IDs in case the items are being re-tagged. for item in items: item.id = None item.album_id = None # Change metadata. if task.should_write_tags(): if task.is_album: autotag.apply_metadata( task.match.info, task.match.mapping, per_disc_numbering=config.per_disc_numbering ) else: autotag.apply_item_metadata(task.item, task.match.info) plugins.send('import_task_apply', config=config, task=task) # Infer album-level fields. if task.is_album: _infer_album_fields(task) # Find existing item entries that these are replacing (for # re-imports). Old album structures are automatically cleaned up # when the last item is removed. task.replaced_items = defaultdict(list) for item in items: dup_items = config.lib.items(library.MatchQuery('path', item.path)) for dup_item in dup_items: task.replaced_items[item].append(dup_item) log.debug('replacing item %i: %s' % (dup_item.id, displayable_path(item.path))) log.debug('%i of %i items replaced' % (len(task.replaced_items), len(items))) # Find old items that should be replaced as part of a duplicate # resolution. duplicate_items = [] if task.remove_duplicates: if task.is_album: for album in _duplicate_check(config.lib, task): duplicate_items += album.items() else: duplicate_items = _item_duplicate_check(config.lib, task) log.debug('removing %i old duplicated items' % len(duplicate_items)) # Delete duplicate files that are located inside the library # directory. for duplicate_path in [i.path for i in duplicate_items]: if config.lib.directory in util.ancestry(duplicate_path): log.debug(u'deleting replaced duplicate %s' % util.displayable_path(duplicate_path)) util.remove(duplicate_path) util.prune_dirs(os.path.dirname(duplicate_path), config.lib.directory) # Add items -- before path changes -- to the library. We add the # items now (rather than at the end) so that album structures # are in place before calls to destination(). with config.lib.transaction(): # Remove old items. for replaced in task.replaced_items.itervalues(): for item in replaced: config.lib.remove(item) for item in duplicate_items: config.lib.remove(item) # Add new ones. if task.is_album: # Add an album. album = config.lib.add_album(items) task.album_id = album.id else: # Add tracks. for item in items: config.lib.add(item)
def apply_choices(config): """A coroutine for applying changes to albums during the autotag process. """ lib = _reopen_lib(config.lib) task = None while True: task = yield task if task.should_skip(): continue items = [i for i in task.items if i] if task.is_album else [task.item] # Clear IDs in case the items are being re-tagged. for item in items: item.id = None item.album_id = None # Change metadata. if task.should_write_tags(): if task.is_album: autotag.apply_metadata(task.items, task.info) else: autotag.apply_item_metadata(task.item, task.info) # Infer album-level fields. if task.is_album: _infer_album_fields(task) # Find existing item entries that these are replacing (for # re-imports). Old album structures are automatically cleaned up # when the last item is removed. replaced_items = defaultdict(list) for item in items: dup_items = lib.items(library.MatchQuery('path', item.path)) for dup_item in dup_items: replaced_items[item].append(dup_item) log.debug('replacing item %i: %s' % (dup_item.id, displayable_path(item.path))) log.debug('%i of %i items replaced' % (len(replaced_items), len(items))) # Find old items that should be replaced as part of a duplicate # resolution. duplicate_items = [] if task.remove_duplicates: if task.is_album: for album in _duplicate_check(lib, task): duplicate_items += album.items() else: duplicate_items = _item_duplicate_check(lib, task) log.debug('removing %i old duplicated items' % len(duplicate_items)) # Delete duplicate files that are located inside the library # directory. for duplicate_path in [i.path for i in duplicate_items]: if lib.directory in util.ancestry(duplicate_path): log.debug(u'deleting replaced duplicate %s' % util.displayable_path(duplicate_path)) util.soft_remove(duplicate_path) util.prune_dirs(os.path.dirname(duplicate_path), lib.directory) # Move/copy files. task.old_paths = [item.path for item in items] for item in items: if config.copy: # If we're replacing an item, then move rather than # copying. old_path = item.path do_copy = not bool(replaced_items[item]) lib.move(item, do_copy, task.is_album) if not do_copy: # If we moved the item, remove the now-nonexistent # file from old_paths. task.old_paths.remove(old_path) if config.write and task.should_write_tags(): item.write() # Add items to library. We consolidate this at the end to avoid # locking while we do the copying and tag updates. try: # Remove old items. for replaced in replaced_items.itervalues(): for item in replaced: lib.remove(item) for item in duplicate_items: lib.remove(item) # Add new ones. if task.is_album: # Add an album. album = lib.add_album(items) task.album_id = album.id else: # Add tracks. for item in items: lib.add(item) finally: lib.save()
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()