mirror of
https://github.com/beetbox/beets.git
synced 2025-12-16 05:34:47 +01:00
The Great Trailing Whitespace Purge of 2012
What can I say? I used to use TextMate!
This commit is contained in:
parent
a9eb249a15
commit
b68e87b92c
47 changed files with 438 additions and 438 deletions
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ def albums_in_dir(path, ignore=()):
|
|||
collapse_root = root
|
||||
collapse_items = []
|
||||
continue
|
||||
|
||||
|
||||
# If it's nonempty, yield it.
|
||||
if items:
|
||||
yield root, items
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -112,7 +112,7 @@ def string_dist(str1, str2):
|
|||
"""
|
||||
str1 = str1.lower()
|
||||
str2 = str2.lower()
|
||||
|
||||
|
||||
# Don't penalize strings that move certain words to the end. For
|
||||
# example, "the something" should be considered equal to
|
||||
# "something, the".
|
||||
|
|
@ -126,7 +126,7 @@ def string_dist(str1, str2):
|
|||
for pat, repl in SD_REPLACE:
|
||||
str1 = re.sub(pat, repl, str1)
|
||||
str2 = re.sub(pat, repl, str2)
|
||||
|
||||
|
||||
# Change the weight for certain string portions matched by a set
|
||||
# of regular expressions. We gradually change the strings and build
|
||||
# up penalties associated with parts of the string that were
|
||||
|
|
@ -137,7 +137,7 @@ def string_dist(str1, str2):
|
|||
# Get strings that drop the pattern.
|
||||
case_str1 = re.sub(pat, '', str1)
|
||||
case_str2 = re.sub(pat, '', str2)
|
||||
|
||||
|
||||
if case_str1 != str1 or case_str2 != str2:
|
||||
# If the pattern was present (i.e., it is deleted in the
|
||||
# the current case), recalculate the distances for the
|
||||
|
|
@ -146,7 +146,7 @@ def string_dist(str1, str2):
|
|||
case_delta = max(0.0, base_dist - case_dist)
|
||||
if case_delta == 0.0:
|
||||
continue
|
||||
|
||||
|
||||
# Shift our baseline strings down (to avoid rematching the
|
||||
# same part of the string) and add a scaled distance
|
||||
# amount to the penalties.
|
||||
|
|
@ -155,7 +155,7 @@ def string_dist(str1, str2):
|
|||
base_dist = case_dist
|
||||
penalty += weight * case_delta
|
||||
dist = base_dist + penalty
|
||||
|
||||
|
||||
return dist
|
||||
|
||||
def current_metadata(items):
|
||||
|
|
@ -191,7 +191,7 @@ def order_items(items, trackinfo):
|
|||
for i, canon_item in enumerate(trackinfo):
|
||||
row.append(track_distance(cur_item, canon_item, i+1))
|
||||
costs.append(row)
|
||||
|
||||
|
||||
# Find a minimum-cost bipartite matching.
|
||||
matching = Munkres().compute(costs)
|
||||
|
||||
|
|
@ -221,7 +221,7 @@ def track_distance(item, track_info, track_index=None, incl_artist=False):
|
|||
diff = min(diff, TRACK_LENGTH_MAX)
|
||||
dist += (diff / TRACK_LENGTH_MAX) * TRACK_LENGTH_WEIGHT
|
||||
dist_max += TRACK_LENGTH_WEIGHT
|
||||
|
||||
|
||||
# Track title.
|
||||
dist += string_dist(item.title, track_info.title) * TRACK_TITLE_WEIGHT
|
||||
dist_max += TRACK_TITLE_WEIGHT
|
||||
|
|
@ -241,7 +241,7 @@ def track_distance(item, track_info, track_index=None, incl_artist=False):
|
|||
if item.track not in (track_index, track_info.medium_index):
|
||||
dist += TRACK_INDEX_WEIGHT
|
||||
dist_max += TRACK_INDEX_WEIGHT
|
||||
|
||||
|
||||
# MusicBrainz track ID.
|
||||
if item.mb_trackid:
|
||||
if item.mb_trackid != track_info.track_id:
|
||||
|
|
@ -262,19 +262,19 @@ def distance(items, album_info):
|
|||
cur_artist, cur_album, _ = current_metadata(items)
|
||||
cur_artist = cur_artist or ''
|
||||
cur_album = cur_album or ''
|
||||
|
||||
|
||||
# These accumulate the possible distance components. The final
|
||||
# distance will be dist/dist_max.
|
||||
dist = 0.0
|
||||
dist_max = 0.0
|
||||
|
||||
|
||||
# Artist/album metadata.
|
||||
if not album_info.va:
|
||||
dist += string_dist(cur_artist, album_info.artist) * ARTIST_WEIGHT
|
||||
dist_max += ARTIST_WEIGHT
|
||||
dist += string_dist(cur_album, album_info.album) * ALBUM_WEIGHT
|
||||
dist_max += ALBUM_WEIGHT
|
||||
|
||||
|
||||
# Track distances.
|
||||
for i, (item, track_info) in enumerate(zip(items, album_info.tracks)):
|
||||
if item:
|
||||
|
|
@ -305,7 +305,7 @@ def match_by_id(items):
|
|||
if not albumids:
|
||||
log.debug('No album IDs found.')
|
||||
return None
|
||||
|
||||
|
||||
# If all album IDs are equal, look up the album.
|
||||
if bool(reduce(lambda x,y: x if x==y else (), albumids)):
|
||||
albumid = albumids[0]
|
||||
|
|
@ -314,7 +314,7 @@ def match_by_id(items):
|
|||
else:
|
||||
log.debug('No album ID consensus.')
|
||||
return None
|
||||
|
||||
|
||||
#fixme In the future, at the expense of performance, we could use
|
||||
# other IDs (i.e., track and artist) in case the album tag isn't
|
||||
# present, but that event seems very unlikely.
|
||||
|
|
@ -398,11 +398,11 @@ def tag_album(items, timid=False, search_artist=None, search_album=None,
|
|||
# Get current metadata.
|
||||
cur_artist, cur_album, artist_consensus = current_metadata(items)
|
||||
log.debug('Tagging %s - %s' % (cur_artist, cur_album))
|
||||
|
||||
|
||||
# The output result (distance, AlbumInfo) tuples (keyed by MB album
|
||||
# ID).
|
||||
candidates = {}
|
||||
|
||||
|
||||
# Try to find album indicated by MusicBrainz IDs.
|
||||
if search_id:
|
||||
log.debug('Searching for album ID: ' + search_id)
|
||||
|
|
@ -427,13 +427,13 @@ def tag_album(items, timid=False, search_artist=None, search_album=None,
|
|||
return cur_artist, cur_album, candidates.values(), rec
|
||||
else:
|
||||
return cur_artist, cur_album, [], RECOMMEND_NONE
|
||||
|
||||
|
||||
# Search terms.
|
||||
if not (search_artist and search_album):
|
||||
# No explicit search terms -- use current metadata.
|
||||
search_artist, search_album = cur_artist, cur_album
|
||||
log.debug(u'Search terms: %s - %s' % (search_artist, search_album))
|
||||
|
||||
|
||||
# Is this album likely to be a "various artist" release?
|
||||
va_likely = ((not artist_consensus) or
|
||||
(search_artist.lower() in VA_ARTISTS) or
|
||||
|
|
@ -446,7 +446,7 @@ def tag_album(items, timid=False, search_artist=None, search_album=None,
|
|||
log.debug(u'Evaluating %i candidates.' % len(search_cands))
|
||||
for info in search_cands:
|
||||
validate_candidate(items, candidates, info)
|
||||
|
||||
|
||||
# Sort and get the recommendation.
|
||||
candidates = sorted(candidates.itervalues())
|
||||
rec = recommendation(candidates)
|
||||
|
|
@ -456,7 +456,7 @@ def tag_item(item, timid=False, search_artist=None, search_title=None,
|
|||
search_id=None):
|
||||
"""Attempts to find metadata for a single track. Returns a
|
||||
`(candidates, recommendation)` pair where `candidates` is a list
|
||||
of `(distance, track_info)` pairs. `search_artist` and
|
||||
of `(distance, track_info)` pairs. `search_artist` and
|
||||
`search_title` may be used to override the current metadata for
|
||||
the purposes of the MusicBrainz title; likewise `search_id`.
|
||||
"""
|
||||
|
|
@ -484,7 +484,7 @@ def tag_item(item, timid=False, search_artist=None, search_title=None,
|
|||
return candidates.values(), rec
|
||||
else:
|
||||
return [], RECOMMEND_NONE
|
||||
|
||||
|
||||
# Search terms.
|
||||
if not (search_artist and search_title):
|
||||
search_artist, search_title = item.artist, item.title
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -502,14 +502,14 @@ def read_tasks(config):
|
|||
if config.incremental:
|
||||
incremental_skipped = 0
|
||||
history_dirs = history_get()
|
||||
|
||||
|
||||
for toppath in config.paths:
|
||||
# Check whether the path is to a file.
|
||||
if config.singletons and not os.path.isdir(syspath(toppath)):
|
||||
item = library.Item.from_path(toppath)
|
||||
yield ImportTask.item_task(item)
|
||||
continue
|
||||
|
||||
|
||||
# Produce paths under this directory.
|
||||
if progress:
|
||||
resume_dir = resume_dirs.get(toppath)
|
||||
|
|
@ -599,7 +599,7 @@ def user_query(config):
|
|||
task = yield task
|
||||
if task.sentinel:
|
||||
continue
|
||||
|
||||
|
||||
# Ask the user for a choice.
|
||||
choice = config.choose_match_func(task, config)
|
||||
task.set_choice(choice)
|
||||
|
|
@ -617,7 +617,7 @@ def user_query(config):
|
|||
while True:
|
||||
item_task = yield
|
||||
item_tasks.append(item_task)
|
||||
ipl = pipeline.Pipeline((emitter(), item_lookup(config),
|
||||
ipl = pipeline.Pipeline((emitter(), item_lookup(config),
|
||||
item_query(config), collector()))
|
||||
ipl.run_sequential()
|
||||
task = pipeline.multiple(item_tasks)
|
||||
|
|
@ -650,14 +650,14 @@ def show_progress(config):
|
|||
# Behave as if ASIS were selected.
|
||||
task.set_null_match()
|
||||
task.set_choice(action.ASIS)
|
||||
|
||||
|
||||
def apply_choices(config):
|
||||
"""A coroutine for applying changes to albums during the autotag
|
||||
process.
|
||||
"""
|
||||
lib = _reopen_lib(config.lib)
|
||||
task = None
|
||||
while True:
|
||||
while True:
|
||||
task = yield task
|
||||
if task.should_skip():
|
||||
continue
|
||||
|
|
@ -894,7 +894,7 @@ def run_import(**kwargs):
|
|||
ImportConfig.
|
||||
"""
|
||||
config = ImportConfig(**kwargs)
|
||||
|
||||
|
||||
# Set up the pipeline.
|
||||
if config.query is None:
|
||||
stages = [read_tasks(config)]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
|||
|
||||
# Mark the option's shortcut letter for display.
|
||||
if (default is None and not numrange and first) \
|
||||
or (isinstance(default, basestring) and
|
||||
or (isinstance(default, basestring) and
|
||||
found_letter.lower() == default.lower()):
|
||||
# The first option is the default; mark it.
|
||||
show_letter = '[%s]' % found_letter.upper()
|
||||
|
|
@ -187,7 +187,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
|||
default = numrange[0]
|
||||
else:
|
||||
default = display_letters[0].lower()
|
||||
|
||||
|
||||
# Make a prompt if one is not provided.
|
||||
if not prompt:
|
||||
prompt_parts = []
|
||||
|
|
@ -249,11 +249,11 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
|||
resp = raw_input(prompt + ' ')
|
||||
while True:
|
||||
resp = resp.strip().lower()
|
||||
|
||||
|
||||
# Try default option.
|
||||
if default is not None and not resp:
|
||||
resp = default
|
||||
|
||||
|
||||
# Try an integer input if available.
|
||||
if numrange:
|
||||
try:
|
||||
|
|
@ -266,13 +266,13 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None,
|
|||
return resp
|
||||
else:
|
||||
resp = None
|
||||
|
||||
|
||||
# Try a normal letter input.
|
||||
if resp:
|
||||
resp = resp[0]
|
||||
if resp in letters:
|
||||
return resp
|
||||
|
||||
|
||||
# Prompt for new input.
|
||||
resp = raw_input(fallback_prompt + ' ')
|
||||
|
||||
|
|
@ -293,7 +293,7 @@ def config_val(config, section, name, default, vtype=None):
|
|||
"""
|
||||
if not config.has_section(section):
|
||||
config.add_section(section)
|
||||
|
||||
|
||||
try:
|
||||
if vtype is bool:
|
||||
return config.getboolean(section, name)
|
||||
|
|
@ -387,7 +387,7 @@ def colordiff(a, b, highlight='red'):
|
|||
|
||||
a_out = []
|
||||
b_out = []
|
||||
|
||||
|
||||
matcher = SequenceMatcher(lambda x: False, a, b)
|
||||
for op, a_start, a_end, b_start, b_end in matcher.get_opcodes():
|
||||
if op == 'equal':
|
||||
|
|
@ -406,7 +406,7 @@ def colordiff(a, b, highlight='red'):
|
|||
b_out.append(colorize(highlight, b[b_start:b_end]))
|
||||
else:
|
||||
assert(False)
|
||||
|
||||
|
||||
return u''.join(a_out), u''.join(b_out)
|
||||
|
||||
def default_paths(pathmod=None):
|
||||
|
|
@ -524,7 +524,7 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
|||
_HelpSubcommand = Subcommand('help', optparse.OptionParser(),
|
||||
help='give detailed help on a specific sub-command',
|
||||
aliases=('?',))
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Create a new subcommand-aware option parser. All of the
|
||||
options to OptionParser.__init__ are supported in addition
|
||||
|
|
@ -533,41 +533,41 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
|||
# The subcommand array, with the help command included.
|
||||
self.subcommands = list(kwargs.pop('subcommands', []))
|
||||
self.subcommands.append(self._HelpSubcommand)
|
||||
|
||||
|
||||
# A more helpful default usage.
|
||||
if 'usage' not in kwargs:
|
||||
kwargs['usage'] = """
|
||||
%prog COMMAND [ARGS...]
|
||||
%prog help COMMAND"""
|
||||
|
||||
|
||||
# Super constructor.
|
||||
optparse.OptionParser.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
# Adjust the help-visible name of each subcommand.
|
||||
for subcommand in self.subcommands:
|
||||
subcommand.parser.prog = '%s %s' % \
|
||||
(self.get_prog_name(), subcommand.name)
|
||||
|
||||
# Our root parser needs to stop on the first unrecognized argument.
|
||||
|
||||
# Our root parser needs to stop on the first unrecognized argument.
|
||||
self.disable_interspersed_args()
|
||||
|
||||
|
||||
def add_subcommand(self, cmd):
|
||||
"""Adds a Subcommand object to the parser's list of commands.
|
||||
"""
|
||||
self.subcommands.append(cmd)
|
||||
|
||||
|
||||
# Add the list of subcommands to the help message.
|
||||
def format_help(self, formatter=None):
|
||||
# Get the original help message, to which we will append.
|
||||
out = optparse.OptionParser.format_help(self, formatter)
|
||||
if formatter is None:
|
||||
formatter = self.formatter
|
||||
|
||||
|
||||
# Subcommands header.
|
||||
result = ["\n"]
|
||||
result.append(formatter.format_heading('Commands'))
|
||||
formatter.indent()
|
||||
|
||||
|
||||
# Generate the display names (including aliases).
|
||||
# Also determine the help position.
|
||||
disp_names = []
|
||||
|
|
@ -577,12 +577,12 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
|||
if subcommand.aliases:
|
||||
name += ' (%s)' % ', '.join(subcommand.aliases)
|
||||
disp_names.append(name)
|
||||
|
||||
|
||||
# Set the help position based on the max width.
|
||||
proposed_help_position = len(name) + formatter.current_indent + 2
|
||||
if proposed_help_position <= formatter.max_help_position:
|
||||
help_position = max(help_position, proposed_help_position)
|
||||
|
||||
help_position = max(help_position, proposed_help_position)
|
||||
|
||||
# Add each subcommand to the output.
|
||||
for subcommand, name in zip(self.subcommands, disp_names):
|
||||
# Lifted directly from optparse.py.
|
||||
|
|
@ -601,11 +601,11 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
|||
result.extend(["%*s%s\n" % (help_position, "", line)
|
||||
for line in help_lines[1:]])
|
||||
formatter.dedent()
|
||||
|
||||
|
||||
# Concatenate the original help message with the subcommand
|
||||
# list.
|
||||
return out + "".join(result)
|
||||
|
||||
|
||||
def _subcommand_for_name(self, name):
|
||||
"""Return the subcommand in self.subcommands matching the
|
||||
given name. The name may either be the name of a subcommand or
|
||||
|
|
@ -616,16 +616,16 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
|||
name in subcommand.aliases:
|
||||
return subcommand
|
||||
return None
|
||||
|
||||
|
||||
def parse_args(self, a=None, v=None):
|
||||
"""Like OptionParser.parse_args, but returns these four items:
|
||||
- options: the options passed to the root parser
|
||||
- subcommand: the Subcommand object that was invoked
|
||||
- suboptions: the options passed to the subcommand parser
|
||||
- subargs: the positional arguments passed to the subcommand
|
||||
"""
|
||||
"""
|
||||
options, args = optparse.OptionParser.parse_args(self, a, v)
|
||||
|
||||
|
||||
if not args:
|
||||
# No command given.
|
||||
self.print_help()
|
||||
|
|
@ -635,7 +635,7 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
|||
subcommand = self._subcommand_for_name(cmdname)
|
||||
if not subcommand:
|
||||
self.error('unknown command ' + cmdname)
|
||||
|
||||
|
||||
suboptions, subargs = subcommand.parser.parse_args(args)
|
||||
|
||||
if subcommand is self._HelpSubcommand:
|
||||
|
|
@ -649,7 +649,7 @@ class SubcommandsOptionParser(optparse.OptionParser):
|
|||
# general
|
||||
self.print_help()
|
||||
self.exit()
|
||||
|
||||
|
||||
return options, subcommand, suboptions, subargs
|
||||
|
||||
|
||||
|
|
@ -701,10 +701,10 @@ def main(args=None, configfh=None):
|
|||
help="destination music directory")
|
||||
parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
|
||||
help='print debugging information')
|
||||
|
||||
|
||||
# Parse the command-line!
|
||||
options, subcommand, suboptions, subargs = parser.parse_args(args)
|
||||
|
||||
|
||||
# Open library file.
|
||||
libpath = options.libpath or \
|
||||
config_val(config, 'beets', 'library', default_libpath)
|
||||
|
|
@ -729,7 +729,7 @@ def main(args=None, configfh=None):
|
|||
replacements)
|
||||
except sqlite3.OperationalError:
|
||||
raise UserError("database file %s could not be opened" % db_path)
|
||||
|
||||
|
||||
# Configure the logger.
|
||||
log = logging.getLogger('beets')
|
||||
if options.verbose:
|
||||
|
|
@ -739,7 +739,7 @@ def main(args=None, configfh=None):
|
|||
log.debug(u'config file: %s' % util.displayable_path(configpath))
|
||||
log.debug(u'library database: %s' % util.displayable_path(lib.path))
|
||||
log.debug(u'library directory: %s' % util.displayable_path(lib.directory))
|
||||
|
||||
|
||||
# Invoke the subcommand.
|
||||
try:
|
||||
subcommand.func(lib, config, suboptions, subargs)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ def _do_query(lib, query, album, also_items=True):
|
|||
raise ui.UserError('No matching albums found.')
|
||||
elif not album and not items:
|
||||
raise ui.UserError('No matching items found.')
|
||||
|
||||
|
||||
return items, albums
|
||||
|
||||
FLOAT_EPSILON = 0.01
|
||||
|
|
@ -212,7 +212,7 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True):
|
|||
if color:
|
||||
cur_length = ui.colorize('red', cur_length)
|
||||
new_length = ui.colorize('red', new_length)
|
||||
|
||||
|
||||
# Possibly colorize changes.
|
||||
if color:
|
||||
cur_title, new_title = ui.colordiff(cur_title, new_title)
|
||||
|
|
@ -223,7 +223,7 @@ def show_change(cur_artist, cur_album, items, info, dist, color=True):
|
|||
# Show filename (non-colorized) when title is not set.
|
||||
if not item.title.strip():
|
||||
cur_title = displayable_path(os.path.basename(item.path))
|
||||
|
||||
|
||||
if cur_title != new_title:
|
||||
lhs, rhs = cur_title, new_title
|
||||
if cur_track != new_track:
|
||||
|
|
@ -345,7 +345,7 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
else:
|
||||
dist, items, info = candidates[0]
|
||||
bypass_candidates = True
|
||||
|
||||
|
||||
while True:
|
||||
# Display and choose from candidates.
|
||||
if not bypass_candidates:
|
||||
|
|
@ -387,7 +387,7 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
line += u' %s' % warning
|
||||
|
||||
print_(line)
|
||||
|
||||
|
||||
# Ask the user for a choice.
|
||||
if singleton:
|
||||
opts = ('Skip', 'Use as-is', 'Enter search', 'enter Id',
|
||||
|
|
@ -416,20 +416,20 @@ def choose_candidate(candidates, singleton, rec, color, timid,
|
|||
else:
|
||||
dist, items, info = candidates[sel-1]
|
||||
bypass_candidates = False
|
||||
|
||||
|
||||
# Show what we're about to do.
|
||||
if singleton:
|
||||
show_item_change(item, info, dist, color)
|
||||
else:
|
||||
show_change(cur_artist, cur_album, items, info, dist, color)
|
||||
|
||||
|
||||
# Exact match => tag automatically if we're not in timid mode.
|
||||
if rec == autotag.RECOMMEND_STRONG and not timid:
|
||||
if singleton:
|
||||
return info
|
||||
else:
|
||||
return info, items
|
||||
|
||||
|
||||
# Ask for confirmation.
|
||||
if singleton:
|
||||
opts = ('Apply', 'More candidates', 'Skip', 'Use as-is',
|
||||
|
|
@ -507,10 +507,10 @@ def choose_match(task, config):
|
|||
candidates, rec = task.candidates, task.rec
|
||||
while True:
|
||||
# Ask for a choice from the user.
|
||||
choice = choose_candidate(candidates, False, rec, config.color,
|
||||
choice = choose_candidate(candidates, False, rec, config.color,
|
||||
config.timid, 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):
|
||||
|
|
@ -687,7 +687,7 @@ def import_files(lib, paths, copy, move, write, autot, logpath, art, threaded,
|
|||
ignore = ignore,
|
||||
resolve_duplicate_func = resolve_duplicate,
|
||||
)
|
||||
|
||||
|
||||
finally:
|
||||
# If we were logging, close the file.
|
||||
if logfile:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -39,11 +39,11 @@ def ancestry(path, pathmod=None):
|
|||
last_path = None
|
||||
while path:
|
||||
path = pathmod.dirname(path)
|
||||
|
||||
|
||||
if path == last_path:
|
||||
break
|
||||
last_path = path
|
||||
|
||||
|
||||
if path: # don't yield ''
|
||||
out.insert(0, path)
|
||||
return out
|
||||
|
|
@ -151,11 +151,11 @@ def components(path, pathmod=None):
|
|||
comps.append(comp)
|
||||
else: # root
|
||||
comps.append(anc)
|
||||
|
||||
|
||||
last = pathmod.basename(path)
|
||||
if last:
|
||||
comps.append(last)
|
||||
|
||||
|
||||
return comps
|
||||
|
||||
def bytestring_path(path):
|
||||
|
|
@ -361,7 +361,7 @@ def levenshtein(s1, s2):
|
|||
return levenshtein(s2, s1)
|
||||
if not s1:
|
||||
return len(s2)
|
||||
|
||||
|
||||
previous_row = xrange(len(s2) + 1)
|
||||
for i, c1 in enumerate(s1):
|
||||
current_row = [i + 1]
|
||||
|
|
@ -371,7 +371,7 @@ def levenshtein(s1, s2):
|
|||
substitutions = previous_row[j] + (c1 != c2)
|
||||
current_row.append(min(insertions, deletions, substitutions))
|
||||
previous_row = current_row
|
||||
|
||||
|
||||
return previous_row[-1]
|
||||
|
||||
def plurality(objs):
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ how you would expect them to.
|
|||
'west'
|
||||
>>> Direction.north < Direction.west
|
||||
True
|
||||
|
||||
|
||||
Enumerations are classes; their instances represent the possible values
|
||||
of the enumeration. Because Python classes must have names, you may
|
||||
provide a `name` parameter to `enum`; if you don't, a meaningless one
|
||||
|
|
@ -45,31 +45,31 @@ import random
|
|||
|
||||
class Enumeration(type):
|
||||
"""A metaclass whose classes are enumerations.
|
||||
|
||||
|
||||
The `values` attribute of the class is used to populate the
|
||||
enumeration. Values may either be a list of enumerated names or a
|
||||
string containing a space-separated list of names. When the class
|
||||
is created, it is instantiated for each name value in `values`.
|
||||
Each such instance is the name of the enumerated item as the sole
|
||||
argument.
|
||||
|
||||
|
||||
The `Enumerated` class is a good choice for a superclass.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(cls, name, bases, dic):
|
||||
super(Enumeration, cls).__init__(name, bases, dic)
|
||||
|
||||
|
||||
if 'values' not in dic:
|
||||
# Do nothing if no values are provided (i.e., with
|
||||
# Enumerated itself).
|
||||
return
|
||||
|
||||
|
||||
# May be called with a single string, in which case we split on
|
||||
# whitespace for convenience.
|
||||
values = dic['values']
|
||||
if isinstance(values, basestring):
|
||||
values = values.split()
|
||||
|
||||
|
||||
# Create the Enumerated instances for each value. We have to use
|
||||
# super's __setattr__ here because we disallow setattr below.
|
||||
super(Enumeration, cls).__setattr__('_items_dict', {})
|
||||
|
|
@ -78,44 +78,44 @@ class Enumeration(type):
|
|||
item = cls(value, len(cls._items_list))
|
||||
cls._items_dict[value] = item
|
||||
cls._items_list.append(item)
|
||||
|
||||
|
||||
def __getattr__(cls, key):
|
||||
try:
|
||||
return cls._items_dict[key]
|
||||
except KeyError:
|
||||
raise AttributeError("enumeration '" + cls.__name__ +
|
||||
"' has no item '" + key + "'")
|
||||
|
||||
|
||||
def __setattr__(cls, key, val):
|
||||
raise TypeError("enumerations do not support attribute assignment")
|
||||
|
||||
|
||||
def __getitem__(cls, key):
|
||||
if isinstance(key, int):
|
||||
return cls._items_list[key]
|
||||
else:
|
||||
return getattr(cls, key)
|
||||
|
||||
|
||||
def __len__(cls):
|
||||
return len(cls._items_list)
|
||||
|
||||
|
||||
def __iter__(cls):
|
||||
return iter(cls._items_list)
|
||||
|
||||
|
||||
def __nonzero__(cls):
|
||||
# Ensures that __len__ doesn't get called before __init__ by
|
||||
# pydoc.
|
||||
return True
|
||||
|
||||
|
||||
class Enumerated(object):
|
||||
"""An item in an enumeration.
|
||||
|
||||
|
||||
Contains instance methods inherited by enumerated objects. The
|
||||
metaclass is preset to `Enumeration` for your convenience.
|
||||
|
||||
Instance attributes:
|
||||
|
||||
Instance attributes:
|
||||
name -- The name of the item.
|
||||
index -- The index of the item in its enumeration.
|
||||
|
||||
|
||||
>>> from enumeration import Enumerated
|
||||
>>> class Garment(Enumerated):
|
||||
... values = 'hat glove belt poncho lederhosen suspenders'
|
||||
|
|
@ -125,9 +125,9 @@ class Enumerated(object):
|
|||
>>> Garment.poncho.wear()
|
||||
now wearing a poncho
|
||||
"""
|
||||
|
||||
|
||||
__metaclass__ = Enumeration
|
||||
|
||||
|
||||
def __init__(self, name, index):
|
||||
self.name = name
|
||||
self.index = index
|
||||
|
|
@ -149,18 +149,18 @@ class Enumerated(object):
|
|||
|
||||
def enum(*values, **kwargs):
|
||||
"""Shorthand for creating a new Enumeration class.
|
||||
|
||||
|
||||
Call with enumeration values as a list, a space-delimited string, or
|
||||
just an argument list. To give the class a name, pass it as the
|
||||
`name` keyword argument. Otherwise, a name will be chosen for you.
|
||||
|
||||
|
||||
The following are all equivalent:
|
||||
|
||||
|
||||
enum('pinkie ring middle index thumb')
|
||||
enum('pinkie', 'ring', 'middle', 'index', 'thumb')
|
||||
enum(['pinkie', 'ring', 'middle', 'index', 'thumb'])
|
||||
"""
|
||||
|
||||
|
||||
if ('name' not in kwargs) or kwargs['name'] is None:
|
||||
# Create a probably-unique name. It doesn't really have to be
|
||||
# unique, but getting distinct names each time helps with
|
||||
|
|
@ -168,11 +168,11 @@ def enum(*values, **kwargs):
|
|||
name = 'Enumeration' + hex(random.randint(0,0xfffffff))[2:].upper()
|
||||
else:
|
||||
name = kwargs['name']
|
||||
|
||||
|
||||
if len(values) == 1:
|
||||
# If there's only one value, we have a couple of alternate calling
|
||||
# styles.
|
||||
if isinstance(values[0], basestring) or hasattr(values[0], '__iter__'):
|
||||
values = values[0]
|
||||
|
||||
|
||||
return type(name, (Enumerated,), {'values': values})
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -409,7 +409,7 @@ class Parser(object):
|
|||
# No function name.
|
||||
self.parts.append(FUNC_DELIM)
|
||||
return
|
||||
|
||||
|
||||
if self.pos >= len(self.string):
|
||||
# Identifier terminates string.
|
||||
self.parts.append(self.string[start_pos:self.pos])
|
||||
|
|
@ -447,7 +447,7 @@ class Parser(object):
|
|||
|
||||
# Extract and advance past the parsed expression.
|
||||
expressions.append(Expression(subparser.parts))
|
||||
self.pos += subparser.pos
|
||||
self.pos += subparser.pos
|
||||
|
||||
if self.pos >= len(self.string) or \
|
||||
self.string[self.pos] == GROUP_CLOSE:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -177,23 +177,23 @@ class FirstPipelineThread(PipelineThread):
|
|||
self.coro = coro
|
||||
self.out_queue = out_queue
|
||||
self.out_queue.acquire()
|
||||
|
||||
|
||||
self.abort_lock = Lock()
|
||||
self.abort_flag = False
|
||||
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
while True:
|
||||
with self.abort_lock:
|
||||
if self.abort_flag:
|
||||
return
|
||||
|
||||
|
||||
# Get the value from the generator.
|
||||
try:
|
||||
msg = self.coro.next()
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
|
||||
# Send messages to the next stage.
|
||||
for msg in _allmsgs(msg):
|
||||
with self.abort_lock:
|
||||
|
|
@ -207,7 +207,7 @@ class FirstPipelineThread(PipelineThread):
|
|||
|
||||
# Generator finished; shut down the pipeline.
|
||||
self.out_queue.release()
|
||||
|
||||
|
||||
class MiddlePipelineThread(PipelineThread):
|
||||
"""A thread running any stage in the pipeline except the first or
|
||||
last.
|
||||
|
|
@ -223,7 +223,7 @@ class MiddlePipelineThread(PipelineThread):
|
|||
try:
|
||||
# Prime the coroutine.
|
||||
self.coro.next()
|
||||
|
||||
|
||||
while True:
|
||||
with self.abort_lock:
|
||||
if self.abort_flag:
|
||||
|
|
@ -233,14 +233,14 @@ class MiddlePipelineThread(PipelineThread):
|
|||
msg = self.in_queue.get()
|
||||
if msg is POISON:
|
||||
break
|
||||
|
||||
|
||||
with self.abort_lock:
|
||||
if self.abort_flag:
|
||||
return
|
||||
|
||||
# Invoke the current stage.
|
||||
out = self.coro.send(msg)
|
||||
|
||||
|
||||
# Send messages to next stage.
|
||||
for msg in _allmsgs(out):
|
||||
with self.abort_lock:
|
||||
|
|
@ -251,7 +251,7 @@ class MiddlePipelineThread(PipelineThread):
|
|||
except:
|
||||
self.abort_all(sys.exc_info())
|
||||
return
|
||||
|
||||
|
||||
# Pipeline is shutting down normally.
|
||||
self.out_queue.release()
|
||||
|
||||
|
|
@ -273,12 +273,12 @@ class LastPipelineThread(PipelineThread):
|
|||
with self.abort_lock:
|
||||
if self.abort_flag:
|
||||
return
|
||||
|
||||
|
||||
# Get the message from the previous stage.
|
||||
msg = self.in_queue.get()
|
||||
if msg is POISON:
|
||||
break
|
||||
|
||||
|
||||
with self.abort_lock:
|
||||
if self.abort_flag:
|
||||
return
|
||||
|
|
@ -308,7 +308,7 @@ class Pipeline(object):
|
|||
self.stages.append((stage,))
|
||||
else:
|
||||
self.stages.append(stage)
|
||||
|
||||
|
||||
def run_sequential(self):
|
||||
"""Run the pipeline sequentially in the current thread. The
|
||||
stages are run one after the other. Only the first coroutine
|
||||
|
|
@ -319,7 +319,7 @@ class Pipeline(object):
|
|||
# "Prime" the coroutines.
|
||||
for coro in coros[1:]:
|
||||
coro.next()
|
||||
|
||||
|
||||
# Begin the pipeline.
|
||||
for out in coros[0]:
|
||||
msgs = _allmsgs(out)
|
||||
|
|
@ -329,7 +329,7 @@ class Pipeline(object):
|
|||
out = coro.send(msg)
|
||||
next_msgs.extend(_allmsgs(out))
|
||||
msgs = next_msgs
|
||||
|
||||
|
||||
def run_parallel(self, queue_size=DEFAULT_QUEUE_SIZE):
|
||||
"""Run the pipeline in parallel using one thread per stage. The
|
||||
messages between the stages are stored in queues of the given
|
||||
|
|
@ -354,11 +354,11 @@ class Pipeline(object):
|
|||
threads.append(
|
||||
LastPipelineThread(coro, queues[-1], threads)
|
||||
)
|
||||
|
||||
|
||||
# Start threads.
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
|
||||
|
||||
# Wait for termination. The final thread lasts the longest.
|
||||
try:
|
||||
# Using a timeout allows us to receive KeyboardInterrupt
|
||||
|
|
@ -371,7 +371,7 @@ class Pipeline(object):
|
|||
for thread in threads:
|
||||
thread.abort()
|
||||
raise
|
||||
|
||||
|
||||
finally:
|
||||
# Make completely sure that all the threads have finished
|
||||
# before we return. They should already be either finished,
|
||||
|
|
@ -388,7 +388,7 @@ class Pipeline(object):
|
|||
# Smoke test.
|
||||
if __name__ == '__main__':
|
||||
import time
|
||||
|
||||
|
||||
# Test a normally-terminating pipeline both in sequence and
|
||||
# in parallel.
|
||||
def produce():
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ class BPDError(Exception):
|
|||
self.message = message
|
||||
self.cmd_name = cmd_name
|
||||
self.index = index
|
||||
|
||||
|
||||
template = Template(u'$resp [$code@$index] {$cmd_name} $message')
|
||||
def response(self):
|
||||
"""Returns a string to be used as the response code for the
|
||||
|
|
@ -120,7 +120,7 @@ ArgumentNotFoundError = make_bpd_error(ERROR_NO_EXIST, 'argument not found')
|
|||
def cast_arg(t, val):
|
||||
"""Attempts to call t on val, raising a CommandArgumentError
|
||||
on ValueError.
|
||||
|
||||
|
||||
If 't' is the special string 'intbool', attempts to cast first
|
||||
to an int and then to a bool (i.e., 1=True, 0=False).
|
||||
"""
|
||||
|
|
@ -142,17 +142,17 @@ class BPDClose(Exception):
|
|||
|
||||
class BaseServer(object):
|
||||
"""A MPD-compatible music player server.
|
||||
|
||||
|
||||
The functions with the `cmd_` prefix are invoked in response to
|
||||
client commands. For instance, if the client says `status`,
|
||||
`cmd_status` will be invoked. The arguments to the client's commands
|
||||
are used as function arguments following the connection issuing the
|
||||
command. The functions may send data on the connection. They may
|
||||
also raise BPDError exceptions to report errors.
|
||||
|
||||
|
||||
This is a generic superclass and doesn't support many commands.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT,
|
||||
password=DEFAULT_PASSWORD):
|
||||
"""Create a new server bound to address `host` and listening
|
||||
|
|
@ -160,7 +160,7 @@ class BaseServer(object):
|
|||
anything significant on the server.
|
||||
"""
|
||||
self.host, self.port, self.password = host, port, password
|
||||
|
||||
|
||||
# Default server values.
|
||||
self.random = False
|
||||
self.repeat = False
|
||||
|
|
@ -174,7 +174,7 @@ class BaseServer(object):
|
|||
|
||||
# Object for random numbers generation
|
||||
self.random_obj = random.Random()
|
||||
|
||||
|
||||
def run(self):
|
||||
"""Block and start listening for connections from clients. An
|
||||
interrupt (^C) closes the server.
|
||||
|
|
@ -188,7 +188,7 @@ class BaseServer(object):
|
|||
single song's metadata.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def _item_id(self, item):
|
||||
"""An abstract method returning the integer id for an item.
|
||||
"""
|
||||
|
|
@ -242,16 +242,16 @@ class BaseServer(object):
|
|||
def cmd_ping(self, conn):
|
||||
"""Succeeds."""
|
||||
pass
|
||||
|
||||
|
||||
def cmd_kill(self, conn):
|
||||
"""Exits the server process."""
|
||||
self.listener.close()
|
||||
exit(0)
|
||||
|
||||
|
||||
def cmd_close(self, conn):
|
||||
"""Closes the connection."""
|
||||
raise BPDClose()
|
||||
|
||||
|
||||
def cmd_password(self, conn, password):
|
||||
"""Attempts password authentication."""
|
||||
if password == self.password:
|
||||
|
|
@ -259,20 +259,20 @@ class BaseServer(object):
|
|||
else:
|
||||
conn.authenticated = False
|
||||
raise BPDError(ERROR_PASSWORD, 'incorrect password')
|
||||
|
||||
|
||||
def cmd_commands(self, conn):
|
||||
"""Lists the commands available to the user."""
|
||||
if self.password and not conn.authenticated:
|
||||
# Not authenticated. Show limited list of commands.
|
||||
for cmd in SAFE_COMMANDS:
|
||||
yield u'command: ' + cmd
|
||||
|
||||
|
||||
else:
|
||||
# Authenticated. Show all commands.
|
||||
for func in dir(self):
|
||||
if func.startswith('cmd_'):
|
||||
yield u'command: ' + func[4:]
|
||||
|
||||
|
||||
def cmd_notcommands(self, conn):
|
||||
"""Lists all unavailable commands."""
|
||||
if self.password and not conn.authenticated:
|
||||
|
|
@ -282,15 +282,15 @@ class BaseServer(object):
|
|||
cmd = func[4:]
|
||||
if cmd not in SAFE_COMMANDS:
|
||||
yield u'command: ' + cmd
|
||||
|
||||
|
||||
else:
|
||||
# Authenticated. No commands are unavailable.
|
||||
pass
|
||||
|
||||
|
||||
def cmd_status(self, conn):
|
||||
"""Returns some status information for use with an
|
||||
implementation of cmd_status.
|
||||
|
||||
|
||||
Gives a list of response-lines for: volume, repeat, random,
|
||||
playlist, playlistlength, and xfade.
|
||||
"""
|
||||
|
|
@ -301,7 +301,7 @@ class BaseServer(object):
|
|||
u'playlistlength: ' + unicode(len(self.playlist)),
|
||||
u'xfade: ' + unicode(self.crossfade),
|
||||
)
|
||||
|
||||
|
||||
if self.current_index == -1:
|
||||
state = u'stop'
|
||||
elif self.paused:
|
||||
|
|
@ -309,7 +309,7 @@ class BaseServer(object):
|
|||
else:
|
||||
state = u'play'
|
||||
yield u'state: ' + state
|
||||
|
||||
|
||||
if self.current_index != -1: # i.e., paused or playing
|
||||
current_id = self._item_id(self.playlist[self.current_index])
|
||||
yield u'song: ' + unicode(self.current_index)
|
||||
|
|
@ -324,34 +324,34 @@ class BaseServer(object):
|
|||
command (for instance, when playing a file).
|
||||
"""
|
||||
self.error = None
|
||||
|
||||
|
||||
def cmd_random(self, conn, state):
|
||||
"""Set or unset random (shuffle) mode."""
|
||||
self.random = cast_arg('intbool', state)
|
||||
|
||||
|
||||
def cmd_repeat(self, conn, state):
|
||||
"""Set or unset repeat mode."""
|
||||
self.repeat = cast_arg('intbool', state)
|
||||
|
||||
|
||||
def cmd_setvol(self, conn, vol):
|
||||
"""Set the player's volume level (0-100)."""
|
||||
vol = cast_arg(int, vol)
|
||||
if vol < VOLUME_MIN or vol > VOLUME_MAX:
|
||||
raise BPDError(ERROR_ARG, u'volume out of range')
|
||||
self.volume = vol
|
||||
|
||||
|
||||
def cmd_crossfade(self, conn, crossfade):
|
||||
"""Set the number of seconds of crossfading."""
|
||||
crossfade = cast_arg(int, crossfade)
|
||||
if crossfade < 0:
|
||||
if crossfade < 0:
|
||||
raise BPDError(ERROR_ARG, u'crossfade time must be nonnegative')
|
||||
|
||||
|
||||
def cmd_clear(self, conn):
|
||||
"""Clear the playlist."""
|
||||
self.playlist = []
|
||||
self.playlist_version += 1
|
||||
self.cmd_stop(conn)
|
||||
|
||||
|
||||
def cmd_delete(self, conn, index):
|
||||
"""Remove the song at index from the playlist."""
|
||||
index = cast_arg(int, index)
|
||||
|
|
@ -369,7 +369,7 @@ class BaseServer(object):
|
|||
|
||||
def cmd_deleteid(self, conn, track_id):
|
||||
self.cmd_delete(conn, self._id_to_index(track_id))
|
||||
|
||||
|
||||
def cmd_move(self, conn, idx_from, idx_to):
|
||||
"""Move a track in the playlist."""
|
||||
idx_from = cast_arg(int, idx_from)
|
||||
|
|
@ -379,7 +379,7 @@ class BaseServer(object):
|
|||
self.playlist.insert(idx_to, track)
|
||||
except IndexError:
|
||||
raise ArgumentIndexError()
|
||||
|
||||
|
||||
# Update currently-playing song.
|
||||
if idx_from == self.current_index:
|
||||
self.current_index = idx_to
|
||||
|
|
@ -387,13 +387,13 @@ class BaseServer(object):
|
|||
self.current_index -= 1
|
||||
elif idx_from > self.current_index >= idx_to:
|
||||
self.current_index += 1
|
||||
|
||||
|
||||
self.playlist_version += 1
|
||||
|
||||
|
||||
def cmd_moveid(self, conn, id_from, idx_to):
|
||||
idx_from = self._id_to_index(idx_from)
|
||||
return self.cmd_move(conn, idx_from, idx_to)
|
||||
|
||||
|
||||
def cmd_swap(self, conn, i, j):
|
||||
"""Swaps two tracks in the playlist."""
|
||||
i = cast_arg(int, i)
|
||||
|
|
@ -403,27 +403,27 @@ class BaseServer(object):
|
|||
track_j = self.playlist[j]
|
||||
except IndexError:
|
||||
raise ArgumentIndexError()
|
||||
|
||||
|
||||
self.playlist[j] = track_i
|
||||
self.playlist[i] = track_j
|
||||
|
||||
|
||||
# Update currently-playing song.
|
||||
if self.current_index == i:
|
||||
self.current_index = j
|
||||
elif self.current_index == j:
|
||||
self.current_index = i
|
||||
|
||||
|
||||
self.playlist_version += 1
|
||||
|
||||
|
||||
def cmd_swapid(self, conn, i_id, j_id):
|
||||
i = self._id_to_index(i_id)
|
||||
j = self._id_to_index(j_id)
|
||||
return self.cmd_swap(conn, i, j)
|
||||
|
||||
|
||||
def cmd_urlhandlers(self, conn):
|
||||
"""Indicates supported URL schemes. None by default."""
|
||||
pass
|
||||
|
||||
|
||||
def cmd_playlistinfo(self, conn, index=-1):
|
||||
"""Gives metadata information about the entire playlist or a
|
||||
single track, given by its index.
|
||||
|
|
@ -440,25 +440,25 @@ class BaseServer(object):
|
|||
yield self._item_info(track)
|
||||
def cmd_playlistid(self, conn, track_id=-1):
|
||||
return self.cmd_playlistinfo(conn, self._id_to_index(track_id))
|
||||
|
||||
|
||||
def cmd_plchanges(self, conn, version):
|
||||
"""Sends playlist changes since the given version.
|
||||
|
||||
|
||||
This is a "fake" implementation that ignores the version and
|
||||
just returns the entire playlist (rather like version=0). This
|
||||
seems to satisfy many clients.
|
||||
"""
|
||||
return self.cmd_playlistinfo(conn)
|
||||
|
||||
|
||||
def cmd_plchangesposid(self, conn, version):
|
||||
"""Like plchanges, but only sends position and id.
|
||||
|
||||
|
||||
Also a dummy implementation.
|
||||
"""
|
||||
for idx, track in enumerate(self.playlist):
|
||||
yield u'cpos: ' + unicode(idx)
|
||||
yield u'Id: ' + unicode(track.id)
|
||||
|
||||
|
||||
def cmd_currentsong(self, conn):
|
||||
"""Sends information about the currently-playing song.
|
||||
"""
|
||||
|
|
@ -474,7 +474,7 @@ class BaseServer(object):
|
|||
return self.cmd_stop(conn)
|
||||
else:
|
||||
return self.cmd_play(conn)
|
||||
|
||||
|
||||
def cmd_previous(self, conn):
|
||||
"""Step back to the last song."""
|
||||
self.current_index = self._prev_idx()
|
||||
|
|
@ -482,33 +482,33 @@ class BaseServer(object):
|
|||
return self.cmd_stop(conn)
|
||||
else:
|
||||
return self.cmd_play(conn)
|
||||
|
||||
|
||||
def cmd_pause(self, conn, state=None):
|
||||
"""Set the pause state playback."""
|
||||
if state is None:
|
||||
self.paused = not self.paused # Toggle.
|
||||
else:
|
||||
self.paused = cast_arg('intbool', state)
|
||||
|
||||
|
||||
def cmd_play(self, conn, index=-1):
|
||||
"""Begin playback, possibly at a specified playlist index."""
|
||||
index = cast_arg(int, index)
|
||||
|
||||
|
||||
if index < -1 or index > len(self.playlist):
|
||||
raise ArgumentIndexError()
|
||||
|
||||
|
||||
if index == -1: # No index specified: start where we are.
|
||||
if not self.playlist: # Empty playlist: stop immediately.
|
||||
return self.cmd_stop(conn)
|
||||
if self.current_index == -1: # No current song.
|
||||
self.current_index = 0 # Start at the beginning.
|
||||
# If we have a current song, just stay there.
|
||||
|
||||
|
||||
else: # Start with the specified index.
|
||||
self.current_index = index
|
||||
|
||||
|
||||
self.paused = False
|
||||
|
||||
|
||||
def cmd_playid(self, conn, track_id=0):
|
||||
track_id = cast_arg(int, track_id)
|
||||
if track_id == -1:
|
||||
|
|
@ -516,12 +516,12 @@ class BaseServer(object):
|
|||
else:
|
||||
index = self._id_to_index(track_id)
|
||||
return self.cmd_play(conn, index)
|
||||
|
||||
|
||||
def cmd_stop(self, conn):
|
||||
"""Stop playback."""
|
||||
self.current_index = -1
|
||||
self.paused = False
|
||||
|
||||
|
||||
def cmd_seek(self, conn, index, pos):
|
||||
"""Seek to a specified point in a specified song."""
|
||||
index = cast_arg(int, index)
|
||||
|
|
@ -548,7 +548,7 @@ class Connection(object):
|
|||
self.server = server
|
||||
self.sock = sock
|
||||
self.authenticated = False
|
||||
|
||||
|
||||
def send(self, lines):
|
||||
"""Send lines, which which is either a single string or an
|
||||
iterable consisting of strings, to the client. A newline is
|
||||
|
|
@ -562,7 +562,7 @@ class Connection(object):
|
|||
if isinstance(out, unicode):
|
||||
out = out.encode('utf8')
|
||||
return self.sock.sendall(out)
|
||||
|
||||
|
||||
def do_command(self, command):
|
||||
"""A coroutine that runs the given command and sends an
|
||||
appropriate response."""
|
||||
|
|
@ -574,20 +574,20 @@ class Connection(object):
|
|||
else:
|
||||
# Send success code.
|
||||
yield self.send(RESP_OK)
|
||||
|
||||
|
||||
def run(self):
|
||||
"""Send a greeting to the client and begin processing commands
|
||||
as they arrive.
|
||||
"""
|
||||
yield self.send(HELLO)
|
||||
|
||||
|
||||
clist = None # Initially, no command list is being constructed.
|
||||
while True:
|
||||
line = (yield self.sock.readline()).strip()
|
||||
if not line:
|
||||
break
|
||||
log.debug(line)
|
||||
|
||||
|
||||
if clist is not None:
|
||||
# Command list already opened.
|
||||
if line == CLIST_END:
|
||||
|
|
@ -595,11 +595,11 @@ class Connection(object):
|
|||
clist = None # Clear the command list.
|
||||
else:
|
||||
clist.append(Command(line))
|
||||
|
||||
|
||||
elif line == CLIST_BEGIN or line == CLIST_VERBOSE_BEGIN:
|
||||
# Begin a command list.
|
||||
clist = CommandList([], line == CLIST_VERBOSE_BEGIN)
|
||||
|
||||
|
||||
else:
|
||||
# Ordinary command.
|
||||
try:
|
||||
|
|
@ -608,7 +608,7 @@ class Connection(object):
|
|||
# Command indicates that the conn should close.
|
||||
self.sock.close()
|
||||
return
|
||||
|
||||
|
||||
@classmethod
|
||||
def handler(cls, server):
|
||||
def _handle(sock):
|
||||
|
|
@ -620,10 +620,10 @@ class Connection(object):
|
|||
class Command(object):
|
||||
"""A command issued by the client for processing by the server.
|
||||
"""
|
||||
|
||||
|
||||
command_re = re.compile(r'^([^ \t]+)[ \t]*')
|
||||
arg_re = re.compile(r'"((?:\\"|[^"])+)"|([^ \t"]+)')
|
||||
|
||||
|
||||
def __init__(self, s):
|
||||
"""Creates a new `Command` from the given string, `s`, parsing
|
||||
the string for command name and arguments.
|
||||
|
|
@ -643,7 +643,7 @@ class Command(object):
|
|||
arg = match[1]
|
||||
arg = arg.decode('utf8')
|
||||
self.args.append(arg)
|
||||
|
||||
|
||||
def run(self, conn):
|
||||
"""A coroutine that executes the command on the given
|
||||
connection.
|
||||
|
|
@ -653,36 +653,36 @@ class Command(object):
|
|||
if not hasattr(conn.server, func_name):
|
||||
raise BPDError(ERROR_UNKNOWN, u'unknown command', self.name)
|
||||
func = getattr(conn.server, func_name)
|
||||
|
||||
|
||||
# Ensure we have permission for this command.
|
||||
if conn.server.password and \
|
||||
not conn.authenticated and \
|
||||
self.name not in SAFE_COMMANDS:
|
||||
raise BPDError(ERROR_PERMISSION, u'insufficient privileges')
|
||||
|
||||
|
||||
try:
|
||||
args = [conn] + self.args
|
||||
results = func(*args)
|
||||
if results:
|
||||
for data in results:
|
||||
yield conn.send(data)
|
||||
|
||||
|
||||
except BPDError, e:
|
||||
# An exposed error. Set the command name and then let
|
||||
# the Connection handle it.
|
||||
e.cmd_name = self.name
|
||||
raise e
|
||||
|
||||
|
||||
except BPDClose:
|
||||
# An indication that the connection should close. Send
|
||||
# it on the Connection.
|
||||
raise
|
||||
|
||||
|
||||
except Exception, e:
|
||||
# An "unintentional" error. Hide it from the client.
|
||||
log.error(traceback.format_exc(e))
|
||||
raise BPDError(ERROR_SYSTEM, u'server error', self.name)
|
||||
|
||||
|
||||
|
||||
class CommandList(list):
|
||||
"""A list of commands issued by the client for processing by the
|
||||
|
|
@ -737,7 +737,7 @@ class Server(BaseServer):
|
|||
self.lib = library
|
||||
self.player = gstplayer.GstPlayer(self.play_finished)
|
||||
self.cmd_update(None)
|
||||
|
||||
|
||||
def run(self):
|
||||
self.player.run()
|
||||
super(Server, self).run()
|
||||
|
|
@ -747,10 +747,10 @@ class Server(BaseServer):
|
|||
track.
|
||||
"""
|
||||
self.cmd_next(None)
|
||||
|
||||
|
||||
|
||||
|
||||
# Metadata helper functions.
|
||||
|
||||
|
||||
def _item_info(self, item):
|
||||
info_lines = [u'file: ' + self.lib.destination(item, fragment=True),
|
||||
u'Time: ' + unicode(int(item.length)),
|
||||
|
|
@ -759,25 +759,25 @@ class Server(BaseServer):
|
|||
u'Album: ' + item.album,
|
||||
u'Genre: ' + item.genre,
|
||||
]
|
||||
|
||||
|
||||
track = unicode(item.track)
|
||||
if item.tracktotal:
|
||||
track += u'/' + unicode(item.tracktotal)
|
||||
info_lines.append(u'Track: ' + track)
|
||||
|
||||
|
||||
info_lines.append(u'Date: ' + unicode(item.year))
|
||||
|
||||
|
||||
try:
|
||||
pos = self._id_to_index(item.id)
|
||||
info_lines.append(u'Pos: ' + unicode(pos))
|
||||
except ArgumentNotFoundError:
|
||||
# Don't include position if not in playlist.
|
||||
pass
|
||||
|
||||
|
||||
info_lines.append(u'Id: ' + unicode(item.id))
|
||||
|
||||
|
||||
return info_lines
|
||||
|
||||
|
||||
def _item_id(self, item):
|
||||
return item.id
|
||||
|
||||
|
|
@ -799,7 +799,7 @@ class Server(BaseServer):
|
|||
|
||||
def _resolve_path(self, path):
|
||||
"""Returns a VFS node or an item ID located at the path given.
|
||||
If the path does not exist, raises a
|
||||
If the path does not exist, raises a
|
||||
"""
|
||||
components = path.split(u'/')
|
||||
node = self.tree
|
||||
|
|
@ -820,12 +820,12 @@ class Server(BaseServer):
|
|||
raise ArgumentNotFoundError()
|
||||
|
||||
return node
|
||||
|
||||
|
||||
def _path_join(self, p1, p2):
|
||||
"""Smashes together two BPD paths."""
|
||||
out = p1 + u'/' + p2
|
||||
return out.replace(u'//', u'/').replace(u'//', u'/')
|
||||
|
||||
|
||||
def cmd_lsinfo(self, conn, path=u"/"):
|
||||
"""Sends info on all the items in the path."""
|
||||
node = self._resolve_path(path)
|
||||
|
|
@ -842,7 +842,7 @@ class Server(BaseServer):
|
|||
# Strip leading slash (libmpc rejects this).
|
||||
dirpath = dirpath[1:]
|
||||
yield u'directory: %s' % dirpath
|
||||
|
||||
|
||||
def _listall(self, basepath, node, info=False):
|
||||
"""Helper function for recursive listing. If info, show
|
||||
tracks' complete info; otherwise, just show items' paths.
|
||||
|
|
@ -864,15 +864,15 @@ class Server(BaseServer):
|
|||
newpath = self._path_join(basepath, name)
|
||||
yield u'directory: ' + newpath
|
||||
for v in self._listall(newpath, subdir, info): yield v
|
||||
|
||||
|
||||
def cmd_listall(self, conn, path=u"/"):
|
||||
"""Send the paths all items in the directory, recursively."""
|
||||
return self._listall(path, self._resolve_path(path), False)
|
||||
def cmd_listallinfo(self, conn, path=u"/"):
|
||||
"""Send info on all the items in the directory, recursively."""
|
||||
return self._listall(path, self._resolve_path(path), True)
|
||||
|
||||
|
||||
|
||||
|
||||
# Playlist manipulation.
|
||||
|
||||
def _all_items(self, node):
|
||||
|
|
@ -889,7 +889,7 @@ class Server(BaseServer):
|
|||
for v in self._all_items(itemid): yield v
|
||||
for name, subdir in sorted(node.dirs.iteritems()):
|
||||
for v in self._all_items(subdir): yield v
|
||||
|
||||
|
||||
def _add(self, path, send_id=False):
|
||||
"""Adds a track or directory to the playlist, specified by the
|
||||
path. If `send_id`, write each item's id to the client.
|
||||
|
|
@ -899,13 +899,13 @@ class Server(BaseServer):
|
|||
if send_id:
|
||||
yield u'Id: ' + unicode(item.id)
|
||||
self.playlist_version += 1
|
||||
|
||||
|
||||
def cmd_add(self, conn, path):
|
||||
"""Adds a track or directory to the playlist, specified by a
|
||||
path.
|
||||
"""
|
||||
return self._add(path, False)
|
||||
|
||||
|
||||
def cmd_addid(self, conn, path):
|
||||
"""Same as `cmd_add` but sends an id back to the client."""
|
||||
return self._add(path, True)
|
||||
|
|
@ -918,13 +918,13 @@ class Server(BaseServer):
|
|||
yield line
|
||||
if self.current_index > -1:
|
||||
item = self.playlist[self.current_index]
|
||||
|
||||
|
||||
yield u'bitrate: ' + unicode(item.bitrate/1000)
|
||||
#fixme: missing 'audio'
|
||||
|
||||
|
||||
(pos, total) = self.player.time()
|
||||
yield u'time: ' + unicode(pos) + u':' + unicode(total)
|
||||
|
||||
|
||||
#fixme: also missing 'updating_db'
|
||||
|
||||
|
||||
|
|
@ -970,7 +970,7 @@ class Server(BaseServer):
|
|||
"""
|
||||
for tag in self.tagtype_map:
|
||||
yield u'tagtype: ' + tag
|
||||
|
||||
|
||||
def _tagtype_lookup(self, tag):
|
||||
"""Uses `tagtype_map` to look up the beets column name for an
|
||||
MPD tagtype (or throw an appropriate exception). Returns both
|
||||
|
|
@ -1005,7 +1005,7 @@ class Server(BaseServer):
|
|||
return beets.library.AndQuery(queries)
|
||||
else: # No key-value pairs.
|
||||
return beets.library.TrueQuery()
|
||||
|
||||
|
||||
def cmd_search(self, conn, *kv):
|
||||
"""Perform a substring match for items."""
|
||||
query = self._metadata_query(beets.library.SubstringQuery,
|
||||
|
|
@ -1013,7 +1013,7 @@ class Server(BaseServer):
|
|||
kv)
|
||||
for item in self.lib.items(query):
|
||||
yield self._item_info(item)
|
||||
|
||||
|
||||
def cmd_find(self, conn, *kv):
|
||||
"""Perform an exact match for items."""
|
||||
query = self._metadata_query(beets.library.MatchQuery,
|
||||
|
|
@ -1021,24 +1021,24 @@ class Server(BaseServer):
|
|||
kv)
|
||||
for item in self.lib.items(query):
|
||||
yield self._item_info(item)
|
||||
|
||||
|
||||
def cmd_list(self, conn, show_tag, *kv):
|
||||
"""List distinct metadata values for show_tag, possibly
|
||||
filtered by matching match_tag to match_term.
|
||||
"""
|
||||
show_tag_canon, show_key = self._tagtype_lookup(show_tag)
|
||||
query = self._metadata_query(beets.library.MatchQuery, None, kv)
|
||||
|
||||
|
||||
clause, subvals = query.clause()
|
||||
statement = 'SELECT DISTINCT ' + show_key + \
|
||||
' FROM items WHERE ' + clause + \
|
||||
' ORDER BY ' + show_key
|
||||
with self.lib.transaction() as tx:
|
||||
rows = tx.query(statement, subvals)
|
||||
|
||||
|
||||
for row in rows:
|
||||
yield show_tag_canon + u': ' + unicode(row[0])
|
||||
|
||||
|
||||
def cmd_count(self, conn, tag, value):
|
||||
"""Returns the number and total time of songs matching the
|
||||
tag/value query.
|
||||
|
|
@ -1048,31 +1048,31 @@ class Server(BaseServer):
|
|||
songs, playtime = query.count(self.lib)
|
||||
yield u'songs: ' + unicode(songs)
|
||||
yield u'playtime: ' + unicode(int(playtime))
|
||||
|
||||
|
||||
|
||||
|
||||
# "Outputs." Just a dummy implementation because we don't control
|
||||
# any outputs.
|
||||
|
||||
|
||||
def cmd_outputs(self, conn):
|
||||
"""List the available outputs."""
|
||||
yield (u'outputid: 0',
|
||||
u'outputname: gstreamer',
|
||||
u'outputenabled: 1',
|
||||
)
|
||||
|
||||
|
||||
def cmd_enableoutput(self, conn, output_id):
|
||||
output_id = cast_arg(int, output_id)
|
||||
if output_id != 0:
|
||||
raise ArgumentIndexError()
|
||||
|
||||
|
||||
def cmd_disableoutput(self, conn, output_id):
|
||||
output_id = cast_arg(int, output_id)
|
||||
if output_id == 0:
|
||||
raise BPDError(ERROR_ARG, u'cannot disable this output')
|
||||
else:
|
||||
raise ArgumentIndexError()
|
||||
|
||||
|
||||
|
||||
|
||||
# Playback control. The functions below hook into the
|
||||
# half-implementations provided by the base class. Together, they're
|
||||
# enough to implement all normal playback functionality.
|
||||
|
|
@ -1081,7 +1081,7 @@ class Server(BaseServer):
|
|||
new_index = index != -1 and index != self.current_index
|
||||
was_paused = self.paused
|
||||
super(Server, self).cmd_play(conn, index)
|
||||
|
||||
|
||||
if self.current_index > -1: # Not stopped.
|
||||
if was_paused and not new_index:
|
||||
# Just unpause.
|
||||
|
|
@ -1099,7 +1099,7 @@ class Server(BaseServer):
|
|||
def cmd_stop(self, conn):
|
||||
super(Server, self).cmd_stop(conn)
|
||||
self.player.stop()
|
||||
|
||||
|
||||
def cmd_seek(self, conn, index, pos):
|
||||
"""Seeks to the specified position in the specified song."""
|
||||
index = cast_arg(int, index)
|
||||
|
|
@ -1152,6 +1152,6 @@ class BPDPlugin(BeetsPlugin):
|
|||
DEFAULT_PASSWORD)
|
||||
debug = opts.debug or False
|
||||
self.start_bpd(lib, host, int(port), password, debug)
|
||||
|
||||
|
||||
cmd.func = func
|
||||
return [cmd]
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ class ThreadException(Exception):
|
|||
self.exc_info = exc_info
|
||||
def reraise(self):
|
||||
raise self.exc_info[0], self.exc_info[1], self.exc_info[2]
|
||||
|
||||
|
||||
def run(root_coro):
|
||||
"""Schedules a coroutine, running it to completion. This
|
||||
encapsulates the Bluelet scheduler, which the root coroutine can
|
||||
|
|
@ -138,7 +138,7 @@ def run(root_coro):
|
|||
# suspended until the delegate completes. This dictionary keeps
|
||||
# track of each running delegate's delegator.
|
||||
delegators = {}
|
||||
|
||||
|
||||
def advance_thread(coro, value, is_exc=False):
|
||||
"""After an event is fired, run a given coroutine associated with
|
||||
it in the threads dict until it yields again. If the coroutine
|
||||
|
|
@ -222,7 +222,7 @@ def run(root_coro):
|
|||
threads[event2coro[event]] = ReturnEvent(None)
|
||||
else:
|
||||
advance_thread(event2coro[event], value)
|
||||
|
||||
|
||||
except ThreadException, te:
|
||||
# Exception raised from inside a thread.
|
||||
event = ExceptionEvent(te.exc_info)
|
||||
|
|
@ -235,7 +235,7 @@ def run(root_coro):
|
|||
# The thread is root-level. Raise in client code.
|
||||
exit_te = te
|
||||
break
|
||||
|
||||
|
||||
except:
|
||||
# For instance, KeyboardInterrupt during select(). Raise
|
||||
# into root thread and terminate others.
|
||||
|
|
@ -414,7 +414,7 @@ def server(host, port, func):
|
|||
yield func(conn)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
listener = Listener(host, port)
|
||||
try:
|
||||
while True:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -27,27 +27,27 @@ import urllib
|
|||
|
||||
class GstPlayer(object):
|
||||
"""A music player abstracting GStreamer's Playbin element.
|
||||
|
||||
|
||||
Create a player object, then call run() to start a thread with a
|
||||
runloop. Then call play_file to play music. Use player.playing
|
||||
to check whether music is currently playing.
|
||||
|
||||
|
||||
A basic play queue is also implemented (just a Python list,
|
||||
player.queue, whose last element is next to play). To use it,
|
||||
just call enqueue() and then play(). When a track finishes and
|
||||
another is available on the queue, it is played automatically.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, finished_callback=None):
|
||||
"""Initialize a player.
|
||||
|
||||
|
||||
If a finished_callback is provided, it is called every time a
|
||||
track started with play_file finishes.
|
||||
|
||||
Once the player has been created, call run() to begin the main
|
||||
runloop in a separate thread.
|
||||
"""
|
||||
|
||||
|
||||
# Set up the Gstreamer player. From the pygst tutorial:
|
||||
# http://pygstdocs.berlios.de/pygst-tutorial/playbin.html
|
||||
self.player = gst.element_factory_make("playbin2", "player")
|
||||
|
|
@ -56,7 +56,7 @@ class GstPlayer(object):
|
|||
bus = self.player.get_bus()
|
||||
bus.add_signal_watch()
|
||||
bus.connect("message", self._handle_message)
|
||||
|
||||
|
||||
# Set up our own stuff.
|
||||
self.playing = False
|
||||
self.finished_callback = finished_callback
|
||||
|
|
@ -68,7 +68,7 @@ class GstPlayer(object):
|
|||
# gst's get_state function returns a 3-tuple; we just want the
|
||||
# status flag in position 1.
|
||||
return self.player.get_state()[1]
|
||||
|
||||
|
||||
def _handle_message(self, bus, message):
|
||||
"""Callback for status updates from GStreamer."""
|
||||
if message.type == gst.MESSAGE_EOS:
|
||||
|
|
@ -115,7 +115,7 @@ class GstPlayer(object):
|
|||
if self._get_state() == gst.STATE_PAUSED:
|
||||
self.player.set_state(gst.STATE_PLAYING)
|
||||
self.playing = True
|
||||
|
||||
|
||||
def pause(self):
|
||||
"""Pause playback."""
|
||||
self.player.set_state(gst.STATE_PAUSED)
|
||||
|
|
@ -128,7 +128,7 @@ class GstPlayer(object):
|
|||
|
||||
def run(self):
|
||||
"""Start a new thread for the player.
|
||||
|
||||
|
||||
Call this function before trying to play any music with
|
||||
play_file() or play().
|
||||
"""
|
||||
|
|
@ -150,7 +150,7 @@ class GstPlayer(object):
|
|||
length = self.player.query_duration(fmt, None)[0]/(10**9)
|
||||
self.cached_time = (pos, length)
|
||||
return (pos, length)
|
||||
|
||||
|
||||
except gst.QueryError:
|
||||
# Stream not ready. For small gaps of time, for instance
|
||||
# after seeking, the time values are unavailable. For this
|
||||
|
|
@ -166,14 +166,14 @@ class GstPlayer(object):
|
|||
if position > cur_len:
|
||||
self.stop()
|
||||
return
|
||||
|
||||
|
||||
fmt = gst.Format(gst.FORMAT_TIME)
|
||||
ns = position * 10**9 # convert to nanoseconds
|
||||
self.player.seek_simple(fmt, gst.SEEK_FLAG_FLUSH, ns)
|
||||
|
||||
|
||||
# save new cached time
|
||||
self.cached_time = (position, cur_len)
|
||||
|
||||
|
||||
def block(self):
|
||||
"""Block until playing finishes."""
|
||||
while self.playing:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -63,11 +63,11 @@ def acoustid_match(path):
|
|||
res = acoustid.lookup(API_KEY, fp, duration,
|
||||
meta='recordings releases')
|
||||
except acoustid.AcoustidError, exc:
|
||||
log.debug('fingerprint matching %s failed: %s' %
|
||||
log.debug('fingerprint matching %s failed: %s' %
|
||||
(repr(path), str(exc)))
|
||||
return None
|
||||
log.debug('chroma: fingerprinted %s' % repr(path))
|
||||
|
||||
|
||||
# Ensure the response is usable and parse it.
|
||||
if res['status'] != 'ok' or not res.get('results'):
|
||||
log.debug('chroma: no match found')
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -38,9 +38,9 @@ class ImportFeedsPlugin(BeetsPlugin):
|
|||
_feeds_dir = ui.config_val(config, 'importfeeds', 'feeds_dir', None)
|
||||
_feeds_dir = os.path.expanduser(_feeds_dir)
|
||||
|
||||
_m3u_name = ui.config_val(config, 'importfeeds', 'm3u_name',
|
||||
_m3u_name = ui.config_val(config, 'importfeeds', 'm3u_name',
|
||||
M3U_DEFAULT_NAME)
|
||||
|
||||
|
||||
if _feeds_dir and not os.path.exists(_feeds_dir):
|
||||
os.makedirs(_feeds_dir)
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ def _get_feeds_dir(lib):
|
|||
return dirpath
|
||||
|
||||
def _build_m3u_filename(basename):
|
||||
"""Builds unique m3u filename by appending given basename to current
|
||||
"""Builds unique m3u filename by appending given basename to current
|
||||
date."""
|
||||
|
||||
basename = re.sub(r"[\s,'\"]", '_', basename)
|
||||
|
|
@ -81,15 +81,15 @@ def _record_items(lib, basename, items):
|
|||
_feeds_dir = _get_feeds_dir(lib)
|
||||
|
||||
paths = []
|
||||
for item in items:
|
||||
for item in items:
|
||||
paths.append(os.path.relpath(item.path, _feeds_dir))
|
||||
|
||||
if 'm3u' in _feeds_formats:
|
||||
m3u_path = os.path.join(_feeds_dir, _m3u_name)
|
||||
m3u_path = os.path.join(_feeds_dir, _m3u_name)
|
||||
_write_m3u(m3u_path, paths)
|
||||
|
||||
|
||||
if 'm3u_multi' in _feeds_formats:
|
||||
m3u_path = _build_m3u_filename(basename)
|
||||
m3u_path = _build_m3u_filename(basename)
|
||||
_write_m3u(m3u_path, paths)
|
||||
|
||||
if 'link' in _feeds_formats:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -67,13 +67,13 @@ def _tags_for(obj):
|
|||
|
||||
def _tags_to_genre(tags):
|
||||
"""Given a tag list, returns a genre. Returns the first tag that is
|
||||
present in the genre whitelist or None if no tag is suitable.
|
||||
present in the genre whitelist or None if no tag is suitable.
|
||||
"""
|
||||
if not tags:
|
||||
return None
|
||||
elif not options['whitelist']:
|
||||
return tags[0].title()
|
||||
|
||||
|
||||
if options.get('c14n'):
|
||||
# Use the canonicalization tree.
|
||||
for tag in tags:
|
||||
|
|
@ -157,13 +157,13 @@ class LastGenrePlugin(plugins.BeetsPlugin):
|
|||
from yaml import load
|
||||
genres_tree = load(open(c14n_filename, 'r'))
|
||||
branches = []
|
||||
flatten_tree(genres_tree, [], branches)
|
||||
options['branches'] = branches
|
||||
flatten_tree(genres_tree, [], branches)
|
||||
options['branches'] = branches
|
||||
options['c14n'] = True
|
||||
|
||||
fallback_str = ui.config_val(config, 'lastgenre', 'fallback_str', None)
|
||||
|
||||
|
||||
|
||||
@LastGenrePlugin.listen('album_imported')
|
||||
def album_imported(lib, album, config):
|
||||
global fallback_str
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ def update_mpd(host='localhost', port=6600, password=None):
|
|||
if 'OK MPD' not in resp:
|
||||
print 'MPD connection failed:', repr(resp)
|
||||
return
|
||||
|
||||
|
||||
if password:
|
||||
s.send('password "%s"\n' % password)
|
||||
resp = s.readline()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ body {
|
|||
right: 0;
|
||||
top: 0;
|
||||
height: 36px;
|
||||
|
||||
|
||||
color: white;
|
||||
|
||||
|
||||
cursor: default;
|
||||
|
||||
/* shadowy border */
|
||||
|
|
@ -33,15 +33,15 @@ body {
|
|||
|
||||
#entities {
|
||||
width: 17em;
|
||||
|
||||
|
||||
position: fixed;
|
||||
top: 36px;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
|
||||
|
||||
z-index: 1;
|
||||
background: #dde4eb;
|
||||
background: #dde4eb;
|
||||
|
||||
/* shadowy border */
|
||||
box-shadow: 0 0 20px #666;
|
||||
|
|
@ -59,7 +59,7 @@ body {
|
|||
}
|
||||
#entities ul {
|
||||
width: 17em;
|
||||
|
||||
|
||||
position: fixed;
|
||||
top: 36px;
|
||||
left: 0;
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ $.fn.player = function(debug) {
|
|||
$.fn.disableSelection = function() {
|
||||
$(this).attr('unselectable', 'on')
|
||||
.css('-moz-user-select', 'none')
|
||||
.each(function() {
|
||||
.each(function() {
|
||||
this.onselectstart = function() { return false; };
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<title>beets</title>
|
||||
|
||||
<link rel="stylesheet"
|
||||
href="{{ url_for('static', filename='beets.css') }}" type="text/css">
|
||||
href="{{ url_for('static', filename='beets.css') }}" type="text/css">
|
||||
|
||||
<script src="{{ url_for('static', filename='jquery.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='underscore.js') }}">
|
||||
|
|
@ -52,7 +52,7 @@
|
|||
<span class="year">(<%= year %>)</span>
|
||||
</span>
|
||||
<span class="title"><%= title %></span>
|
||||
|
||||
|
||||
<button class="play">▶</button>
|
||||
|
||||
<dl>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ class Timecop(object):
|
|||
|
||||
def time(self):
|
||||
return self.now
|
||||
|
||||
|
||||
def sleep(self, amount):
|
||||
self.now += amount
|
||||
|
||||
|
|
@ -179,7 +179,7 @@ class DummyIO(object):
|
|||
res = self.stdout.get()
|
||||
self.stdout.clear()
|
||||
return res
|
||||
|
||||
|
||||
def readcount(self):
|
||||
return self.stdin.reads
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ def _make_trackinfo():
|
|||
ti.append(TrackInfo('two', None, 'some artist', length=1))
|
||||
ti.append(TrackInfo('three', None, 'some artist', length=1))
|
||||
return ti
|
||||
|
||||
|
||||
class TrackDistanceTest(unittest.TestCase):
|
||||
def test_identical_tracks(self):
|
||||
item = _make_item('one', 1)
|
||||
|
|
@ -258,13 +258,13 @@ class AlbumsInDirTest(unittest.TestCase):
|
|||
# create a directory structure for testing
|
||||
self.base = os.path.abspath(os.path.join(_common.RSRC, 'tempdir'))
|
||||
os.mkdir(self.base)
|
||||
|
||||
|
||||
os.mkdir(os.path.join(self.base, 'album1'))
|
||||
os.mkdir(os.path.join(self.base, 'album2'))
|
||||
os.mkdir(os.path.join(self.base, 'more'))
|
||||
os.mkdir(os.path.join(self.base, 'more', 'album3'))
|
||||
os.mkdir(os.path.join(self.base, 'more', 'album4'))
|
||||
|
||||
|
||||
_mkmp3(os.path.join(self.base, 'album1', 'album1song1.mp3'))
|
||||
_mkmp3(os.path.join(self.base, 'album1', 'album1song2.mp3'))
|
||||
_mkmp3(os.path.join(self.base, 'album2', 'album2song.mp3'))
|
||||
|
|
@ -272,11 +272,11 @@ class AlbumsInDirTest(unittest.TestCase):
|
|||
_mkmp3(os.path.join(self.base, 'more', 'album4', 'album4song.mp3'))
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.base)
|
||||
|
||||
|
||||
def test_finds_all_albums(self):
|
||||
albums = list(autotag.albums_in_dir(self.base))
|
||||
self.assertEqual(len(albums), 4)
|
||||
|
||||
|
||||
def test_separates_contents(self):
|
||||
found = []
|
||||
for _, album in autotag.albums_in_dir(self.base):
|
||||
|
|
@ -285,7 +285,7 @@ class AlbumsInDirTest(unittest.TestCase):
|
|||
self.assertTrue('2' in found)
|
||||
self.assertTrue('3' in found)
|
||||
self.assertTrue('4' in found)
|
||||
|
||||
|
||||
def test_finds_multiple_songs(self):
|
||||
for _, album in autotag.albums_in_dir(self.base):
|
||||
n = re.search(r'album(.)song', album[0].path).group(1)
|
||||
|
|
@ -351,7 +351,7 @@ class OrderingTest(unittest.TestCase):
|
|||
'title': title, 'track': track,
|
||||
'mb_trackid': '', 'mb_albumid': '', 'mb_artistid': '',
|
||||
})
|
||||
|
||||
|
||||
def test_order_corrects_metadata(self):
|
||||
items = []
|
||||
items.append(self.item('one', 1))
|
||||
|
|
@ -482,46 +482,46 @@ class ApplyTest(unittest.TestCase):
|
|||
va = False,
|
||||
mediums = 2,
|
||||
)
|
||||
|
||||
|
||||
def test_titles_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self.assertEqual(self.items[0].title, 'oneNew')
|
||||
self.assertEqual(self.items[1].title, 'twoNew')
|
||||
|
||||
|
||||
def test_album_and_artist_applied_to_all(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self.assertEqual(self.items[0].album, 'albumNew')
|
||||
self.assertEqual(self.items[1].album, 'albumNew')
|
||||
self.assertEqual(self.items[0].artist, 'artistNew')
|
||||
self.assertEqual(self.items[1].artist, 'artistNew')
|
||||
|
||||
|
||||
def test_track_index_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self.assertEqual(self.items[0].track, 1)
|
||||
self.assertEqual(self.items[1].track, 2)
|
||||
|
||||
|
||||
def test_track_total_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self.assertEqual(self.items[0].tracktotal, 2)
|
||||
self.assertEqual(self.items[1].tracktotal, 2)
|
||||
|
||||
|
||||
def test_disc_index_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self.assertEqual(self.items[0].disc, 1)
|
||||
self.assertEqual(self.items[1].disc, 2)
|
||||
|
||||
|
||||
def test_disc_total_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self.assertEqual(self.items[0].disctotal, 2)
|
||||
self.assertEqual(self.items[1].disctotal, 2)
|
||||
|
||||
|
||||
def test_mb_trackid_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
self.assertEqual(self.items[0].mb_trackid,
|
||||
'dfa939ec-118c-4d0f-84a0-60f3d1e6522c')
|
||||
self.assertEqual(self.items[1].mb_trackid,
|
||||
'40130ed1-a27c-42fd-a328-1ebefb6caef4')
|
||||
|
||||
|
||||
def test_mb_albumid_and_artistid_applied(self):
|
||||
autotag.apply_metadata(self.items, self.info)
|
||||
for item in self.items:
|
||||
|
|
@ -611,59 +611,59 @@ class StringDistanceTest(unittest.TestCase):
|
|||
def test_equal_strings(self):
|
||||
dist = match.string_dist('Some String', 'Some String')
|
||||
self.assertEqual(dist, 0.0)
|
||||
|
||||
|
||||
def test_different_strings(self):
|
||||
dist = match.string_dist('Some String', 'Totally Different')
|
||||
self.assertNotEqual(dist, 0.0)
|
||||
|
||||
|
||||
def test_punctuation_ignored(self):
|
||||
dist = match.string_dist('Some String', 'Some.String!')
|
||||
self.assertEqual(dist, 0.0)
|
||||
|
||||
|
||||
def test_case_ignored(self):
|
||||
dist = match.string_dist('Some String', 'sOME sTring')
|
||||
self.assertEqual(dist, 0.0)
|
||||
|
||||
def test_leading_the_has_lower_weight(self):
|
||||
|
||||
def test_leading_the_has_lower_weight(self):
|
||||
dist1 = match.string_dist('XXX Band Name', 'Band Name')
|
||||
dist2 = match.string_dist('The Band Name', 'Band Name')
|
||||
self.assert_(dist2 < dist1)
|
||||
|
||||
def test_parens_have_lower_weight(self):
|
||||
|
||||
def test_parens_have_lower_weight(self):
|
||||
dist1 = match.string_dist('One .Two.', 'One')
|
||||
dist2 = match.string_dist('One (Two)', 'One')
|
||||
self.assert_(dist2 < dist1)
|
||||
|
||||
def test_brackets_have_lower_weight(self):
|
||||
|
||||
def test_brackets_have_lower_weight(self):
|
||||
dist1 = match.string_dist('One .Two.', 'One')
|
||||
dist2 = match.string_dist('One [Two]', 'One')
|
||||
self.assert_(dist2 < dist1)
|
||||
|
||||
def test_ep_label_has_zero_weight(self):
|
||||
|
||||
def test_ep_label_has_zero_weight(self):
|
||||
dist = match.string_dist('My Song (EP)', 'My Song')
|
||||
self.assertEqual(dist, 0.0)
|
||||
|
||||
def test_featured_has_lower_weight(self):
|
||||
|
||||
def test_featured_has_lower_weight(self):
|
||||
dist1 = match.string_dist('My Song blah Someone', 'My Song')
|
||||
dist2 = match.string_dist('My Song feat Someone', 'My Song')
|
||||
self.assert_(dist2 < dist1)
|
||||
|
||||
def test_postfix_the(self):
|
||||
|
||||
def test_postfix_the(self):
|
||||
dist = match.string_dist('The Song Title', 'Song Title, The')
|
||||
self.assertEqual(dist, 0.0)
|
||||
|
||||
def test_postfix_a(self):
|
||||
|
||||
def test_postfix_a(self):
|
||||
dist = match.string_dist('A Song Title', 'Song Title, A')
|
||||
self.assertEqual(dist, 0.0)
|
||||
|
||||
def test_postfix_an(self):
|
||||
|
||||
def test_postfix_an(self):
|
||||
dist = match.string_dist('An Album Title', 'Album Title, An')
|
||||
self.assertEqual(dist, 0.0)
|
||||
|
||||
|
||||
def test_empty_strings(self):
|
||||
dist = match.string_dist('', '')
|
||||
self.assertEqual(dist, 0.0)
|
||||
|
||||
|
||||
def test_solo_pattern(self):
|
||||
# Just make sure these don't crash.
|
||||
match.string_dist('The ', '')
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -30,12 +30,12 @@ class MoveTest(unittest.TestCase, _common.ExtraAsserts):
|
|||
# make a temporary file
|
||||
self.path = join(_common.RSRC, 'temp.mp3')
|
||||
shutil.copy(join(_common.RSRC, 'full.mp3'), self.path)
|
||||
|
||||
|
||||
# add it to a temporary library
|
||||
self.lib = beets.library.Library(':memory:')
|
||||
self.i = beets.library.Item.from_path(self.path)
|
||||
self.lib.add(self.i)
|
||||
|
||||
|
||||
# set up the destination
|
||||
self.libdir = join(_common.RSRC, 'testlibdir')
|
||||
self.lib.directory = self.libdir
|
||||
|
|
@ -47,7 +47,7 @@ class MoveTest(unittest.TestCase, _common.ExtraAsserts):
|
|||
self.dest = join(self.libdir, 'one', 'two', 'three.mp3')
|
||||
|
||||
self.otherdir = join(_common.RSRC, 'testotherdir')
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
if os.path.exists(self.path):
|
||||
os.remove(self.path)
|
||||
|
|
@ -55,15 +55,15 @@ class MoveTest(unittest.TestCase, _common.ExtraAsserts):
|
|||
shutil.rmtree(self.libdir)
|
||||
if os.path.exists(self.otherdir):
|
||||
shutil.rmtree(self.otherdir)
|
||||
|
||||
|
||||
def test_move_arrives(self):
|
||||
self.lib.move(self.i)
|
||||
self.assertExists(self.dest)
|
||||
|
||||
|
||||
def test_move_to_custom_dir(self):
|
||||
self.lib.move(self.i, basedir=self.otherdir)
|
||||
self.assertExists(join(self.otherdir, 'one', 'two', 'three.mp3'))
|
||||
|
||||
|
||||
def test_move_departs(self):
|
||||
self.lib.move(self.i)
|
||||
self.assertNotExists(self.path)
|
||||
|
|
@ -77,15 +77,15 @@ class MoveTest(unittest.TestCase, _common.ExtraAsserts):
|
|||
self.lib.move(self.i)
|
||||
self.assertNotExists(old_path)
|
||||
self.assertNotExists(os.path.dirname(old_path))
|
||||
|
||||
|
||||
def test_copy_arrives(self):
|
||||
self.lib.move(self.i, copy=True)
|
||||
self.assertExists(self.dest)
|
||||
|
||||
|
||||
def test_copy_does_not_depart(self):
|
||||
self.lib.move(self.i, copy=True)
|
||||
self.assertExists(self.path)
|
||||
|
||||
|
||||
def test_move_changes_path(self):
|
||||
self.lib.move(self.i)
|
||||
self.assertEqual(self.i.path, util.normpath(self.dest))
|
||||
|
|
@ -124,7 +124,7 @@ class MoveTest(unittest.TestCase, _common.ExtraAsserts):
|
|||
self.assertNotEqual(self.i.path, dest)
|
||||
self.assertEqual(os.path.dirname(self.i.path),
|
||||
os.path.dirname(dest))
|
||||
|
||||
|
||||
class HelperTest(unittest.TestCase):
|
||||
def test_ancestry_works_on_file(self):
|
||||
p = '/a/b/c'
|
||||
|
|
@ -138,7 +138,7 @@ class HelperTest(unittest.TestCase):
|
|||
p = 'a/b/c'
|
||||
a = ['a', 'a/b']
|
||||
self.assertEqual(util.ancestry(p), a)
|
||||
|
||||
|
||||
def test_components_works_on_file(self):
|
||||
p = '/a/b/c'
|
||||
a = ['/', 'a', 'b', 'c']
|
||||
|
|
@ -268,11 +268,11 @@ class ArtFileTest(unittest.TestCase, _common.ExtraAsserts):
|
|||
i2.artist = 'someArtist'
|
||||
ai = self.lib.add_album((i2,))
|
||||
self.lib.move(i2, True)
|
||||
|
||||
|
||||
self.assertEqual(ai.artpath, None)
|
||||
ai.set_art(newart)
|
||||
self.assertTrue(os.path.exists(ai.artpath))
|
||||
|
||||
|
||||
def test_setart_to_existing_art_works(self):
|
||||
os.remove(self.art)
|
||||
|
||||
|
|
@ -332,7 +332,7 @@ class ArtFileTest(unittest.TestCase, _common.ExtraAsserts):
|
|||
newart = os.path.join(self.libdir, 'newart.jpg')
|
||||
touch(newart)
|
||||
os.chmod(newart, 0400) # read-only
|
||||
|
||||
|
||||
try:
|
||||
i2 = item()
|
||||
i2.path = self.i.path
|
||||
|
|
@ -340,11 +340,11 @@ class ArtFileTest(unittest.TestCase, _common.ExtraAsserts):
|
|||
ai = self.lib.add_album((i2,))
|
||||
self.lib.move(i2, True)
|
||||
ai.set_art(newart)
|
||||
|
||||
|
||||
mode = stat.S_IMODE(os.stat(ai.artpath).st_mode)
|
||||
self.assertTrue(mode & stat.S_IRGRP)
|
||||
self.assertTrue(os.access(ai.artpath, os.W_OK))
|
||||
|
||||
|
||||
finally:
|
||||
# Make everything writable so it can be cleaned up.
|
||||
os.chmod(newart, 0777)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -700,22 +700,22 @@ class DuplicateCheckTest(unittest.TestCase):
|
|||
self.assertTrue(res)
|
||||
|
||||
def test_duplicate_item_apply(self):
|
||||
res = importer._item_duplicate_check(self.lib,
|
||||
res = importer._item_duplicate_check(self.lib,
|
||||
self._item_task(False))
|
||||
self.assertTrue(res)
|
||||
|
||||
def test_different_item_apply(self):
|
||||
res = importer._item_duplicate_check(self.lib,
|
||||
res = importer._item_duplicate_check(self.lib,
|
||||
self._item_task(False, 'xxx', 'yyy'))
|
||||
self.assertFalse(res)
|
||||
|
||||
def test_duplicate_item_asis(self):
|
||||
res = importer._item_duplicate_check(self.lib,
|
||||
res = importer._item_duplicate_check(self.lib,
|
||||
self._item_task(True))
|
||||
self.assertTrue(res)
|
||||
|
||||
def test_different_item_asis(self):
|
||||
res = importer._item_duplicate_check(self.lib,
|
||||
res = importer._item_duplicate_check(self.lib,
|
||||
self._item_task(True, 'xxx', 'yyy'))
|
||||
self.assertFalse(res)
|
||||
|
||||
|
|
@ -725,7 +725,7 @@ class DuplicateCheckTest(unittest.TestCase):
|
|||
self.assertFalse(res)
|
||||
|
||||
def test_duplicate_item_existing(self):
|
||||
res = importer._item_duplicate_check(self.lib,
|
||||
res = importer._item_duplicate_check(self.lib,
|
||||
self._item_task(False, existing=True))
|
||||
self.assertFalse(res)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ class MBAlbumInfoTest(unittest.TestCase):
|
|||
d = mb.album_info(release)
|
||||
self.assertEqual(d.year, 1987)
|
||||
self.assertEqual(d.month, 3)
|
||||
|
||||
|
||||
def test_no_durations(self):
|
||||
tracks = [self._make_track('TITLE', 'ID', None)]
|
||||
release = self._make_release(tracks=tracks)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ class EdgeTest(unittest.TestCase):
|
|||
# rather than just a number.
|
||||
f = beets.mediafile.MediaFile(os.path.join(_common.RSRC, 'bpm.mp3'))
|
||||
self.assertEqual(f.bpm, 128)
|
||||
|
||||
|
||||
def test_discc_alternate_field(self):
|
||||
# Different taggers use different vorbis comments to reflect
|
||||
# the disc and disc count fields: ensure that the alternative
|
||||
|
|
@ -119,27 +119,27 @@ class SafetyTest(unittest.TestCase):
|
|||
self.assertRaises(exc, beets.mediafile.MediaFile, fn)
|
||||
finally:
|
||||
os.unlink(fn) # delete the temporary file
|
||||
|
||||
|
||||
def test_corrupt_mp3_raises_unreadablefileerror(self):
|
||||
# Make sure we catch Mutagen reading errors appropriately.
|
||||
self._exccheck('corrupt.mp3', beets.mediafile.UnreadableFileError)
|
||||
|
||||
|
||||
def test_corrupt_mp4_raises_unreadablefileerror(self):
|
||||
self._exccheck('corrupt.m4a', beets.mediafile.UnreadableFileError)
|
||||
|
||||
|
||||
def test_corrupt_flac_raises_unreadablefileerror(self):
|
||||
self._exccheck('corrupt.flac', beets.mediafile.UnreadableFileError)
|
||||
|
||||
|
||||
def test_corrupt_ogg_raises_unreadablefileerror(self):
|
||||
self._exccheck('corrupt.ogg', beets.mediafile.UnreadableFileError)
|
||||
|
||||
def test_invalid_ogg_header_raises_unreadablefileerror(self):
|
||||
self._exccheck('corrupt.ogg', beets.mediafile.UnreadableFileError,
|
||||
'OggS\x01vorbis')
|
||||
|
||||
|
||||
def test_corrupt_monkeys_raises_unreadablefileerror(self):
|
||||
self._exccheck('corrupt.ape', beets.mediafile.UnreadableFileError)
|
||||
|
||||
|
||||
def test_invalid_extension_raises_filetypeerror(self):
|
||||
self._exccheck('something.unknown', beets.mediafile.FileTypeError)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -56,14 +56,14 @@ def MakeReadOnlyTest(path, field, value):
|
|||
return ReadOnlyTest
|
||||
|
||||
def MakeWritingTest(path, correct_dict, field, testsuffix='_test'):
|
||||
|
||||
|
||||
class WritingTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# make a copy of the file we'll work on
|
||||
root, ext = os.path.splitext(path)
|
||||
self.tpath = root + testsuffix + ext
|
||||
shutil.copy(path, self.tpath)
|
||||
|
||||
|
||||
# generate the new value we'll try storing
|
||||
if field == 'art':
|
||||
self.value = 'xxx'
|
||||
|
|
@ -82,18 +82,18 @@ def MakeWritingTest(path, correct_dict, field, testsuffix='_test'):
|
|||
else:
|
||||
raise ValueError('unknown field type ' + \
|
||||
str(type(correct_dict[field])))
|
||||
|
||||
def runTest(self):
|
||||
|
||||
def runTest(self):
|
||||
# write new tag
|
||||
a = beets.mediafile.MediaFile(self.tpath)
|
||||
setattr(a, field, self.value)
|
||||
a.save()
|
||||
|
||||
|
||||
# verify ALL tags are correct with modification
|
||||
b = beets.mediafile.MediaFile(self.tpath)
|
||||
for readfield in correct_dict.keys():
|
||||
got = getattr(b, readfield)
|
||||
|
||||
|
||||
# Make sure the modified field was changed correctly...
|
||||
if readfield == field:
|
||||
message = field + ' modified incorrectly (changed to ' + \
|
||||
|
|
@ -103,7 +103,7 @@ def MakeWritingTest(path, correct_dict, field, testsuffix='_test'):
|
|||
self.assertAlmostEqual(got, self.value, msg=message)
|
||||
else:
|
||||
self.assertEqual(got, self.value, message)
|
||||
|
||||
|
||||
# ... and that no other field was changed.
|
||||
else:
|
||||
# MPEG-4: ReplayGain not implented.
|
||||
|
|
@ -113,7 +113,7 @@ def MakeWritingTest(path, correct_dict, field, testsuffix='_test'):
|
|||
# The value should be what it was originally most of the
|
||||
# time.
|
||||
correct = correct_dict[readfield]
|
||||
|
||||
|
||||
# The date field, however, is modified when its components
|
||||
# change.
|
||||
if readfield=='date' and field in ('year', 'month', 'day'):
|
||||
|
|
@ -128,7 +128,7 @@ def MakeWritingTest(path, correct_dict, field, testsuffix='_test'):
|
|||
# And vice-versa.
|
||||
if field=='date' and readfield in ('year', 'month', 'day'):
|
||||
correct = getattr(self.value, readfield)
|
||||
|
||||
|
||||
message = readfield + ' changed when it should not have' \
|
||||
' (expected ' + repr(correct) + ', got ' + \
|
||||
repr(got) + ') when modifying ' + field + \
|
||||
|
|
@ -137,11 +137,11 @@ def MakeWritingTest(path, correct_dict, field, testsuffix='_test'):
|
|||
self.assertAlmostEqual(got, correct, msg=message)
|
||||
else:
|
||||
self.assertEqual(got, correct, message)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
if os.path.exists(self.tpath):
|
||||
os.remove(self.tpath)
|
||||
|
||||
|
||||
return WritingTest
|
||||
|
||||
correct_dicts = {
|
||||
|
|
@ -278,7 +278,7 @@ read_only_correct_dicts = {
|
|||
'bitdepth': 16,
|
||||
'channels': 2,
|
||||
},
|
||||
|
||||
|
||||
'full.ogg': {
|
||||
'length': 1.0,
|
||||
'bitrate': 48000,
|
||||
|
|
@ -287,7 +287,7 @@ read_only_correct_dicts = {
|
|||
'bitdepth': 0,
|
||||
'channels': 1,
|
||||
},
|
||||
|
||||
|
||||
'full.ape': {
|
||||
'length': 1.0,
|
||||
'bitrate': 112040,
|
||||
|
|
@ -342,7 +342,7 @@ test_files = {
|
|||
|
||||
def suite():
|
||||
s = unittest.TestSuite()
|
||||
|
||||
|
||||
# General tests.
|
||||
for kind, tagsets in test_files.items():
|
||||
for tagset in tagsets:
|
||||
|
|
@ -350,13 +350,13 @@ def suite():
|
|||
correct_dict = correct_dicts[tagset]
|
||||
for test in suite_for_file(path, correct_dict):
|
||||
s.addTest(test)
|
||||
|
||||
|
||||
# Special test for missing ID3 tag.
|
||||
for test in suite_for_file(os.path.join(_common.RSRC, 'empty.mp3'),
|
||||
correct_dicts['empty'],
|
||||
writing=False):
|
||||
s.addTest(test)
|
||||
|
||||
|
||||
# Special test for advanced release date.
|
||||
for test in suite_for_file(os.path.join(_common.RSRC, 'date.mp3'),
|
||||
correct_dicts['date']):
|
||||
|
|
@ -367,7 +367,7 @@ def suite():
|
|||
path = os.path.join(_common.RSRC, fname)
|
||||
for field, value in correct_dict.iteritems():
|
||||
s.addTest(MakeReadOnlyTest(path, field, value)())
|
||||
|
||||
|
||||
return s
|
||||
|
||||
def test_nose_suite():
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ class CommandParseTest(unittest.TestCase):
|
|||
s = ur'command "hello \\ there"'
|
||||
c = bpd.Command(s)
|
||||
self.assertEqual(c.args, [u'hello \ there'])
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -121,52 +121,52 @@ class GetTest(unittest.TestCase, AssertsMixin):
|
|||
q = ''
|
||||
results = self.lib.items(q)
|
||||
self.assert_matched_all(results)
|
||||
|
||||
|
||||
def test_get_none(self):
|
||||
q = None
|
||||
results = self.lib.items(q)
|
||||
self.assert_matched_all(results)
|
||||
|
||||
|
||||
def test_get_one_keyed_term(self):
|
||||
q = 'artist:Lil'
|
||||
results = self.lib.items(q)
|
||||
self.assert_matched(results, 'Littlest Things')
|
||||
self.assert_done(results)
|
||||
|
||||
|
||||
def test_get_one_keyed_regexp(self):
|
||||
q = r'artist::L.+y'
|
||||
results = self.lib.items(q)
|
||||
self.assert_matched(results, 'Littlest Things')
|
||||
self.assert_done(results)
|
||||
|
||||
|
||||
def test_get_one_unkeyed_term(self):
|
||||
q = 'Terry'
|
||||
results = self.lib.items(q)
|
||||
self.assert_matched(results, 'Boracay')
|
||||
self.assert_done(results)
|
||||
|
||||
|
||||
def test_get_one_unkeyed_regexp(self):
|
||||
q = r':y$'
|
||||
results = self.lib.items(q)
|
||||
self.assert_matched(results, 'Boracay')
|
||||
self.assert_done(results)
|
||||
|
||||
|
||||
def test_get_no_matches(self):
|
||||
q = 'popebear'
|
||||
results = self.lib.items(q)
|
||||
self.assert_done(results)
|
||||
|
||||
|
||||
def test_invalid_key(self):
|
||||
q = 'pope:bear'
|
||||
results = self.lib.items(q)
|
||||
self.assert_matched_all(results)
|
||||
|
||||
|
||||
def test_term_case_insensitive(self):
|
||||
q = 'UNCoVER'
|
||||
results = self.lib.items(q)
|
||||
self.assert_matched(results, 'Lovers Who Uncover')
|
||||
self.assert_done(results)
|
||||
|
||||
|
||||
def test_regexp_case_sensitive(self):
|
||||
q = r':UNCoVER'
|
||||
results = self.lib.items(q)
|
||||
|
|
@ -175,19 +175,19 @@ class GetTest(unittest.TestCase, AssertsMixin):
|
|||
results = self.lib.items(q)
|
||||
self.assert_matched(results, 'Lovers Who Uncover')
|
||||
self.assert_done(results)
|
||||
|
||||
|
||||
def test_term_case_insensitive_with_key(self):
|
||||
q = 'album:stiLL'
|
||||
results = self.lib.items(q)
|
||||
self.assert_matched(results, 'Littlest Things')
|
||||
self.assert_done(results)
|
||||
|
||||
|
||||
def test_key_case_insensitive(self):
|
||||
q = 'ArTiST:Allen'
|
||||
results = self.lib.items(q)
|
||||
self.assert_matched(results, 'Littlest Things')
|
||||
self.assert_done(results)
|
||||
|
||||
|
||||
def test_unkeyed_term_matches_multiple_columns(self):
|
||||
q = 'little'
|
||||
results = self.lib.items(q)
|
||||
|
|
@ -195,7 +195,7 @@ class GetTest(unittest.TestCase, AssertsMixin):
|
|||
self.assert_matched(results, 'Lovers Who Uncover')
|
||||
self.assert_matched(results, 'Boracay')
|
||||
self.assert_done(results)
|
||||
|
||||
|
||||
def test_unkeyed_regexp_matches_multiple_columns(self):
|
||||
q = r':^T'
|
||||
results = self.lib.items(q)
|
||||
|
|
@ -203,21 +203,21 @@ class GetTest(unittest.TestCase, AssertsMixin):
|
|||
self.assert_matched(results, 'Lovers Who Uncover')
|
||||
self.assert_matched(results, 'Boracay')
|
||||
self.assert_done(results)
|
||||
|
||||
|
||||
def test_keyed_term_matches_only_one_column(self):
|
||||
q = 'artist:little'
|
||||
results = self.lib.items(q)
|
||||
self.assert_matched(results, 'Lovers Who Uncover')
|
||||
self.assert_matched(results, 'Boracay')
|
||||
self.assert_done(results)
|
||||
|
||||
|
||||
def test_keyed_regexp_matches_only_one_column(self):
|
||||
q = r'album::\sS'
|
||||
results = self.lib.items(q)
|
||||
self.assert_matched(results, 'Littlest Things')
|
||||
self.assert_matched(results, 'Lovers Who Uncover')
|
||||
self.assert_done(results)
|
||||
|
||||
|
||||
def test_multiple_terms_narrow_search(self):
|
||||
q = 'little ones'
|
||||
results = self.lib.items(q)
|
||||
|
|
@ -333,7 +333,7 @@ class PathQueryTest(unittest.TestCase, AssertsMixin):
|
|||
q = 'path:/xyzzy/'
|
||||
results = self.lib.items(q)
|
||||
self.assert_done(results)
|
||||
|
||||
|
||||
def test_fragment_no_match(self):
|
||||
q = 'path:/b/'
|
||||
results = self.lib.items(q)
|
||||
|
|
@ -390,7 +390,7 @@ class BrowseTest(unittest.TestCase, AssertsMixin):
|
|||
def test_albums_matches_albumartist(self):
|
||||
albums = list(self.lib.albums('panda'))
|
||||
self.assertEqual(len(albums), 1)
|
||||
|
||||
|
||||
def test_items_matches_title(self):
|
||||
items = self.lib.items('boracay')
|
||||
self.assert_matched(items, 'Boracay')
|
||||
|
|
@ -401,7 +401,7 @@ class BrowseTest(unittest.TestCase, AssertsMixin):
|
|||
self.assert_done(items)
|
||||
|
||||
#FIXME Haven't tested explicit (non-query) criteria.
|
||||
|
||||
|
||||
class CountTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.lib = beets.library.Library(':memory:')
|
||||
|
|
@ -420,7 +420,7 @@ class CountTest(unittest.TestCase):
|
|||
songs, totaltime = beets.library.TrueQuery().count(tx)
|
||||
self.assertEqual(songs, 0)
|
||||
self.assertEqual(totaltime, 0.0)
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -128,19 +128,19 @@ class ParseTest(unittest.TestCase):
|
|||
|
||||
def test_unclosed_braces_symbol(self):
|
||||
self.assertEqual(list(_normparse(u'a ${ b')), [u'a ${ b'])
|
||||
|
||||
|
||||
def test_empty_braces_symbol(self):
|
||||
self.assertEqual(list(_normparse(u'a ${} b')), [u'a ${} b'])
|
||||
|
||||
def test_call_without_args_at_end(self):
|
||||
self.assertEqual(list(_normparse(u'foo %bar')), [u'foo %bar'])
|
||||
|
||||
|
||||
def test_call_without_args(self):
|
||||
self.assertEqual(list(_normparse(u'foo %bar baz')), [u'foo %bar baz'])
|
||||
|
||||
def test_call_with_unclosed_args(self):
|
||||
self.assertEqual(list(_normparse(u'foo %bar{ baz')), [u'foo %bar{ baz'])
|
||||
|
||||
|
||||
def test_call_with_unclosed_multiple_args(self):
|
||||
self.assertEqual(list(_normparse(u'foo %bar{bar,bar baz')),
|
||||
[u'foo %bar{bar,bar baz'])
|
||||
|
|
@ -150,26 +150,26 @@ class ParseTest(unittest.TestCase):
|
|||
self.assertEqual(len(parts), 1)
|
||||
self._assert_call(parts[0], u"foo", 1)
|
||||
self.assertEqual(list(_normexpr(parts[0].args[0])), [])
|
||||
|
||||
|
||||
def test_call_single_arg(self):
|
||||
parts = list(_normparse(u'%foo{bar}'))
|
||||
self.assertEqual(len(parts), 1)
|
||||
self._assert_call(parts[0], u"foo", 1)
|
||||
self.assertEqual(list(_normexpr(parts[0].args[0])), [u'bar'])
|
||||
|
||||
|
||||
def test_call_two_args(self):
|
||||
parts = list(_normparse(u'%foo{bar,baz}'))
|
||||
self.assertEqual(len(parts), 1)
|
||||
self._assert_call(parts[0], u"foo", 2)
|
||||
self.assertEqual(list(_normexpr(parts[0].args[0])), [u'bar'])
|
||||
self.assertEqual(list(_normexpr(parts[0].args[1])), [u'baz'])
|
||||
|
||||
|
||||
def test_call_with_escaped_sep(self):
|
||||
parts = list(_normparse(u'%foo{bar$,baz}'))
|
||||
self.assertEqual(len(parts), 1)
|
||||
self._assert_call(parts[0], u"foo", 1)
|
||||
self.assertEqual(list(_normexpr(parts[0].args[0])), [u'bar,baz'])
|
||||
|
||||
|
||||
def test_call_with_escaped_close(self):
|
||||
parts = list(_normparse(u'%foo{bar$}baz}'))
|
||||
self.assertEqual(len(parts), 1)
|
||||
|
|
@ -217,10 +217,10 @@ class EvalTest(unittest.TestCase):
|
|||
|
||||
def test_plain_text(self):
|
||||
self.assertEqual(self._eval(u"foo"), u"foo")
|
||||
|
||||
|
||||
def test_subtitute_value(self):
|
||||
self.assertEqual(self._eval(u"$foo"), u"bar")
|
||||
|
||||
|
||||
def test_subtitute_value_in_text(self):
|
||||
self.assertEqual(self._eval(u"hello $foo world"), u"hello bar world")
|
||||
|
||||
|
|
@ -242,7 +242,7 @@ class EvalTest(unittest.TestCase):
|
|||
def test_function_call_exception(self):
|
||||
res = self._eval(u"%lower{a,b,c,d,e}")
|
||||
self.assertTrue(isinstance(res, basestring))
|
||||
|
||||
|
||||
def test_function_returning_integer(self):
|
||||
self.assertEqual(self._eval(u"%len{foo}"), u"3")
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ class ListTest(unittest.TestCase):
|
|||
|
||||
def tearDown(self):
|
||||
self.io.restore()
|
||||
|
||||
|
||||
def test_list_outputs_item(self):
|
||||
commands.list_items(self.lib, '', False, False, None)
|
||||
out = self.io.getoutput()
|
||||
|
|
@ -74,7 +74,7 @@ class ListTest(unittest.TestCase):
|
|||
commands.list_items(self.lib, '', True, True, None)
|
||||
out = self.io.getoutput()
|
||||
self.assertEqual(out.strip(), u'xxx')
|
||||
|
||||
|
||||
def test_list_album_omits_title(self):
|
||||
commands.list_items(self.lib, '', True, False, None)
|
||||
out = self.io.getoutput()
|
||||
|
|
@ -85,7 +85,7 @@ class ListTest(unittest.TestCase):
|
|||
out = self.io.getoutput()
|
||||
self.assertTrue(u'the artist' in out)
|
||||
self.assertTrue(u'the album artist' not in out)
|
||||
|
||||
|
||||
def test_list_album_uses_album_artist(self):
|
||||
commands.list_items(self.lib, '', True, False, None)
|
||||
out = self.io.getoutput()
|
||||
|
|
@ -407,7 +407,7 @@ class PrintTest(unittest.TestCase):
|
|||
self.io.install()
|
||||
def tearDown(self):
|
||||
self.io.restore()
|
||||
|
||||
|
||||
def test_print_without_locale(self):
|
||||
lang = os.environ.get('LANG')
|
||||
if lang:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ class VFSTest(unittest.TestCase):
|
|||
self.lib.add(_common.item())
|
||||
self.lib.add_album([_common.item()])
|
||||
self.tree = vfs.libtree(self.lib)
|
||||
|
||||
|
||||
def test_singleton_item(self):
|
||||
self.assertEqual(self.tree.dirs['tracks'].dirs['the artist'].
|
||||
files['the title'], 1)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue