def remove_items(lib, query, album, delete=False): """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: print_(item.artist + ' - ' + item.album + ' - ' + item.title) # 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. if album: for al in albums: al.remove(delete) else: for item in items: lib.remove(item, delete) lib.save()
def show_stats(lib, query): """Shows some statistics about the matched items.""" items = lib.items(query) total_size = 0 total_time = 0.0 total_items = 0 artists = set() albums = set() for item in items: #fixme This is approximate, so people might complain that # this total size doesn't match "du -sh". Could fix this # by putting total file size in the database. total_size += int(item.length * item.bitrate / 8) total_time += item.length total_items += 1 artists.add(item.artist) albums.add(item.album) print_("""Tracks: %i Total time: %s Total size: %s Artists: %i Albums: %i""" % ( total_items, ui.human_seconds(total_time), ui.human_bytes(total_size), len(artists), len(albums) ))
def choose_match(task, config): """Given an initial autotagging of items, go through an interactive dance with the user to ask for a choice of metadata. Returns an (info, items) pair, ASIS, or SKIP. """ # Show what we're tagging. print_() print_(task.path) if config.quiet: # No input; just make a decision. if task.rec == autotag.RECOMMEND_STRONG: dist, items, info = task.candidates[0] show_change(task.cur_artist, task.cur_album, items, info, dist, config.color) return info, items else: return _quiet_fall_back(config) # Loop until we have a choice. candidates, rec = task.candidates, task.rec while True: # Ask for a choice from the user. choice = choose_candidate(candidates, False, rec, config.color, config.timid, task.cur_artist, task.cur_album) # Choose which tags to use. if choice in (importer.action.SKIP, importer.action.ASIS, importer.action.TRACKS): # Pass selection to main control flow. return choice elif choice is importer.action.MANUAL: # Try again with manual search terms. search_artist, search_album = manual_search(False) try: _, _, candidates, rec = \ autotag.tag_album(task.items, config.timid, search_artist, search_album) except autotag.AutotagError: candidates, rec = None, None elif choice is importer.action.MANUAL_ID: # Try a manually-entered ID. search_id = manual_id(False) if search_id: try: _, _, candidates, rec = \ autotag.tag_album(task.items, config.timid, search_id=search_id) except autotag.AutotagError: candidates, rec = None, None else: # We have a candidate! Finish tagging. Here, choice is # an (info, items) pair as desired. assert not isinstance(choice, importer.action) return choice
def _quiet_fall_back(config): """Show the user that the default action is being taken because we're in quiet mode and the recommendation is not strong. """ if config.quiet_fallback == importer.action.SKIP: print_('Skipping.') elif config.quiet_fallback == importer.action.ASIS: print_('Importing as-is.') else: assert(False) return config.quiet_fallback
def _showdiff(field, oldval, newval, color): """Prints out a human-readable field difference line.""" # Considering floats incomparable for perfect equality, introduce # an epsilon tolerance. if isinstance(oldval, float) and isinstance(newval, float) and \ abs(oldval - newval) < FLOAT_EPSILON: return if newval != oldval: if color: oldval, newval = ui.colordiff(oldval, newval) else: oldval, newval = unicode(oldval), unicode(newval) print_(u' %s: %s -> %s' % (field, oldval, newval))
def list_items(lib, query, album, path, fmt): """Print out items in lib matching query. If album, then search for albums instead of single items. If path, print the matched objects' paths instead of human-readable information about them. """ if fmt is None: # If no specific template is supplied, use a default. if album: fmt = u'$albumartist - $album' else: fmt = u'$artist - $album - $title' template = Template(fmt) if album: for album in lib.albums(query): if path: print_(album.item_dir()) elif fmt is not None: print_(template.substitute(album._record)) else: for item in lib.items(query): if path: print_(item.path) elif fmt is not None: print_(template.substitute(item.record))
def choose_item(task, config): """Ask the user for a choice about tagging a single item. Returns either an action constant or a TrackMatch object. """ print_() print_(task.item.path) candidates, rec = task.candidates, task.rec if config.quiet: # Quiet mode; make a decision. if rec == autotag.RECOMMEND_STRONG: match = candidates[0] show_item_change(task.item, match, config.color) return match else: return _quiet_fall_back(config) while True: # Ask for a choice. choice = choose_candidate(candidates, True, rec, config.color, config.timid, item=task.item) if choice in (importer.action.SKIP, importer.action.ASIS): return choice elif choice == importer.action.TRACKS: assert False # TRACKS is only legal for albums. elif choice == importer.action.MANUAL: # Continue in the loop with a new set of candidates. search_artist, search_title = manual_search(True) candidates, rec = autotag.tag_item(task.item, config.timid, search_artist, search_title) elif choice == importer.action.MANUAL_ID: # Ask for a track ID. search_id = manual_id(True) if search_id: candidates, rec = autotag.tag_item(task.item, config.timid, search_id=search_id) else: # Chose a candidate. assert isinstance(choice, autotag.TrackMatch) return choice
def show_album(artist, album, partial=False): if artist: album_description = u' %s - %s' % (artist, album) elif album: album_description = u' %s' % album else: album_description = u' (unknown album)' # Add a suffix if this is a partial match. if partial: warning = PARTIAL_MATCH_MESSAGE else: warning = None if color and warning: warning = ui.colorize('yellow', warning) out = album_description if warning: out += u' ' + warning print_(out)
def choose_item(task, config): """Ask the user for a choice about tagging a single item. Returns either an action constant or a TrackInfo object. """ print_() print_(task.item.path) candidates, rec = task.item_match if config.quiet: # Quiet mode; make a decision. if rec == autotag.RECOMMEND_STRONG: dist, track_info = candidates[0] show_item_change(task.item, track_info, dist, config.color) return track_info else: return _quiet_fall_back(config) while True: # Ask for a choice. choice = choose_candidate(candidates, True, rec, config.color, config.timid, item=task.item) if choice in (importer.action.SKIP, importer.action.ASIS): return choice elif choice == importer.action.TRACKS: assert False # TRACKS is only legal for albums. elif choice == importer.action.MANUAL: # Continue in the loop with a new set of candidates. search_artist, search_title = manual_search(True) candidates, rec = autotag.tag_item(task.item, config.timid, search_artist, search_title) elif choice == importer.action.MANUAL_ID: # Ask for a track ID. search_id = manual_id(True) if search_id: candidates, rec = autotag.tag_item(task.item, config.timid, search_id=search_id) else: # Chose a candidate. assert not isinstance(choice, importer.action) return choice
def show_album(artist, album): if artist: print_(' %s - %s' % (artist, album)) elif album: print_(' %s' % album) else: print_(' (unknown album)')
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 list_items(lib, query, album, path): """Print out items in lib matching query. If album, then search for albums instead of single items. If path, print the matched objects' paths instead of human-readable information about them. """ if album: for album in lib.albums(query): if path: print_(album.item_dir()) else: print_(album.albumartist + u' - ' + album.album) else: for item in lib.items(query): if path: print_(item.path) else: print_(item.artist + u' - ' + item.album + u' - ' + item.title)
def list_items(lib, query, album, path, fmt): """Print out items in lib matching query. If album, then search for albums instead of single items. If path, print the matched objects' paths instead of human-readable information about them. """ template = Template(fmt) if album: for album in lib.albums(query): if path: print_(album.item_dir()) elif fmt is not None: print_(album.evaluate_template(template)) else: for item in lib.items(query): if path: print_(item.path) elif fmt is not None: print_(item.evaluate_template(template, lib))
def show_version(lib, config, opts, args): print_('beets version %s' % lib.beets.__version__) # Show plugins. names = [] for plugin in plugins.find_plugins(): modname = plugin.__module__ names.append(modname.split('.')[-1]) if names: print_('plugins:', ', '.join(names)) else: print_('no plugins loaded')
def choose_candidate(candidates, singleton, rec, color, timid, cur_artist=None, cur_album=None, item=None, itemcount=None, per_disc_numbering=False): """Given a sorted list of candidates, ask the user for a selection of which candidate to use. Applies to both full albums and singletons (tracks). Candidates are either AlbumMatch or TrackMatch objects depending on `singleton`. for albums, `cur_artist`, `cur_album`, and `itemcount` must be provided. For singletons, `item` must be provided. Returns the result of the choice, which may SKIP, ASIS, TRACKS, or MANUAL or a candidate (an AlbumMatch/TrackMatch object). """ # Sanity check. if singleton: assert item is not None else: assert cur_artist is not None assert cur_album is not None # Zero candidates. if not candidates: if singleton: print_("No matching recordings found.") opts = ('Use as-is', 'Skip', 'Enter search', 'enter Id', 'aBort') else: print_( "No matching release found for {0} tracks.".format(itemcount)) print_('For help, see: ' 'https://github.com/sampsyo/beets/wiki/FAQ#wiki-nomatch') opts = ('Use as-is', 'as Tracks', 'Skip', 'Enter search', 'enter Id', 'aBort') sel = ui.input_options(opts, color=color) if sel == 'u': return importer.action.ASIS elif sel == 't': assert not singleton return importer.action.TRACKS elif sel == 'e': return importer.action.MANUAL elif sel == 's': return importer.action.SKIP elif sel == 'b': raise importer.ImportAbort() elif sel == 'i': return importer.action.MANUAL_ID else: assert False # Is the change good enough? bypass_candidates = False if rec != autotag.RECOMMEND_NONE: match = candidates[0] bypass_candidates = True while True: # Display and choose from candidates. if not bypass_candidates: # Display list of candidates. if singleton: print_('Finding tags for track "%s - %s".' % (item.artist, item.title)) print_('Candidates:') for i, match in enumerate(candidates): print_('%i. %s - %s (%s)' % (i + 1, match.info.artist, match.info.title, dist_string(match.distance, color))) else: print_('Finding tags for album "%s - %s".' % (cur_artist, cur_album)) print_('Candidates:') for i, match in enumerate(candidates): line = '%i. %s - %s' % (i + 1, match.info.artist, match.info.album) # Label and year disambiguation, if available. label, year = None, None if match.info.label: label = match.info.label if match.info.year: year = unicode(match.info.year) if label and year: line += u' [%s, %s]' % (label, year) elif label: line += u' [%s]' % label elif year: line += u' [%s]' % year line += ' (%s)' % dist_string(match.distance, color) # Point out the partial matches. if match.extra_items or match.extra_tracks: warning = PARTIAL_MATCH_MESSAGE if color: warning = ui.colorize('yellow', warning) line += u' %s' % warning print_(line) # Ask the user for a choice. if singleton: opts = ('Skip', 'Use as-is', 'Enter search', 'enter Id', 'aBort') else: opts = ('Skip', 'Use as-is', 'as Tracks', 'Enter search', 'enter Id', 'aBort') sel = ui.input_options(opts, numrange=(1, len(candidates)), color=color) if sel == 's': return importer.action.SKIP elif sel == 'u': return importer.action.ASIS elif sel == 'e': return importer.action.MANUAL elif sel == 't': assert not singleton return importer.action.TRACKS elif sel == 'b': raise importer.ImportAbort() elif sel == 'i': return importer.action.MANUAL_ID else: # Numerical selection. if singleton: match = candidates[sel - 1] else: match = candidates[sel - 1] bypass_candidates = False # Show what we're about to do. if singleton: show_item_change(item, match, color) else: show_change(cur_artist, cur_album, match, color, per_disc_numbering) # Exact match => tag automatically if we're not in timid mode. if rec == autotag.RECOMMEND_STRONG and not timid: return match # Ask for confirmation. if singleton: opts = ('Apply', 'More candidates', 'Skip', 'Use as-is', 'Enter search', 'enter Id', 'aBort') else: opts = ('Apply', 'More candidates', 'Skip', 'Use as-is', 'as Tracks', 'Enter search', 'enter Id', 'aBort') sel = ui.input_options(opts, color=color) if sel == 'a': return match elif sel == 'm': pass elif sel == 's': return importer.action.SKIP elif sel == 'u': return importer.action.ASIS elif sel == 't': assert not singleton return importer.action.TRACKS elif sel == 'e': return importer.action.MANUAL elif sel == 'b': raise importer.ImportAbort() elif sel == 'i': return importer.action.MANUAL_ID
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 show_change(cur_artist, cur_album, match, color=True, per_disc_numbering=False): """Print out a representation of the changes that will be made if an album's tags are changed according to `match`, which must be an AlbumMatch object. """ def show_album(artist, album, partial=False): if artist: album_description = u' %s - %s' % (artist, album) elif album: album_description = u' %s' % album else: album_description = u' (unknown album)' # Add a suffix if this is a partial match. if partial: warning = PARTIAL_MATCH_MESSAGE else: warning = None if color and warning: warning = ui.colorize('yellow', warning) out = album_description if warning: out += u' ' + warning print_(out) def format_index(track_info): """Return a string representing the track index of the given TrackInfo object. """ if per_disc_numbering: if match.info.mediums > 1: return u'{0}-{1}'.format(track_info.medium, track_info.medium_index) else: return unicode(track_info.medium_index) else: return unicode(track_info.index) # Identify the album in question. if cur_artist != match.info.artist or \ (cur_album != match.info.album and match.info.album != VARIOUS_ARTISTS): artist_l, artist_r = cur_artist or '', match.info.artist album_l, album_r = cur_album or '', match.info.album if artist_r == VARIOUS_ARTISTS: # Hide artists for VA releases. artist_l, artist_r = u'', u'' if color: artist_l, artist_r = ui.colordiff(artist_l, artist_r) album_l, album_r = ui.colordiff(album_l, album_r) print_("Correcting tags from:") show_album(artist_l, album_l) print_("To:") show_album(artist_r, album_r) else: message = u"Tagging: %s - %s" % (match.info.artist, match.info.album) if match.extra_items or match.extra_tracks: warning = PARTIAL_MATCH_MESSAGE if color: warning = ui.colorize('yellow', PARTIAL_MATCH_MESSAGE) message += u' ' + warning print_(message) # Distance/similarity. print_('(Similarity: %s)' % dist_string(match.distance, color)) # Tracks. pairs = match.mapping.items() pairs.sort(key=lambda (_, track_info): track_info.index) for item, track_info in pairs: # Get displayable LHS and RHS values. cur_track = unicode(item.track) new_track = format_index(track_info) tracks_differ = item.track not in (track_info.index, track_info.medium_index) cur_title = item.title new_title = track_info.title if item.length and track_info.length: cur_length = ui.human_seconds_short(item.length) new_length = ui.human_seconds_short(track_info.length) if color: cur_length = ui.colorize('red', cur_length) new_length = ui.colorize('red', new_length) # Possibly colorize changes. if color: cur_title, new_title = ui.colordiff(cur_title, new_title) cur_track = ui.colorize('red', cur_track) new_track = ui.colorize('red', new_track) # Show filename (non-colorized) when title is not set. if not item.title.strip(): cur_title = displayable_path(os.path.basename(item.path)) if cur_title != new_title: lhs, rhs = cur_title, new_title if tracks_differ: lhs += u' (%s)' % cur_track rhs += u' (%s)' % new_track print_(u" * %s -> %s" % (lhs, rhs)) else: line = u' * %s' % item.title display = False if tracks_differ: display = True line += u' (%s -> %s)' % (cur_track, new_track) if item.length and track_info.length and \ abs(item.length - track_info.length) > 2.0: display = True line += u' (%s vs. %s)' % (cur_length, new_length) if display: print_(line) # Missing and unmatched tracks. for track_info in match.extra_tracks: line = u' * Missing track: {0} ({1})'.format(track_info.title, format_index(track_info)) if color: line = ui.colorize('yellow', line) print_(line) for item in match.extra_items: line = u' * Unmatched track: {0} ({1})'.format(item.title, item.track) if color: line = ui.colorize('yellow', line) print_(line)
def choose_candidate(candidates, singleton, rec, color, timid, cur_artist=None, cur_album=None, item=None, itemcount=None, per_disc_numbering=False): """Given a sorted list of candidates, ask the user for a selection of which candidate to use. Applies to both full albums and singletons (tracks). Candidates are either AlbumMatch or TrackMatch objects depending on `singleton`. for albums, `cur_artist`, `cur_album`, and `itemcount` must be provided. For singletons, `item` must be provided. Returns the result of the choice, which may SKIP, ASIS, TRACKS, or MANUAL or a candidate (an AlbumMatch/TrackMatch object). """ # Sanity check. if singleton: assert item is not None else: assert cur_artist is not None assert cur_album is not None # Zero candidates. if not candidates: if singleton: print_("No matching recordings found.") opts = ('Use as-is', 'Skip', 'Enter search', 'enter Id', 'aBort') else: print_("No matching release found for {0} tracks." .format(itemcount)) print_('For help, see: ' 'https://github.com/sampsyo/beets/wiki/FAQ#wiki-nomatch') opts = ('Use as-is', 'as Tracks', 'Skip', 'Enter search', 'enter Id', 'aBort') sel = ui.input_options(opts, color=color) if sel == 'u': return importer.action.ASIS elif sel == 't': assert not singleton return importer.action.TRACKS elif sel == 'e': return importer.action.MANUAL elif sel == 's': return importer.action.SKIP elif sel == 'b': raise importer.ImportAbort() elif sel == 'i': return importer.action.MANUAL_ID else: assert False # Is the change good enough? bypass_candidates = False if rec != autotag.RECOMMEND_NONE: match = candidates[0] bypass_candidates = True while True: # Display and choose from candidates. if not bypass_candidates: # Display list of candidates. if singleton: print_('Finding tags for track "%s - %s".' % (item.artist, item.title)) print_('Candidates:') for i, match in enumerate(candidates): print_('%i. %s - %s (%s)' % (i + 1, match.info.artist, match.info.title, dist_string(match.distance, color))) else: print_('Finding tags for album "%s - %s".' % (cur_artist, cur_album)) print_('Candidates:') for i, match in enumerate(candidates): line = '%i. %s - %s' % (i + 1, match.info.artist, match.info.album) # Label and year disambiguation, if available. label, year = None, None if match.info.label: label = match.info.label if match.info.year: year = unicode(match.info.year) if label and year: line += u' [%s, %s]' % (label, year) elif label: line += u' [%s]' % label elif year: line += u' [%s]' % year line += ' (%s)' % dist_string(match.distance, color) # Point out the partial matches. if match.extra_items or match.extra_tracks: warning = PARTIAL_MATCH_MESSAGE if color: warning = ui.colorize('yellow', warning) line += u' %s' % warning print_(line) # Ask the user for a choice. if singleton: opts = ('Skip', 'Use as-is', 'Enter search', 'enter Id', 'aBort') else: opts = ('Skip', 'Use as-is', 'as Tracks', 'Enter search', 'enter Id', 'aBort') sel = ui.input_options(opts, numrange=(1, len(candidates)), color=color) if sel == 's': return importer.action.SKIP elif sel == 'u': return importer.action.ASIS elif sel == 'e': return importer.action.MANUAL elif sel == 't': assert not singleton return importer.action.TRACKS elif sel == 'b': raise importer.ImportAbort() elif sel == 'i': return importer.action.MANUAL_ID else: # Numerical selection. if singleton: match = candidates[sel - 1] else: match = candidates[sel - 1] bypass_candidates = False # Show what we're about to do. if singleton: show_item_change(item, match, color) else: show_change(cur_artist, cur_album, match, color, per_disc_numbering) # Exact match => tag automatically if we're not in timid mode. if rec == autotag.RECOMMEND_STRONG and not timid: return match # Ask for confirmation. if singleton: opts = ('Apply', 'More candidates', 'Skip', 'Use as-is', 'Enter search', 'enter Id', 'aBort') else: opts = ('Apply', 'More candidates', 'Skip', 'Use as-is', 'as Tracks', 'Enter search', 'enter Id', 'aBort') sel = ui.input_options(opts, color=color) if sel == 'a': return match elif sel == 'm': pass elif sel == 's': return importer.action.SKIP elif sel == 'u': return importer.action.ASIS elif sel == 't': assert not singleton return importer.action.TRACKS elif sel == 'e': return importer.action.MANUAL elif sel == 'b': raise importer.ImportAbort() elif sel == 'i': return importer.action.MANUAL_ID
def choose_candidate(candidates, singleton, rec, color, timid, cur_artist=None, cur_album=None, item=None): """Given a sorted list of candidates, ask the user for a selection of which candidate to use. Applies to both full albums and singletons (tracks). For albums, the candidates are `(dist, items, info)` triples and `cur_artist` and `cur_album` must be provided. For singletons, the candidates are `(dist, info)` pairs and `item` must be provided. Returns the result of the choice, which may SKIP, ASIS, TRACKS, or MANUAL or a candidate. For albums, a candidate is a `(info, items)` pair; for items, it is just a TrackInfo object. """ # Sanity check. if singleton: assert item is not None else: assert cur_artist is not None assert cur_album is not None # Zero candidates. if not candidates: print_("No match found.") if singleton: opts = ('Use as-is', 'Skip', 'Enter search', 'enter Id', 'aBort') else: opts = ('Use as-is', 'as Tracks', 'Skip', 'Enter search', 'enter Id', 'aBort') sel = ui.input_options(opts, color=color) if sel == 'u': return importer.action.ASIS elif sel == 't': assert not singleton return importer.action.TRACKS elif sel == 'e': return importer.action.MANUAL elif sel == 's': return importer.action.SKIP elif sel == 'b': raise importer.ImportAbort() elif sel == 'i': return importer.action.MANUAL_ID else: assert False # Is the change good enough? bypass_candidates = False if rec != autotag.RECOMMEND_NONE: if singleton: dist, info = candidates[0] else: dist, items, info = candidates[0] bypass_candidates = True while True: # Display and choose from candidates. if not bypass_candidates: # Display list of candidates. if singleton: print_('Finding tags for track "%s - %s".' % (item.artist, item.title)) print_('Candidates:') for i, (dist, info) in enumerate(candidates): print_('%i. %s - %s (%s)' % (i+1, info.artist, info.title, dist_string(dist, color))) else: print_('Finding tags for album "%s - %s".' % (cur_artist, cur_album)) print_('Candidates:') for i, (dist, items, info) in enumerate(candidates): line = '%i. %s - %s' % (i+1, info.artist, info.album) # Label and year disambiguation, if available. label, year = None, None if info.label: label = info.label if info.year: year = unicode(info.year) if label and year: line += u' [%s, %s]' % (label, year) elif label: line += u' [%s]' % label elif year: line += u' [%s]' % year line += ' (%s)' % dist_string(dist, color) # Point out the partial matches. if None in items: warning = PARTIAL_MATCH_MESSAGE if color: warning = ui.colorize('yellow', warning) line += u' %s' % warning print_(line) # Ask the user for a choice. if singleton: opts = ('Skip', 'Use as-is', 'Enter search', 'enter Id', 'aBort') else: opts = ('Skip', 'Use as-is', 'as Tracks', 'Enter search', 'enter Id', 'aBort') sel = ui.input_options(opts, numrange=(1, len(candidates)), color=color) if sel == 's': return importer.action.SKIP elif sel == 'u': return importer.action.ASIS elif sel == 'e': return importer.action.MANUAL elif sel == 't': assert not singleton return importer.action.TRACKS elif sel == 'b': raise importer.ImportAbort() elif sel == 'i': return importer.action.MANUAL_ID else: # Numerical selection. if singleton: dist, info = candidates[sel-1] else: dist, items, info = candidates[sel-1] bypass_candidates = False # Show what we're about to do. if singleton: show_item_change(item, info, dist, color) else: show_change(cur_artist, cur_album, items, info, dist, color) # Exact match => tag automatically if we're not in timid mode. if rec == autotag.RECOMMEND_STRONG and not timid: if singleton: return info else: return info, items # Ask for confirmation. if singleton: opts = ('Apply', 'More candidates', 'Skip', 'Use as-is', 'Enter search', 'enter Id', 'aBort') else: opts = ('Apply', 'More candidates', 'Skip', 'Use as-is', 'as Tracks', 'Enter search', 'enter Id', 'aBort') sel = ui.input_options(opts, color=color) if sel == 'a': if singleton: return info else: return info, items elif sel == 'm': pass elif sel == 's': return importer.action.SKIP elif sel == 'u': return importer.action.ASIS elif sel == 't': assert not singleton return importer.action.TRACKS elif sel == 'e': return importer.action.MANUAL elif sel == 'b': raise importer.ImportAbort() elif sel == 'i': return importer.action.MANUAL_ID
def show_item_change(item, info, dist, color): """Print out the change that would occur by tagging `item` with the metadata from `info`. """ cur_artist, new_artist = item.artist, info.artist cur_title, new_title = item.title, info.title if cur_artist != new_artist or cur_title != new_title: if color: cur_artist, new_artist = ui.colordiff(cur_artist, new_artist) cur_title, new_title = ui.colordiff(cur_title, new_title) print_("Correcting track tags from:") print_(" %s - %s" % (cur_artist, cur_title)) print_("To:") print_(" %s - %s" % (new_artist, new_title)) else: print_("Tagging track: %s - %s" % (cur_artist, cur_title)) print_('(Similarity: %s)' % dist_string(dist, color))
def show_change(cur_artist, cur_album, items, info, dist, color=True): """Print out a representation of the changes that will be made if tags are changed from (cur_artist, cur_album, items) to info with distance dist. """ def show_album(artist, album): if artist: print_(' %s - %s' % (artist, album)) elif album: print_(' %s' % album) else: print_(' (unknown album)') # Identify the album in question. if cur_artist != info.artist or \ (cur_album != info.album and info.album != VARIOUS_ARTISTS): artist_l, artist_r = cur_artist or '', info.artist album_l, album_r = cur_album or '', info.album if artist_r == VARIOUS_ARTISTS: # Hide artists for VA releases. artist_l, artist_r = u'', u'' if color: artist_l, artist_r = ui.colordiff(artist_l, artist_r) album_l, album_r = ui.colordiff(album_l, album_r) print_("Correcting tags from:") show_album(artist_l, album_l) print_("To:") show_album(artist_r, album_r) else: print_("Tagging: %s - %s" % (info.artist, info.album)) # Distance/similarity. print_('(Similarity: %s)' % dist_string(dist, color)) # Tracks. for i, (item, track_info) in enumerate(zip(items, info.tracks)): cur_track = str(item.track) new_track = str(i+1) cur_title = item.title new_title = track_info.title # Possibly colorize changes. if color: cur_title, new_title = ui.colordiff(cur_title, new_title) if cur_track != new_track: cur_track = ui.colorize('red', cur_track) new_track = ui.colorize('red', new_track) # Show filename (non-colorized) when title is not set. if not item.title.strip(): cur_title = os.path.basename(item.path) if cur_title != new_title and cur_track != new_track: print_(" * %s (%s) -> %s (%s)" % ( cur_title, cur_track, new_title, new_track )) elif cur_title != new_title: print_(" * %s -> %s" % (cur_title, new_title)) elif cur_track != new_track: print_(" * %s (%s -> %s)" % (item.title, cur_track, new_track))
def show_change(cur_artist, cur_album, items, info, dist, color=True): """Print out a representation of the changes that will be made if tags are changed from (cur_artist, cur_album, items) to info with distance dist. """ def show_album(artist, album, partial=False): if artist: album_description = u' %s - %s' % (artist, album) elif album: album_description = u' %s' % album else: album_description = u' (unknown album)' # Add a suffix if this is a partial match. if partial: warning = PARTIAL_MATCH_MESSAGE else: warning = None if color and warning: warning = ui.colorize('yellow', warning) out = album_description if warning: out += u' ' + warning print_(out) # Record if the match is partial or not. partial_match = None in items # Identify the album in question. if cur_artist != info.artist or \ (cur_album != info.album and info.album != VARIOUS_ARTISTS): artist_l, artist_r = cur_artist or '', info.artist album_l, album_r = cur_album or '', info.album if artist_r == VARIOUS_ARTISTS: # Hide artists for VA releases. artist_l, artist_r = u'', u'' if color: artist_l, artist_r = ui.colordiff(artist_l, artist_r) album_l, album_r = ui.colordiff(album_l, album_r) print_("Correcting tags from:") show_album(artist_l, album_l) print_("To:") show_album(artist_r, album_r) else: message = u"Tagging: %s - %s" % (info.artist, info.album) if partial_match: warning = PARTIAL_MATCH_MESSAGE if color: warning = ui.colorize('yellow', PARTIAL_MATCH_MESSAGE) message += u' ' + warning print_(message) # Distance/similarity. print_('(Similarity: %s)' % dist_string(dist, color)) # Tracks. missing_tracks = [] for i, (item, track_info) in enumerate(zip(items, info.tracks)): if not item: missing_tracks.append((i, track_info)) continue # Get displayable LHS and RHS values. cur_track = unicode(item.track) new_track = unicode(i+1) cur_title = item.title new_title = track_info.title if item.length and track_info.length: cur_length = ui.human_seconds_short(item.length) new_length = ui.human_seconds_short(track_info.length) if color: cur_length = ui.colorize('red', cur_length) new_length = ui.colorize('red', new_length) # Possibly colorize changes. if color: cur_title, new_title = ui.colordiff(cur_title, new_title) if cur_track != new_track: cur_track = ui.colorize('red', cur_track) new_track = ui.colorize('red', new_track) # Show filename (non-colorized) when title is not set. if not item.title.strip(): cur_title = displayable_path(os.path.basename(item.path)) if cur_title != new_title: lhs, rhs = cur_title, new_title if cur_track != new_track: lhs += u' (%s)' % cur_track rhs += u' (%s)' % new_track print_(u" * %s -> %s" % (lhs, rhs)) else: line = u' * %s' % item.title display = False if cur_track != new_track: display = True line += u' (%s -> %s)' % (cur_track, new_track) if item.length and track_info.length and \ abs(item.length - track_info.length) > 2.0: display = True line += u' (%s -> %s)' % (cur_length, new_length) if display: print_(line) for i, track_info in missing_tracks: line = u' * Missing track: %s (%d)' % (track_info.title, i+1) if color: line = ui.colorize('yellow', line) print_(line)