1# -*- coding: utf-8 -*- 2# This file is part of beets. 3# Copyright 2016, Adrian Sampson. 4# 5# Permission is hereby granted, free of charge, to any person obtaining 6# a copy of this software and associated documentation files (the 7# "Software"), to deal in the Software without restriction, including 8# without limitation the rights to use, copy, modify, merge, publish, 9# distribute, sublicense, and/or sell copies of the Software, and to 10# permit persons to whom the Software is furnished to do so, subject to 11# the following conditions: 12# 13# The above copyright notice and this permission notice shall be 14# included in all copies or substantial portions of the Software. 15 16"""This module contains all of the core logic for beets' command-line 17interface. To invoke the CLI, just call beets.ui.main(). The actual 18CLI commands are implemented in the ui.commands module. 19""" 20 21from __future__ import division, absolute_import, print_function 22 23import optparse 24import textwrap 25import sys 26from difflib import SequenceMatcher 27import sqlite3 28import errno 29import re 30import struct 31import traceback 32import os.path 33from six.moves import input 34 35from beets import logging 36from beets import library 37from beets import plugins 38from beets import util 39from beets.util.functemplate import template 40from beets import config 41from beets.util import confit, as_string 42from beets.autotag import mb 43from beets.dbcore import query as db_query 44from beets.dbcore import db 45import six 46 47# On Windows platforms, use colorama to support "ANSI" terminal colors. 48if sys.platform == 'win32': 49 try: 50 import colorama 51 except ImportError: 52 pass 53 else: 54 colorama.init() 55 56 57log = logging.getLogger('beets') 58if not log.handlers: 59 log.addHandler(logging.StreamHandler()) 60log.propagate = False # Don't propagate to root handler. 61 62 63PF_KEY_QUERIES = { 64 'comp': u'comp:true', 65 'singleton': u'singleton:true', 66} 67 68 69class UserError(Exception): 70 """UI exception. Commands should throw this in order to display 71 nonrecoverable errors to the user. 72 """ 73 74 75# Encoding utilities. 76 77 78def _in_encoding(): 79 """Get the encoding to use for *inputting* strings from the console. 80 """ 81 return _stream_encoding(sys.stdin) 82 83 84def _out_encoding(): 85 """Get the encoding to use for *outputting* strings to the console. 86 """ 87 return _stream_encoding(sys.stdout) 88 89 90def _stream_encoding(stream, default='utf-8'): 91 """A helper for `_in_encoding` and `_out_encoding`: get the stream's 92 preferred encoding, using a configured override or a default 93 fallback if neither is not specified. 94 """ 95 # Configured override? 96 encoding = config['terminal_encoding'].get() 97 if encoding: 98 return encoding 99 100 # For testing: When sys.stdout or sys.stdin is a StringIO under the 101 # test harness, it doesn't have an `encoding` attribute. Just use 102 # UTF-8. 103 if not hasattr(stream, 'encoding'): 104 return default 105 106 # Python's guessed output stream encoding, or UTF-8 as a fallback 107 # (e.g., when piped to a file). 108 return stream.encoding or default 109 110 111def decargs(arglist): 112 """Given a list of command-line argument bytestrings, attempts to 113 decode them to Unicode strings when running under Python 2. 114 """ 115 if six.PY2: 116 return [s.decode(util.arg_encoding()) for s in arglist] 117 else: 118 return arglist 119 120 121def print_(*strings, **kwargs): 122 """Like print, but rather than raising an error when a character 123 is not in the terminal's encoding's character set, just silently 124 replaces it. 125 126 The arguments must be Unicode strings: `unicode` on Python 2; `str` on 127 Python 3. 128 129 The `end` keyword argument behaves similarly to the built-in `print` 130 (it defaults to a newline). 131 """ 132 if not strings: 133 strings = [u''] 134 assert isinstance(strings[0], six.text_type) 135 136 txt = u' '.join(strings) 137 txt += kwargs.get('end', u'\n') 138 139 # Encode the string and write it to stdout. 140 if six.PY2: 141 # On Python 2, sys.stdout expects bytes. 142 out = txt.encode(_out_encoding(), 'replace') 143 sys.stdout.write(out) 144 else: 145 # On Python 3, sys.stdout expects text strings and uses the 146 # exception-throwing encoding error policy. To avoid throwing 147 # errors and use our configurable encoding override, we use the 148 # underlying bytes buffer instead. 149 if hasattr(sys.stdout, 'buffer'): 150 out = txt.encode(_out_encoding(), 'replace') 151 sys.stdout.buffer.write(out) 152 sys.stdout.buffer.flush() 153 else: 154 # In our test harnesses (e.g., DummyOut), sys.stdout.buffer 155 # does not exist. We instead just record the text string. 156 sys.stdout.write(txt) 157 158 159# Configuration wrappers. 160 161def _bool_fallback(a, b): 162 """Given a boolean or None, return the original value or a fallback. 163 """ 164 if a is None: 165 assert isinstance(b, bool) 166 return b 167 else: 168 assert isinstance(a, bool) 169 return a 170 171 172def should_write(write_opt=None): 173 """Decide whether a command that updates metadata should also write 174 tags, using the importer configuration as the default. 175 """ 176 return _bool_fallback(write_opt, config['import']['write'].get(bool)) 177 178 179def should_move(move_opt=None): 180 """Decide whether a command that updates metadata should also move 181 files when they're inside the library, using the importer 182 configuration as the default. 183 184 Specifically, commands should move files after metadata updates only 185 when the importer is configured *either* to move *or* to copy files. 186 They should avoid moving files when the importer is configured not 187 to touch any filenames. 188 """ 189 return _bool_fallback( 190 move_opt, 191 config['import']['move'].get(bool) or 192 config['import']['copy'].get(bool) 193 ) 194 195 196# Input prompts. 197 198def input_(prompt=None): 199 """Like `input`, but decodes the result to a Unicode string. 200 Raises a UserError if stdin is not available. The prompt is sent to 201 stdout rather than stderr. A printed between the prompt and the 202 input cursor. 203 """ 204 # raw_input incorrectly sends prompts to stderr, not stdout, so we 205 # use print_() explicitly to display prompts. 206 # http://bugs.python.org/issue1927 207 if prompt: 208 print_(prompt, end=u' ') 209 210 try: 211 resp = input() 212 except EOFError: 213 raise UserError(u'stdin stream ended while input required') 214 215 if six.PY2: 216 return resp.decode(_in_encoding(), 'ignore') 217 else: 218 return resp 219 220 221def input_options(options, require=False, prompt=None, fallback_prompt=None, 222 numrange=None, default=None, max_width=72): 223 """Prompts a user for input. The sequence of `options` defines the 224 choices the user has. A single-letter shortcut is inferred for each 225 option; the user's choice is returned as that single, lower-case 226 letter. The options should be provided as lower-case strings unless 227 a particular shortcut is desired; in that case, only that letter 228 should be capitalized. 229 230 By default, the first option is the default. `default` can be provided to 231 override this. If `require` is provided, then there is no default. The 232 prompt and fallback prompt are also inferred but can be overridden. 233 234 If numrange is provided, it is a pair of `(high, low)` (both ints) 235 indicating that, in addition to `options`, the user may enter an 236 integer in that inclusive range. 237 238 `max_width` specifies the maximum number of columns in the 239 automatically generated prompt string. 240 """ 241 # Assign single letters to each option. Also capitalize the options 242 # to indicate the letter. 243 letters = {} 244 display_letters = [] 245 capitalized = [] 246 first = True 247 for option in options: 248 # Is a letter already capitalized? 249 for letter in option: 250 if letter.isalpha() and letter.upper() == letter: 251 found_letter = letter 252 break 253 else: 254 # Infer a letter. 255 for letter in option: 256 if not letter.isalpha(): 257 continue # Don't use punctuation. 258 if letter not in letters: 259 found_letter = letter 260 break 261 else: 262 raise ValueError(u'no unambiguous lettering found') 263 264 letters[found_letter.lower()] = option 265 index = option.index(found_letter) 266 267 # Mark the option's shortcut letter for display. 268 if not require and ( 269 (default is None and not numrange and first) or 270 (isinstance(default, six.string_types) and 271 found_letter.lower() == default.lower())): 272 # The first option is the default; mark it. 273 show_letter = '[%s]' % found_letter.upper() 274 is_default = True 275 else: 276 show_letter = found_letter.upper() 277 is_default = False 278 279 # Colorize the letter shortcut. 280 show_letter = colorize('action_default' if is_default else 'action', 281 show_letter) 282 283 # Insert the highlighted letter back into the word. 284 capitalized.append( 285 option[:index] + show_letter + option[index + 1:] 286 ) 287 display_letters.append(found_letter.upper()) 288 289 first = False 290 291 # The default is just the first option if unspecified. 292 if require: 293 default = None 294 elif default is None: 295 if numrange: 296 default = numrange[0] 297 else: 298 default = display_letters[0].lower() 299 300 # Make a prompt if one is not provided. 301 if not prompt: 302 prompt_parts = [] 303 prompt_part_lengths = [] 304 if numrange: 305 if isinstance(default, int): 306 default_name = six.text_type(default) 307 default_name = colorize('action_default', default_name) 308 tmpl = '# selection (default %s)' 309 prompt_parts.append(tmpl % default_name) 310 prompt_part_lengths.append(len(tmpl % six.text_type(default))) 311 else: 312 prompt_parts.append('# selection') 313 prompt_part_lengths.append(len(prompt_parts[-1])) 314 prompt_parts += capitalized 315 prompt_part_lengths += [len(s) for s in options] 316 317 # Wrap the query text. 318 prompt = '' 319 line_length = 0 320 for i, (part, length) in enumerate(zip(prompt_parts, 321 prompt_part_lengths)): 322 # Add punctuation. 323 if i == len(prompt_parts) - 1: 324 part += '?' 325 else: 326 part += ',' 327 length += 1 328 329 # Choose either the current line or the beginning of the next. 330 if line_length + length + 1 > max_width: 331 prompt += '\n' 332 line_length = 0 333 334 if line_length != 0: 335 # Not the beginning of the line; need a space. 336 part = ' ' + part 337 length += 1 338 339 prompt += part 340 line_length += length 341 342 # Make a fallback prompt too. This is displayed if the user enters 343 # something that is not recognized. 344 if not fallback_prompt: 345 fallback_prompt = u'Enter one of ' 346 if numrange: 347 fallback_prompt += u'%i-%i, ' % numrange 348 fallback_prompt += ', '.join(display_letters) + ':' 349 350 resp = input_(prompt) 351 while True: 352 resp = resp.strip().lower() 353 354 # Try default option. 355 if default is not None and not resp: 356 resp = default 357 358 # Try an integer input if available. 359 if numrange: 360 try: 361 resp = int(resp) 362 except ValueError: 363 pass 364 else: 365 low, high = numrange 366 if low <= resp <= high: 367 return resp 368 else: 369 resp = None 370 371 # Try a normal letter input. 372 if resp: 373 resp = resp[0] 374 if resp in letters: 375 return resp 376 377 # Prompt for new input. 378 resp = input_(fallback_prompt) 379 380 381def input_yn(prompt, require=False): 382 """Prompts the user for a "yes" or "no" response. The default is 383 "yes" unless `require` is `True`, in which case there is no default. 384 """ 385 sel = input_options( 386 ('y', 'n'), require, prompt, u'Enter Y or N:' 387 ) 388 return sel == u'y' 389 390 391def input_select_objects(prompt, objs, rep): 392 """Prompt to user to choose all, none, or some of the given objects. 393 Return the list of selected objects. 394 395 `prompt` is the prompt string to use for each question (it should be 396 phrased as an imperative verb). `rep` is a function to call on each 397 object to print it out when confirming objects individually. 398 """ 399 choice = input_options( 400 (u'y', u'n', u's'), False, 401 u'%s? (Yes/no/select)' % prompt) 402 print() # Blank line. 403 404 if choice == u'y': # Yes. 405 return objs 406 407 elif choice == u's': # Select. 408 out = [] 409 for obj in objs: 410 rep(obj) 411 answer = input_options( 412 ('y', 'n', 'q'), True, u'%s? (yes/no/quit)' % prompt, 413 u'Enter Y or N:' 414 ) 415 if answer == u'y': 416 out.append(obj) 417 elif answer == u'q': 418 return out 419 return out 420 421 else: # No. 422 return [] 423 424 425# Human output formatting. 426 427def human_bytes(size): 428 """Formats size, a number of bytes, in a human-readable way.""" 429 powers = [u'', u'K', u'M', u'G', u'T', u'P', u'E', u'Z', u'Y', u'H'] 430 unit = 'B' 431 for power in powers: 432 if size < 1024: 433 return u"%3.1f %s%s" % (size, power, unit) 434 size /= 1024.0 435 unit = u'iB' 436 return u"big" 437 438 439def human_seconds(interval): 440 """Formats interval, a number of seconds, as a human-readable time 441 interval using English words. 442 """ 443 units = [ 444 (1, u'second'), 445 (60, u'minute'), 446 (60, u'hour'), 447 (24, u'day'), 448 (7, u'week'), 449 (52, u'year'), 450 (10, u'decade'), 451 ] 452 for i in range(len(units) - 1): 453 increment, suffix = units[i] 454 next_increment, _ = units[i + 1] 455 interval /= float(increment) 456 if interval < next_increment: 457 break 458 else: 459 # Last unit. 460 increment, suffix = units[-1] 461 interval /= float(increment) 462 463 return u"%3.1f %ss" % (interval, suffix) 464 465 466def human_seconds_short(interval): 467 """Formats a number of seconds as a short human-readable M:SS 468 string. 469 """ 470 interval = int(interval) 471 return u'%i:%02i' % (interval // 60, interval % 60) 472 473 474# Colorization. 475 476# ANSI terminal colorization code heavily inspired by pygments: 477# http://dev.pocoo.org/hg/pygments-main/file/b2deea5b5030/pygments/console.py 478# (pygments is by Tim Hatch, Armin Ronacher, et al.) 479COLOR_ESCAPE = "\x1b[" 480DARK_COLORS = { 481 "black": 0, 482 "darkred": 1, 483 "darkgreen": 2, 484 "brown": 3, 485 "darkyellow": 3, 486 "darkblue": 4, 487 "purple": 5, 488 "darkmagenta": 5, 489 "teal": 6, 490 "darkcyan": 6, 491 "lightgray": 7 492} 493LIGHT_COLORS = { 494 "darkgray": 0, 495 "red": 1, 496 "green": 2, 497 "yellow": 3, 498 "blue": 4, 499 "fuchsia": 5, 500 "magenta": 5, 501 "turquoise": 6, 502 "cyan": 6, 503 "white": 7 504} 505RESET_COLOR = COLOR_ESCAPE + "39;49;00m" 506 507# These abstract COLOR_NAMES are lazily mapped on to the actual color in COLORS 508# as they are defined in the configuration files, see function: colorize 509COLOR_NAMES = ['text_success', 'text_warning', 'text_error', 'text_highlight', 510 'text_highlight_minor', 'action_default', 'action'] 511COLORS = None 512 513 514def _colorize(color, text): 515 """Returns a string that prints the given text in the given color 516 in a terminal that is ANSI color-aware. The color must be something 517 in DARK_COLORS or LIGHT_COLORS. 518 """ 519 if color in DARK_COLORS: 520 escape = COLOR_ESCAPE + "%im" % (DARK_COLORS[color] + 30) 521 elif color in LIGHT_COLORS: 522 escape = COLOR_ESCAPE + "%i;01m" % (LIGHT_COLORS[color] + 30) 523 else: 524 raise ValueError(u'no such color %s', color) 525 return escape + text + RESET_COLOR 526 527 528def colorize(color_name, text): 529 """Colorize text if colored output is enabled. (Like _colorize but 530 conditional.) 531 """ 532 if not config['ui']['color'] or 'NO_COLOR' in os.environ.keys(): 533 return text 534 535 global COLORS 536 if not COLORS: 537 COLORS = dict((name, 538 config['ui']['colors'][name].as_str()) 539 for name in COLOR_NAMES) 540 # In case a 3rd party plugin is still passing the actual color ('red') 541 # instead of the abstract color name ('text_error') 542 color = COLORS.get(color_name) 543 if not color: 544 log.debug(u'Invalid color_name: {0}', color_name) 545 color = color_name 546 return _colorize(color, text) 547 548 549def _colordiff(a, b, highlight='text_highlight', 550 minor_highlight='text_highlight_minor'): 551 """Given two values, return the same pair of strings except with 552 their differences highlighted in the specified color. Strings are 553 highlighted intelligently to show differences; other values are 554 stringified and highlighted in their entirety. 555 """ 556 if not isinstance(a, six.string_types) \ 557 or not isinstance(b, six.string_types): 558 # Non-strings: use ordinary equality. 559 a = six.text_type(a) 560 b = six.text_type(b) 561 if a == b: 562 return a, b 563 else: 564 return colorize(highlight, a), colorize(highlight, b) 565 566 if isinstance(a, bytes) or isinstance(b, bytes): 567 # A path field. 568 a = util.displayable_path(a) 569 b = util.displayable_path(b) 570 571 a_out = [] 572 b_out = [] 573 574 matcher = SequenceMatcher(lambda x: False, a, b) 575 for op, a_start, a_end, b_start, b_end in matcher.get_opcodes(): 576 if op == 'equal': 577 # In both strings. 578 a_out.append(a[a_start:a_end]) 579 b_out.append(b[b_start:b_end]) 580 elif op == 'insert': 581 # Right only. 582 b_out.append(colorize(highlight, b[b_start:b_end])) 583 elif op == 'delete': 584 # Left only. 585 a_out.append(colorize(highlight, a[a_start:a_end])) 586 elif op == 'replace': 587 # Right and left differ. Colorise with second highlight if 588 # it's just a case change. 589 if a[a_start:a_end].lower() != b[b_start:b_end].lower(): 590 color = highlight 591 else: 592 color = minor_highlight 593 a_out.append(colorize(color, a[a_start:a_end])) 594 b_out.append(colorize(color, b[b_start:b_end])) 595 else: 596 assert(False) 597 598 return u''.join(a_out), u''.join(b_out) 599 600 601def colordiff(a, b, highlight='text_highlight'): 602 """Colorize differences between two values if color is enabled. 603 (Like _colordiff but conditional.) 604 """ 605 if config['ui']['color']: 606 return _colordiff(a, b, highlight) 607 else: 608 return six.text_type(a), six.text_type(b) 609 610 611def get_path_formats(subview=None): 612 """Get the configuration's path formats as a list of query/template 613 pairs. 614 """ 615 path_formats = [] 616 subview = subview or config['paths'] 617 for query, view in subview.items(): 618 query = PF_KEY_QUERIES.get(query, query) # Expand common queries. 619 path_formats.append((query, template(view.as_str()))) 620 return path_formats 621 622 623def get_replacements(): 624 """Confit validation function that reads regex/string pairs. 625 """ 626 replacements = [] 627 for pattern, repl in config['replace'].get(dict).items(): 628 repl = repl or '' 629 try: 630 replacements.append((re.compile(pattern), repl)) 631 except re.error: 632 raise UserError( 633 u'malformed regular expression in replace: {0}'.format( 634 pattern 635 ) 636 ) 637 return replacements 638 639 640def term_width(): 641 """Get the width (columns) of the terminal.""" 642 fallback = config['ui']['terminal_width'].get(int) 643 644 # The fcntl and termios modules are not available on non-Unix 645 # platforms, so we fall back to a constant. 646 try: 647 import fcntl 648 import termios 649 except ImportError: 650 return fallback 651 652 try: 653 buf = fcntl.ioctl(0, termios.TIOCGWINSZ, ' ' * 4) 654 except IOError: 655 return fallback 656 try: 657 height, width = struct.unpack('hh', buf) 658 except struct.error: 659 return fallback 660 return width 661 662 663FLOAT_EPSILON = 0.01 664 665 666def _field_diff(field, old, new): 667 """Given two Model objects, format their values for `field` and 668 highlight changes among them. Return a human-readable string. If the 669 value has not changed, return None instead. 670 """ 671 oldval = old.get(field) 672 newval = new.get(field) 673 674 # If no change, abort. 675 if isinstance(oldval, float) and isinstance(newval, float) and \ 676 abs(oldval - newval) < FLOAT_EPSILON: 677 return None 678 elif oldval == newval: 679 return None 680 681 # Get formatted values for output. 682 oldstr = old.formatted().get(field, u'') 683 newstr = new.formatted().get(field, u'') 684 685 # For strings, highlight changes. For others, colorize the whole 686 # thing. 687 if isinstance(oldval, six.string_types): 688 oldstr, newstr = colordiff(oldval, newstr) 689 else: 690 oldstr = colorize('text_error', oldstr) 691 newstr = colorize('text_error', newstr) 692 693 return u'{0} -> {1}'.format(oldstr, newstr) 694 695 696def show_model_changes(new, old=None, fields=None, always=False): 697 """Given a Model object, print a list of changes from its pristine 698 version stored in the database. Return a boolean indicating whether 699 any changes were found. 700 701 `old` may be the "original" object to avoid using the pristine 702 version from the database. `fields` may be a list of fields to 703 restrict the detection to. `always` indicates whether the object is 704 always identified, regardless of whether any changes are present. 705 """ 706 old = old or new._db._get(type(new), new.id) 707 708 # Build up lines showing changed fields. 709 changes = [] 710 for field in old: 711 # Subset of the fields. Never show mtime. 712 if field == 'mtime' or (fields and field not in fields): 713 continue 714 715 # Detect and show difference for this field. 716 line = _field_diff(field, old, new) 717 if line: 718 changes.append(u' {0}: {1}'.format(field, line)) 719 720 # New fields. 721 for field in set(new) - set(old): 722 if fields and field not in fields: 723 continue 724 725 changes.append(u' {0}: {1}'.format( 726 field, 727 colorize('text_highlight', new.formatted()[field]) 728 )) 729 730 # Print changes. 731 if changes or always: 732 print_(format(old)) 733 if changes: 734 print_(u'\n'.join(changes)) 735 736 return bool(changes) 737 738 739def show_path_changes(path_changes): 740 """Given a list of tuples (source, destination) that indicate the 741 path changes, log the changes as INFO-level output to the beets log. 742 The output is guaranteed to be unicode. 743 744 Every pair is shown on a single line if the terminal width permits it, 745 else it is split over two lines. E.g., 746 747 Source -> Destination 748 749 vs. 750 751 Source 752 -> Destination 753 """ 754 sources, destinations = zip(*path_changes) 755 756 # Ensure unicode output 757 sources = list(map(util.displayable_path, sources)) 758 destinations = list(map(util.displayable_path, destinations)) 759 760 # Calculate widths for terminal split 761 col_width = (term_width() - len(' -> ')) // 2 762 max_width = len(max(sources + destinations, key=len)) 763 764 if max_width > col_width: 765 # Print every change over two lines 766 for source, dest in zip(sources, destinations): 767 log.info(u'{0} \n -> {1}', source, dest) 768 else: 769 # Print every change on a single line, and add a header 770 title_pad = max_width - len('Source ') + len(' -> ') 771 772 log.info(u'Source {0} Destination', ' ' * title_pad) 773 for source, dest in zip(sources, destinations): 774 pad = max_width - len(source) 775 log.info(u'{0} {1} -> {2}', source, ' ' * pad, dest) 776 777 778# Helper functions for option parsing. 779 780def _store_dict(option, opt_str, value, parser): 781 """Custom action callback to parse options which have ``key=value`` 782 pairs as values. All such pairs passed for this option are 783 aggregated into a dictionary. 784 """ 785 dest = option.dest 786 option_values = getattr(parser.values, dest, None) 787 788 if option_values is None: 789 # This is the first supplied ``key=value`` pair of option. 790 # Initialize empty dictionary and get a reference to it. 791 setattr(parser.values, dest, dict()) 792 option_values = getattr(parser.values, dest) 793 794 try: 795 key, value = map(lambda s: util.text_string(s), value.split('=')) 796 if not (key and value): 797 raise ValueError 798 except ValueError: 799 raise UserError( 800 "supplied argument `{0}' is not of the form `key=value'" 801 .format(value)) 802 803 option_values[key] = value 804 805 806class CommonOptionsParser(optparse.OptionParser, object): 807 """Offers a simple way to add common formatting options. 808 809 Options available include: 810 - matching albums instead of tracks: add_album_option() 811 - showing paths instead of items/albums: add_path_option() 812 - changing the format of displayed items/albums: add_format_option() 813 814 The last one can have several behaviors: 815 - against a special target 816 - with a certain format 817 - autodetected target with the album option 818 819 Each method is fully documented in the related method. 820 """ 821 def __init__(self, *args, **kwargs): 822 super(CommonOptionsParser, self).__init__(*args, **kwargs) 823 self._album_flags = False 824 # this serves both as an indicator that we offer the feature AND allows 825 # us to check whether it has been specified on the CLI - bypassing the 826 # fact that arguments may be in any order 827 828 def add_album_option(self, flags=('-a', '--album')): 829 """Add a -a/--album option to match albums instead of tracks. 830 831 If used then the format option can auto-detect whether we're setting 832 the format for items or albums. 833 Sets the album property on the options extracted from the CLI. 834 """ 835 album = optparse.Option(*flags, action='store_true', 836 help=u'match albums instead of tracks') 837 self.add_option(album) 838 self._album_flags = set(flags) 839 840 def _set_format(self, option, opt_str, value, parser, target=None, 841 fmt=None, store_true=False): 842 """Internal callback that sets the correct format while parsing CLI 843 arguments. 844 """ 845 if store_true: 846 setattr(parser.values, option.dest, True) 847 848 # Use the explicitly specified format, or the string from the option. 849 if fmt: 850 value = fmt 851 elif value: 852 value, = decargs([value]) 853 else: 854 value = u'' 855 856 parser.values.format = value 857 if target: 858 config[target._format_config_key].set(value) 859 else: 860 if self._album_flags: 861 if parser.values.album: 862 target = library.Album 863 else: 864 # the option is either missing either not parsed yet 865 if self._album_flags & set(parser.rargs): 866 target = library.Album 867 else: 868 target = library.Item 869 config[target._format_config_key].set(value) 870 else: 871 config[library.Item._format_config_key].set(value) 872 config[library.Album._format_config_key].set(value) 873 874 def add_path_option(self, flags=('-p', '--path')): 875 """Add a -p/--path option to display the path instead of the default 876 format. 877 878 By default this affects both items and albums. If add_album_option() 879 is used then the target will be autodetected. 880 881 Sets the format property to u'$path' on the options extracted from the 882 CLI. 883 """ 884 path = optparse.Option(*flags, nargs=0, action='callback', 885 callback=self._set_format, 886 callback_kwargs={'fmt': u'$path', 887 'store_true': True}, 888 help=u'print paths for matched items or albums') 889 self.add_option(path) 890 891 def add_format_option(self, flags=('-f', '--format'), target=None): 892 """Add -f/--format option to print some LibModel instances with a 893 custom format. 894 895 `target` is optional and can be one of ``library.Item``, 'item', 896 ``library.Album`` and 'album'. 897 898 Several behaviors are available: 899 - if `target` is given then the format is only applied to that 900 LibModel 901 - if the album option is used then the target will be autodetected 902 - otherwise the format is applied to both items and albums. 903 904 Sets the format property on the options extracted from the CLI. 905 """ 906 kwargs = {} 907 if target: 908 if isinstance(target, six.string_types): 909 target = {'item': library.Item, 910 'album': library.Album}[target] 911 kwargs['target'] = target 912 913 opt = optparse.Option(*flags, action='callback', 914 callback=self._set_format, 915 callback_kwargs=kwargs, 916 help=u'print with custom format') 917 self.add_option(opt) 918 919 def add_all_common_options(self): 920 """Add album, path and format options. 921 """ 922 self.add_album_option() 923 self.add_path_option() 924 self.add_format_option() 925 926 927# Subcommand parsing infrastructure. 928# 929# This is a fairly generic subcommand parser for optparse. It is 930# maintained externally here: 931# http://gist.github.com/462717 932# There you will also find a better description of the code and a more 933# succinct example program. 934 935class Subcommand(object): 936 """A subcommand of a root command-line application that may be 937 invoked by a SubcommandOptionParser. 938 """ 939 def __init__(self, name, parser=None, help='', aliases=(), hide=False): 940 """Creates a new subcommand. name is the primary way to invoke 941 the subcommand; aliases are alternate names. parser is an 942 OptionParser responsible for parsing the subcommand's options. 943 help is a short description of the command. If no parser is 944 given, it defaults to a new, empty CommonOptionsParser. 945 """ 946 self.name = name 947 self.parser = parser or CommonOptionsParser() 948 self.aliases = aliases 949 self.help = help 950 self.hide = hide 951 self._root_parser = None 952 953 def print_help(self): 954 self.parser.print_help() 955 956 def parse_args(self, args): 957 return self.parser.parse_args(args) 958 959 @property 960 def root_parser(self): 961 return self._root_parser 962 963 @root_parser.setter 964 def root_parser(self, root_parser): 965 self._root_parser = root_parser 966 self.parser.prog = '{0} {1}'.format( 967 as_string(root_parser.get_prog_name()), self.name) 968 969 970class SubcommandsOptionParser(CommonOptionsParser): 971 """A variant of OptionParser that parses subcommands and their 972 arguments. 973 """ 974 975 def __init__(self, *args, **kwargs): 976 """Create a new subcommand-aware option parser. All of the 977 options to OptionParser.__init__ are supported in addition 978 to subcommands, a sequence of Subcommand objects. 979 """ 980 # A more helpful default usage. 981 if 'usage' not in kwargs: 982 kwargs['usage'] = u""" 983 %prog COMMAND [ARGS...] 984 %prog help COMMAND""" 985 kwargs['add_help_option'] = False 986 987 # Super constructor. 988 super(SubcommandsOptionParser, self).__init__(*args, **kwargs) 989 990 # Our root parser needs to stop on the first unrecognized argument. 991 self.disable_interspersed_args() 992 993 self.subcommands = [] 994 995 def add_subcommand(self, *cmds): 996 """Adds a Subcommand object to the parser's list of commands. 997 """ 998 for cmd in cmds: 999 cmd.root_parser = self 1000 self.subcommands.append(cmd) 1001 1002 # Add the list of subcommands to the help message. 1003 def format_help(self, formatter=None): 1004 # Get the original help message, to which we will append. 1005 out = super(SubcommandsOptionParser, self).format_help(formatter) 1006 if formatter is None: 1007 formatter = self.formatter 1008 1009 # Subcommands header. 1010 result = ["\n"] 1011 result.append(formatter.format_heading('Commands')) 1012 formatter.indent() 1013 1014 # Generate the display names (including aliases). 1015 # Also determine the help position. 1016 disp_names = [] 1017 help_position = 0 1018 subcommands = [c for c in self.subcommands if not c.hide] 1019 subcommands.sort(key=lambda c: c.name) 1020 for subcommand in subcommands: 1021 name = subcommand.name 1022 if subcommand.aliases: 1023 name += ' (%s)' % ', '.join(subcommand.aliases) 1024 disp_names.append(name) 1025 1026 # Set the help position based on the max width. 1027 proposed_help_position = len(name) + formatter.current_indent + 2 1028 if proposed_help_position <= formatter.max_help_position: 1029 help_position = max(help_position, proposed_help_position) 1030 1031 # Add each subcommand to the output. 1032 for subcommand, name in zip(subcommands, disp_names): 1033 # Lifted directly from optparse.py. 1034 name_width = help_position - formatter.current_indent - 2 1035 if len(name) > name_width: 1036 name = "%*s%s\n" % (formatter.current_indent, "", name) 1037 indent_first = help_position 1038 else: 1039 name = "%*s%-*s " % (formatter.current_indent, "", 1040 name_width, name) 1041 indent_first = 0 1042 result.append(name) 1043 help_width = formatter.width - help_position 1044 help_lines = textwrap.wrap(subcommand.help, help_width) 1045 help_line = help_lines[0] if help_lines else '' 1046 result.append("%*s%s\n" % (indent_first, "", help_line)) 1047 result.extend(["%*s%s\n" % (help_position, "", line) 1048 for line in help_lines[1:]]) 1049 formatter.dedent() 1050 1051 # Concatenate the original help message with the subcommand 1052 # list. 1053 return out + "".join(result) 1054 1055 def _subcommand_for_name(self, name): 1056 """Return the subcommand in self.subcommands matching the 1057 given name. The name may either be the name of a subcommand or 1058 an alias. If no subcommand matches, returns None. 1059 """ 1060 for subcommand in self.subcommands: 1061 if name == subcommand.name or \ 1062 name in subcommand.aliases: 1063 return subcommand 1064 return None 1065 1066 def parse_global_options(self, args): 1067 """Parse options up to the subcommand argument. Returns a tuple 1068 of the options object and the remaining arguments. 1069 """ 1070 options, subargs = self.parse_args(args) 1071 1072 # Force the help command 1073 if options.help: 1074 subargs = ['help'] 1075 elif options.version: 1076 subargs = ['version'] 1077 return options, subargs 1078 1079 def parse_subcommand(self, args): 1080 """Given the `args` left unused by a `parse_global_options`, 1081 return the invoked subcommand, the subcommand options, and the 1082 subcommand arguments. 1083 """ 1084 # Help is default command 1085 if not args: 1086 args = ['help'] 1087 1088 cmdname = args.pop(0) 1089 subcommand = self._subcommand_for_name(cmdname) 1090 if not subcommand: 1091 raise UserError(u"unknown command '{0}'".format(cmdname)) 1092 1093 suboptions, subargs = subcommand.parse_args(args) 1094 return subcommand, suboptions, subargs 1095 1096 1097optparse.Option.ALWAYS_TYPED_ACTIONS += ('callback',) 1098 1099 1100# The main entry point and bootstrapping. 1101 1102def _load_plugins(config): 1103 """Load the plugins specified in the configuration. 1104 """ 1105 paths = config['pluginpath'].as_str_seq(split=False) 1106 paths = [util.normpath(p) for p in paths] 1107 log.debug(u'plugin paths: {0}', util.displayable_path(paths)) 1108 1109 # On Python 3, the search paths need to be unicode. 1110 paths = [util.py3_path(p) for p in paths] 1111 1112 # Extend the `beetsplug` package to include the plugin paths. 1113 import beetsplug 1114 beetsplug.__path__ = paths + beetsplug.__path__ 1115 1116 # For backwards compatibility, also support plugin paths that 1117 # *contain* a `beetsplug` package. 1118 sys.path += paths 1119 1120 plugins.load_plugins(config['plugins'].as_str_seq()) 1121 plugins.send("pluginload") 1122 return plugins 1123 1124 1125def _setup(options, lib=None): 1126 """Prepare and global state and updates it with command line options. 1127 1128 Returns a list of subcommands, a list of plugins, and a library instance. 1129 """ 1130 # Configure the MusicBrainz API. 1131 mb.configure() 1132 1133 config = _configure(options) 1134 1135 plugins = _load_plugins(config) 1136 1137 # Get the default subcommands. 1138 from beets.ui.commands import default_commands 1139 1140 subcommands = list(default_commands) 1141 subcommands.extend(plugins.commands()) 1142 1143 if lib is None: 1144 lib = _open_library(config) 1145 plugins.send("library_opened", lib=lib) 1146 1147 # Add types and queries defined by plugins. 1148 library.Item._types.update(plugins.types(library.Item)) 1149 library.Album._types.update(plugins.types(library.Album)) 1150 library.Item._queries.update(plugins.named_queries(library.Item)) 1151 library.Album._queries.update(plugins.named_queries(library.Album)) 1152 1153 return subcommands, plugins, lib 1154 1155 1156def _configure(options): 1157 """Amend the global configuration object with command line options. 1158 """ 1159 # Add any additional config files specified with --config. This 1160 # special handling lets specified plugins get loaded before we 1161 # finish parsing the command line. 1162 if getattr(options, 'config', None) is not None: 1163 overlay_path = options.config 1164 del options.config 1165 config.set_file(overlay_path) 1166 else: 1167 overlay_path = None 1168 config.set_args(options) 1169 1170 # Configure the logger. 1171 if config['verbose'].get(int): 1172 log.set_global_level(logging.DEBUG) 1173 else: 1174 log.set_global_level(logging.INFO) 1175 1176 if overlay_path: 1177 log.debug(u'overlaying configuration: {0}', 1178 util.displayable_path(overlay_path)) 1179 1180 config_path = config.user_config_path() 1181 if os.path.isfile(config_path): 1182 log.debug(u'user configuration: {0}', 1183 util.displayable_path(config_path)) 1184 else: 1185 log.debug(u'no user configuration found at {0}', 1186 util.displayable_path(config_path)) 1187 1188 log.debug(u'data directory: {0}', 1189 util.displayable_path(config.config_dir())) 1190 return config 1191 1192 1193def _open_library(config): 1194 """Create a new library instance from the configuration. 1195 """ 1196 dbpath = util.bytestring_path(config['library'].as_filename()) 1197 try: 1198 lib = library.Library( 1199 dbpath, 1200 config['directory'].as_filename(), 1201 get_path_formats(), 1202 get_replacements(), 1203 ) 1204 lib.get_item(0) # Test database connection. 1205 except (sqlite3.OperationalError, sqlite3.DatabaseError) as db_error: 1206 log.debug(u'{}', traceback.format_exc()) 1207 raise UserError(u"database file {0} cannot not be opened: {1}".format( 1208 util.displayable_path(dbpath), 1209 db_error 1210 )) 1211 log.debug(u'library database: {0}\n' 1212 u'library directory: {1}', 1213 util.displayable_path(lib.path), 1214 util.displayable_path(lib.directory)) 1215 return lib 1216 1217 1218def _raw_main(args, lib=None): 1219 """A helper function for `main` without top-level exception 1220 handling. 1221 """ 1222 parser = SubcommandsOptionParser() 1223 parser.add_format_option(flags=('--format-item',), target=library.Item) 1224 parser.add_format_option(flags=('--format-album',), target=library.Album) 1225 parser.add_option('-l', '--library', dest='library', 1226 help=u'library database file to use') 1227 parser.add_option('-d', '--directory', dest='directory', 1228 help=u"destination music directory") 1229 parser.add_option('-v', '--verbose', dest='verbose', action='count', 1230 help=u'log more details (use twice for even more)') 1231 parser.add_option('-c', '--config', dest='config', 1232 help=u'path to configuration file') 1233 parser.add_option('-h', '--help', dest='help', action='store_true', 1234 help=u'show this help message and exit') 1235 parser.add_option('--version', dest='version', action='store_true', 1236 help=optparse.SUPPRESS_HELP) 1237 1238 options, subargs = parser.parse_global_options(args) 1239 1240 # Special case for the `config --edit` command: bypass _setup so 1241 # that an invalid configuration does not prevent the editor from 1242 # starting. 1243 if subargs and subargs[0] == 'config' \ 1244 and ('-e' in subargs or '--edit' in subargs): 1245 from beets.ui.commands import config_edit 1246 return config_edit() 1247 1248 test_lib = bool(lib) 1249 subcommands, plugins, lib = _setup(options, lib) 1250 parser.add_subcommand(*subcommands) 1251 1252 subcommand, suboptions, subargs = parser.parse_subcommand(subargs) 1253 subcommand.func(lib, suboptions, subargs) 1254 1255 plugins.send('cli_exit', lib=lib) 1256 if not test_lib: 1257 # Clean up the library unless it came from the test harness. 1258 lib._close() 1259 1260 1261def main(args=None): 1262 """Run the main command-line interface for beets. Includes top-level 1263 exception handlers that print friendly error messages. 1264 """ 1265 try: 1266 _raw_main(args) 1267 except UserError as exc: 1268 message = exc.args[0] if exc.args else None 1269 log.error(u'error: {0}', message) 1270 sys.exit(1) 1271 except util.HumanReadableException as exc: 1272 exc.log(log) 1273 sys.exit(1) 1274 except library.FileOperationError as exc: 1275 # These errors have reasonable human-readable descriptions, but 1276 # we still want to log their tracebacks for debugging. 1277 log.debug('{}', traceback.format_exc()) 1278 log.error('{}', exc) 1279 sys.exit(1) 1280 except confit.ConfigError as exc: 1281 log.error(u'configuration error: {0}', exc) 1282 sys.exit(1) 1283 except db_query.InvalidQueryError as exc: 1284 log.error(u'invalid query: {0}', exc) 1285 sys.exit(1) 1286 except IOError as exc: 1287 if exc.errno == errno.EPIPE: 1288 # "Broken pipe". End silently. 1289 sys.stderr.close() 1290 else: 1291 raise 1292 except KeyboardInterrupt: 1293 # Silently ignore ^C except in verbose mode. 1294 log.debug(u'{}', traceback.format_exc()) 1295 except db.DBAccessError as exc: 1296 log.error( 1297 u'database access error: {0}\n' 1298 u'the library file might have a permissions problem', 1299 exc 1300 ) 1301 sys.exit(1) 1302