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. if album: allowed_keys = library.ALBUM_KEYS else: allowed_keys = library.ITEM_KEYS_WRITABLE + ['added'] 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] = _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(): curval = getattr(obj, field) _showdiff(field, curval, 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(): 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 _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 _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 _process_item(item, lib, copy=False, move=False, delete=False, tag=False, format=None): """Process Item `item` in `lib`. """ if copy: item.move_file(dest=copy, copy=True) item.store() if move: item.move_file(dest=move, copy=False) item.store() if delete: item.remove(delete=True) if tag: try: k, v = tag.split('=') except: raise UserError('%s: can\'t parse k=v tag: %s' % (PLUGIN, tag)) setattr(k, v) item.store() print_obj(item, lib, fmt=format)
def similar(lib, src_item, threshold=0.15, fmt='${difference}: ${path}'): for item in lib.items(): if item.path != src_item.path: d = diff(item, src_item) if d < threshold: s = fmt.replace('${difference}', '{:2.2f}'.format(d)) ui.print_obj(item, lib, s)
def remove_items(lib, query, album, delete, config): """Remove items matching query from lib. If album, then match and remove whole albums. If delete, also remove files from disk. """ # Get the matching items. items, albums = _do_query(lib, query, album) # Show all the items. for item in items: ui.print_obj(item, lib, config) # Confirm with user. print_() if delete: prompt = 'Really DELETE %i files (y/n)?' % len(items) else: prompt = 'Really remove %i items from the library (y/n)?' % \ len(items) if not ui.input_yn(prompt, True): return # Remove (and possibly delete) items. with lib.transaction(): if album: for al in albums: al.remove(delete) else: for item in items: lib.remove(item, delete)
def dupl_finder(lib, opts, args): """Lists and possibly deletes duplicates """ opts = vars(opts) # handle special printing formats if opts["output_format"]: fmt = opts["output_format"] else: fmt = '$albumartist - $album - $title' if opts["output_count"]: fmt += ": ({0})" key_list = gen_keylist(opts) if len(key_list) == 0: # no option set, setting default one key_list = ['title', 'artist', 'album'] res = check_key(key_list, lib.items(query = args)) for key, match_list in res.iteritems(): num = len(match_list) if num > 1: # found duplicates if opts["output_count"]: print_obj(match_list[0], lib, fmt=fmt.format(num)) else: for match in match_list: print_obj(match, lib, fmt=fmt.format(num)) print ""
def fuzzy_list(lib, opts, args): query = decargs(args) query = ' '.join(query).lower() queryMatcher = difflib.SequenceMatcher(b=query) if opts.threshold is not None: threshold = float(opts.threshold) else: threshold = config['fuzzy']['threshold'].as_number() if opts.path: fmt = '$path' else: fmt = opts.format template = Template(fmt) if fmt else None if opts.album: objs = lib.albums() else: objs = lib.items() items = filter(lambda i: is_match(queryMatcher, i, album=opts.album, threshold=threshold), objs) for item in items: print_obj(item, lib, template) if opts.verbose: print(is_match(queryMatcher, item, album=opts.album, verbose=True)[1])
def fuzzy_list(lib, config, opts, args): query = decargs(args) query = ' '.join(query).lower() queryMatcher = difflib.SequenceMatcher(b=query) if opts.threshold is not None: threshold = float(opts.threshold) else: threshold = float(conf['threshold']) if opts.path: fmt = '$path' else: fmt = opts.format template = Template(fmt) if fmt else None if opts.album: objs = lib.albums() else: objs = lib.items() items = filter(lambda i: is_match(queryMatcher, i, album=opts.album, threshold=threshold), objs) for item in items: print_obj(item, lib, config, template) if opts.verbose: print(is_match(queryMatcher, i, album=opts.album, verbose=True)[1])
def remove_items(lib, query, album, delete): """Remove items matching query from lib. If album, then match and remove whole albums. If delete, also remove files from disk. """ # Get the matching items. items, albums = _do_query(lib, query, album) # Show all the items. for item in items: ui.print_obj(item, lib) # Confirm with user. print_() if delete: prompt = 'Really DELETE %i files (y/n)?' % len(items) else: prompt = 'Really remove %i items from the library (y/n)?' % \ len(items) if not ui.input_yn(prompt, True): return # Remove (and possibly delete) items. with lib.transaction(): if album: for al in albums: al.remove(delete) else: for item in items: lib.remove(item, delete)
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 list_items(lib, query, album, fmt): """Print out items in lib matching query. If album, then search for albums instead of single items. """ tmpl = Template(ui._pick_format(album, fmt)) if album: for album in lib.albums(query): ui.print_obj(album, lib, tmpl) else: for item in lib.items(query): ui.print_obj(item, lib, tmpl)
def list_items(lib, query, album, fmt, config): """Print out items in lib matching query. If album, then search for albums instead of single items. """ tmpl = Template(fmt) if fmt else Template(ui._pick_format(config, album)) if album: for album in lib.albums(query): ui.print_obj(album, lib, config, tmpl) else: for item in lib.items(query): ui.print_obj(item, lib, config, tmpl)
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 random_item(lib, config, opts, args): query = decargs(args) if opts.path: fmt = '$path' else: fmt = opts.format template = Template(fmt) if fmt else None if opts.album: objs = list(lib.albums(query=query)) else: objs = list(lib.items(query=query)) number = min(len(objs), opts.number) objs = random.sample(objs, number) for item in objs: print_obj(item, lib, config, template)
def random_item(lib, opts, args): query = decargs(args) if opts.path: fmt = '$path' else: fmt = opts.format template = Template(fmt) if fmt else None if opts.album: objs = list(lib.albums(query=query)) else: objs = list(lib.items(query=query)) number = min(len(objs), opts.number) objs = random.sample(objs, number) for item in objs: print_obj(item, lib, template)
def write_items(lib, query, pretend): """Write tag information from the database to the respective files in the filesystem. """ items, albums = _do_query(lib, query, False, False) for item in items: # Item deleted? if not os.path.exists(syspath(item.path)): log.info(u'missing file: {0}'.format( util.displayable_path(item.path) )) continue # Get an Item object reflecting the "clean" (on-disk) state. try: clean_item = library.Item.from_path(item.path) except Exception as exc: log.error(u'error reading {0}: {1}'.format( displayable_path(item.path), exc )) continue # Get the keys that have changed between the version. changed = set() for key in library.ITEM_KEYS_META: if item[key] != clean_item[key]: changed.add(key) # Did anything change? if changed: ui.print_obj(item, lib) for key in changed: _showdiff(key, clean_item[key], item[key]) # Actually write the tags. if not pretend: try: item.write() except Exception as exc: log.error(u'could not write {0}: {1}'.format( util.displayable_path(item.path), exc )) continue
def random_item(lib, opts, args): query = decargs(args) if opts.path: fmt = '$path' else: fmt = opts.format template = Template(fmt) if fmt else None if opts.album: objs = list(lib.albums(query=query)) else: objs = list(lib.items(query=query)) if opts.equal_chance: # Group the objects by artist so we can sample from them. key = attrgetter('albumartist') objs.sort(key=key) objs_by_artists = {} for artist, v in groupby(objs, key): objs_by_artists[artist] = list(v) objs = [] for _ in range(opts.number): # Terminate early if we're out of objects to select. if not objs_by_artists: break # Choose an artist and an object for that artist, removing # this choice from the pool. artist = random.choice(objs_by_artists.keys()) objs_from_artist = objs_by_artists[artist] i = random.randint(0, len(objs_from_artist) - 1) objs.append(objs_from_artist.pop(i)) # Remove the artist if we've used up all of its objects. if not objs_from_artist: del objs_by_artists[artist] else: number = min(len(objs), opts.number) objs = random.sample(objs, number) for item in objs: print_obj(item, lib, template)
def _process_item(item, lib, copy=False, move=False, delete=False, tag=False, format=None): """Process Item `item` in `lib`. """ if copy: item.move(basedir=copy, copy=True) item.store() if move: item.move(basedir=move, copy=False) item.store() if delete: item.remove(delete=True) if tag: try: k, v = tag.split('=') except: raise UserError('%s: can\'t parse k=v tag: %s' % (PLUGIN, tag)) setattr(k, v) item.store() print_obj(item, lib, fmt=format)
def write_items(lib, query, pretend): """Write tag information from the database to the respective files in the filesystem. """ items, albums = _do_query(lib, query, False, False) for item in items: # Item deleted? if not os.path.exists(syspath(item.path)): log.info(u'missing file: {0}'.format( util.displayable_path(item.path))) continue # Get an Item object reflecting the "clean" (on-disk) state. try: clean_item = library.Item.from_path(item.path) except Exception as exc: log.error(u'error reading {0}: {1}'.format( displayable_path(item.path), exc)) continue # Get the keys that have changed between the version. changed = set() for key in library.ITEM_KEYS_META: if item[key] != clean_item[key]: changed.add(key) # Did anything change? if changed: ui.print_obj(item, lib) for key in changed: _showdiff(key, clean_item[key], item[key]) # Actually write the tags. if not pretend: try: item.write() except Exception as exc: log.error(u'could not write {0}: {1}'.format( util.displayable_path(item.path), exc)) continue
def _miss(lib, opts, args): self.config.set_args(opts) fmt = self.config['format'].get() count = self.config['count'].get() total = self.config['total'].get() albums = lib.albums(decargs(args)) if total: print(sum([_missing_count(a) for a in albums])) return for album in albums: if count: missing = _missing_count(album) if missing: fmt = "$album: {}".format(missing) print_obj(album, lib, fmt=fmt) continue for item in _missing(album): print_obj(item, lib, fmt=fmt)
def random_item(lib, opts, args): query = decargs(args) if opts.path: fmt = '$path' else: fmt = opts.format template = Template(fmt) if fmt else None if opts.album: objs = list(lib.albums(query=query)) else: objs = list(lib.items(query=query)) if opts.equal_chance: key = attrgetter('albumartist') objs.sort(key=key) # {artists: objects} objs_by_artists = {artist: list(v) for artist, v in groupby(objs, key)} artists = objs_by_artists.keys() # {artist: count} selected_artists = collections.defaultdict(int) for _ in range(opts.number): selected_artists[random.choice(artists)] += 1 objs = [] for artist, count in selected_artists.items(): objs_from_artist = objs_by_artists[artist] number = min(count, len(objs_from_artist)) objs.extend(random.sample(objs_from_artist, number)) else: number = min(len(objs), opts.number) objs = random.sample(objs, number) for item in objs: print_obj(item, lib, template)
def _dup(lib, opts, args): self.config.set_args(opts) fmt = self.config['format'].get() count = self.config['count'].get() album = self.config['album'].get() full = self.config['full'].get() if album: items = lib.albums(decargs(args)) else: items = lib.items(decargs(args)) # Default format string for count mode. if count and not fmt: if album: fmt = '$albumartist - $album' else: fmt = '$albumartist - $album - $title' fmt += ': {}' for obj_id, obj_count, objs in _duplicates(items, full): if obj_id: # Skip empty IDs. for o in objs: print_obj(o, lib, fmt=fmt.format(obj_count))
def _miss(lib, opts, args): self.config.set_args(opts) fmt = self.config['format'].get() count = self.config['count'].get() total = self.config['total'].get() albums = lib.albums(decargs(args)) if total: print(sum([_missing_count(a) for a in albums])) return # Default format string for count mode. if count and not fmt: fmt = '$albumartist - $album: $missing' for album in albums: if count: missing = _missing_count(album) if missing: print_obj(album, lib, fmt=fmt) else: for item in _missing(album): print_obj(item, lib, fmt=fmt)
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 update_items(lib, query, album, move, color, pretend, config): """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, config) 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. ui.print_obj(item, lib, config) 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 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) 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) 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 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. ui.print_obj(item, lib) for key, (oldval, newval) in changes.iteritems(): _showdiff(key, oldval, newval) # 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()