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