diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 3c86b2969..e766854a7 100644 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -39,6 +39,8 @@ from beets import library from beets import config from beets.util.confit import _package_path +VARIOUS_ARTISTS = u'Various Artists' + # Global logger. log = logging.getLogger('beets') @@ -47,10 +49,8 @@ log = logging.getLogger('beets') default_commands = [] - # Utilities. - def _do_query(lib, query, album, also_items=True): """For commands that operate on matched items, performs a query and returns a list of matching items and a list of matching @@ -79,8 +79,11 @@ def _do_query(lib, query, album, also_items=True): # fields: Shows a list of available fields for queries and format strings. -fields_cmd = ui.Subcommand('fields', - help='show fields available for queries and format strings') +fields_cmd = ui.Subcommand( + 'fields', + help='show fields available for queries and format strings' +) + def fields_func(lib, opts, args): def _print_rows(names): @@ -112,8 +115,6 @@ default_commands.append(fields_cmd) # import: Autotagger and importer. -VARIOUS_ARTISTS = u'Various Artists' - # Importer utilities and support. def disambig_string(info): @@ -145,6 +146,7 @@ def disambig_string(info): if disambig: return u', '.join(disambig) + def dist_string(dist): """Formats a distance (a float) as a colorized similarity percentage string. @@ -158,6 +160,7 @@ def dist_string(dist): out = ui.colorize('red', out) return out + def penalty_string(distance, limit=None): """Returns a colorized string that indicates all the penalties applied to a distance object. @@ -173,6 +176,7 @@ def penalty_string(distance, limit=None): penalties = penalties[:limit] + ['...'] return ui.colorize('yellow', '(%s)' % ', '.join(penalties)) + def show_change(cur_artist, cur_album, match): """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 @@ -213,13 +217,13 @@ def show_change(cur_artist, cur_album, match): (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 + 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'' artist_l, artist_r = ui.colordiff(artist_l, artist_r) - album_l, album_r = ui.colordiff(album_l, album_r) + album_l, album_r = ui.colordiff(album_l, album_r) print_("Correcting tags from:") show_album(artist_l, album_l) @@ -292,14 +296,14 @@ def show_change(cur_artist, cur_album, match): else: color = 'red' if (cur_track + new_track).count('-') == 1: - lhs_track, rhs_track = ui.colorize(color, cur_track), \ - ui.colorize(color, new_track) + lhs_track, rhs_track = (ui.colorize(color, cur_track), + ui.colorize(color, new_track)) else: color = 'red' lhs_track, rhs_track = ui.color_diff_suffix(cur_track, new_track) - templ = ui.colorize(color, u' (#') + u'{0}' + \ - ui.colorize(color, u')') + templ = (ui.colorize(color, u' (#') + u'{0}' + + ui.colorize(color, u')')) lhs += templ.format(lhs_track) rhs += templ.format(rhs_track) lhs_width += len(cur_track) + 4 @@ -312,8 +316,8 @@ def show_change(cur_artist, cur_album, match): new_length = ui.human_seconds_short(track_info.length) lhs_length, rhs_length = ui.color_diff_suffix(cur_length, new_length) - templ = ui.colorize('red', u' (') + u'{0}' + \ - ui.colorize('red', u')') + templ = (ui.colorize('red', u' (') + u'{0}' + + ui.colorize('red', u')')) lhs += templ.format(lhs_length) rhs += templ.format(rhs_length) lhs_width += len(cur_length) + 3 @@ -357,6 +361,7 @@ def show_change(cur_artist, cur_album, match): line += ' (%s)' % ui.human_seconds_short(item.length) print_(ui.colorize('yellow', line)) + def show_item_change(item, match): """Print out the change that would occur by tagging `item` with the metadata from `match`, a TrackMatch object. @@ -394,6 +399,7 @@ def show_item_change(item, match): info.append(ui.colorize('lightgray', '(%s)' % disambig)) print_(' '.join(info)) + def _summary_judment(rec): """Determines whether a decision should be made without even asking the user. This occurs in quiet mode and when an action is chosen for @@ -426,6 +432,7 @@ def _summary_judment(rec): print_('Importing as-is.') return action + def choose_candidate(candidates, singleton, rec, cur_artist=None, cur_album=None, item=None, itemcount=None): """Given a sorted list of candidates, ask the user for a selection @@ -597,6 +604,7 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None, elif sel == 'i': return importer.action.MANUAL_ID + def manual_search(singleton): """Input either an artist and album (for full albums) or artist and track name (for singletons) for manual search. @@ -605,12 +613,14 @@ def manual_search(singleton): name = input_('Track:' if singleton else 'Album:') return artist.strip(), name.strip() + def manual_id(singleton): """Input an ID, either for an album ("release") or a track ("recording"). """ prompt = u'Enter {0} ID:'.format('recording' if singleton else 'release') return input_(prompt).strip() + class TerminalImportSession(importer.ImportSession): """An import session that runs in a terminal. """ @@ -637,12 +647,14 @@ class TerminalImportSession(importer.ImportSession): candidates, rec = task.candidates, task.rec while True: # Ask for a choice from the user. - choice = choose_candidate(candidates, False, rec, task.cur_artist, - task.cur_album, itemcount=len(task.items)) + choice = choose_candidate( + candidates, False, rec, task.cur_artist, task.cur_album, + itemcount=len(task.items) + ) # Choose which tags to use. if choice in (importer.action.SKIP, importer.action.ASIS, - importer.action.TRACKS, importer.action.ALBUMS): + importer.action.TRACKS, importer.action.ALBUMS): # Pass selection to main control flow. return choice elif choice is importer.action.MANUAL: @@ -688,18 +700,18 @@ class TerminalImportSession(importer.ImportSession): if choice in (importer.action.SKIP, importer.action.ASIS): return choice elif choice == importer.action.TRACKS: - assert False # TRACKS is only legal for albums. + 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, search_artist, - search_title) + 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, - search_id=search_id) + search_id=search_id) else: # Chose a candidate. assert isinstance(choice, autotag.TrackMatch) @@ -740,6 +752,7 @@ class TerminalImportSession(importer.ImportSession): # The import command. + def import_files(lib, paths, query): """Import the files in the given list of paths or matching the query. @@ -783,43 +796,79 @@ def import_files(lib, paths, query): # Emit event. plugins.send('import', lib=lib, paths=paths) -import_cmd = ui.Subcommand('import', help='import new music', - aliases=('imp', 'im')) -import_cmd.parser.add_option('-c', '--copy', action='store_true', - default=None, help="copy tracks into library directory (default)") -import_cmd.parser.add_option('-C', '--nocopy', action='store_false', - dest='copy', help="don't copy tracks (opposite of -c)") -import_cmd.parser.add_option('-w', '--write', action='store_true', - default=None, help="write new metadata to files' tags (default)") -import_cmd.parser.add_option('-W', '--nowrite', action='store_false', - dest='write', help="don't write metadata (opposite of -w)") -import_cmd.parser.add_option('-a', '--autotag', action='store_true', - dest='autotag', help="infer tags for imported files (default)") -import_cmd.parser.add_option('-A', '--noautotag', action='store_false', - dest='autotag', - help="don't infer tags for imported files (opposite of -a)") -import_cmd.parser.add_option('-p', '--resume', action='store_true', - default=None, help="resume importing if interrupted") -import_cmd.parser.add_option('-P', '--noresume', action='store_false', - dest='resume', help="do not try to resume importing") -import_cmd.parser.add_option('-q', '--quiet', action='store_true', - dest='quiet', help="never prompt for input: skip albums instead") -import_cmd.parser.add_option('-l', '--log', dest='log', - help='file to log untaggable albums for later review') -import_cmd.parser.add_option('-s', '--singletons', action='store_true', - help='import individual tracks instead of full albums') -import_cmd.parser.add_option('-t', '--timid', dest='timid', - action='store_true', help='always confirm all actions') -import_cmd.parser.add_option('-L', '--library', dest='library', - action='store_true', help='retag items matching a query') -import_cmd.parser.add_option('-i', '--incremental', dest='incremental', - action='store_true', help='skip already-imported directories') -import_cmd.parser.add_option('-I', '--noincremental', dest='incremental', - action='store_false', help='do not skip already-imported directories') -import_cmd.parser.add_option('--flat', dest='flat', - action='store_true', help='import an entire tree as a single album') -import_cmd.parser.add_option('-g', '--group-albums', dest='group_albums', - action='store_true', help='group tracks in a folder into seperate albums') +import_cmd = ui.Subcommand( + 'import', help='import new music', aliases=('imp', 'im') +) +import_cmd.parser.add_option( + '-c', '--copy', action='store_true', default=None, + help="copy tracks into library directory (default)" +) +import_cmd.parser.add_option( + '-C', '--nocopy', action='store_false', dest='copy', + help="don't copy tracks (opposite of -c)" +) +import_cmd.parser.add_option( + '-w', '--write', action='store_true', default=None, + help="write new metadata to files' tags (default)" +) +import_cmd.parser.add_option( + '-W', '--nowrite', action='store_false', dest='write', + help="don't write metadata (opposite of -w)" +) +import_cmd.parser.add_option( + '-a', '--autotag', action='store_true', dest='autotag', + help="infer tags for imported files (default)" +) +import_cmd.parser.add_option( + '-A', '--noautotag', action='store_false', dest='autotag', + help="don't infer tags for imported files (opposite of -a)" +) +import_cmd.parser.add_option( + '-p', '--resume', action='store_true', default=None, + help="resume importing if interrupted" +) +import_cmd.parser.add_option( + '-P', '--noresume', action='store_false', dest='resume', + help="do not try to resume importing" +) +import_cmd.parser.add_option( + '-q', '--quiet', action='store_true', dest='quiet', + help="never prompt for input: skip albums instead" +) +import_cmd.parser.add_option( + '-l', '--log', dest='log', + help='file to log untaggable albums for later review' +) +import_cmd.parser.add_option( + '-s', '--singletons', action='store_true', + help='import individual tracks instead of full albums' +) +import_cmd.parser.add_option( + '-t', '--timid', dest='timid', action='store_true', + help='always confirm all actions' +) +import_cmd.parser.add_option( + '-L', '--library', dest='library', action='store_true', + help='retag items matching a query' +) +import_cmd.parser.add_option( + '-i', '--incremental', dest='incremental', action='store_true', + help='skip already-imported directories' +) +import_cmd.parser.add_option( + '-I', '--noincremental', dest='incremental', action='store_false', + help='do not skip already-imported directories' +) +import_cmd.parser.add_option( + '--flat', dest='flat', action='store_true', + help='import an entire tree as a single album' +) +import_cmd.parser.add_option( + '-g', '--group-albums', dest='group_albums', action='store_true', + help='group tracks in a folder into seperate albums' +) + + def import_func(lib, opts, args): config['import'].set_args(opts) @@ -856,13 +905,22 @@ def list_items(lib, query, album, fmt): for item in lib.items(query): ui.print_obj(item, lib, tmpl) + list_cmd = ui.Subcommand('list', help='query the library', aliases=('ls',)) -list_cmd.parser.add_option('-a', '--album', action='store_true', - help='show matching albums instead of tracks') -list_cmd.parser.add_option('-p', '--path', action='store_true', - help='print paths for matched items or albums') -list_cmd.parser.add_option('-f', '--format', action='store', - help='print with custom format', default=None) +list_cmd.parser.add_option( + '-a', '--album', action='store_true', + help='show matching albums instead of tracks' +) +list_cmd.parser.add_option( + '-p', '--path', action='store_true', + help='print paths for matched items or albums' +) +list_cmd.parser.add_option( + '-f', '--format', action='store', + help='print with custom format', default=None +) + + def list_func(lib, opts, args): if opts.path: fmt = '$path' @@ -897,7 +955,7 @@ def update_items(lib, query, album, move, pretend): # 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)) + (displayable_path(item.path), item.mtime)) continue # Read new data. @@ -961,18 +1019,32 @@ def update_items(lib, query, album, move, pretend): log.debug('moving album %i' % album_id) album.move() -update_cmd = ui.Subcommand('update', - help='update the library', aliases=('upd','up',)) -update_cmd.parser.add_option('-a', '--album', action='store_true', - help='match albums instead of tracks') -update_cmd.parser.add_option('-M', '--nomove', action='store_false', - default=True, dest='move', help="don't move files in library") -update_cmd.parser.add_option('-p', '--pretend', action='store_true', - help="show all changes but do nothing") -update_cmd.parser.add_option('-f', '--format', action='store', - help='print with custom format', default=None) + +update_cmd = ui.Subcommand( + 'update', help='update the library', aliases=('upd', 'up',) +) +update_cmd.parser.add_option( + '-a', '--album', action='store_true', + help='match albums instead of tracks' +) +update_cmd.parser.add_option( + '-M', '--nomove', action='store_false', default=True, dest='move', + help="don't move files in library" +) +update_cmd.parser.add_option( + '-p', '--pretend', action='store_true', + help="show all changes but do nothing" +) +update_cmd.parser.add_option( + '-f', '--format', action='store', + help='print with custom format', default=None +) + + def update_func(lib, opts, args): update_items(lib, decargs(args), opts.album, opts.move, opts.pretend) + + update_cmd.func = update_func default_commands.append(update_cmd) @@ -1005,14 +1077,24 @@ def remove_items(lib, query, album, delete): for obj in (albums if album else items): obj.remove(delete) -remove_cmd = ui.Subcommand('remove', - help='remove matching items from the library', aliases=('rm',)) -remove_cmd.parser.add_option("-d", "--delete", action="store_true", - help="also remove files from disk") -remove_cmd.parser.add_option('-a', '--album', action='store_true', - help='match albums instead of tracks') + +remove_cmd = ui.Subcommand( + 'remove', help='remove matching items from the library', aliases=('rm',) +) +remove_cmd.parser.add_option( + "-d", "--delete", action="store_true", + help="also remove files from disk" +) +remove_cmd.parser.add_option( + '-a', '--album', action='store_true', + help='match albums instead of tracks' +) + + def remove_func(lib, opts, args): remove_items(lib, decargs(args), opts.album, opts.delete) + + remove_cmd.func = remove_func default_commands.append(remove_cmd) @@ -1050,12 +1132,20 @@ Artists: {4} Albums: {5}""".format(total_items, ui.human_seconds(total_time), total_time, size_str, len(artists), len(albums))) -stats_cmd = ui.Subcommand('stats', - help='show statistics about the library or a query') -stats_cmd.parser.add_option('-e', '--exact', action='store_true', - help='get exact file sizes') + +stats_cmd = ui.Subcommand( + 'stats', help='show statistics about the library or a query' +) +stats_cmd.parser.add_option( + '-e', '--exact', action='store_true', + help='get exact file sizes' +) + + def stats_func(lib, opts, args): show_stats(lib, decargs(args), opts.exact) + + stats_cmd.func = stats_func default_commands.append(stats_cmd) @@ -1070,8 +1160,11 @@ def show_version(lib, opts, args): print_('plugins:', ', '.join(names)) else: print_('no plugins loaded') -version_cmd = ui.Subcommand('version', - help='output version information') + + +version_cmd = ui.Subcommand( + 'version', help='output version information' +) version_cmd.func = show_version default_commands.append(version_cmd) @@ -1130,7 +1223,7 @@ def modify_items(lib, mods, dels, query, write, move, album, confirm): for obj in changed: if move: cur_path = obj.path - if lib.directory in ancestry(cur_path): # In library? + if lib.directory in ancestry(cur_path): # In library? log.debug('moving object %s' % cur_path) obj.move() @@ -1163,20 +1256,35 @@ def modify_parse_args(args): query.append(arg) return query, mods, dels -modify_cmd = ui.Subcommand('modify', - help='change metadata fields', aliases=('mod',)) -modify_cmd.parser.add_option('-M', '--nomove', action='store_false', - default=True, dest='move', help="don't move files in library") -modify_cmd.parser.add_option('-w', '--write', action='store_true', - default=None, help="write new metadata to files' tags (default)") -modify_cmd.parser.add_option('-W', '--nowrite', action='store_false', - dest='write', help="don't write metadata (opposite of -w)") -modify_cmd.parser.add_option('-a', '--album', action='store_true', - help='modify whole albums instead of tracks') -modify_cmd.parser.add_option('-y', '--yes', action='store_true', - help='skip confirmation') -modify_cmd.parser.add_option('-f', '--format', action='store', - help='print with custom format', default=None) +modify_cmd = ui.Subcommand( + 'modify', help='change metadata fields', aliases=('mod',) +) +modify_cmd.parser.add_option( + '-M', '--nomove', action='store_false', default=True, dest='move', + help="don't move files in library" +) +modify_cmd.parser.add_option( + '-w', '--write', action='store_true', default=None, + help="write new metadata to files' tags (default)" +) +modify_cmd.parser.add_option( + '-W', '--nowrite', action='store_false', dest='write', + help="don't write metadata (opposite of -w)" +) +modify_cmd.parser.add_option( + '-a', '--album', action='store_true', + help='modify whole albums instead of tracks' +) +modify_cmd.parser.add_option( + '-y', '--yes', action='store_true', + help='skip confirmation' +) +modify_cmd.parser.add_option( + '-f', '--format', action='store', + help='print with custom format', default=None +) + + def modify_func(lib, opts, args): query, mods, dels = modify_parse_args(decargs(args)) if not mods and not dels: @@ -1185,6 +1293,8 @@ def modify_func(lib, opts, args): config['import']['write'].get(bool) modify_items(lib, mods, dels, query, write, opts.move, opts.album, not opts.yes) + + modify_cmd.func = modify_func default_commands.append(modify_cmd) @@ -1208,14 +1318,24 @@ def move_items(lib, dest, query, copy, album): obj.move(copy, basedir=dest) obj.store() -move_cmd = ui.Subcommand('move', - help='move or copy items', aliases=('mv',)) -move_cmd.parser.add_option('-d', '--dest', metavar='DIR', dest='dest', - help='destination directory') -move_cmd.parser.add_option('-c', '--copy', default=False, action='store_true', - help='copy instead of moving') -move_cmd.parser.add_option('-a', '--album', default=False, action='store_true', - help='match whole albums instead of tracks') + +move_cmd = ui.Subcommand( + 'move', help='move or copy items', aliases=('mv',) +) +move_cmd.parser.add_option( + '-d', '--dest', metavar='DIR', dest='dest', + help='destination directory' +) +move_cmd.parser.add_option( + '-c', '--copy', default=False, action='store_true', + help='copy instead of moving' +) +move_cmd.parser.add_option( + '-a', '--album', default=False, action='store_true', + help='match whole albums instead of tracks' +) + + def move_func(lib, opts, args): dest = opts.dest if dest is not None: @@ -1224,6 +1344,8 @@ def move_func(lib, opts, args): raise ui.UserError('no such directory: %s' % dest) move_items(lib, dest, decargs(args), opts.copy, opts.album) + + move_cmd.func = move_func default_commands.append(move_cmd) @@ -1260,11 +1382,18 @@ def write_items(lib, query, pretend): if changed and not pretend: item.try_write() + write_cmd = ui.Subcommand('write', help='write tag information to files') -write_cmd.parser.add_option('-p', '--pretend', action='store_true', - help="show all changes but do nothing") +write_cmd.parser.add_option( + '-p', '--pretend', action='store_true', + help="show all changes but do nothing" +) + + def write_func(lib, opts, args): write_items(lib, decargs(args), opts.pretend) + + write_cmd.func = write_func default_commands.append(write_cmd) @@ -1273,12 +1402,20 @@ default_commands.append(write_cmd) config_cmd = ui.Subcommand('config', help='show or edit the user configuration') -config_cmd.parser.add_option('-p', '--paths', action='store_true', - help='show files that configuration was loaded from') -config_cmd.parser.add_option('-e', '--edit', action='store_true', - help='edit user configuration with $EDITOR') -config_cmd.parser.add_option('-d', '--defaults', action='store_true', - help='include the default configuration') +config_cmd.parser.add_option( + '-p', '--paths', action='store_true', + help='show files that configuration was loaded from' +) +config_cmd.parser.add_option( + '-e', '--edit', action='store_true', + help='edit user configuration with $EDITOR' +) +config_cmd.parser.add_option( + '-d', '--defaults', action='store_true', + help='include the default configuration' +) + + def config_func(lib, opts, args): # Make sure lazy configuration is loaded config.resolve() @@ -1328,14 +1465,19 @@ def config_func(lib, opts, args): else: print(config.dump(full=opts.defaults)) + config_cmd.func = config_func default_commands.append(config_cmd) # completion: print completion script -completion_cmd = ui.Subcommand('completion', - help='print shell script that provides command line completion') +completion_cmd = ui.Subcommand( + 'completion', + help='print shell script that provides command line completion' +) + + def print_completion(*args): for line in completion_script(default_commands + plugins.commands()): print(line, end='') @@ -1345,6 +1487,7 @@ def print_completion(*args): log.warn(u'Warning: Unable to find the bash-completion package. ' u'Command line completion might not work.') + def completion_script(commands): """Yield the full completion shell script as strings. @@ -1407,7 +1550,8 @@ def completion_script(commands): # Fields yield " fields='%s'\n" % ' '.join( - set(library.Item._fields.keys() + library.Album._fields.keys())) + set(library.Item._fields.keys() + library.Album._fields.keys()) + ) # Command options for cmd, opts in options.items(): diff --git a/setup.cfg b/setup.cfg index 6a6a96f97..07793890c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,4 +8,4 @@ ignore=E241 # List of files that have not been cleand up yet. We will try to reduce # this with each commit -exclude=test/*,beets/ui/commands.py +exclude=test/*