beets-1.3.8/0000755000076500000240000000000012406440351013570 5ustar asampsonstaff00000000000000beets-1.3.8/beets/0000755000076500000240000000000012406440351014672 5ustar asampsonstaff00000000000000beets-1.3.8/beets/__init__.py0000644000076500000240000000152312404675421017012 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. __version__ = '1.3.8' __author__ = 'Adrian Sampson ' import beets.library from beets.util import confit Library = beets.library.Library config = confit.LazyConfig('beets', __name__) beets-1.3.8/beets/autotag/0000755000076500000240000000000012406440351016336 5ustar asampsonstaff00000000000000beets-1.3.8/beets/autotag/__init__.py0000644000076500000240000001121512404675421020455 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Facilities for automatically determining files' correct metadata. """ import logging from beets import config # Parts of external interface. from .hooks import AlbumInfo, TrackInfo, AlbumMatch, TrackMatch # noqa from .match import tag_item, tag_album # noqa from .match import Recommendation # noqa # Global logger. log = logging.getLogger('beets') # Additional utilities for the main interface. def apply_item_metadata(item, track_info): """Set an item's metadata from its matched TrackInfo object. """ item.artist = track_info.artist item.artist_sort = track_info.artist_sort item.artist_credit = track_info.artist_credit item.title = track_info.title item.mb_trackid = track_info.track_id if track_info.artist_id: item.mb_artistid = track_info.artist_id # At the moment, the other metadata is left intact (including album # and track number). Perhaps these should be emptied? def apply_metadata(album_info, mapping): """Set the items' metadata to match an AlbumInfo object using a mapping from Items to TrackInfo objects. """ for item, track_info in mapping.iteritems(): # Album, artist, track count. if track_info.artist: item.artist = track_info.artist else: item.artist = album_info.artist item.albumartist = album_info.artist item.album = album_info.album # Artist sort and credit names. item.artist_sort = track_info.artist_sort or album_info.artist_sort item.artist_credit = (track_info.artist_credit or album_info.artist_credit) item.albumartist_sort = album_info.artist_sort item.albumartist_credit = album_info.artist_credit # Release date. for prefix in '', 'original_': if config['original_date'] and not prefix: # Ignore specific release date. continue for suffix in 'year', 'month', 'day': key = prefix + suffix value = getattr(album_info, key) or 0 # If we don't even have a year, apply nothing. if suffix == 'year' and not value: break # Otherwise, set the fetched value (or 0 for the month # and day if not available). item[key] = value # If we're using original release date for both fields, # also set item.year = info.original_year, etc. if config['original_date']: item[suffix] = value # Title. item.title = track_info.title if config['per_disc_numbering']: item.track = track_info.medium_index or track_info.index item.tracktotal = track_info.medium_total or len(album_info.tracks) else: item.track = track_info.index item.tracktotal = len(album_info.tracks) # Disc and disc count. item.disc = track_info.medium item.disctotal = album_info.mediums # MusicBrainz IDs. item.mb_trackid = track_info.track_id item.mb_albumid = album_info.album_id if track_info.artist_id: item.mb_artistid = track_info.artist_id else: item.mb_artistid = album_info.artist_id item.mb_albumartistid = album_info.artist_id item.mb_releasegroupid = album_info.releasegroup_id # Compilation flag. item.comp = album_info.va # Miscellaneous metadata. for field in ('albumtype', 'label', 'asin', 'catalognum', 'script', 'language', 'country', 'albumstatus', 'media', 'albumdisambig'): value = getattr(album_info, field) if value is not None: item[field] = value if track_info.disctitle is not None: item.disctitle = track_info.disctitle beets-1.3.8/beets/autotag/hooks.py0000644000076500000240000004733112322324223020037 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Glue between metadata sources and the matching logic.""" import logging from collections import namedtuple import re from beets import plugins from beets import config from beets.autotag import mb from beets.util import levenshtein from unidecode import unidecode log = logging.getLogger('beets') # Classes used to represent candidate options. class AlbumInfo(object): """Describes a canonical release that may be used to match a release in the library. Consists of these data members: - ``album``: the release title - ``album_id``: MusicBrainz ID; UUID fragment only - ``artist``: name of the release's primary artist - ``artist_id`` - ``tracks``: list of TrackInfo objects making up the release - ``asin``: Amazon ASIN - ``albumtype``: string describing the kind of release - ``va``: boolean: whether the release has "various artists" - ``year``: release year - ``month``: release month - ``day``: release day - ``label``: music label responsible for the release - ``mediums``: the number of discs in this release - ``artist_sort``: name of the release's artist for sorting - ``releasegroup_id``: MBID for the album's release group - ``catalognum``: the label's catalog number for the release - ``script``: character set used for metadata - ``language``: human language of the metadata - ``country``: the release country - ``albumstatus``: MusicBrainz release status (Official, etc.) - ``media``: delivery mechanism (Vinyl, etc.) - ``albumdisambig``: MusicBrainz release disambiguation comment - ``artist_credit``: Release-specific artist name - ``data_source``: The original data source (MusicBrainz, Discogs, etc.) - ``data_url``: The data source release URL. The fields up through ``tracks`` are required. The others are optional and may be None. """ def __init__(self, album, album_id, artist, artist_id, tracks, asin=None, albumtype=None, va=False, year=None, month=None, day=None, label=None, mediums=None, artist_sort=None, releasegroup_id=None, catalognum=None, script=None, language=None, country=None, albumstatus=None, media=None, albumdisambig=None, artist_credit=None, original_year=None, original_month=None, original_day=None, data_source=None, data_url=None): self.album = album self.album_id = album_id self.artist = artist self.artist_id = artist_id self.tracks = tracks self.asin = asin self.albumtype = albumtype self.va = va self.year = year self.month = month self.day = day self.label = label self.mediums = mediums self.artist_sort = artist_sort self.releasegroup_id = releasegroup_id self.catalognum = catalognum self.script = script self.language = language self.country = country self.albumstatus = albumstatus self.media = media self.albumdisambig = albumdisambig self.artist_credit = artist_credit self.original_year = original_year self.original_month = original_month self.original_day = original_day self.data_source = data_source self.data_url = data_url # Work around a bug in python-musicbrainz-ngs that causes some # strings to be bytes rather than Unicode. # https://github.com/alastair/python-musicbrainz-ngs/issues/85 def decode(self, codec='utf8'): """Ensure that all string attributes on this object, and the constituent `TrackInfo` objects, are decoded to Unicode. """ for fld in ['album', 'artist', 'albumtype', 'label', 'artist_sort', 'catalognum', 'script', 'language', 'country', 'albumstatus', 'albumdisambig', 'artist_credit', 'media']: value = getattr(self, fld) if isinstance(value, str): setattr(self, fld, value.decode(codec, 'ignore')) if self.tracks: for track in self.tracks: track.decode(codec) class TrackInfo(object): """Describes a canonical track present on a release. Appears as part of an AlbumInfo's ``tracks`` list. Consists of these data members: - ``title``: name of the track - ``track_id``: MusicBrainz ID; UUID fragment only - ``artist``: individual track artist name - ``artist_id`` - ``length``: float: duration of the track in seconds - ``index``: position on the entire release - ``medium``: the disc number this track appears on in the album - ``medium_index``: the track's position on the disc - ``medium_total``: the number of tracks on the item's disc - ``artist_sort``: name of the track artist for sorting - ``disctitle``: name of the individual medium (subtitle) - ``artist_credit``: Recording-specific artist name Only ``title`` and ``track_id`` are required. The rest of the fields may be None. The indices ``index``, ``medium``, and ``medium_index`` are all 1-based. """ def __init__(self, title, track_id, artist=None, artist_id=None, length=None, index=None, medium=None, medium_index=None, medium_total=None, artist_sort=None, disctitle=None, artist_credit=None, data_source=None, data_url=None): self.title = title self.track_id = track_id self.artist = artist self.artist_id = artist_id self.length = length self.index = index self.medium = medium self.medium_index = medium_index self.medium_total = medium_total self.artist_sort = artist_sort self.disctitle = disctitle self.artist_credit = artist_credit self.data_source = data_source self.data_url = data_url # As above, work around a bug in python-musicbrainz-ngs. def decode(self, codec='utf8'): """Ensure that all string attributes on this object are decoded to Unicode. """ for fld in ['title', 'artist', 'medium', 'artist_sort', 'disctitle', 'artist_credit']: value = getattr(self, fld) if isinstance(value, str): setattr(self, fld, value.decode(codec, 'ignore')) # Candidate distance scoring. # Parameters for string distance function. # Words that can be moved to the end of a string using a comma. SD_END_WORDS = ['the', 'a', 'an'] # Reduced weights for certain portions of the string. SD_PATTERNS = [ (r'^the ', 0.1), (r'[\[\(]?(ep|single)[\]\)]?', 0.0), (r'[\[\(]?(featuring|feat|ft)[\. :].+', 0.1), (r'\(.*?\)', 0.3), (r'\[.*?\]', 0.3), (r'(, )?(pt\.|part) .+', 0.2), ] # Replacements to use before testing distance. SD_REPLACE = [ (r'&', 'and'), ] def _string_dist_basic(str1, str2): """Basic edit distance between two strings, ignoring non-alphanumeric characters and case. Comparisons are based on a transliteration/lowering to ASCII characters. Normalized by string length. """ str1 = unidecode(str1) str2 = unidecode(str2) str1 = re.sub(r'[^a-z0-9]', '', str1.lower()) str2 = re.sub(r'[^a-z0-9]', '', str2.lower()) if not str1 and not str2: return 0.0 return levenshtein(str1, str2) / float(max(len(str1), len(str2))) def string_dist(str1, str2): """Gives an "intuitive" edit distance between two strings. This is an edit distance, normalized by the string length, with a number of tweaks that reflect intuition about text. """ if str1 is None and str2 is None: return 0.0 if str1 is None or str2 is None: return 1.0 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". for word in SD_END_WORDS: if str1.endswith(', %s' % word): str1 = '%s %s' % (word, str1[:-len(word) - 2]) if str2.endswith(', %s' % word): str2 = '%s %s' % (word, str2[:-len(word) - 2]) # Perform a couple of basic normalizing substitutions. 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 # deleted. base_dist = _string_dist_basic(str1, str2) penalty = 0.0 for pat, weight in SD_PATTERNS: # 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 # modified strings. case_dist = _string_dist_basic(case_str1, case_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. str1 = case_str1 str2 = case_str2 base_dist = case_dist penalty += weight * case_delta return base_dist + penalty class LazyClassProperty(object): """A decorator implementing a read-only property that is *lazy* in the sense that the getter is only invoked once. Subsequent accesses through *any* instance use the cached result. """ def __init__(self, getter): self.getter = getter self.computed = False def __get__(self, obj, owner): if not self.computed: self.value = self.getter(owner) self.computed = True return self.value class Distance(object): """Keeps track of multiple distance penalties. Provides a single weighted distance for all penalties as well as a weighted distance for each individual penalty. """ def __init__(self): self._penalties = {} @LazyClassProperty def _weights(cls): """A dictionary from keys to floating-point weights. """ weights_view = config['match']['distance_weights'] weights = {} for key in weights_view.keys(): weights[key] = weights_view[key].as_number() return weights # Access the components and their aggregates. @property def distance(self): """Return a weighted and normalized distance across all penalties. """ dist_max = self.max_distance if dist_max: return self.raw_distance / self.max_distance return 0.0 @property def max_distance(self): """Return the maximum distance penalty (normalization factor). """ dist_max = 0.0 for key, penalty in self._penalties.iteritems(): dist_max += len(penalty) * self._weights[key] return dist_max @property def raw_distance(self): """Return the raw (denormalized) distance. """ dist_raw = 0.0 for key, penalty in self._penalties.iteritems(): dist_raw += sum(penalty) * self._weights[key] return dist_raw def items(self): """Return a list of (key, dist) pairs, with `dist` being the weighted distance, sorted from highest to lowest. Does not include penalties with a zero value. """ list_ = [] for key in self._penalties: dist = self[key] if dist: list_.append((key, dist)) # Convert distance into a negative float we can sort items in # ascending order (for keys, when the penalty is equal) and # still get the items with the biggest distance first. return sorted(list_, key=lambda (key, dist): (0 - dist, key)) # Behave like a float. def __cmp__(self, other): return cmp(self.distance, other) def __float__(self): return self.distance def __sub__(self, other): return self.distance - other def __rsub__(self, other): return other - self.distance # Behave like a dict. def __getitem__(self, key): """Returns the weighted distance for a named penalty. """ dist = sum(self._penalties[key]) * self._weights[key] dist_max = self.max_distance if dist_max: return dist / dist_max return 0.0 def __iter__(self): return iter(self.items()) def __len__(self): return len(self.items()) def keys(self): return [key for key, _ in self.items()] def update(self, dist): """Adds all the distance penalties from `dist`. """ if not isinstance(dist, Distance): raise ValueError( '`dist` must be a Distance object, not {0}'.format(type(dist)) ) for key, penalties in dist._penalties.iteritems(): self._penalties.setdefault(key, []).extend(penalties) # Adding components. def _eq(self, value1, value2): """Returns True if `value1` is equal to `value2`. `value1` may be a compiled regular expression, in which case it will be matched against `value2`. """ if isinstance(value1, re._pattern_type): return bool(value1.match(value2)) return value1 == value2 def add(self, key, dist): """Adds a distance penalty. `key` must correspond with a configured weight setting. `dist` must be a float between 0.0 and 1.0, and will be added to any existing distance penalties for the same key. """ if not 0.0 <= dist <= 1.0: raise ValueError( '`dist` must be between 0.0 and 1.0, not {0}'.format(dist) ) self._penalties.setdefault(key, []).append(dist) def add_equality(self, key, value, options): """Adds a distance penalty of 1.0 if `value` doesn't match any of the values in `options`. If an option is a compiled regular expression, it will be considered equal if it matches against `value`. """ if not isinstance(options, (list, tuple)): options = [options] for opt in options: if self._eq(opt, value): dist = 0.0 break else: dist = 1.0 self.add(key, dist) def add_expr(self, key, expr): """Adds a distance penalty of 1.0 if `expr` evaluates to True, or 0.0. """ if expr: self.add(key, 1.0) else: self.add(key, 0.0) def add_number(self, key, number1, number2): """Adds a distance penalty of 1.0 for each number of difference between `number1` and `number2`, or 0.0 when there is no difference. Use this when there is no upper limit on the difference between the two numbers. """ diff = abs(number1 - number2) if diff: for i in range(diff): self.add(key, 1.0) else: self.add(key, 0.0) def add_priority(self, key, value, options): """Adds a distance penalty that corresponds to the position at which `value` appears in `options`. A distance penalty of 0.0 for the first option, or 1.0 if there is no matching option. If an option is a compiled regular expression, it will be considered equal if it matches against `value`. """ if not isinstance(options, (list, tuple)): options = [options] unit = 1.0 / (len(options) or 1) for i, opt in enumerate(options): if self._eq(opt, value): dist = i * unit break else: dist = 1.0 self.add(key, dist) def add_ratio(self, key, number1, number2): """Adds a distance penalty for `number1` as a ratio of `number2`. `number1` is bound at 0 and `number2`. """ number = float(max(min(number1, number2), 0)) if number2: dist = number / number2 else: dist = 0.0 self.add(key, dist) def add_string(self, key, str1, str2): """Adds a distance penalty based on the edit distance between `str1` and `str2`. """ dist = string_dist(str1, str2) self.add(key, dist) # Structures that compose all the information for a candidate match. AlbumMatch = namedtuple('AlbumMatch', ['distance', 'info', 'mapping', 'extra_items', 'extra_tracks']) TrackMatch = namedtuple('TrackMatch', ['distance', 'info']) # Aggregation of sources. def album_for_mbid(release_id): """Get an AlbumInfo object for a MusicBrainz release ID. Return None if the ID is not found. """ try: return mb.album_for_id(release_id) except mb.MusicBrainzAPIError as exc: exc.log(log) def track_for_mbid(recording_id): """Get a TrackInfo object for a MusicBrainz recording ID. Return None if the ID is not found. """ try: return mb.track_for_id(recording_id) except mb.MusicBrainzAPIError as exc: exc.log(log) def albums_for_id(album_id): """Get a list of albums for an ID.""" candidates = [album_for_mbid(album_id)] candidates.extend(plugins.album_for_id(album_id)) return filter(None, candidates) def tracks_for_id(track_id): """Get a list of tracks for an ID.""" candidates = [track_for_mbid(track_id)] candidates.extend(plugins.track_for_id(track_id)) return filter(None, candidates) def album_candidates(items, artist, album, va_likely): """Search for album matches. ``items`` is a list of Item objects that make up the album. ``artist`` and ``album`` are the respective names (strings), which may be derived from the item list or may be entered by the user. ``va_likely`` is a boolean indicating whether the album is likely to be a "various artists" release. """ out = [] # Base candidates if we have album and artist to match. if artist and album: try: out.extend(mb.match_album(artist, album, len(items))) except mb.MusicBrainzAPIError as exc: exc.log(log) # Also add VA matches from MusicBrainz where appropriate. if va_likely and album: try: out.extend(mb.match_album(None, album, len(items))) except mb.MusicBrainzAPIError as exc: exc.log(log) # Candidates from plugins. out.extend(plugins.candidates(items, artist, album, va_likely)) return out def item_candidates(item, artist, title): """Search for item matches. ``item`` is the Item to be matched. ``artist`` and ``title`` are strings and either reflect the item or are specified by the user. """ out = [] # MusicBrainz candidates. if artist and title: try: out.extend(mb.match_track(artist, title)) except mb.MusicBrainzAPIError as exc: exc.log(log) # Plugin candidates. out.extend(plugins.item_candidates(item, artist, title)) return out beets-1.3.8/beets/autotag/match.py0000644000076500000240000004415112406436341020015 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Matches existing metadata with canonical information to identify releases and tracks. """ from __future__ import division import datetime import logging import re from munkres import Munkres from beets import plugins from beets import config from beets.util import plurality from beets.autotag import hooks from beets.util.enumeration import OrderedEnum # Artist signals that indicate "various artists". These are used at the # album level to determine whether a given release is likely a VA # release and also on the track level to to remove the penalty for # differing artists. VA_ARTISTS = (u'', u'various artists', u'various', u'va', u'unknown') # Global logger. log = logging.getLogger('beets') # Recommendation enumeration. class Recommendation(OrderedEnum): """Indicates a qualitative suggestion to the user about what should be done with a given match. """ none = 0 low = 1 medium = 2 strong = 3 # Primary matching functionality. def current_metadata(items): """Extract the likely current metadata for an album given a list of its items. Return two dictionaries: - The most common value for each field. - Whether each field's value was unanimous (values are booleans). """ assert items # Must be nonempty. likelies = {} consensus = {} fields = ['artist', 'album', 'albumartist', 'year', 'disctotal', 'mb_albumid', 'label', 'catalognum', 'country', 'media', 'albumdisambig'] for field in fields: values = [item[field] for item in items if item] likelies[field], freq = plurality(values) consensus[field] = (freq == len(values)) # If there's an album artist consensus, use this for the artist. if consensus['albumartist'] and likelies['albumartist']: likelies['artist'] = likelies['albumartist'] return likelies, consensus def assign_items(items, tracks): """Given a list of Items and a list of TrackInfo objects, find the best mapping between them. Returns a mapping from Items to TrackInfo objects, a set of extra Items, and a set of extra TrackInfo objects. These "extra" objects occur when there is an unequal number of objects of the two types. """ # Construct the cost matrix. costs = [] for item in items: row = [] for i, track in enumerate(tracks): row.append(track_distance(item, track)) costs.append(row) # Find a minimum-cost bipartite matching. matching = Munkres().compute(costs) # Produce the output matching. mapping = dict((items[i], tracks[j]) for (i, j) in matching) extra_items = list(set(items) - set(mapping.keys())) extra_items.sort(key=lambda i: (i.disc, i.track, i.title)) extra_tracks = list(set(tracks) - set(mapping.values())) extra_tracks.sort(key=lambda t: (t.index, t.title)) return mapping, extra_items, extra_tracks def track_index_changed(item, track_info): """Returns True if the item and track info index is different. Tolerates per disc and per release numbering. """ return item.track not in (track_info.medium_index, track_info.index) def track_distance(item, track_info, incl_artist=False): """Determines the significance of a track metadata change. Returns a Distance object. `incl_artist` indicates that a distance component should be included for the track artist (i.e., for various-artist releases). """ dist = hooks.Distance() # Length. if track_info.length: diff = abs(item.length - track_info.length) - \ config['match']['track_length_grace'].as_number() dist.add_ratio('track_length', diff, config['match']['track_length_max'].as_number()) # Title. dist.add_string('track_title', item.title, track_info.title) # Artist. Only check if there is actually an artist in the track data. if incl_artist and track_info.artist and \ item.artist.lower() not in VA_ARTISTS: dist.add_string('track_artist', item.artist, track_info.artist) # Track index. if track_info.index and item.track: dist.add_expr('track_index', track_index_changed(item, track_info)) # Track ID. if item.mb_trackid: dist.add_expr('track_id', item.mb_trackid != track_info.track_id) # Plugins. dist.update(plugins.track_distance(item, track_info)) return dist def distance(items, album_info, mapping): """Determines how "significant" an album metadata change would be. Returns a Distance object. `album_info` is an AlbumInfo object reflecting the album to be compared. `items` is a sequence of all Item objects that will be matched (order is not important). `mapping` is a dictionary mapping Items to TrackInfo objects; the keys are a subset of `items` and the values are a subset of `album_info.tracks`. """ likelies, _ = current_metadata(items) dist = hooks.Distance() # Artist, if not various. if not album_info.va: dist.add_string('artist', likelies['artist'], album_info.artist) # Album. dist.add_string('album', likelies['album'], album_info.album) # Current or preferred media. if album_info.media: # Preferred media options. patterns = config['match']['preferred']['media'].as_str_seq() options = [re.compile(r'(\d+x)?(%s)' % pat, re.I) for pat in patterns] if options: dist.add_priority('media', album_info.media, options) # Current media. elif likelies['media']: dist.add_equality('media', album_info.media, likelies['media']) # Mediums. if likelies['disctotal'] and album_info.mediums: dist.add_number('mediums', likelies['disctotal'], album_info.mediums) # Prefer earliest release. if album_info.year and config['match']['preferred']['original_year']: # Assume 1889 (earliest first gramophone discs) if we don't know the # original year. original = album_info.original_year or 1889 diff = abs(album_info.year - original) diff_max = abs(datetime.date.today().year - original) dist.add_ratio('year', diff, diff_max) # Year. elif likelies['year'] and album_info.year: if likelies['year'] in (album_info.year, album_info.original_year): # No penalty for matching release or original year. dist.add('year', 0.0) elif album_info.original_year: # Prefer matchest closest to the release year. diff = abs(likelies['year'] - album_info.year) diff_max = abs(datetime.date.today().year - album_info.original_year) dist.add_ratio('year', diff, diff_max) else: # Full penalty when there is no original year. dist.add('year', 1.0) # Preferred countries. patterns = config['match']['preferred']['countries'].as_str_seq() options = [re.compile(pat, re.I) for pat in patterns] if album_info.country and options: dist.add_priority('country', album_info.country, options) # Country. elif likelies['country'] and album_info.country: dist.add_string('country', likelies['country'], album_info.country) # Label. if likelies['label'] and album_info.label: dist.add_string('label', likelies['label'], album_info.label) # Catalog number. if likelies['catalognum'] and album_info.catalognum: dist.add_string('catalognum', likelies['catalognum'], album_info.catalognum) # Disambiguation. if likelies['albumdisambig'] and album_info.albumdisambig: dist.add_string('albumdisambig', likelies['albumdisambig'], album_info.albumdisambig) # Album ID. if likelies['mb_albumid']: dist.add_equality('album_id', likelies['mb_albumid'], album_info.album_id) # Tracks. dist.tracks = {} for item, track in mapping.iteritems(): dist.tracks[track] = track_distance(item, track, album_info.va) dist.add('tracks', dist.tracks[track].distance) # Missing tracks. for i in range(len(album_info.tracks) - len(mapping)): dist.add('missing_tracks', 1.0) # Unmatched tracks. for i in range(len(items) - len(mapping)): dist.add('unmatched_tracks', 1.0) # Plugins. dist.update(plugins.album_distance(items, album_info, mapping)) return dist def match_by_id(items): """If the items are tagged with a MusicBrainz album ID, returns an AlbumInfo object for the corresponding album. Otherwise, returns None. """ # Is there a consensus on the MB album ID? albumids = [item.mb_albumid for item in items if item.mb_albumid] if not albumids: log.debug(u'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] log.debug(u'Searching for discovered album ID: {0}'.format(albumid)) return hooks.album_for_mbid(albumid) else: log.debug(u'No album ID consensus.') def _recommendation(results): """Given a sorted list of AlbumMatch or TrackMatch objects, return a recommendation based on the results' distances. If the recommendation is higher than the configured maximum for an applied penalty, the recommendation will be downgraded to the configured maximum for that penalty. """ if not results: # No candidates: no recommendation. return Recommendation.none # Basic distance thresholding. min_dist = results[0].distance if min_dist < config['match']['strong_rec_thresh'].as_number(): # Strong recommendation level. rec = Recommendation.strong elif min_dist <= config['match']['medium_rec_thresh'].as_number(): # Medium recommendation level. rec = Recommendation.medium elif len(results) == 1: # Only a single candidate. rec = Recommendation.low elif results[1].distance - min_dist >= \ config['match']['rec_gap_thresh'].as_number(): # Gap between first two candidates is large. rec = Recommendation.low else: # No conclusion. Return immediately. Can't be downgraded any further. return Recommendation.none # Downgrade to the max rec if it is lower than the current rec for an # applied penalty. keys = set(min_dist.keys()) if isinstance(results[0], hooks.AlbumMatch): for track_dist in min_dist.tracks.values(): keys.update(track_dist.keys()) max_rec_view = config['match']['max_rec'] for key in keys: if key in max_rec_view.keys(): max_rec = max_rec_view[key].as_choice({ 'strong': Recommendation.strong, 'medium': Recommendation.medium, 'low': Recommendation.low, 'none': Recommendation.none, }) rec = min(rec, max_rec) return rec def _add_candidate(items, results, info): """Given a candidate AlbumInfo object, attempt to add the candidate to the output dictionary of AlbumMatch objects. This involves checking the track count, ordering the items, checking for duplicates, and calculating the distance. """ log.debug(u'Candidate: {0} - {1}'.format(info.artist, info.album)) # Discard albums with zero tracks. if not info.tracks: log.debug('No tracks.') return # Don't duplicate. if info.album_id in results: log.debug(u'Duplicate.') return # Discard matches without required tags. for req_tag in config['match']['required'].as_str_seq(): if getattr(info, req_tag) is None: log.debug(u'Ignored. Missing required tag: {0}'.format(req_tag)) return # Find mapping between the items and the track info. mapping, extra_items, extra_tracks = assign_items(items, info.tracks) # Get the change distance. dist = distance(items, info, mapping) # Skip matches with ignored penalties. penalties = [key for _, key in dist] for penalty in config['match']['ignored'].as_str_seq(): if penalty in penalties: log.debug(u'Ignored. Penalty: {0}'.format(penalty)) return log.debug(u'Success. Distance: {0}'.format(dist)) results[info.album_id] = hooks.AlbumMatch(dist, info, mapping, extra_items, extra_tracks) def tag_album(items, search_artist=None, search_album=None, search_id=None): """Return a tuple of a artist name, an album name, a list of `AlbumMatch` candidates from the metadata backend, and a `Recommendation`. The artist and album are the most common values of these fields among `items`. The `AlbumMatch` objects are generated by searching the metadata backends. By default, the metadata of the items is used for the search. This can be customized by setting the parameters. The `mapping` field of the album has the matched `items` as keys. The recommendation is calculated from the match qualitiy of the candidates. """ # Get current metadata. likelies, consensus = current_metadata(items) cur_artist = likelies['artist'] cur_album = likelies['album'] log.debug(u'Tagging {0} - {1}'.format(cur_artist, cur_album)) # The output result (distance, AlbumInfo) tuples (keyed by MB album # ID). candidates = {} # Search by explicit ID. if search_id is not None: log.debug(u'Searching for album ID: {0}'.format(search_id)) search_cands = hooks.albums_for_id(search_id) # Use existing metadata or text search. else: # Try search based on current ID. id_info = match_by_id(items) if id_info: _add_candidate(items, candidates, id_info) rec = _recommendation(candidates.values()) log.debug(u'Album ID match recommendation is {0}'.format(str(rec))) if candidates and not config['import']['timid']: # If we have a very good MBID match, return immediately. # Otherwise, this match will compete against metadata-based # matches. if rec == Recommendation.strong: log.debug(u'ID match.') return cur_artist, cur_album, candidates.values(), rec # 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: {0} - {1}'.format(search_artist, search_album)) # Is this album likely to be a "various artist" release? va_likely = ((not consensus['artist']) or (search_artist.lower() in VA_ARTISTS) or any(item.comp for item in items)) log.debug(u'Album might be VA: {0}'.format(str(va_likely))) # Get the results from the data sources. search_cands = hooks.album_candidates(items, search_artist, search_album, va_likely) log.debug(u'Evaluating {0} candidates.'.format(len(search_cands))) for info in search_cands: _add_candidate(items, candidates, info) # Sort and get the recommendation. candidates = sorted(candidates.itervalues()) rec = _recommendation(candidates) return cur_artist, cur_album, candidates, rec def tag_item(item, 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 TrackMatch objects. `search_artist` and `search_title` may be used to override the current metadata for the purposes of the MusicBrainz title; likewise `search_id`. """ # Holds candidates found so far: keys are MBIDs; values are # (distance, TrackInfo) pairs. candidates = {} # First, try matching by MusicBrainz ID. trackid = search_id or item.mb_trackid if trackid: log.debug(u'Searching for track ID: {0}'.format(trackid)) for track_info in hooks.tracks_for_id(trackid): dist = track_distance(item, track_info, incl_artist=True) candidates[track_info.track_id] = \ hooks.TrackMatch(dist, track_info) # If this is a good match, then don't keep searching. rec = _recommendation(candidates.values()) if rec == Recommendation.strong and not config['import']['timid']: log.debug(u'Track ID match.') return candidates.values(), rec # If we're searching by ID, don't proceed. if search_id is not None: if candidates: return candidates.values(), rec else: return [], Recommendation.none # Search terms. if not (search_artist and search_title): search_artist, search_title = item.artist, item.title log.debug(u'Item search terms: {0} - {1}'.format(search_artist, search_title)) # Get and evaluate candidate metadata. for track_info in hooks.item_candidates(item, search_artist, search_title): dist = track_distance(item, track_info, incl_artist=True) candidates[track_info.track_id] = hooks.TrackMatch(dist, track_info) # Sort by distance and return with recommendation. log.debug(u'Found {0} candidates.'.format(len(candidates))) candidates = sorted(candidates.itervalues()) rec = _recommendation(candidates) return candidates, rec beets-1.3.8/beets/autotag/mb.py0000644000076500000240000003313412405373453017321 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Searches for albums in the MusicBrainz database. """ import logging import musicbrainzngs import re import traceback from urlparse import urljoin import beets.autotag.hooks import beets from beets import util from beets import config SEARCH_LIMIT = 5 VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' BASE_URL = 'http://musicbrainz.org/' musicbrainzngs.set_useragent('beets', beets.__version__, 'http://beets.radbox.org/') class MusicBrainzAPIError(util.HumanReadableException): """An error while talking to MusicBrainz. The `query` field is the parameter to the action and may have any type. """ def __init__(self, reason, verb, query, tb=None): self.query = query super(MusicBrainzAPIError, self).__init__(reason, verb, tb) def get_message(self): return u'{0} in {1} with query {2}'.format( self._reasonstr(), self.verb, repr(self.query) ) log = logging.getLogger('beets') RELEASE_INCLUDES = ['artists', 'media', 'recordings', 'release-groups', 'labels', 'artist-credits', 'aliases'] TRACK_INCLUDES = ['artists', 'aliases'] def track_url(trackid): return urljoin(BASE_URL, 'recording/' + trackid) def album_url(albumid): return urljoin(BASE_URL, 'release/' + albumid) def configure(): """Set up the python-musicbrainz-ngs module according to settings from the beets configuration. This should be called at startup. """ musicbrainzngs.set_hostname(config['musicbrainz']['host'].get(unicode)) musicbrainzngs.set_rate_limit( config['musicbrainz']['ratelimit_interval'].as_number(), config['musicbrainz']['ratelimit'].get(int), ) def _preferred_alias(aliases): """Given an list of alias structures for an artist credit, select and return the user's preferred alias alias or None if no matching alias is found. """ if not aliases: return # Only consider aliases that have locales set. aliases = [a for a in aliases if 'locale' in a] # Search configured locales in order. for locale in config['import']['languages'].as_str_seq(): # Find matching primary aliases for this locale. matches = [a for a in aliases if a['locale'] == locale and 'primary' in a] # Skip to the next locale if we have no matches if not matches: continue return matches[0] def _flatten_artist_credit(credit): """Given a list representing an ``artist-credit`` block, flatten the data into a triple of joined artist name strings: canonical, sort, and credit. """ artist_parts = [] artist_sort_parts = [] artist_credit_parts = [] for el in credit: if isinstance(el, basestring): # Join phrase. artist_parts.append(el) artist_credit_parts.append(el) artist_sort_parts.append(el) else: alias = _preferred_alias(el['artist'].get('alias-list', ())) # An artist. if alias: cur_artist_name = alias['alias'] else: cur_artist_name = el['artist']['name'] artist_parts.append(cur_artist_name) # Artist sort name. if alias: artist_sort_parts.append(alias['sort-name']) elif 'sort-name' in el['artist']: artist_sort_parts.append(el['artist']['sort-name']) else: artist_sort_parts.append(cur_artist_name) # Artist credit. if 'name' in el: artist_credit_parts.append(el['name']) else: artist_credit_parts.append(cur_artist_name) return ( ''.join(artist_parts), ''.join(artist_sort_parts), ''.join(artist_credit_parts), ) def track_info(recording, index=None, medium=None, medium_index=None, medium_total=None): """Translates a MusicBrainz recording result dictionary into a beets ``TrackInfo`` object. Three parameters are optional and are used only for tracks that appear on releases (non-singletons): ``index``, the overall track number; ``medium``, the disc number; ``medium_index``, the track's index on its medium; ``medium_total``, the number of tracks on the medium. Each number is a 1-based index. """ info = beets.autotag.hooks.TrackInfo( recording['title'], recording['id'], index=index, medium=medium, medium_index=medium_index, medium_total=medium_total, data_url=track_url(recording['id']), ) if recording.get('artist-credit'): # Get the artist names. info.artist, info.artist_sort, info.artist_credit = \ _flatten_artist_credit(recording['artist-credit']) # Get the ID and sort name of the first artist. artist = recording['artist-credit'][0]['artist'] info.artist_id = artist['id'] if recording.get('length'): info.length = int(recording['length']) / (1000.0) info.decode() return info def _set_date_str(info, date_str, original=False): """Given a (possibly partial) YYYY-MM-DD string and an AlbumInfo object, set the object's release date fields appropriately. If `original`, then set the original_year, etc., fields. """ if date_str: date_parts = date_str.split('-') for key in ('year', 'month', 'day'): if date_parts: date_part = date_parts.pop(0) try: date_num = int(date_part) except ValueError: continue if original: key = 'original_' + key setattr(info, key, date_num) def album_info(release): """Takes a MusicBrainz release result dictionary and returns a beets AlbumInfo object containing the interesting data about that release. """ # Get artist name using join phrases. artist_name, artist_sort_name, artist_credit_name = \ _flatten_artist_credit(release['artist-credit']) # Basic info. track_infos = [] index = 0 for medium in release['medium-list']: disctitle = medium.get('title') for track in medium['track-list']: # Basic information from the recording. index += 1 ti = track_info( track['recording'], index, int(medium['position']), int(track['position']), len(medium['track-list']), ) ti.disctitle = disctitle # Prefer track data, where present, over recording data. if track.get('title'): ti.title = track['title'] if track.get('artist-credit'): # Get the artist names. ti.artist, ti.artist_sort, ti.artist_credit = \ _flatten_artist_credit(track['artist-credit']) ti.artist_id = track['artist-credit'][0]['artist']['id'] if track.get('length'): ti.length = int(track['length']) / (1000.0) track_infos.append(ti) info = beets.autotag.hooks.AlbumInfo( release['title'], release['id'], artist_name, release['artist-credit'][0]['artist']['id'], track_infos, mediums=len(release['medium-list']), artist_sort=artist_sort_name, artist_credit=artist_credit_name, data_source='MusicBrainz', data_url=album_url(release['id']), ) info.va = info.artist_id == VARIOUS_ARTISTS_ID info.asin = release.get('asin') info.releasegroup_id = release['release-group']['id'] info.country = release.get('country') info.albumstatus = release.get('status') # Build up the disambiguation string from the release group and release. disambig = [] if release['release-group'].get('disambiguation'): disambig.append(release['release-group'].get('disambiguation')) if release.get('disambiguation'): disambig.append(release.get('disambiguation')) info.albumdisambig = u', '.join(disambig) # Release type not always populated. if 'type' in release['release-group']: reltype = release['release-group']['type'] if reltype: info.albumtype = reltype.lower() # Release dates. release_date = release.get('date') release_group_date = release['release-group'].get('first-release-date') if not release_date: # Fall back if release-specific date is not available. release_date = release_group_date _set_date_str(info, release_date, False) _set_date_str(info, release_group_date, True) # Label name. if release.get('label-info-list'): label_info = release['label-info-list'][0] if label_info.get('label'): label = label_info['label']['name'] if label != '[no label]': info.label = label info.catalognum = label_info.get('catalog-number') # Text representation data. if release.get('text-representation'): rep = release['text-representation'] info.script = rep.get('script') info.language = rep.get('language') # Media (format). if release['medium-list']: first_medium = release['medium-list'][0] info.media = first_medium.get('format') info.decode() return info def match_album(artist, album, tracks=None, limit=SEARCH_LIMIT): """Searches for a single album ("release" in MusicBrainz parlance) and returns an iterator over AlbumInfo objects. May raise a MusicBrainzAPIError. The query consists of an artist name, an album name, and, optionally, a number of tracks on the album. """ # Build search criteria. criteria = {'release': album.lower()} if artist is not None: criteria['artist'] = artist.lower() else: # Various Artists search. criteria['arid'] = VARIOUS_ARTISTS_ID if tracks is not None: criteria['tracks'] = str(tracks) # Abort if we have no search terms. if not any(criteria.itervalues()): return try: res = musicbrainzngs.search_releases(limit=limit, **criteria) except musicbrainzngs.MusicBrainzError as exc: raise MusicBrainzAPIError(exc, 'release search', criteria, traceback.format_exc()) for release in res['release-list']: # The search result is missing some data (namely, the tracks), # so we just use the ID and fetch the rest of the information. albuminfo = album_for_id(release['id']) if albuminfo is not None: yield albuminfo def match_track(artist, title, limit=SEARCH_LIMIT): """Searches for a single track and returns an iterable of TrackInfo objects. May raise a MusicBrainzAPIError. """ criteria = { 'artist': artist.lower(), 'recording': title.lower(), } if not any(criteria.itervalues()): return try: res = musicbrainzngs.search_recordings(limit=limit, **criteria) except musicbrainzngs.MusicBrainzError as exc: raise MusicBrainzAPIError(exc, 'recording search', criteria, traceback.format_exc()) for recording in res['recording-list']: yield track_info(recording) def _parse_id(s): """Search for a MusicBrainz ID in the given string and return it. If no ID can be found, return None. """ # Find the first thing that looks like a UUID/MBID. match = re.search('[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}', s) if match: return match.group() def album_for_id(releaseid): """Fetches an album by its MusicBrainz ID and returns an AlbumInfo object or None if the album is not found. May raise a MusicBrainzAPIError. """ albumid = _parse_id(releaseid) if not albumid: log.debug(u'Invalid MBID ({0}).'.format(releaseid)) return try: res = musicbrainzngs.get_release_by_id(albumid, RELEASE_INCLUDES) except musicbrainzngs.ResponseError: log.debug(u'Album ID match failed.') return None except musicbrainzngs.MusicBrainzError as exc: raise MusicBrainzAPIError(exc, 'get release by ID', albumid, traceback.format_exc()) return album_info(res['release']) def track_for_id(releaseid): """Fetches a track by its MusicBrainz ID. Returns a TrackInfo object or None if no track is found. May raise a MusicBrainzAPIError. """ trackid = _parse_id(releaseid) if not trackid: log.debug(u'Invalid MBID ({0}).'.format(releaseid)) return try: res = musicbrainzngs.get_recording_by_id(trackid, TRACK_INCLUDES) except musicbrainzngs.ResponseError: log.debug(u'Track ID match failed.') return None except musicbrainzngs.MusicBrainzError as exc: raise MusicBrainzAPIError(exc, 'get recording by ID', trackid, traceback.format_exc()) return track_info(res['recording']) beets-1.3.8/beets/config_default.yaml0000644000076500000240000000407412406127245020540 0ustar asampsonstaff00000000000000library: library.db directory: ~/Music import: write: yes copy: yes move: no delete: no resume: ask incremental: no quiet_fallback: skip none_rec_action: ask timid: no log: autotag: yes quiet: no singletons: no default_action: apply languages: [] detail: no flat: no group_albums: no clutter: ["Thumbs.DB", ".DS_Store"] ignore: [".*", "*~", "System Volume Information"] replace: '[\\/]': _ '^\.': _ '[\x00-\x1f]': _ '[<>:"\?\*\|]': _ '\.$': _ '\s+$': '' '^\s+': '' path_sep_replace: _ asciify_paths: false art_filename: cover max_filename_length: 0 plugins: [] pluginpath: [] threaded: yes color: yes timeout: 5.0 per_disc_numbering: no verbose: no terminal_encoding: utf8 original_date: no id3v23: no ui: terminal_width: 80 length_diff_thresh: 10.0 list_format_item: $artist - $album - $title list_format_album: $albumartist - $album time_format: '%Y-%m-%d %H:%M:%S' sort_album: albumartist+ album+ sort_item: artist+ album+ disc+ track+ paths: default: $albumartist/$album%aunique{}/$track $title singleton: Non-Album/$artist/$title comp: Compilations/$album%aunique{}/$track $title statefile: state.pickle musicbrainz: host: musicbrainz.org ratelimit: 1 ratelimit_interval: 1.0 match: strong_rec_thresh: 0.04 medium_rec_thresh: 0.25 rec_gap_thresh: 0.25 max_rec: missing_tracks: medium unmatched_tracks: medium distance_weights: source: 2.0 artist: 3.0 album: 3.0 media: 1.0 mediums: 1.0 year: 1.0 country: 0.5 label: 0.5 catalognum: 0.5 albumdisambig: 0.5 album_id: 5.0 tracks: 2.0 missing_tracks: 0.9 unmatched_tracks: 0.6 track_title: 3.0 track_artist: 2.0 track_index: 1.0 track_length: 2.0 track_id: 5.0 preferred: countries: [] media: [] original_year: no ignored: [] required: [] track_length_grace: 10 track_length_max: 30 beets-1.3.8/beets/dbcore/0000755000076500000240000000000012406440351016130 5ustar asampsonstaff00000000000000beets-1.3.8/beets/dbcore/__init__.py0000644000076500000240000000175212405703320020243 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """DBCore is an abstract database package that forms the basis for beets' Library. """ from .db import Model, Database from .query import Query, FieldQuery, MatchQuery, AndQuery, OrQuery from .types import Type from .queryparse import query_from_strings from .queryparse import sort_from_strings from .queryparse import parse_sorted_query # flake8: noqa beets-1.3.8/beets/dbcore/db.py0000644000076500000240000006304412406127715017104 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """The central Model and Database constructs for DBCore. """ import time import os from collections import defaultdict import threading import sqlite3 import contextlib import collections import beets from beets.util.functemplate import Template from beets.dbcore import types from .query import MatchQuery, NullSort, TrueQuery class FormattedMapping(collections.Mapping): """A `dict`-like formatted view of a model. The accessor `mapping[key]` returns the formated version of `model[key]` as a unicode string. If `for_path` is true, all path separators in the formatted values are replaced. """ def __init__(self, model, for_path=False): self.for_path = for_path self.model = model self.model_keys = model.keys(True) def __getitem__(self, key): if key in self.model_keys: return self._get_formatted(self.model, key) else: raise KeyError(key) def __iter__(self): return iter(self.model_keys) def __len__(self): return len(self.model_keys) def get(self, key, default=None): if default is None: default = self.model._type(key).format(None) return super(FormattedMapping, self).get(key, default) def _get_formatted(self, model, key): value = model._type(key).format(model.get(key)) if isinstance(value, bytes): value = value.decode('utf8', 'ignore') if self.for_path: sep_repl = beets.config['path_sep_replace'].get(unicode) for sep in (os.path.sep, os.path.altsep): if sep: value = value.replace(sep, sep_repl) return value # Abstract base for model classes. class Model(object): """An abstract object representing an object in the database. Model objects act like dictionaries (i.e., the allow subscript access like ``obj['field']``). The same field set is available via attribute access as a shortcut (i.e., ``obj.field``). Three kinds of attributes are available: * **Fixed attributes** come from a predetermined list of field names. These fields correspond to SQLite table columns and are thus fast to read, write, and query. * **Flexible attributes** are free-form and do not need to be listed ahead of time. * **Computed attributes** are read-only fields computed by a getter function provided by a plugin. Access to all three field types is uniform: ``obj.field`` works the same regardless of whether ``field`` is fixed, flexible, or computed. Model objects can optionally be associated with a `Library` object, in which case they can be loaded and stored from the database. Dirty flags are used to track which fields need to be stored. """ # Abstract components (to be provided by subclasses). _table = None """The main SQLite table name. """ _flex_table = None """The flex field SQLite table name. """ _fields = {} """A mapping indicating available "fixed" fields on this type. The keys are field names and the values are `Type` objects. """ _search_fields = () """The fields that should be queried by default by unqualified query terms. """ _types = {} """Optional Types for non-fixed (i.e., flexible and computed) fields. """ _sorts = {} """Optional named sort criteria. The keys are strings and the values are subclasses of `Sort`. """ @classmethod def _getters(cls): """Return a mapping from field names to getter functions. """ # We could cache this if it becomes a performance problem to # gather the getter mapping every time. raise NotImplementedError() def _template_funcs(self): """Return a mapping from function names to text-transformer functions. """ # As above: we could consider caching this result. raise NotImplementedError() # Basic operation. def __init__(self, db=None, **values): """Create a new object with an optional Database association and initial field values. """ self._db = db self._dirty = set() self._values_fixed = {} self._values_flex = {} # Initial contents. self.update(values) self.clear_dirty() @classmethod def _awaken(cls, db=None, fixed_values={}, flex_values={}): """Create an object with values drawn from the database. This is a performance optimization: the checks involved with ordinary construction are bypassed. """ obj = cls(db) for key, value in fixed_values.iteritems(): obj._values_fixed[key] = cls._type(key).from_sql(value) for key, value in flex_values.iteritems(): obj._values_flex[key] = cls._type(key).from_sql(value) return obj def __repr__(self): return '{0}({1})'.format( type(self).__name__, ', '.join('{0}={1!r}'.format(k, v) for k, v in dict(self).items()), ) def clear_dirty(self): """Mark all fields as *clean* (i.e., not needing to be stored to the database). """ self._dirty = set() def _check_db(self, need_id=True): """Ensure that this object is associated with a database row: it has a reference to a database (`_db`) and an id. A ValueError exception is raised otherwise. """ if not self._db: raise ValueError('{0} has no database'.format(type(self).__name__)) if need_id and not self.id: raise ValueError('{0} has no id'.format(type(self).__name__)) # Essential field accessors. @classmethod def _type(self, key): """Get the type of a field, a `Type` instance. If the field has no explicit type, it is given the base `Type`, which does no conversion. """ return self._fields.get(key) or self._types.get(key) or types.DEFAULT def __getitem__(self, key): """Get the value for a field. Raise a KeyError if the field is not available. """ getters = self._getters() if key in getters: # Computed. return getters[key](self) elif key in self._fields: # Fixed. return self._values_fixed.get(key) elif key in self._values_flex: # Flexible. return self._values_flex[key] else: raise KeyError(key) def __setitem__(self, key, value): """Assign the value for a field. """ # Choose where to place the value. if key in self._fields: source = self._values_fixed else: source = self._values_flex # If the field has a type, filter the value. value = self._type(key).normalize(value) # Assign value and possibly mark as dirty. old_value = source.get(key) source[key] = value if old_value != value: self._dirty.add(key) def __delitem__(self, key): """Remove a flexible attribute from the model. """ if key in self._values_flex: # Flexible. del self._values_flex[key] self._dirty.add(key) # Mark for dropping on store. elif key in self._getters(): # Computed. raise KeyError('computed field {0} cannot be deleted'.format(key)) elif key in self._fields: # Fixed. raise KeyError('fixed field {0} cannot be deleted'.format(key)) else: raise KeyError('no such field {0}'.format(key)) def keys(self, computed=False): """Get a list of available field names for this object. The `computed` parameter controls whether computed (plugin-provided) fields are included in the key list. """ base_keys = list(self._fields) + self._values_flex.keys() if computed: return base_keys + self._getters().keys() else: return base_keys # Act like a dictionary. def update(self, values): """Assign all values in the given dict. """ for key, value in values.items(): self[key] = value def items(self): """Iterate over (key, value) pairs that this object contains. Computed fields are not included. """ for key in self: yield key, self[key] def get(self, key, default=None): """Get the value for a given key or `default` if it does not exist. """ if key in self: return self[key] else: return default def __contains__(self, key): """Determine whether `key` is an attribute on this object. """ return key in self.keys(True) def __iter__(self): """Iterate over the available field names (excluding computed fields). """ return iter(self.keys()) # Convenient attribute access. def __getattr__(self, key): if key.startswith('_'): raise AttributeError('model has no attribute {0!r}'.format(key)) else: try: return self[key] except KeyError: raise AttributeError('no such field {0!r}'.format(key)) def __setattr__(self, key, value): if key.startswith('_'): super(Model, self).__setattr__(key, value) else: self[key] = value def __delattr__(self, key): if key.startswith('_'): super(Model, self).__delattr__(key) else: del self[key] # Database interaction (CRUD methods). def store(self): """Save the object's metadata into the library database. """ self._check_db() # Build assignments for query. assignments = [] subvars = [] for key in self._fields: if key != 'id' and key in self._dirty: self._dirty.remove(key) assignments.append(key + '=?') value = self._type(key).to_sql(self[key]) subvars.append(value) assignments = ','.join(assignments) with self._db.transaction() as tx: # Main table update. if assignments: query = 'UPDATE {0} SET {1} WHERE id=?'.format( self._table, assignments ) subvars.append(self.id) tx.mutate(query, subvars) # Modified/added flexible attributes. for key, value in self._values_flex.items(): if key in self._dirty: self._dirty.remove(key) tx.mutate( 'INSERT INTO {0} ' '(entity_id, key, value) ' 'VALUES (?, ?, ?);'.format(self._flex_table), (self.id, key, value), ) # Deleted flexible attributes. for key in self._dirty: tx.mutate( 'DELETE FROM {0} ' 'WHERE entity_id=? AND key=?'.format(self._flex_table), (self.id, key) ) self.clear_dirty() def load(self): """Refresh the object's metadata from the library database. """ self._check_db() stored_obj = self._db._get(type(self), self.id) assert stored_obj is not None, "object {0} not in DB".format(self.id) self._values_fixed = {} self._values_flex = {} self.update(dict(stored_obj)) self.clear_dirty() def remove(self): """Remove the object's associated rows from the database. """ self._check_db() with self._db.transaction() as tx: tx.mutate( 'DELETE FROM {0} WHERE id=?'.format(self._table), (self.id,) ) tx.mutate( 'DELETE FROM {0} WHERE entity_id=?'.format(self._flex_table), (self.id,) ) def add(self, db=None): """Add the object to the library database. This object must be associated with a database; you can provide one via the `db` parameter or use the currently associated database. The object's `id` and `added` fields are set along with any current field values. """ if db: self._db = db self._check_db(False) with self._db.transaction() as tx: new_id = tx.mutate( 'INSERT INTO {0} DEFAULT VALUES'.format(self._table) ) self.id = new_id self.added = time.time() # Mark every non-null field as dirty and store. for key in self: if self[key] is not None: self._dirty.add(key) self.store() # Formatting and templating. _formatter = FormattedMapping def formatted(self, for_path=False): """Get a mapping containing all values on this object formatted as human-readable unicode strings. """ return self._formatter(self, for_path) def evaluate_template(self, template, for_path=False): """Evaluate a template (a string or a `Template` object) using the object's fields. If `for_path` is true, then no new path separators will be added to the template. """ # Perform substitution. if isinstance(template, basestring): template = Template(template) return template.substitute(self.formatted(for_path), self._template_funcs()) # Parsing. @classmethod def _parse(cls, key, string): """Parse a string as a value for the given key. """ if not isinstance(string, basestring): raise TypeError("_parse() argument must be a string") return cls._type(key).parse(string) # Database controller and supporting interfaces. class Results(object): """An item query result set. Iterating over the collection lazily constructs LibModel objects that reflect database rows. """ def __init__(self, model_class, rows, db, query=None, sort=None): """Create a result set that will construct objects of type `model_class`, which should be a subclass of `LibModel`, out of the query result mapping in `rows`. The new objects are associated with the database `db`. If `query` is provided, it is used as a predicate to filter the results for a "slow query" that cannot be evaluated by the database directly. If `sort` is provided, it is used to sort the full list of results before returning. This means it is a "slow sort" and all objects must be built before returning the first one. """ self.model_class = model_class self.rows = rows self.db = db self.query = query self.sort = sort def __iter__(self): """Construct Python objects for all rows that pass the query predicate. """ if self.sort: # Slow sort. Must build the full list first. objects = [] for row in self.rows: obj = self._make_model(row) # check the predicate if any if not self.query or self.query.match(obj): objects.append(obj) # Now that we have the full list, we can sort it objects = self.sort.sort(objects) for o in objects: yield o else: for row in self.rows: obj = self._make_model(row) # check the predicate if any if not self.query or self.query.match(obj): yield obj def _make_model(self, row): # Get the flexible attributes for the object. with self.db.transaction() as tx: flex_rows = tx.query( 'SELECT * FROM {0} WHERE entity_id=?'.format( self.model_class._flex_table ), (row['id'],) ) cols = dict(row) values = dict((k, v) for (k, v) in cols.items() if not k[:4] == 'flex') flex_values = dict((row['key'], row['value']) for row in flex_rows) # Construct the Python object obj = self.model_class._awaken(self.db, values, flex_values) return obj def __len__(self): """Get the number of matching objects. """ if self.query: # A slow query. Fall back to testing every object. count = 0 for obj in self: count += 1 return count else: # A fast query. Just count the rows. return len(self.rows) def __nonzero__(self): """Does this result contain any objects? """ return bool(len(self)) def __getitem__(self, n): """Get the nth item in this result set. This is inefficient: all items up to n are materialized and thrown away. """ it = iter(self) try: for i in range(n): it.next() return it.next() except StopIteration: raise IndexError('result index {0} out of range'.format(n)) def get(self): """Return the first matching object, or None if no objects match. """ it = iter(self) try: return it.next() except StopIteration: return None class Transaction(object): """A context manager for safe, concurrent access to the database. All SQL commands should be executed through a transaction. """ def __init__(self, db): self.db = db def __enter__(self): """Begin a transaction. This transaction may be created while another is active in a different thread. """ with self.db._tx_stack() as stack: first = not stack stack.append(self) if first: # Beginning a "root" transaction, which corresponds to an # SQLite transaction. self.db._db_lock.acquire() return self def __exit__(self, exc_type, exc_value, traceback): """Complete a transaction. This must be the most recently entered but not yet exited transaction. If it is the last active transaction, the database updates are committed. """ with self.db._tx_stack() as stack: assert stack.pop() is self empty = not stack if empty: # Ending a "root" transaction. End the SQLite transaction. self.db._connection().commit() self.db._db_lock.release() def query(self, statement, subvals=()): """Execute an SQL statement with substitution values and return a list of rows from the database. """ cursor = self.db._connection().execute(statement, subvals) return cursor.fetchall() def mutate(self, statement, subvals=()): """Execute an SQL statement with substitution values and return the row ID of the last affected row. """ cursor = self.db._connection().execute(statement, subvals) return cursor.lastrowid def script(self, statements): """Execute a string containing multiple SQL statements.""" self.db._connection().executescript(statements) class Database(object): """A container for Model objects that wraps an SQLite database as the backend. """ _models = () """The Model subclasses representing tables in this database. """ def __init__(self, path): self.path = path self._connections = {} self._tx_stacks = defaultdict(list) # A lock to protect the _connections and _tx_stacks maps, which # both map thread IDs to private resources. self._shared_map_lock = threading.Lock() # A lock to protect access to the database itself. SQLite does # allow multiple threads to access the database at the same # time, but many users were experiencing crashes related to this # capability: where SQLite was compiled without HAVE_USLEEP, its # backoff algorithm in the case of contention was causing # whole-second sleeps (!) that would trigger its internal # timeout. Using this lock ensures only one SQLite transaction # is active at a time. self._db_lock = threading.Lock() # Set up database schema. for model_cls in self._models: self._make_table(model_cls._table, model_cls._fields) self._make_attribute_table(model_cls._flex_table) # Primitive access control: connections and transactions. def _connection(self): """Get a SQLite connection object to the underlying database. One connection object is created per thread. """ thread_id = threading.current_thread().ident with self._shared_map_lock: if thread_id in self._connections: return self._connections[thread_id] else: # Make a new connection. conn = sqlite3.connect( self.path, timeout=beets.config['timeout'].as_number(), ) # Access SELECT results like dictionaries. conn.row_factory = sqlite3.Row self._connections[thread_id] = conn return conn @contextlib.contextmanager def _tx_stack(self): """A context manager providing access to the current thread's transaction stack. The context manager synchronizes access to the stack map. Transactions should never migrate across threads. """ thread_id = threading.current_thread().ident with self._shared_map_lock: yield self._tx_stacks[thread_id] def transaction(self): """Get a :class:`Transaction` object for interacting directly with the underlying SQLite database. """ return Transaction(self) # Schema setup and migration. def _make_table(self, table, fields): """Set up the schema of the database. `fields` is a mapping from field names to `Type`s. Columns are added if necessary. """ # Get current schema. with self.transaction() as tx: rows = tx.query('PRAGMA table_info(%s)' % table) current_fields = set([row[1] for row in rows]) field_names = set(fields.keys()) if current_fields.issuperset(field_names): # Table exists and has all the required columns. return if not current_fields: # No table exists. columns = [] for name, typ in fields.items(): columns.append('{0} {1}'.format(name, typ.sql)) setup_sql = 'CREATE TABLE {0} ({1});\n'.format(table, ', '.join(columns)) else: # Table exists does not match the field set. setup_sql = '' for name, typ in fields.items(): if name in current_fields: continue setup_sql += 'ALTER TABLE {0} ADD COLUMN {1} {2};\n'.format( table, name, typ.sql ) with self.transaction() as tx: tx.script(setup_sql) def _make_attribute_table(self, flex_table): """Create a table and associated index for flexible attributes for the given entity (if they don't exist). """ with self.transaction() as tx: tx.script(""" CREATE TABLE IF NOT EXISTS {0} ( id INTEGER PRIMARY KEY, entity_id INTEGER, key TEXT, value TEXT, UNIQUE(entity_id, key) ON CONFLICT REPLACE); CREATE INDEX IF NOT EXISTS {0}_by_entity ON {0} (entity_id); """.format(flex_table)) # Querying. def _fetch(self, model_cls, query=None, sort=None): """Fetch the objects of type `model_cls` matching the given query. The query may be given as a string, string sequence, a Query object, or None (to fetch everything). `sort` is an `Sort` object. """ query = query or TrueQuery() # A null query. sort = sort or NullSort() # Unsorted. where, subvals = query.clause() order_by = sort.order_clause() sql = ("SELECT * FROM {0} WHERE {1} {2}").format( model_cls._table, where or '1', "ORDER BY {0}".format(order_by) if order_by else '', ) with self.transaction() as tx: rows = tx.query(sql, subvals) return Results( model_cls, rows, self, None if where else query, # Slow query component. sort if sort.is_slow() else None, # Slow sort component. ) def _get(self, model_cls, id): """Get a Model object by its id or None if the id does not exist. """ return self._fetch(model_cls, MatchQuery('id', id)).get() beets-1.3.8/beets/dbcore/query.py0000644000076500000240000004750412406436341017665 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """The Query type hierarchy for DBCore. """ import re from operator import attrgetter from beets import util from datetime import datetime, timedelta class Query(object): """An abstract class representing a query into the item database. """ def clause(self): """Generate an SQLite expression implementing the query. Return a clause string, a sequence of substitution values for the clause, and a Query object representing the "remainder" Returns (clause, subvals) where clause is a valid sqlite WHERE clause implementing the query and subvals is a list of items to be substituted for ?s in the clause. """ return None, () def match(self, item): """Check whether this query matches a given Item. Can be used to perform queries on arbitrary sets of Items. """ raise NotImplementedError class FieldQuery(Query): """An abstract query that searches in a specific field for a pattern. Subclasses must provide a `value_match` class method, which determines whether a certain pattern string matches a certain value string. Subclasses may also provide `col_clause` to implement the same matching functionality in SQLite. """ def __init__(self, field, pattern, fast=True): self.field = field self.pattern = pattern self.fast = fast def col_clause(self): return None, () def clause(self): if self.fast: return self.col_clause() else: # Matching a flexattr. This is a slow query. return None, () @classmethod def value_match(cls, pattern, value): """Determine whether the value matches the pattern. Both arguments are strings. """ raise NotImplementedError() def match(self, item): return self.value_match(self.pattern, item.get(self.field)) class MatchQuery(FieldQuery): """A query that looks for exact matches in an item field.""" def col_clause(self): return self.field + " = ?", [self.pattern] @classmethod def value_match(cls, pattern, value): return pattern == value class NoneQuery(FieldQuery): def __init__(self, field, fast=True): self.field = field self.fast = fast def col_clause(self): return self.field + " IS NULL", () @classmethod def match(self, item): try: return item[self.field] is None except KeyError: return True class StringFieldQuery(FieldQuery): """A FieldQuery that converts values to strings before matching them. """ @classmethod def value_match(cls, pattern, value): """Determine whether the value matches the pattern. The value may have any type. """ return cls.string_match(pattern, util.as_string(value)) @classmethod def string_match(cls, pattern, value): """Determine whether the value matches the pattern. Both arguments are strings. Subclasses implement this method. """ raise NotImplementedError() class SubstringQuery(StringFieldQuery): """A query that matches a substring in a specific item field.""" def col_clause(self): pattern = (self.pattern .replace('\\', '\\\\') .replace('%', '\\%') .replace('_', '\\_')) search = '%' + pattern + '%' clause = self.field + " like ? escape '\\'" subvals = [search] return clause, subvals @classmethod def string_match(cls, pattern, value): return pattern.lower() in value.lower() class RegexpQuery(StringFieldQuery): """A query that matches a regular expression in a specific item field. """ @classmethod def string_match(cls, pattern, value): try: res = re.search(pattern, value) except re.error: # Invalid regular expression. return False return res is not None class BooleanQuery(MatchQuery): """Matches a boolean field. Pattern should either be a boolean or a string reflecting a boolean. """ def __init__(self, field, pattern, fast=True): super(BooleanQuery, self).__init__(field, pattern, fast) if isinstance(pattern, basestring): self.pattern = util.str2bool(pattern) self.pattern = int(self.pattern) class BytesQuery(MatchQuery): """Match a raw bytes field (i.e., a path). This is a necessary hack to work around the `sqlite3` module's desire to treat `str` and `unicode` equivalently in Python 2. Always use this query instead of `MatchQuery` when matching on BLOB values. """ def __init__(self, field, pattern): super(BytesQuery, self).__init__(field, pattern) # Use a buffer representation of the pattern for SQLite # matching. This instructs SQLite to treat the blob as binary # rather than encoded Unicode. if isinstance(self.pattern, basestring): # Implicitly coerce Unicode strings to their bytes # equivalents. if isinstance(self.pattern, unicode): self.pattern = self.pattern.encode('utf8') self.buf_pattern = buffer(self.pattern) elif isinstance(self.pattern, buffer): self.buf_pattern = self.pattern self.pattern = bytes(self.pattern) def col_clause(self): return self.field + " = ?", [self.buf_pattern] class NumericQuery(FieldQuery): """Matches numeric fields. A syntax using Ruby-style range ellipses (``..``) lets users specify one- or two-sided ranges. For example, ``year:2001..`` finds music released since the turn of the century. """ def _convert(self, s): """Convert a string to a numeric type (float or int). If the string cannot be converted, return None. """ # This is really just a bit of fun premature optimization. try: return int(s) except ValueError: try: return float(s) except ValueError: return None def __init__(self, field, pattern, fast=True): super(NumericQuery, self).__init__(field, pattern, fast) parts = pattern.split('..', 1) if len(parts) == 1: # No range. self.point = self._convert(parts[0]) self.rangemin = None self.rangemax = None else: # One- or two-sided range. self.point = None self.rangemin = self._convert(parts[0]) self.rangemax = self._convert(parts[1]) def match(self, item): if self.field not in item: return False value = item[self.field] if isinstance(value, basestring): value = self._convert(value) if self.point is not None: return value == self.point else: if self.rangemin is not None and value < self.rangemin: return False if self.rangemax is not None and value > self.rangemax: return False return True def col_clause(self): if self.point is not None: return self.field + '=?', (self.point,) else: if self.rangemin is not None and self.rangemax is not None: return (u'{0} >= ? AND {0} <= ?'.format(self.field), (self.rangemin, self.rangemax)) elif self.rangemin is not None: return u'{0} >= ?'.format(self.field), (self.rangemin,) elif self.rangemax is not None: return u'{0} <= ?'.format(self.field), (self.rangemax,) else: return '1', () class CollectionQuery(Query): """An abstract query class that aggregates other queries. Can be indexed like a list to access the sub-queries. """ def __init__(self, subqueries=()): self.subqueries = subqueries # Act like a sequence. def __len__(self): return len(self.subqueries) def __getitem__(self, key): return self.subqueries[key] def __iter__(self): return iter(self.subqueries) def __contains__(self, item): return item in self.subqueries def clause_with_joiner(self, joiner): """Returns a clause created by joining together the clauses of all subqueries with the string joiner (padded by spaces). """ clause_parts = [] subvals = [] for subq in self.subqueries: subq_clause, subq_subvals = subq.clause() if not subq_clause: # Fall back to slow query. return None, () clause_parts.append('(' + subq_clause + ')') subvals += subq_subvals clause = (' ' + joiner + ' ').join(clause_parts) return clause, subvals class AnyFieldQuery(CollectionQuery): """A query that matches if a given FieldQuery subclass matches in any field. The individual field query class is provided to the constructor. """ def __init__(self, pattern, fields, cls): self.pattern = pattern self.fields = fields self.query_class = cls subqueries = [] for field in self.fields: subqueries.append(cls(field, pattern, True)) super(AnyFieldQuery, self).__init__(subqueries) def clause(self): return self.clause_with_joiner('or') def match(self, item): for subq in self.subqueries: if subq.match(item): return True return False class MutableCollectionQuery(CollectionQuery): """A collection query whose subqueries may be modified after the query is initialized. """ def __setitem__(self, key, value): self.subqueries[key] = value def __delitem__(self, key): del self.subqueries[key] class AndQuery(MutableCollectionQuery): """A conjunction of a list of other queries.""" def clause(self): return self.clause_with_joiner('and') def match(self, item): return all([q.match(item) for q in self.subqueries]) class OrQuery(MutableCollectionQuery): """A conjunction of a list of other queries.""" def clause(self): return self.clause_with_joiner('or') def match(self, item): return any([q.match(item) for q in self.subqueries]) class TrueQuery(Query): """A query that always matches.""" def clause(self): return '1', () def match(self, item): return True class FalseQuery(Query): """A query that never matches.""" def clause(self): return '0', () def match(self, item): return False # Time/date queries. def _to_epoch_time(date): """Convert a `datetime` object to an integer number of seconds since the (local) Unix epoch. """ epoch = datetime.fromtimestamp(0) delta = date - epoch try: return int(delta.total_seconds()) except AttributeError: # datetime.timedelta.total_seconds() is not available on Python 2.6 return delta.seconds + delta.days * 24 * 3600 def _parse_periods(pattern): """Parse a string containing two dates separated by two dots (..). Return a pair of `Period` objects. """ parts = pattern.split('..', 1) if len(parts) == 1: instant = Period.parse(parts[0]) return (instant, instant) else: start = Period.parse(parts[0]) end = Period.parse(parts[1]) return (start, end) class Period(object): """A period of time given by a date, time and precision. Example: 2014-01-01 10:50:30 with precision 'month' represents all instants of time during January 2014. """ precisions = ('year', 'month', 'day') date_formats = ('%Y', '%Y-%m', '%Y-%m-%d') def __init__(self, date, precision): """Create a period with the given date (a `datetime` object) and precision (a string, one of "year", "month", or "day"). """ if precision not in Period.precisions: raise ValueError('Invalid precision ' + str(precision)) self.date = date self.precision = precision @classmethod def parse(cls, string): """Parse a date and return a `Period` object or `None` if the string is empty. """ if not string: return None ordinal = string.count('-') if ordinal >= len(cls.date_formats): raise ValueError('date is not in one of the formats ' + ', '.join(cls.date_formats)) date_format = cls.date_formats[ordinal] date = datetime.strptime(string, date_format) precision = cls.precisions[ordinal] return cls(date, precision) def open_right_endpoint(self): """Based on the precision, convert the period to a precise `datetime` for use as a right endpoint in a right-open interval. """ precision = self.precision date = self.date if 'year' == self.precision: return date.replace(year=date.year + 1, month=1) elif 'month' == precision: if (date.month < 12): return date.replace(month=date.month + 1) else: return date.replace(year=date.year + 1, month=1) elif 'day' == precision: return date + timedelta(days=1) else: raise ValueError('unhandled precision ' + str(precision)) class DateInterval(object): """A closed-open interval of dates. A left endpoint of None means since the beginning of time. A right endpoint of None means towards infinity. """ def __init__(self, start, end): if start is not None and end is not None and not start < end: raise ValueError("start date {0} is not before end date {1}" .format(start, end)) self.start = start self.end = end @classmethod def from_periods(cls, start, end): """Create an interval with two Periods as the endpoints. """ end_date = end.open_right_endpoint() if end is not None else None start_date = start.date if start is not None else None return cls(start_date, end_date) def contains(self, date): if self.start is not None and date < self.start: return False if self.end is not None and date >= self.end: return False return True def __str__(self): return'[{0}, {1})'.format(self.start, self.end) class DateQuery(FieldQuery): """Matches date fields stored as seconds since Unix epoch time. Dates can be specified as ``year-month-day`` strings where only year is mandatory. The value of a date field can be matched against a date interval by using an ellipsis interval syntax similar to that of NumericQuery. """ def __init__(self, field, pattern, fast=True): super(DateQuery, self).__init__(field, pattern, fast) start, end = _parse_periods(pattern) self.interval = DateInterval.from_periods(start, end) def match(self, item): timestamp = float(item[self.field]) date = datetime.utcfromtimestamp(timestamp) return self.interval.contains(date) _clause_tmpl = "{0} {1} ?" def col_clause(self): clause_parts = [] subvals = [] if self.interval.start: clause_parts.append(self._clause_tmpl.format(self.field, ">=")) subvals.append(_to_epoch_time(self.interval.start)) if self.interval.end: clause_parts.append(self._clause_tmpl.format(self.field, "<")) subvals.append(_to_epoch_time(self.interval.end)) if clause_parts: # One- or two-sided interval. clause = ' AND '.join(clause_parts) else: # Match any date. clause = '1' return clause, subvals # Sorting. class Sort(object): """An abstract class representing a sort operation for a query into the item database. """ def order_clause(self): """Generates a SQL fragment to be used in a ORDER BY clause, or None if no fragment is used (i.e., this is a slow sort). """ return None def sort(self, items): """Sort the list of objects and return a list. """ return sorted(items) def is_slow(self): """Indicate whether this query is *slow*, meaning that it cannot be executed in SQL and must be executed in Python. """ return False class MultipleSort(Sort): """Sort that encapsulates multiple sub-sorts. """ def __init__(self, sorts=None): self.sorts = sorts or [] def add_sort(self, sort): self.sorts.append(sort) def _sql_sorts(self): """Return the list of sub-sorts for which we can be (at least partially) fast. A contiguous suffix of fast (SQL-capable) sub-sorts are executable in SQL. The remaining, even if they are fast independently, must be executed slowly. """ sql_sorts = [] for sort in reversed(self.sorts): if not sort.order_clause() is None: sql_sorts.append(sort) else: break sql_sorts.reverse() return sql_sorts def order_clause(self): order_strings = [] for sort in self._sql_sorts(): order = sort.order_clause() order_strings.append(order) return ", ".join(order_strings) def is_slow(self): for sort in self.sorts: if sort.is_slow(): return True return False def sort(self, items): slow_sorts = [] switch_slow = False for sort in reversed(self.sorts): if switch_slow: slow_sorts.append(sort) elif sort.order_clause() is None: switch_slow = True slow_sorts.append(sort) else: pass for sort in slow_sorts: items = sort.sort(items) return items def __repr__(self): return u'MultipleSort({0})'.format(repr(self.sorts)) class FieldSort(Sort): """An abstract sort criterion that orders by a specific field (of any kind). """ def __init__(self, field, ascending=True): self.field = field self.ascending = ascending def sort(self, objs): # TODO: Conversion and null-detection here. In Python 3, # comparisons with None fail. We should also support flexible # attributes with different types without falling over. return sorted(objs, key=attrgetter(self.field), reverse=not self.ascending) def __repr__(self): return u'<{0}: {1}{2}>'.format( type(self).__name__, self.field, '+' if self.ascending else '-', ) class FixedFieldSort(FieldSort): """Sort object to sort on a fixed field. """ def order_clause(self): order = "ASC" if self.ascending else "DESC" return "{0} {1}".format(self.field, order) class SlowFieldSort(FieldSort): """A sort criterion by some model field other than a fixed field: i.e., a computed or flexible field. """ def is_slow(self): return True class NullSort(Sort): """No sorting. Leave results unsorted.""" def sort(items): return items beets-1.3.8/beets/dbcore/queryparse.py0000644000076500000240000001414312405720547020714 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Parsing of strings into DBCore queries. """ import re import itertools from . import query PARSE_QUERY_PART_REGEX = re.compile( # Non-capturing optional segment for the keyword. r'(?:' r'(\S+?)' # The field key. r'(? (None, 'stapler', SubstringQuery) 'color:red' -> ('color', 'red', SubstringQuery) ':^Quiet' -> (None, '^Quiet', RegexpQuery) 'color::b..e' -> ('color', 'b..e', RegexpQuery) Prefixes may be "escaped" with a backslash to disable the keying behavior. """ part = part.strip() match = PARSE_QUERY_PART_REGEX.match(part) assert match # Regex should always match. key = match.group(1) term = match.group(2).replace('\:', ':') # Match the search term against the list of prefixes. for pre, query_class in prefixes.items(): if term.startswith(pre): return key, term[len(pre):], query_class # No matching prefix: use type-based or fallback/default query. query_class = query_classes.get(key, default_class) return key, term, query_class def construct_query_part(model_cls, prefixes, query_part): """Create a query from a single query component, `query_part`, for querying instances of `model_cls`. Return a `Query` instance. """ # Shortcut for empty query parts. if not query_part: return query.TrueQuery() # Get the query classes for each possible field. query_classes = {} for k, t in itertools.chain(model_cls._fields.items(), model_cls._types.items()): query_classes[k] = t.query # Parse the string. key, pattern, query_class = \ parse_query_part(query_part, query_classes, prefixes) # No key specified. if key is None: if issubclass(query_class, query.FieldQuery): # The query type matches a specific field, but none was # specified. So we use a version of the query that matches # any field. return query.AnyFieldQuery(pattern, model_cls._search_fields, query_class) else: # Other query type. return query_class(pattern) key = key.lower() return query_class(key.lower(), pattern, key in model_cls._fields) def query_from_strings(query_cls, model_cls, prefixes, query_parts): """Creates a collection query of type `query_cls` from a list of strings in the format used by parse_query_part. `model_cls` determines how queries are constructed from strings. """ subqueries = [] for part in query_parts: subqueries.append(construct_query_part(model_cls, prefixes, part)) if not subqueries: # No terms in query. subqueries = [query.TrueQuery()] return query_cls(subqueries) def construct_sort_part(model_cls, part): """Create a `Sort` from a single string criterion. `model_cls` is the `Model` being queried. `part` is a single string ending in ``+`` or ``-`` indicating the sort. """ assert part, "part must be a field name and + or -" field = part[:-1] assert field, "field is missing" direction = part[-1] assert direction in ('+', '-'), "part must end with + or -" is_ascending = direction == '+' if field in model_cls._sorts: sort = model_cls._sorts[field](model_cls, is_ascending) elif field in model_cls._fields: sort = query.FixedFieldSort(field, is_ascending) else: # Flexible or computed. sort = query.SlowFieldSort(field, is_ascending) return sort def sort_from_strings(model_cls, sort_parts): """Create a `Sort` from a list of sort criteria (strings). """ if not sort_parts: return query.NullSort() else: sort = query.MultipleSort() for part in sort_parts: sort.add_sort(construct_sort_part(model_cls, part)) return sort def parse_sorted_query(model_cls, parts, prefixes={}, query_cls=query.AndQuery): """Given a list of strings, create the `Query` and `Sort` that they represent. """ # Separate query token and sort token. query_parts = [] sort_parts = [] for part in parts: if part.endswith((u'+', u'-')) and u':' not in part: sort_parts.append(part) else: query_parts.append(part) # Parse each. q = query_from_strings( query_cls, model_cls, prefixes, query_parts ) s = sort_from_strings(model_cls, sort_parts) return q, s beets-1.3.8/beets/dbcore/types.py0000644000076500000240000001335712405617172017665 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Representation of type information for DBCore model fields. """ from . import query from beets.util import str2bool # Abstract base. class Type(object): """An object encapsulating the type of a model field. Includes information about how to store, query, format, and parse a given field. """ sql = u'TEXT' """The SQLite column type for the value. """ query = query.SubstringQuery """The `Query` subclass to be used when querying the field. """ model_type = unicode """The Python type that is used to represent the value in the model. The model is guaranteed to return a value of this type if the field is accessed. To this end, the constructor is used by the `normalize` and `from_sql` methods and the `default` property. """ @property def null(self): """The value to be exposed when the underlying value is None. """ return self.model_type() def format(self, value): """Given a value of this type, produce a Unicode string representing the value. This is used in template evaluation. """ if value is None: value = self.null # `self.null` might be `None` if value is None: value = u'' if isinstance(value, bytes): value = value.decode('utf8', 'ignore') return unicode(value) def parse(self, string): """Parse a (possibly human-written) string and return the indicated value of this type. """ try: return self.model_type(string) except ValueError: return self.null def normalize(self, value): """Given a value that will be assigned into a field of this type, normalize the value to have the appropriate type. This base implementation only reinterprets `None`. """ if value is None: return self.null else: # TODO This should eventually be replaced by # `self.model_type(value)` return value def from_sql(self, sql_value): """Receives the value stored in the SQL backend and return the value to be stored in the model. For fixed fields the type of `value` is determined by the column type affinity given in the `sql` property and the SQL to Python mapping of the database adapter. For more information see: http://www.sqlite.org/datatype3.html https://docs.python.org/2/library/sqlite3.html#sqlite-and-python-types Flexible fields have the type afinity `TEXT`. This means the `sql_value` is either a `buffer` or a `unicode` object` and the method must handle these in addition. """ if isinstance(sql_value, buffer): sql_value = bytes(sql_value).decode('utf8', 'ignore') if isinstance(sql_value, unicode): return self.parse(sql_value) else: return self.normalize(sql_value) def to_sql(self, model_value): """Convert a value as stored in the model object to a value used by the database adapter. """ return model_value # Reusable types. class Default(Type): null = None class Integer(Type): """A basic integer type. """ sql = u'INTEGER' query = query.NumericQuery model_type = int class PaddedInt(Integer): """An integer field that is formatted with a given number of digits, padded with zeroes. """ def __init__(self, digits): self.digits = digits def format(self, value): return u'{0:0{1}d}'.format(value or 0, self.digits) class ScaledInt(Integer): """An integer whose formatting operation scales the number by a constant and adds a suffix. Good for units with large magnitudes. """ def __init__(self, unit, suffix=u''): self.unit = unit self.suffix = suffix def format(self, value): return u'{0}{1}'.format((value or 0) // self.unit, self.suffix) class Id(Integer): """An integer used as the row id or a foreign key in a SQLite table. This type is nullable: None values are not translated to zero. """ null = None def __init__(self, primary=True): if primary: self.sql = u'INTEGER PRIMARY KEY' class Float(Type): """A basic floating-point type. """ sql = u'REAL' query = query.NumericQuery model_type = float def format(self, value): return u'{0:.1f}'.format(value or 0.0) class NullFloat(Float): """Same as `Float`, but does not normalize `None` to `0.0`. """ null = None class String(Type): """A Unicode string type. """ sql = u'TEXT' query = query.SubstringQuery class Boolean(Type): """A boolean type. """ sql = u'INTEGER' query = query.BooleanQuery model_type = bool def format(self, value): return unicode(bool(value)) def parse(self, string): return str2bool(string) # Shared instances of common types. DEFAULT = Default() INTEGER = Integer() PRIMARY_ID = Id(True) FOREIGN_ID = Id(False) FLOAT = Float() NULL_FLOAT = NullFloat() STRING = String() BOOLEAN = Boolean() beets-1.3.8/beets/importer.py0000644000076500000240000013241612406436341017120 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Provides the basic, interface-agnostic workflow for importing and autotagging music files. """ from __future__ import print_function import os import re import logging import pickle import itertools from collections import defaultdict from tempfile import mkdtemp from bisect import insort, bisect_left from contextlib import contextmanager import shutil from beets import autotag from beets import library from beets import dbcore from beets import plugins from beets import util from beets import config from beets.util import pipeline, sorted_walk, ancestry from beets.util import syspath, normpath, displayable_path from enum import Enum from beets import mediafile action = Enum('action', ['SKIP', 'ASIS', 'TRACKS', 'MANUAL', 'APPLY', 'MANUAL_ID', 'ALBUMS']) QUEUE_SIZE = 128 SINGLE_ARTIST_THRESH = 0.25 VARIOUS_ARTISTS = u'Various Artists' PROGRESS_KEY = 'tagprogress' HISTORY_KEY = 'taghistory' # Global logger. log = logging.getLogger('beets') class ImportAbort(Exception): """Raised when the user aborts the tagging operation. """ pass # Utilities. def _open_state(): """Reads the state file, returning a dictionary.""" try: with open(config['statefile'].as_filename()) as f: return pickle.load(f) except Exception as exc: # The `pickle` module can emit all sorts of exceptions during # unpickling, including ImportError. We use a catch-all # exception to avoid enumerating them all (the docs don't even have a # full list!). log.debug(u'state file could not be read: {0}'.format(exc)) return {} def _save_state(state): """Writes the state dictionary out to disk.""" try: with open(config['statefile'].as_filename(), 'w') as f: pickle.dump(state, f) except IOError as exc: log.error(u'state file could not be written: {0}'.format(exc)) # Utilities for reading and writing the beets progress file, which # allows long tagging tasks to be resumed when they pause (or crash). def progress_read(): state = _open_state() return state.setdefault(PROGRESS_KEY, {}) @contextmanager def progress_write(): state = _open_state() progress = state.setdefault(PROGRESS_KEY, {}) yield progress _save_state(state) def progress_add(toppath, *paths): """Record that the files under all of the `paths` have been imported under `toppath`. """ with progress_write() as state: imported = state.setdefault(toppath, []) for path in paths: # Normally `progress_add` will be called with the path # argument increasing. This is because of the ordering in # `albums_in_dir`. We take advantage of that to make the # code faster if imported and imported[len(imported) - 1] <= path: imported.append(path) else: insort(imported, path) def progress_element(toppath, path): """Return whether `path` has been imported in `toppath`. """ state = progress_read() if toppath not in state: return False imported = state[toppath] i = bisect_left(imported, path) return i != len(imported) and imported[i] == path def has_progress(toppath): """Return `True` if there exist paths that have already been imported under `toppath`. """ state = progress_read() return toppath in state def progress_reset(toppath): with progress_write() as state: if toppath in state: del state[toppath] # Similarly, utilities for manipulating the "incremental" import log. # This keeps track of all directories that were ever imported, which # allows the importer to only import new stuff. def history_add(paths): """Indicate that the import of the album in `paths` is completed and should not be repeated in incremental imports. """ state = _open_state() if HISTORY_KEY not in state: state[HISTORY_KEY] = set() state[HISTORY_KEY].add(tuple(paths)) _save_state(state) def history_get(): """Get the set of completed path tuples in incremental imports. """ state = _open_state() if HISTORY_KEY not in state: return set() return state[HISTORY_KEY] # Abstract session class. class ImportSession(object): """Controls an import action. Subclasses should implement methods to communicate with the user or otherwise make decisions. """ def __init__(self, lib, logfile, paths, query): """Create a session. `lib` is a Library object. `logfile` is a file-like object open for writing or None if no logging is to be performed. Either `paths` or `query` is non-null and indicates the source of files to be imported. """ self.lib = lib self.logfile = logfile self.paths = paths self.query = query self.seen_idents = set() self._is_resuming = dict() # Normalize the paths. if self.paths: self.paths = map(normpath, self.paths) def set_config(self, config): """Set `config` property from global import config and make implied changes. """ # FIXME: Maybe this function should not exist and should instead # provide "decision wrappers" like "should_resume()", etc. iconfig = dict(config) self.config = iconfig # Incremental and progress are mutually exclusive. if iconfig['incremental']: iconfig['resume'] = False # When based on a query instead of directories, never # save progress or try to resume. if self.query is not None: iconfig['resume'] = False iconfig['incremental'] = False # Copy and move are mutually exclusive. if iconfig['move']: iconfig['copy'] = False # Only delete when copying. if not iconfig['copy']: iconfig['delete'] = False self.want_resume = config['resume'].as_choice([True, False, 'ask']) def tag_log(self, status, paths): """Log a message about a given album to logfile. The status should reflect the reason the album couldn't be tagged. """ if self.logfile: print(u'{0} {1}'.format(status, displayable_path(paths)), file=self.logfile) self.logfile.flush() def log_choice(self, task, duplicate=False): """Logs the task's current choice if it should be logged. If ``duplicate``, then this is a secondary choice after a duplicate was detected and a decision was made. """ paths = task.paths if duplicate: # Duplicate: log all three choices (skip, keep both, and trump). if task.should_remove_duplicates: self.tag_log('duplicate-replace', paths) elif task.choice_flag in (action.ASIS, action.APPLY): self.tag_log('duplicate-keep', paths) elif task.choice_flag is (action.SKIP): self.tag_log('duplicate-skip', paths) else: # Non-duplicate: log "skip" and "asis" choices. if task.choice_flag is action.ASIS: self.tag_log('asis', paths) elif task.choice_flag is action.SKIP: self.tag_log('skip', paths) def should_resume(self, path): raise NotImplementedError def choose_match(self, task): raise NotImplementedError def resolve_duplicate(self, task, found_duplicates): raise NotImplementedError def choose_item(self, task): raise NotImplementedError def run(self): """Run the import task. """ self.set_config(config['import']) # Set up the pipeline. if self.query is None: stages = [read_tasks(self)] else: stages = [query_tasks(self)] if self.config['group_albums'] and \ not self.config['singletons']: # Split directory tasks into one task for each album stages += [group_albums(self)] if self.config['autotag']: # Only look up and query the user when autotagging. # FIXME We should also resolve duplicates when not # autotagging. stages += [lookup_candidates(self), user_query(self)] else: stages += [import_asis(self)] stages += [apply_choices(self)] for stage_func in plugins.import_stages(): stages.append(plugin_stage(self, stage_func)) stages += [manipulate_files(self)] pl = pipeline.Pipeline(stages) # Run the pipeline. try: if config['threaded']: pl.run_parallel(QUEUE_SIZE) else: pl.run_sequential() except ImportAbort: # User aborted operation. Silently stop. pass # Incremental and resumed imports def already_imported(self, toppath, paths): """Returns true if the files belonging to this task have already been imported in a previous session. """ if self.is_resuming(toppath) \ and all(map(lambda p: progress_element(toppath, p), paths)): return True if self.config['incremental'] \ and tuple(paths) in self.history_dirs: return True return False @property def history_dirs(self): if not hasattr(self, '_history_dirs'): self._history_dirs = history_get() return self._history_dirs def is_resuming(self, toppath): """Return `True` if user wants to resume import of this path. You have to call `ask_resume` first to determine the return value. """ return self._is_resuming.get(toppath, False) def ask_resume(self, toppath): """If import of `toppath` was aborted in an earlier session, ask user if she wants to resume the import. Determines the return value of `is_resuming(toppath)`. """ if self.want_resume and has_progress(toppath): # Either accept immediately or prompt for input to decide. if self.want_resume is True or \ self.should_resume(toppath): log.warn(u'Resuming interrupted import of {0}'.format( util.displayable_path(toppath))) self._is_resuming[toppath] = True else: # Clear progress; we're starting from the top. progress_reset(toppath) # The importer task class. class ImportTask(object): """Represents a single set of items to be imported along with its intermediate state. May represent an album or a single item. The import session and stages call the following methods in the given order. * `lookup_candidates()` Sets the `common_artist`, `common_album`, `candidates`, and `rec` attributes. `candidates` is a list of `AlbumMatch` objects. * `choose_match()` Uses the session to set the `match` attribute from the `candidates` list. * `find_duplicates()` Returns a list of albums from `lib` with the same artist and album name as the task. * `apply_metadata()` Sets the attributes of the items from the task's `match` attribute. * `add()` Add the imported items and album to the database. * `manipulate_files()` Copy, move, and write files depending on the session configuration. * `finalize()` Update the import progress and cleanup the file system. """ def __init__(self, toppath=None, paths=None, items=None): self.toppath = toppath self.paths = paths self.items = items self.choice_flag = None # TODO remove this eventually self.should_remove_duplicates = False self.is_album = True def set_null_candidates(self): """Set the candidates to indicate no album match was found. """ self.cur_artist = None self.cur_album = None self.candidates = None self.rec = None def set_choice(self, choice): """Given an AlbumMatch or TrackMatch object or an action constant, indicates that an action has been selected for this task. """ # Not part of the task structure: assert choice not in (action.MANUAL, action.MANUAL_ID) assert choice != action.APPLY # Only used internally. if choice in (action.SKIP, action.ASIS, action.TRACKS, action.ALBUMS): self.choice_flag = choice self.match = None else: self.choice_flag = action.APPLY # Implicit choice. self.match = choice def save_progress(self): """Updates the progress state to indicate that this album has finished. """ if self.toppath: progress_add(self.toppath, *self.paths) def save_history(self): """Save the directory in the history for incremental imports. """ if self.paths: history_add(self.paths) # Logical decisions. @property def apply(self): return self.choice_flag == action.APPLY @property def skip(self): return self.choice_flag == action.SKIP # Convenient data. def chosen_ident(self): """Returns identifying metadata about the current choice. For albums, this is an (artist, album) pair. For items, this is (artist, title). May only be called when the choice flag is ASIS (in which case the data comes from the files' current metadata) or APPLY (data comes from the choice). """ if self.choice_flag is action.ASIS: return (self.cur_artist, self.cur_album) elif self.choice_flag is action.APPLY: return (self.match.info.artist, self.match.info.album) def imported_items(self): """Return a list of Items that should be added to the library. If the tasks applies an album match the method only returns the matched items. """ if self.choice_flag == action.ASIS: return list(self.items) # FIXME this should be a simple attribute. There should be no # need to retrieve the keys of `match.mapping`. This requires # that we remove unmatched items from the list. elif self.choice_flag == action.APPLY: return self.match.mapping.keys() else: assert False def apply_metadata(self): """Copy metadata from match info to the items. """ # TODO call should be more descriptive like # apply_metadata(self.match, self.items) autotag.apply_metadata(self.match.info, self.match.mapping) def duplicate_items(self, lib): duplicate_items = [] for album in self.find_duplicates(lib): duplicate_items += album.items() return duplicate_items def remove_duplicates(self, lib): duplicate_items = self.duplicate_items(lib) log.debug(u'removing {0} old duplicated items' .format(len(duplicate_items))) for item in duplicate_items: item.remove() if lib.directory in util.ancestry(item.path): log.debug(u'deleting duplicate {0}' .format(util.displayable_path(item.path))) util.remove(item.path) util.prune_dirs(os.path.dirname(item.path), lib.directory) def finalize(self, session): """Save progress, clean up files, and emit plugin event. """ # FIXME the session argument is unfortunate. It should be # present as an attribute of the task. # Update progress. if session.want_resume: self.save_progress() if session.config['incremental']: self.save_history() if not self.skip: self.cleanup(copy=session.config['copy'], delete=session.config['delete'], move=session.config['move']) self._emit_imported(session.lib) def cleanup(self, copy=False, delete=False, move=False): """Remove and prune imported paths. """ # FIXME Maybe the keywords should be task properties. # FIXME This shouldn't be here. Skipping should be handled in # the stages. if self.skip: return items = self.imported_items() # When copying and deleting originals, delete old files. if copy and delete: new_paths = [os.path.realpath(item.path) for item in items] for old_path in self.old_paths: # Only delete files that were actually copied. if old_path not in new_paths: util.remove(syspath(old_path), False) self.prune(old_path) # When moving, prune empty directories containing the original files. elif move: for old_path in self.old_paths: self.prune(old_path) def _emit_imported(self, lib): # FIXME This shouldn't be here. Skipping should be handled in # the stages. if self.skip: return plugins.send('album_imported', lib=lib, album=self.album) def lookup_candidates(self): """Retrieve and store candidates for this album. """ artist, album, candidates, recommendation = \ autotag.tag_album(self.items) self.cur_artist = artist self.cur_album = album self.candidates = candidates self.rec = recommendation def find_duplicates(self, lib): """Return a list of albums from `lib` with the same artist and album name as the task. """ artist, album = self.chosen_ident() if artist is None: # As-is import with no artist. Skip check. return [] duplicates = [] task_paths = set(i.path for i in self.items if i) duplicate_query = dbcore.AndQuery(( dbcore.MatchQuery('albumartist', artist), dbcore.MatchQuery('album', album), )) for album in lib.albums(duplicate_query): # Check whether the album is identical in contents, in which # case it is not a duplicate (will be replaced). album_paths = set(i.path for i in album.items()) if album_paths != task_paths: duplicates.append(album) return duplicates def align_album_level_fields(self): """Make the some album fields equal across `self.items` """ changes = {} if self.choice_flag == action.ASIS: # Taking metadata "as-is". Guess whether this album is VA. plur_albumartist, freq = util.plurality( [i.albumartist or i.artist for i in self.items] ) if freq == len(self.items) or \ (freq > 1 and float(freq) / len(self.items) >= SINGLE_ARTIST_THRESH): # Single-artist album. changes['albumartist'] = plur_albumartist changes['comp'] = False else: # VA. changes['albumartist'] = VARIOUS_ARTISTS changes['comp'] = True elif self.choice_flag == action.APPLY: # Applying autotagged metadata. Just get AA from the first # item. if not self.items[0].albumartist: changes['albumartist'] = self.items[0].artist if not self.items[0].mb_albumartistid: changes['mb_albumartistid'] = self.items[0].mb_artistid # Apply new metadata. for item in self.items: item.update(changes) def manipulate_files(self, move=False, copy=False, write=False, session=None): items = self.imported_items() # Save the original paths of all items for deletion and pruning # in the next step (finalization). self.old_paths = [item.path for item in items] for item in items: if session.config['move']: # Just move the file. item.move(False) elif session.config['copy']: # If it's a reimport, move in-library files and copy # out-of-library files. Otherwise, copy and keep track # of the old path. old_path = item.path if self.replaced_items[item]: # This is a reimport. Move in-library files and copy # out-of-library files. if session.lib.directory in util.ancestry(old_path): item.move(False) # We moved the item, so remove the # now-nonexistent file from old_paths. self.old_paths.remove(old_path) else: item.move(True) else: # A normal import. Just copy files and keep track of # old paths. item.move(True) if session.config['write'] and self.apply: item.try_write() with session.lib.transaction(): for item in self.imported_items(): item.store() plugins.send('import_task_files', session=session, task=self) def add(self, lib): """Add the items as an album to the library and remove replaced items. """ self.align_album_level_fields() with lib.transaction(): self.remove_replaced(lib) self.album = lib.add_album(self.imported_items()) def remove_replaced(self, lib): """Removes all the items from the library that have the same path as an item from this task. Records the replaced items in the `replaced_items` dictionary """ self.replaced_items = defaultdict(list) for item in self.imported_items(): dup_items = list(lib.items( dbcore.query.BytesQuery('path', item.path) )) self.replaced_items[item] = dup_items for dup_item in dup_items: log.debug(u'replacing item {0}: {1}' .format(dup_item.id, displayable_path(item.path))) dup_item.remove() log.debug(u'{0} of {1} items replaced' .format(len(self.replaced_items), len(self.imported_items()))) def choose_match(self, session): """Ask the session which match should apply and apply it. """ choice = session.choose_match(self) self.set_choice(choice) session.log_choice(self) def reload(self): """Reload albums and items from the database. """ for item in self.imported_items(): item.load() self.album.load() # Utilities. def prune(self, filename): """Prune any empty directories above the given file. If this task has no `toppath` or the file path provided is not within the `toppath`, then this function has no effect. Similarly, if the file still exists, no pruning is performed, so it's safe to call when the file in question may not have been removed. """ if self.toppath and not os.path.exists(filename): util.prune_dirs(os.path.dirname(filename), self.toppath, clutter=config['clutter'].as_str_seq()) class SingletonImportTask(ImportTask): """ImportTask for a single track that is not associated to an album. """ def __init__(self, toppath, item): super(SingletonImportTask, self).__init__(toppath, [item.path]) self.item = item self.is_album = False self.paths = [item.path] def chosen_ident(self): assert self.choice_flag in (action.ASIS, action.APPLY) if self.choice_flag is action.ASIS: return (self.item.artist, self.item.title) elif self.choice_flag is action.APPLY: return (self.match.info.artist, self.match.info.title) def imported_items(self): return [self.item] def apply_metadata(self): autotag.apply_item_metadata(self.item, self.match.info) def _emit_imported(self, lib): # FIXME This shouldn't be here. Skipped tasks should be removed from # the pipeline. if self.skip: return for item in self.imported_items(): plugins.send('item_imported', lib=lib, item=item) def lookup_candidates(self): candidates, recommendation = autotag.tag_item(self.item) self.candidates = candidates self.rec = recommendation def find_duplicates(self, lib): """Return a list of items from `lib` that have the same artist and title as the task. """ artist, title = self.chosen_ident() found_items = [] query = dbcore.AndQuery(( dbcore.MatchQuery('artist', artist), dbcore.MatchQuery('title', title), )) for other_item in lib.items(query): # Existing items not considered duplicates. if other_item.path != self.item.path: found_items.append(other_item) return found_items duplicate_items = find_duplicates def add(self, lib): with lib.transaction(): self.remove_replaced(lib) lib.add(self.item) def infer_album_fields(self): raise NotImplementedError def choose_match(self, session): """Ask the session which match should apply and apply it. """ choice = session.choose_item(self) self.set_choice(choice) session.log_choice(self) def reload(self): self.item.load() # FIXME The inheritance relationships are inverted. This is why there # are so many methods which pass. We should introduce a new # BaseImportTask class. class SentinelImportTask(ImportTask): """This class marks the progress of an import and does not import any items itself. If only `toppath` is set the task indicats the end of a top-level directory import. If the `paths` argument is givent, too, the task indicates the progress in the `toppath` import. """ def __init__(self, toppath=None, paths=None): self.toppath = toppath self.paths = paths # TODO Remove the remaining attributes eventually self.items = None self.should_remove_duplicates = False self.is_album = True self.choice_flag = None def save_history(self): pass def save_progress(self): if self.paths is None: # "Done" sentinel. progress_reset(self.toppath) else: # "Directory progress" sentinel for singletons progress_add(self.toppath, *self.paths) def skip(self): return True def set_choice(self, choice): raise NotImplementedError def cleanup(self, **kwargs): pass def _emit_imported(self, session): pass class ArchiveImportTask(SentinelImportTask): """Additional methods for handling archives. Use when `toppath` points to a `zip`, `tar`, or `rar` archive. """ def __init__(self, toppath): super(ArchiveImportTask, self).__init__(toppath) self.extracted = False @classmethod def is_archive(cls, path): """Returns true if the given path points to an archive that can be handled. """ if not os.path.isfile(path): return False for path_test, _ in cls.handlers(): if path_test(path): return True return False @classmethod def handlers(cls): """Returns a list of archive handlers. Each handler is a `(path_test, ArchiveClass)` tuple. `path_test` is a function that returns `True` if the given path can be handled by `ArchiveClass`. `ArchiveClass` is a class that implements the same interface as `tarfile.TarFile`. """ if not hasattr(cls, '_handlers'): cls._handlers = [] from zipfile import is_zipfile, ZipFile cls._handlers.append((is_zipfile, ZipFile)) from tarfile import is_tarfile, TarFile cls._handlers.append((is_tarfile, TarFile)) try: from rarfile import is_rarfile, RarFile except ImportError: pass else: cls._handlers.append((is_rarfile, RarFile)) return cls._handlers def cleanup(self, **kwargs): """Removes the temporary directory the archive was extracted to. """ if self.extracted: shutil.rmtree(self.toppath) def extract(self): """Extracts the archive to a temporary directory and sets `toppath` to that directory. """ for path_test, handler_class in self.handlers(): if path_test(self.toppath): break try: extract_to = mkdtemp() archive = handler_class(self.toppath, mode='r') archive.extractall(extract_to) finally: archive.close() self.extracted = True self.toppath = extract_to class ImportTaskFactory(object): """Create album and singleton import tasks from paths for toppaths in session. The `singleton()` and `album()` methods accept paths and return instances of `SingletonImportTask` and `ImportTask`, respectively. `None` is returned if either no media file items could be created from the paths or if the paths have already been imported. In both cases it logs messages. """ def __init__(self, toppath, session): self.toppath = toppath self.session = session self.skipped = 0 def singleton(self, path): if self.session.already_imported(self.toppath, [path]): log.debug(u'Skipping previously-imported path: {0}' .format(displayable_path(path))) self.skipped += 1 return None item = self.read_item(path) if item: return SingletonImportTask(self.toppath, item) else: return None def album(self, paths, dir=None): """Return `ImportTask` with all media files from paths. `dir` is a common parent directory of all paths. """ if not paths: return None if dir: dirs = [dir] else: dirs = list(set(os.path.dirname(p) for p in paths)) if self.session.already_imported(self.toppath, dirs): log.debug(u'Skipping previously-imported path: {0}' .format(displayable_path(dirs))) self.skipped += 1 return None items = map(self.read_item, paths) items = [item for item in items if item] if items: return ImportTask(self.toppath, dirs, items) else: return None def sentinel(self, paths=None): return SentinelImportTask(self.toppath, paths) def read_item(self, path): """Return an item created from the path. If an item could not be read it returns None and logs an error. """ # TODO remove this method. Should be handled in ImportTask creation. try: return library.Item.from_path(path) except library.ReadError as exc: if isinstance(exc.reason, mediafile.FileTypeError): # Silently ignore non-music files. pass elif isinstance(exc.reason, mediafile.UnreadableFileError): log.warn(u'unreadable file: {0}'.format( displayable_path(path)) ) else: log.error(u'error reading {0}: {1}'.format( displayable_path(path), exc, )) # Full-album pipeline stages. def read_tasks(session): """A generator yielding all the albums (as ImportTask objects) found in the user-specified list of paths. In the case of a singleton import, yields single-item tasks instead. """ skipped = 0 for toppath in session.paths: task_factory = ImportTaskFactory(toppath, session) # Determine if we want to resume import of the toppath session.ask_resume(toppath) # Extract archives. archive_task = None if ArchiveImportTask.is_archive(syspath(toppath)): if not (session.config['move'] or session.config['copy']): log.warn(u"Archive importing requires either " "'copy' or 'move' to be enabled.") continue log.debug(u'extracting archive {0}' .format(displayable_path(toppath))) archive_task = ArchiveImportTask(toppath) try: archive_task.extract() except Exception as exc: log.error(u'extraction failed: {0}'.format(exc)) continue # Continue reading albums from the extracted directory. toppath = archive_task.toppath # Check whether the path is to a file. if not os.path.isdir(syspath(toppath)): if session.config['singletons']: task = task_factory.singleton(toppath) else: task = task_factory.album([toppath], dir=toppath) if task: yield task yield task_factory.sentinel() continue # A flat album import merges all items into one album. if session.config['flat'] and not session.config['singletons']: paths = [] for _, item_paths in albums_in_dir(toppath): paths += item_paths task = task_factory.album(paths) if task: yield task yield task_factory.sentinel() continue # Produce paths under this directory. for dirs, paths in albums_in_dir(toppath): if session.config['singletons']: for path in paths: task = task_factory.singleton(path) if task: yield task yield task_factory.sentinel(dirs) else: task = task_factory.album(paths) if task: yield task # Indicate the directory is finished. # FIXME hack to delete extracted archives if archive_task is None: yield task_factory.sentinel() else: yield archive_task # Show skipped directories. if skipped: log.info(u'Skipped {0} directories.'.format(skipped)) def query_tasks(session): """A generator that works as a drop-in-replacement for read_tasks. Instead of finding files from the filesystem, a query is used to match items from the library. """ if session.config['singletons']: # Search for items. for item in session.lib.items(session.query): yield SingletonImportTask(None, item) else: # Search for albums. for album in session.lib.albums(session.query): log.debug(u'yielding album {0}: {1} - {2}' .format(album.id, album.albumartist, album.album)) items = list(album.items()) # Clear IDs from re-tagged items so they appear "fresh" when # we add them back to the library. for item in items: item.id = None item.album_id = None yield ImportTask(None, [album.item_dir()], items) @pipeline.mutator_stage def lookup_candidates(session, task): """A coroutine for performing the initial MusicBrainz lookup for an album. It accepts lists of Items and yields (items, cur_artist, cur_album, candidates, rec) tuples. If no match is found, all of the yielded parameters (except items) are None. """ if task.skip: # FIXME This gets duplicated a lot. We need a better # abstraction. return plugins.send('import_task_start', session=session, task=task) log.debug(u'Looking up: {0}'.format(displayable_path(task.paths))) task.lookup_candidates() @pipeline.stage def user_query(session, task): """A coroutine for interfacing with the user about the tagging process. The coroutine accepts an ImportTask objects. It uses the session's `choose_match` method to determine the `action` for this task. Depending on the action additional stages are exectuted and the processed task is yielded. It emits the ``import_task_choice`` event for plugins. Plugins have acces to the choice via the ``taks.choice_flag`` property and may choose to change it. """ if task.skip: return task # Ask the user for a choice. task.choose_match(session) plugins.send('import_task_choice', session=session, task=task) # As-tracks: transition to singleton workflow. if task.choice_flag is action.TRACKS: # Set up a little pipeline for dealing with the singletons. def emitter(task): for item in task.items: yield SingletonImportTask(task.toppath, item) yield SentinelImportTask(task.toppath, task.paths) ipl = pipeline.Pipeline([ emitter(task), lookup_candidates(session), user_query(session), ]) return pipeline.multiple(ipl.pull()) # As albums: group items by albums and create task for each album if task.choice_flag is action.ALBUMS: ipl = pipeline.Pipeline([ iter([task]), group_albums(session), lookup_candidates(session), user_query(session) ]) return pipeline.multiple(ipl.pull()) resolve_duplicates(session, task) return task def resolve_duplicates(session, task): """Check if a task conflicts with items or albums already imported and ask the session to resolve this. """ if task.choice_flag in (action.ASIS, action.APPLY): ident = task.chosen_ident() found_duplicates = task.find_duplicates(session.lib) if ident in session.seen_idents or found_duplicates: session.resolve_duplicate(task, found_duplicates) session.log_choice(task, True) session.seen_idents.add(ident) @pipeline.mutator_stage def import_asis(session, task): """Select the `action.ASIS` choice for all tasks. This stage replaces the initial_lookup and user_query stages when the importer is run without autotagging. """ if task.skip: return log.info(displayable_path(task.paths)) # Behave as if ASIS were selected. task.set_null_candidates() task.set_choice(action.ASIS) @pipeline.mutator_stage def apply_choices(session, task): """A coroutine for applying changes to albums and singletons during the autotag process. """ if task.skip: return # Change metadata. if task.apply: task.apply_metadata() plugins.send('import_task_apply', session=session, task=task) task.add(session.lib) @pipeline.mutator_stage def plugin_stage(session, func, task): """A coroutine (pipeline stage) that calls the given function with each non-skipped import task. These stages occur between applying metadata changes and moving/copying/writing files. """ if task.skip: return func(session, task) # Stage may modify DB, so re-load cached item data. # FIXME Importer plugins should not modify the database but instead # the albums and items attached to tasks. task.reload() @pipeline.stage def manipulate_files(session, task): """A coroutine (pipeline stage) that performs necessary file manipulations *after* items have been added to the library and finalizes each task. """ if not task.skip: if task.should_remove_duplicates: task.remove_duplicates(session.lib) task.manipulate_files( move=session.config['move'], copy=session.config['copy'], write=session.config['write'], session=session, ) # Progress, cleanup, and event. task.finalize(session) def group_albums(session): """Group the items of a task by albumartist and album name and create a new task for each album. Yield the tasks as a multi message. """ def group(item): return (item.albumartist or item.artist, item.album) task = None while True: task = yield task if task.skip: continue tasks = [] for _, items in itertools.groupby(task.items, group): tasks.append(ImportTask(items=list(items))) tasks.append(SentinelImportTask(task.toppath, task.paths)) task = pipeline.multiple(tasks) MULTIDISC_MARKERS = (r'dis[ck]', r'cd') MULTIDISC_PAT_FMT = r'^(.*%s[\W_]*)\d' def albums_in_dir(path): """Recursively searches the given directory and returns an iterable of (paths, items) where paths is a list of directories and items is a list of Items that is probably an album. Specifically, any folder containing any media files is an album. """ collapse_pat = collapse_paths = collapse_items = None ignore = config['ignore'].as_str_seq() for root, dirs, files in sorted_walk(path, ignore=ignore, logger=log): items = [os.path.join(root, f) for f in files] # If we're currently collapsing the constituent directories in a # multi-disc album, check whether we should continue collapsing # and add the current directory. If so, just add the directory # and move on to the next directory. If not, stop collapsing. if collapse_paths: if (not collapse_pat and collapse_paths[0] in ancestry(root)) or \ (collapse_pat and collapse_pat.match(os.path.basename(root))): # Still collapsing. collapse_paths.append(root) collapse_items += items continue else: # Collapse finished. Yield the collapsed directory and # proceed to process the current one. if collapse_items: yield collapse_paths, collapse_items collapse_pat = collapse_paths = collapse_items = None # Check whether this directory looks like the *first* directory # in a multi-disc sequence. There are two indicators: the file # is named like part of a multi-disc sequence (e.g., "Title Disc # 1") or it contains no items but only directories that are # named in this way. start_collapsing = False for marker in MULTIDISC_MARKERS: marker_pat = re.compile(MULTIDISC_PAT_FMT % marker, re.I) match = marker_pat.match(os.path.basename(root)) # Is this directory the root of a nested multi-disc album? if dirs and not items: # Check whether all subdirectories have the same prefix. start_collapsing = True subdir_pat = None for subdir in dirs: # The first directory dictates the pattern for # the remaining directories. if not subdir_pat: match = marker_pat.match(subdir) if match: subdir_pat = re.compile( r'^%s\d' % re.escape(match.group(1)), re.I ) else: start_collapsing = False break # Subsequent directories must match the pattern. elif not subdir_pat.match(subdir): start_collapsing = False break # If all subdirectories match, don't check other # markers. if start_collapsing: break # Is this directory the first in a flattened multi-disc album? elif match: start_collapsing = True # Set the current pattern to match directories with the same # prefix as this one, followed by a digit. collapse_pat = re.compile( r'^%s\d' % re.escape(match.group(1)), re.I ) break # If either of the above heuristics indicated that this is the # beginning of a multi-disc album, initialize the collapsed # directory and item lists and check the next directory. if start_collapsing: # Start collapsing; continue to the next iteration. collapse_paths = [root] collapse_items = items continue # If it's nonempty, yield it. if items: yield [root], items # Clear out any unfinished collapse. if collapse_paths and collapse_items: yield collapse_paths, collapse_items beets-1.3.8/beets/library.py0000644000076500000240000012345612406436341016727 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """The core data store and collection logic for beets. """ import os import sys import logging import shlex import unicodedata import time import re from unidecode import unidecode from beets.mediafile import MediaFile, MutagenError, UnreadableFileError from beets import plugins from beets import util from beets.util import bytestring_path, syspath, normpath, samefile from beets.util.functemplate import Template from beets import dbcore from beets.dbcore import types import beets log = logging.getLogger('beets') # Library-specific query types. class PathQuery(dbcore.FieldQuery): """A query that matches all items under a given path.""" def __init__(self, field, pattern, fast=True): super(PathQuery, self).__init__(field, pattern, fast) # Match the path as a single file. self.file_path = util.bytestring_path(util.normpath(pattern)) # As a directory (prefix). self.dir_path = util.bytestring_path(os.path.join(self.file_path, '')) def match(self, item): return (item.path == self.file_path) or \ item.path.startswith(self.dir_path) def clause(self): dir_pat = buffer(self.dir_path + '%') file_blob = buffer(self.file_path) return '({0} = ?) || ({0} LIKE ?)'.format(self.field), \ (file_blob, dir_pat) # Library-specific field types. class DateType(types.Float): # TODO representation should be `datetime` object # TODO distinguish beetween date and time types query = dbcore.query.DateQuery def format(self, value): return time.strftime(beets.config['time_format'].get(unicode), time.localtime(value or 0)) def parse(self, string): try: # Try a formatted date string. return time.mktime( time.strptime(string, beets.config['time_format'].get(unicode)) ) except ValueError: # Fall back to a plain timestamp number. try: return float(string) except ValueError: return self.null class PathType(types.Type): sql = u'BLOB' query = PathQuery model_type = bytes def format(self, value): return util.displayable_path(value) def parse(self, string): return normpath(bytestring_path(string)) def normalize(self, value): if isinstance(value, unicode): # Paths stored internally as encoded bytes. return bytestring_path(value) elif isinstance(value, buffer): # SQLite must store bytestings as buffers to avoid decoding. # We unwrap buffers to bytes. return bytes(value) else: return value def from_sql(self, sql_value): return self.normalize(sql_value) def to_sql(self, value): if isinstance(value, str): value = buffer(value) return value class MusicalKey(types.String): """String representing the musical key of a song. The standard format is C, Cm, C#, C#m, etc. """ ENHARMONIC = { r'db': 'c#', r'eb': 'd#', r'gb': 'f#', r'ab': 'g#', r'bb': 'a#', } def parse(self, key): key = key.lower() for flat, sharp in self.ENHARMONIC.items(): key = re.sub(flat, sharp, key) key = re.sub(r'[\W\s]+minor', 'm', key) return key.capitalize() def normalize(self, key): if key is None: return None else: return self.parse(key) # Library-specific sort types. class SmartArtistSort(dbcore.query.Sort): """Sort by artist (either album artist or track artist), prioritizing the sort field over the raw field. """ def __init__(self, model_cls, ascending=True): self.album = model_cls is Album self.ascending = ascending def order_clause(self): order = "ASC" if self.ascending else "DESC" if self.album: field = 'albumartist' else: field = 'artist' return ('(CASE {0}_sort WHEN NULL THEN {0} ' 'WHEN "" THEN {0} ' 'ELSE {0}_sort END) {1}').format(field, order) def sort(self, objs): if self.album: key = lambda a: a.albumartist_sort or a.albumartist else: key = lambda i: i.artist_sort or i.artist return sorted(objs, key=key, reverse=not self.ascending) # Special path format key. PF_KEY_DEFAULT = 'default' # Exceptions. class FileOperationError(Exception): """Indicates an error when interacting with a file on disk. Possibilities include an unsupported media type, a permissions error, and an unhandled Mutagen exception. """ def __init__(self, path, reason): """Create an exception describing an operation on the file at `path` with the underlying (chained) exception `reason`. """ super(FileOperationError, self).__init__(path, reason) self.path = path self.reason = reason def __unicode__(self): """Get a string representing the error. Describes both the underlying reason and the file path in question. """ return u'{0}: {1}'.format( util.displayable_path(self.path), unicode(self.reason) ) def __str__(self): return unicode(self).encode('utf8') class ReadError(FileOperationError): """An error while reading a file (i.e. in `Item.read`). """ def __unicode__(self): return u'error reading ' + super(ReadError, self).__unicode__() class WriteError(FileOperationError): """An error while writing a file (i.e. in `Item.write`). """ def __unicode__(self): return u'error writing ' + super(WriteError, self).__unicode__() # Item and Album model classes. class LibModel(dbcore.Model): """Shared concrete functionality for Items and Albums. """ def _template_funcs(self): funcs = DefaultTemplateFunctions(self, self._db).functions() funcs.update(plugins.template_funcs()) return funcs def store(self): super(LibModel, self).store() plugins.send('database_change', lib=self._db) def remove(self): super(LibModel, self).remove() plugins.send('database_change', lib=self._db) def add(self, lib=None): super(LibModel, self).add(lib) plugins.send('database_change', lib=self._db) class FormattedItemMapping(dbcore.db.FormattedMapping): """Add lookup for album-level fields. Album-level fields take precedence if `for_path` is true. """ def __init__(self, item, for_path=False): super(FormattedItemMapping, self).__init__(item, for_path) self.album = item.get_album() self.album_keys = [] if self.album: for key in self.album.keys(True): if key in Album.item_keys or key not in item._fields.keys(): self.album_keys.append(key) self.all_keys = set(self.model_keys).union(self.album_keys) def _get(self, key): """Get the value for a key, either from the album or the item. Raise a KeyError for invalid keys. """ if self.for_path and key in self.album_keys: return self._get_formatted(self.album, key) elif key in self.model_keys: return self._get_formatted(self.model, key) elif key in self.album_keys: return self._get_formatted(self.album, key) else: raise KeyError(key) def __getitem__(self, key): """Get the value for a key. Certain unset values are remapped. """ value = self._get(key) # `artist` and `albumartist` fields fall back to one another. # This is helpful in path formats when the album artist is unset # on as-is imports. if key == 'artist' and not value: return self._get('albumartist') elif key == 'albumartist' and not value: return self._get('artist') else: return value def __iter__(self): return iter(self.all_keys) def __len__(self): return len(self.all_keys) class Item(LibModel): _table = 'items' _flex_table = 'item_attributes' _fields = { 'id': types.PRIMARY_ID, 'path': PathType(), 'album_id': types.FOREIGN_ID, 'title': types.STRING, 'artist': types.STRING, 'artist_sort': types.STRING, 'artist_credit': types.STRING, 'album': types.STRING, 'albumartist': types.STRING, 'albumartist_sort': types.STRING, 'albumartist_credit': types.STRING, 'genre': types.STRING, 'composer': types.STRING, 'grouping': types.STRING, 'year': types.PaddedInt(4), 'month': types.PaddedInt(2), 'day': types.PaddedInt(2), 'track': types.PaddedInt(2), 'tracktotal': types.PaddedInt(2), 'disc': types.PaddedInt(2), 'disctotal': types.PaddedInt(2), 'lyrics': types.STRING, 'comments': types.STRING, 'bpm': types.INTEGER, 'comp': types.BOOLEAN, 'mb_trackid': types.STRING, 'mb_albumid': types.STRING, 'mb_artistid': types.STRING, 'mb_albumartistid': types.STRING, 'albumtype': types.STRING, 'label': types.STRING, 'acoustid_fingerprint': types.STRING, 'acoustid_id': types.STRING, 'mb_releasegroupid': types.STRING, 'asin': types.STRING, 'catalognum': types.STRING, 'script': types.STRING, 'language': types.STRING, 'country': types.STRING, 'albumstatus': types.STRING, 'media': types.STRING, 'albumdisambig': types.STRING, 'disctitle': types.STRING, 'encoder': types.STRING, 'rg_track_gain': types.NULL_FLOAT, 'rg_track_peak': types.NULL_FLOAT, 'rg_album_gain': types.NULL_FLOAT, 'rg_album_peak': types.NULL_FLOAT, 'original_year': types.PaddedInt(4), 'original_month': types.PaddedInt(2), 'original_day': types.PaddedInt(2), 'initial_key': MusicalKey(), 'length': types.FLOAT, 'bitrate': types.ScaledInt(1000, u'kbps'), 'format': types.STRING, 'samplerate': types.ScaledInt(1000, u'kHz'), 'bitdepth': types.INTEGER, 'channels': types.INTEGER, 'mtime': DateType(), 'added': DateType(), } _search_fields = ('artist', 'title', 'comments', 'album', 'albumartist', 'genre') _media_fields = set(MediaFile.readable_fields()) \ .intersection(_fields.keys()) """Set of item fields that are backed by `MediaFile` fields. Any kind of field (fixed, flexible, and computed) may be a media field. Only these fields are read from disk in `read` and written in `write`. """ _formatter = FormattedItemMapping _sorts = {'artist': SmartArtistSort} @classmethod def _getters(cls): getters = plugins.item_field_getters() getters['singleton'] = lambda i: i.album_id is None return getters @classmethod def from_path(cls, path): """Creates a new item from the media file at the specified path. """ # Initiate with values that aren't read from files. i = cls(album_id=None) i.read(path) i.mtime = i.current_mtime() # Initial mtime. return i def __setitem__(self, key, value): """Set the item's value for a standard field or a flexattr. """ # Encode unicode paths and read buffers. if key == 'path': if isinstance(value, unicode): value = bytestring_path(value) elif isinstance(value, buffer): value = str(value) if key in MediaFile.fields(): self.mtime = 0 # Reset mtime on dirty. super(Item, self).__setitem__(key, value) def update(self, values): """Set all key/value pairs in the mapping. If mtime is specified, it is not reset (as it might otherwise be). """ super(Item, self).update(values) if self.mtime == 0 and 'mtime' in values: self.mtime = values['mtime'] def get_album(self): """Get the Album object that this item belongs to, if any, or None if the item is a singleton or is not associated with a library. """ if not self._db: return None return self._db.get_album(self) # Interaction with file metadata. def read(self, read_path=None): """Read the metadata from the associated file. If `read_path` is specified, read metadata from that file instead. Updates all the properties in `_media_fields` from the media file. Raises a `ReadError` if the file could not be read. """ if read_path is None: read_path = self.path else: read_path = normpath(read_path) try: mediafile = MediaFile(syspath(read_path)) except (OSError, IOError, UnreadableFileError) as exc: raise ReadError(read_path, exc) for key in self._media_fields: value = getattr(mediafile, key) if isinstance(value, (int, long)): # Filter values wider than 64 bits (in signed representation). # SQLite cannot store them. py26: Post transition, we can use: # value.bit_length() > 63 if abs(value) >= 2 ** 63: value = 0 self[key] = value # Database's mtime should now reflect the on-disk value. if read_path == self.path: self.mtime = self.current_mtime() self.path = read_path def write(self, path=None): """Write the item's metadata to a media file. All fields in `_media_fields` are written to disk according to the values on this object. Can raise either a `ReadError` or a `WriteError`. """ if path is None: path = self.path else: path = normpath(path) tags = dict(self) plugins.send('write', item=self, path=path, tags=tags) try: mediafile = MediaFile(syspath(path), id3v23=beets.config['id3v23'].get(bool)) except (OSError, IOError, UnreadableFileError) as exc: raise ReadError(self.path, exc) mediafile.update(tags) try: mediafile.save() except (OSError, IOError, MutagenError) as exc: raise WriteError(self.path, exc) # The file has a new mtime. if path == self.path: self.mtime = self.current_mtime() plugins.send('after_write', item=self, path=path) def try_write(self, path=None): """Calls `write()` but catches and logs `FileOperationError` exceptions. Returns `False` an exception was caught and `True` otherwise. """ try: self.write(path) return True except FileOperationError as exc: log.error(exc) return False def try_sync(self, write=None): """Synchronize the item with the database and the media file tags, updating them with this object's current state. By default, the current `path` for the item is used to write tags. If `write` is `False`, no tags are written. If `write` is a path, tags are written to that file instead. Similar to calling :meth:`write` and :meth:`store`. """ if write is True: write = None if write is not False: self.try_write(path=write) self.store() # Files themselves. def move_file(self, dest, copy=False): """Moves or copies the item's file, updating the path value if the move succeeds. If a file exists at ``dest``, then it is slightly modified to be unique. """ if not util.samefile(self.path, dest): dest = util.unique_path(dest) if copy: util.copy(self.path, dest) plugins.send("item_copied", item=self, source=self.path, destination=dest) else: plugins.send("before_item_moved", item=self, source=self.path, destination=dest) util.move(self.path, dest) plugins.send("item_moved", item=self, source=self.path, destination=dest) # Either copying or moving succeeded, so update the stored path. self.path = dest def current_mtime(self): """Returns the current mtime of the file, rounded to the nearest integer. """ return int(os.path.getmtime(syspath(self.path))) # Model methods. def remove(self, delete=False, with_album=True): """Removes the item. If `delete`, then the associated file is removed from disk. If `with_album`, then the item's album (if any) is removed if it the item was the last in the album. """ super(Item, self).remove() # Remove the album if it is empty. if with_album: album = self.get_album() if album and not album.items(): album.remove(delete, False) # Send a 'item_removed' signal to plugins plugins.send('item_removed', item=self) # Delete the associated file. if delete: util.remove(self.path) util.prune_dirs(os.path.dirname(self.path), self._db.directory) self._db._memotable = {} def move(self, copy=False, basedir=None, with_album=True): """Move the item to its designated location within the library directory (provided by destination()). Subdirectories are created as needed. If the operation succeeds, the item's path field is updated to reflect the new location. If copy is True, moving the file is copied rather than moved. basedir overrides the library base directory for the destination. If the item is in an album, the album is given an opportunity to move its art. (This can be disabled by passing with_album=False.) The item is stored to the database if it is in the database, so any dirty fields prior to the move() call will be written as a side effect. You probably want to call save() to commit the DB transaction. """ self._check_db() dest = self.destination(basedir=basedir) # Create necessary ancestry for the move. util.mkdirall(dest) # Perform the move and store the change. old_path = self.path self.move_file(dest, copy) self.store() # If this item is in an album, move its art. if with_album: album = self.get_album() if album: album.move_art(copy) album.store() # Prune vacated directory. if not copy: util.prune_dirs(os.path.dirname(old_path), self._db.directory) # Templating. def destination(self, fragment=False, basedir=None, platform=None, path_formats=None): """Returns the path in the library directory designated for the item (i.e., where the file ought to be). fragment makes this method return just the path fragment underneath the root library directory; the path is also returned as Unicode instead of encoded as a bytestring. basedir can override the library's base directory for the destination. """ self._check_db() platform = platform or sys.platform basedir = basedir or self._db.directory path_formats = path_formats or self._db.path_formats # Use a path format based on a query, falling back on the # default. for query, path_format in path_formats: if query == PF_KEY_DEFAULT: continue query, _ = parse_query_string(query, type(self)) if query.match(self): # The query matches the item! Use the corresponding path # format. break else: # No query matched; fall back to default. for query, path_format in path_formats: if query == PF_KEY_DEFAULT: break else: assert False, "no default path format" if isinstance(path_format, Template): subpath_tmpl = path_format else: subpath_tmpl = Template(path_format) # Evaluate the selected template. subpath = self.evaluate_template(subpath_tmpl, True) # Prepare path for output: normalize Unicode characters. if platform == 'darwin': subpath = unicodedata.normalize('NFD', subpath) else: subpath = unicodedata.normalize('NFC', subpath) if beets.config['asciify_paths']: subpath = unidecode(subpath) # Truncate components and remove forbidden characters. subpath = util.sanitize_path(subpath, self._db.replacements) # Encode for the filesystem. if not fragment: subpath = bytestring_path(subpath) # Preserve extension. _, extension = os.path.splitext(self.path) if fragment: # Outputting Unicode. extension = extension.decode('utf8', 'ignore') subpath += extension.lower() # Truncate too-long components. maxlen = beets.config['max_filename_length'].get(int) if not maxlen: # When zero, try to determine from filesystem. maxlen = util.max_filename_length(self._db.directory) subpath = util.truncate_path(subpath, maxlen) if fragment: return subpath else: return normpath(os.path.join(basedir, subpath)) class Album(LibModel): """Provides access to information about albums stored in a library. Reflects the library's "albums" table, including album art. """ _table = 'albums' _flex_table = 'album_attributes' _fields = { 'id': types.PRIMARY_ID, 'artpath': PathType(), 'added': DateType(), 'albumartist': types.STRING, 'albumartist_sort': types.STRING, 'albumartist_credit': types.STRING, 'album': types.STRING, 'genre': types.STRING, 'year': types.PaddedInt(4), 'month': types.PaddedInt(2), 'day': types.PaddedInt(2), 'tracktotal': types.PaddedInt(2), 'disctotal': types.PaddedInt(2), 'comp': types.BOOLEAN, 'mb_albumid': types.STRING, 'mb_albumartistid': types.STRING, 'albumtype': types.STRING, 'label': types.STRING, 'mb_releasegroupid': types.STRING, 'asin': types.STRING, 'catalognum': types.STRING, 'script': types.STRING, 'language': types.STRING, 'country': types.STRING, 'albumstatus': types.STRING, 'media': types.STRING, 'albumdisambig': types.STRING, 'rg_album_gain': types.NULL_FLOAT, 'rg_album_peak': types.NULL_FLOAT, 'original_year': types.PaddedInt(4), 'original_month': types.PaddedInt(2), 'original_day': types.PaddedInt(2), } _search_fields = ('album', 'albumartist', 'genre') _sorts = { 'albumartist': SmartArtistSort, 'artist': SmartArtistSort, } item_keys = [ 'added', 'albumartist', 'albumartist_sort', 'albumartist_credit', 'album', 'genre', 'year', 'month', 'day', 'tracktotal', 'disctotal', 'comp', 'mb_albumid', 'mb_albumartistid', 'albumtype', 'label', 'mb_releasegroupid', 'asin', 'catalognum', 'script', 'language', 'country', 'albumstatus', 'media', 'albumdisambig', 'rg_album_gain', 'rg_album_peak', 'original_year', 'original_month', 'original_day', ] """List of keys that are set on an album's items. """ @classmethod def _getters(cls): # In addition to plugin-provided computed fields, also expose # the album's directory as `path`. getters = plugins.album_field_getters() getters['path'] = Album.item_dir return getters def items(self): """Returns an iterable over the items associated with this album. """ return self._db.items(dbcore.MatchQuery('album_id', self.id)) def remove(self, delete=False, with_items=True): """Removes this album and all its associated items from the library. If delete, then the items' files are also deleted from disk, along with any album art. The directories containing the album are also removed (recursively) if empty. Set with_items to False to avoid removing the album's items. """ super(Album, self).remove() # Delete art file. if delete: artpath = self.artpath if artpath: util.remove(artpath) # Remove (and possibly delete) the constituent items. if with_items: for item in self.items(): item.remove(delete, False) def move_art(self, copy=False): """Move or copy any existing album art so that it remains in the same directory as the items. """ old_art = self.artpath if not old_art: return new_art = self.art_destination(old_art) if new_art == old_art: return new_art = util.unique_path(new_art) log.debug(u'moving album art {0} to {1}' .format(util.displayable_path(old_art), util.displayable_path(new_art))) if copy: util.copy(old_art, new_art) else: util.move(old_art, new_art) self.artpath = new_art # Prune old path when moving. if not copy: util.prune_dirs(os.path.dirname(old_art), self._db.directory) def move(self, copy=False, basedir=None): """Moves (or copies) all items to their destination. Any album art moves along with them. basedir overrides the library base directory for the destination. The album is stored to the database, persisting any modifications to its metadata. """ basedir = basedir or self._db.directory # Ensure new metadata is available to items for destination # computation. self.store() # Move items. items = list(self.items()) for item in items: item.move(copy, basedir=basedir, with_album=False) # Move art. self.move_art(copy) self.store() def item_dir(self): """Returns the directory containing the album's first item, provided that such an item exists. """ item = self.items().get() if not item: raise ValueError('empty album') return os.path.dirname(item.path) def art_destination(self, image, item_dir=None): """Returns a path to the destination for the album art image for the album. `image` is the path of the image that will be moved there (used for its extension). The path construction uses the existing path of the album's items, so the album must contain at least one item or item_dir must be provided. """ image = bytestring_path(image) item_dir = item_dir or self.item_dir() filename_tmpl = Template(beets.config['art_filename'].get(unicode)) subpath = self.evaluate_template(filename_tmpl, True) if beets.config['asciify_paths']: subpath = unidecode(subpath) subpath = util.sanitize_path(subpath, replacements=self._db.replacements) subpath = bytestring_path(subpath) _, ext = os.path.splitext(image) dest = os.path.join(item_dir, subpath + ext) return bytestring_path(dest) def set_art(self, path, copy=True): """Sets the album's cover art to the image at the given path. The image is copied (or moved) into place, replacing any existing art. """ path = bytestring_path(path) oldart = self.artpath artdest = self.art_destination(path) if oldart and samefile(path, oldart): # Art already set. return elif samefile(path, artdest): # Art already in place. self.artpath = path return # Normal operation. if oldart == artdest: util.remove(oldart) artdest = util.unique_path(artdest) if copy: util.copy(path, artdest) else: util.move(path, artdest) self.artpath = artdest def store(self): """Update the database with the album information. The album's tracks are also updated. """ # Get modified track fields. track_updates = {} for key in self.item_keys: if key in self._dirty: track_updates[key] = self[key] with self._db.transaction(): super(Album, self).store() if track_updates: for item in self.items(): for key, value in track_updates.items(): item[key] = value item.store() def try_sync(self, write=True): """Synchronize the album and its items with the database and their files by updating them with this object's current state. `write` indicates whether to write tags to the item files. """ self.store() for item in self.items(): item.try_sync(bool(write)) # Query construction helpers. def parse_query_parts(parts, model_cls): """Given a beets query string as a list of components, return the `Query` and `Sort` they represent. Like `dbcore.parse_sorted_query`, with beets query prefixes and special path query detection. """ # Get query types and their prefix characters. prefixes = {':': dbcore.query.RegexpQuery} prefixes.update(plugins.queries()) # Special-case path-like queries, which are non-field queries # containing path separators (/). if 'path' in model_cls._fields: path_parts = [] non_path_parts = [] for s in parts: if s.find(os.sep, 0, s.find(':')) != -1: # Separator precedes colon. path_parts.append(s) else: non_path_parts.append(s) else: path_parts = () non_path_parts = parts query, sort = dbcore.parse_sorted_query( model_cls, non_path_parts, prefixes ) # Add path queries to aggregate query. if path_parts: query.subqueries += [PathQuery('path', s) for s in path_parts] return query, sort def parse_query_string(s, model_cls): """Given a beets query string, return the `Query` and `Sort` they represent. The string is split into components using shell-like syntax. """ # A bug in Python < 2.7.3 prevents correct shlex splitting of # Unicode strings. # http://bugs.python.org/issue6988 if isinstance(s, unicode): s = s.encode('utf8') parts = [p.decode('utf8') for p in shlex.split(s)] return parse_query_parts(parts, model_cls) # The Library: interface to the database. class Library(dbcore.Database): """A database of music containing songs and albums. """ _models = (Item, Album) def __init__(self, path='library.blb', directory='~/Music', path_formats=((PF_KEY_DEFAULT, '$artist/$album/$track $title'),), replacements=None): if path != ':memory:': self.path = bytestring_path(normpath(path)) super(Library, self).__init__(path) self.directory = bytestring_path(normpath(directory)) self.path_formats = path_formats self.replacements = replacements self._memotable = {} # Used for template substitution performance. # Adding objects to the database. def add(self, obj): """Add the :class:`Item` or :class:`Album` object to the library database. Return the object's new id. """ obj.add(self) self._memotable = {} return obj.id def add_album(self, items): """Create a new album consisting of a list of items. The items are added to the database if they don't yet have an ID. Return a new :class:`Album` object. The list items must not be empty. """ if not items: raise ValueError(u'need at least one item') # Create the album structure using metadata from the first item. values = dict((key, items[0][key]) for key in Album.item_keys) album = Album(self, **values) # Add the album structure and set the items' album_id fields. # Store or add the items. with self.transaction(): album.add(self) for item in items: item.album_id = album.id if item.id is None: item.add(self) else: item.store() return album # Querying. def _fetch(self, model_cls, query, sort=None): """Parse a query and fetch. If a order specification is present in the query string the `sort` argument is ignored. """ # Parse the query, if necessary. parsed_sort = None if isinstance(query, basestring): query, parsed_sort = parse_query_string(query, model_cls) elif isinstance(query, (list, tuple)): query, parsed_sort = parse_query_parts(query, model_cls) # Any non-null sort specified by the parsed query overrides the # provided sort. if parsed_sort and not isinstance(parsed_sort, dbcore.query.NullSort): sort = parsed_sort return super(Library, self)._fetch( model_cls, query, sort ) def albums(self, query=None, sort=None): """Get :class:`Album` objects matching the query. """ sort = sort or dbcore.sort_from_strings( Album, beets.config['sort_album'].as_str_seq() ) return self._fetch(Album, query, sort) def items(self, query=None, sort=None): """Get :class:`Item` objects matching the query. """ sort = sort or dbcore.sort_from_strings( Item, beets.config['sort_item'].as_str_seq() ) return self._fetch(Item, query, sort) # Convenience accessors. def get_item(self, id): """Fetch an :class:`Item` by its ID. Returns `None` if no match is found. """ return self._get(Item, id) def get_album(self, item_or_id): """Given an album ID or an item associated with an album, return an :class:`Album` object for the album. If no such album exists, returns `None`. """ if isinstance(item_or_id, int): album_id = item_or_id else: album_id = item_or_id.album_id if album_id is None: return None return self._get(Album, album_id) # Default path template resources. def _int_arg(s): """Convert a string argument to an integer for use in a template function. May raise a ValueError. """ return int(s.strip()) class DefaultTemplateFunctions(object): """A container class for the default functions provided to path templates. These functions are contained in an object to provide additional context to the functions -- specifically, the Item being evaluated. """ _prefix = 'tmpl_' def __init__(self, item=None, lib=None): """Paramaterize the functions. If `item` or `lib` is None, then some functions (namely, ``aunique``) will always evaluate to the empty string. """ self.item = item self.lib = lib def functions(self): """Returns a dictionary containing the functions defined in this object. The keys are function names (as exposed in templates) and the values are Python functions. """ out = {} for key in self._func_names: out[key[len(self._prefix):]] = getattr(self, key) return out @staticmethod def tmpl_lower(s): """Convert a string to lower case.""" return s.lower() @staticmethod def tmpl_upper(s): """Covert a string to upper case.""" return s.upper() @staticmethod def tmpl_title(s): """Convert a string to title case.""" return s.title() @staticmethod def tmpl_left(s, chars): """Get the leftmost characters of a string.""" return s[0:_int_arg(chars)] @staticmethod def tmpl_right(s, chars): """Get the rightmost characters of a string.""" return s[-_int_arg(chars):] @staticmethod def tmpl_if(condition, trueval, falseval=u''): """If ``condition`` is nonempty and nonzero, emit ``trueval``; otherwise, emit ``falseval`` (if provided). """ try: int_condition = _int_arg(condition) except ValueError: if condition.lower() == "false": return falseval else: condition = int_condition if condition: return trueval else: return falseval @staticmethod def tmpl_asciify(s): """Translate non-ASCII characters to their ASCII equivalents. """ return unidecode(s) @staticmethod def tmpl_time(s, format): """Format a time value using `strftime`. """ cur_fmt = beets.config['time_format'].get(unicode) return time.strftime(format, time.strptime(s, cur_fmt)) def tmpl_aunique(self, keys=None, disam=None): """Generate a string that is guaranteed to be unique among all albums in the library who share the same set of keys. A fields from "disam" is used in the string if one is sufficient to disambiguate the albums. Otherwise, a fallback opaque value is used. Both "keys" and "disam" should be given as whitespace-separated lists of field names. """ # Fast paths: no album, no item or library, or memoized value. if not self.item or not self.lib: return u'' if self.item.album_id is None: return u'' memokey = ('aunique', keys, disam, self.item.album_id) memoval = self.lib._memotable.get(memokey) if memoval is not None: return memoval keys = keys or 'albumartist album' disam = disam or 'albumtype year label catalognum albumdisambig' keys = keys.split() disam = disam.split() album = self.lib.get_album(self.item) if not album: # Do nothing for singletons. self.lib._memotable[memokey] = u'' return u'' # Find matching albums to disambiguate with. subqueries = [] for key in keys: value = getattr(album, key) subqueries.append(dbcore.MatchQuery(key, value)) albums = self.lib.albums(dbcore.AndQuery(subqueries)) # If there's only one album to matching these details, then do # nothing. if len(albums) == 1: self.lib._memotable[memokey] = u'' return u'' # Find the first disambiguator that distinguishes the albums. for disambiguator in disam: # Get the value for each album for the current field. disam_values = set([getattr(a, disambiguator) for a in albums]) # If the set of unique values is equal to the number of # albums in the disambiguation set, we're done -- this is # sufficient disambiguation. if len(disam_values) == len(albums): break else: # No disambiguator distinguished all fields. res = u' {0}'.format(album.id) self.lib._memotable[memokey] = res return res # Flatten disambiguation value into a string. disam_value = album.formatted(True).get(disambiguator) res = u' [{0}]'.format(disam_value) self.lib._memotable[memokey] = res return res # Get the name of tmpl_* functions in the above class. DefaultTemplateFunctions._func_names = \ [s for s in dir(DefaultTemplateFunctions) if s.startswith(DefaultTemplateFunctions._prefix)] beets-1.3.8/beets/mediafile.py0000644000076500000240000017136112405373453017203 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Handles low-level interfacing for files' tags. Wraps Mutagen to automatically detect file types and provide a unified interface for a useful subset of music files' tags. Usage: >>> f = MediaFile('Lucy.mp3') >>> f.title u'Lucy in the Sky with Diamonds' >>> f.artist = 'The Beatles' >>> f.save() A field will always return a reasonable value of the correct type, even if no tag is present. If no value is available, the value will be false (e.g., zero or the empty string). Internally ``MediaFile`` uses ``MediaField`` descriptors to access the data from the tags. In turn ``MediaField`` uses a number of ``StorageStyle`` strategies to handle format specific logic. """ import mutagen import mutagen.mp3 import mutagen.oggopus import mutagen.oggvorbis import mutagen.mp4 import mutagen.flac import mutagen.monkeysaudio import mutagen.asf import mutagen.aiff import datetime import re import base64 import math import struct import imghdr import os import logging import traceback import enum from beets.util import displayable_path __all__ = ['UnreadableFileError', 'FileTypeError', 'MediaFile'] log = logging.getLogger('beets') # Human-readable type names. TYPES = { 'mp3': 'MP3', 'aac': 'AAC', 'alac': 'ALAC', 'ogg': 'OGG', 'opus': 'Opus', 'flac': 'FLAC', 'ape': 'APE', 'wv': 'WavPack', 'mpc': 'Musepack', 'asf': 'Windows Media', 'aiff': 'AIFF', } # Exceptions. class UnreadableFileError(Exception): """Mutagen is not able to extract information from the file. """ def __init__(self, path): Exception.__init__(self, displayable_path(path)) class FileTypeError(UnreadableFileError): """Reading this type of file is not supported. If passed the `mutagen_type` argument this indicates that the mutagen type is not supported by `Mediafile`. """ def __init__(self, path, mutagen_type=None): path = displayable_path(path) if mutagen_type is None: msg = path else: msg = u'{0}: of mutagen type {1}'.format(path, mutagen_type) Exception.__init__(self, msg) class MutagenError(UnreadableFileError): """Raised when Mutagen fails unexpectedly---probably due to a bug. """ def __init__(self, path, mutagen_exc): msg = u'{0}: {1}'.format(displayable_path(path), mutagen_exc) Exception.__init__(self, msg) # Utility. def _safe_cast(out_type, val): """Try to covert val to out_type but never raise an exception. If the value can't be converted, then a sensible default value is returned. out_type should be bool, int, or unicode; otherwise, the value is just passed through. """ if val is None: return None if out_type == int: if isinstance(val, int) or isinstance(val, float): # Just a number. return int(val) else: # Process any other type as a string. if not isinstance(val, basestring): val = unicode(val) # Get a number from the front of the string. val = re.match(r'[0-9]*', val.strip()).group(0) if not val: return 0 else: return int(val) elif out_type == bool: try: # Should work for strings, bools, ints: return bool(int(val)) except ValueError: return False elif out_type == unicode: if isinstance(val, str): return val.decode('utf8', 'ignore') elif isinstance(val, unicode): return val else: return unicode(val) elif out_type == float: if isinstance(val, int) or isinstance(val, float): return float(val) else: if not isinstance(val, basestring): val = unicode(val) match = re.match(r'[\+-]?[0-9\.]+', val.strip()) if match: val = match.group(0) if val: return float(val) return 0.0 else: return val # Image coding for ASF/WMA. def _unpack_asf_image(data): """Unpack image data from a WM/Picture tag. Return a tuple containing the MIME type, the raw image data, a type indicator, and the image's description. This function is treated as "untrusted" and could throw all manner of exceptions (out-of-bounds, etc.). We should clean this up sometime so that the failure modes are well-defined. """ type, size = struct.unpack_from(" 0: gain = math.log10(maxgain / 1000.0) * -10 else: # Invalid gain value found. gain = 0.0 # SoundCheck stores peak values as the actual value of the sample, # and again separately for the left and right channels. We need to # convert this to a percentage of full scale, which is 32768 for a # 16 bit sample. Once again, we play it safe by using the larger of # the two values. peak = max(soundcheck[6:8]) / 32768.0 return round(gain, 2), round(peak, 6) def _sc_encode(gain, peak): """Encode ReplayGain gain/peak values as a Sound Check string. """ # SoundCheck stores the peak value as the actual value of the # sample, rather than the percentage of full scale that RG uses, so # we do a simple conversion assuming 16 bit samples. peak *= 32768.0 # SoundCheck stores absolute RMS values in some unknown units rather # than the dB values RG uses. We can calculate these absolute values # from the gain ratio using a reference value of 1000 units. We also # enforce the maximum value here, which is equivalent to about # -18.2dB. g1 = min(round((10 ** (gain / -10)) * 1000), 65534) # Same as above, except our reference level is 2500 units. g2 = min(round((10 ** (gain / -10)) * 2500), 65534) # The purpose of these values are unknown, but they also seem to be # unused so we just use zero. uk = 0 values = (g1, g1, g2, g2, uk, uk, peak, peak, uk, uk) return (u' %08X' * 10) % values # Cover art and other images. def _image_mime_type(data): """Return the MIME type of the image data (a bytestring). """ kind = imghdr.what(None, h=data) if kind in ['gif', 'jpeg', 'png', 'tiff', 'bmp']: return 'image/{0}'.format(kind) elif kind == 'pgm': return 'image/x-portable-graymap' elif kind == 'pbm': return 'image/x-portable-bitmap' elif kind == 'ppm': return 'image/x-portable-pixmap' elif kind == 'xbm': return 'image/x-xbitmap' else: return 'image/x-{0}'.format(kind) class ImageType(enum.Enum): """Indicates the kind of an `Image` stored in a file's tag. """ other = 0 icon = 1 other_icon = 2 front = 3 back = 4 leaflet = 5 media = 6 lead_artist = 7 artist = 8 conductor = 9 group = 10 composer = 11 lyricist = 12 recording_location = 13 recording_session = 14 performance = 15 screen_capture = 16 fish = 17 illustration = 18 artist_logo = 19 publisher_logo = 20 class Image(object): """Strucuture representing image data and metadata that can be stored and retrieved from tags. The structure has four properties. * ``data`` The binary data of the image * ``desc`` An optional descritpion of the image * ``type`` An instance of `ImageType` indicating the kind of image * ``mime_type`` Read-only property that contains the mime type of the binary data """ def __init__(self, data, desc=None, type=None): self.data = data self.desc = desc if isinstance(type, int): type = list(ImageType)[type] self.type = type @property def mime_type(self): if self.data: return _image_mime_type(self.data) @property def type_index(self): if self.type is None: # This method is used when a tag format requires the type # index to be set, so we return "other" as the default value. return 0 return self.type.value # StorageStyle classes describe strategies for accessing values in # Mutagen file objects. class StorageStyle(object): """A strategy for storing a value for a certain tag format (or set of tag formats). This basic StorageStyle describes simple 1:1 mapping from raw values to keys in a Mutagen file object; subclasses describe more sophisticated translations or format-specific access strategies. MediaFile uses a StorageStyle via three methods: ``get()``, ``set()``, and ``delete()``. It passes a Mutagen file object to each. Internally, the StorageStyle implements ``get()`` and ``set()`` using two steps that may be overridden by subtypes. To get a value, the StorageStyle first calls ``fetch()`` to retrieve the value corresponding to a key and then ``deserialize()`` to convert the raw Mutagen value to a consumable Python value. Similarly, to set a field, we call ``serialize()`` to encode the value and then ``store()`` to assign the result into the Mutagen object. Each StorageStyle type has a class-level `formats` attribute that is a list of strings indicating the formats that the style applies to. MediaFile only uses StorageStyles that apply to the correct type for a given audio file. """ formats = ['FLAC', 'OggOpus', 'OggTheora', 'OggSpeex', 'OggVorbis', 'OggFlac', 'APEv2File', 'WavPack', 'Musepack', 'MonkeysAudio'] """List of mutagen classes the StorageStyle can handle. """ def __init__(self, key, as_type=unicode, suffix=None, float_places=2): """Create a basic storage strategy. Parameters: - `key`: The key on the Mutagen file object used to access the field's data. - `as_type`: The Python type that the value is stored as internally (`unicode`, `int`, `bool`, or `bytes`). - `suffix`: When `as_type` is a string type, append this before storing the value. - `float_places`: When the value is a floating-point number and encoded as a string, the number of digits to store after the decimal point. """ self.key = key self.as_type = as_type self.suffix = suffix self.float_places = float_places # Convert suffix to correct string type. if self.suffix and self.as_type is unicode: self.suffix = self.as_type(self.suffix) # Getter. def get(self, mutagen_file): """Get the value for the field using this style. """ return self.deserialize(self.fetch(mutagen_file)) def fetch(self, mutagen_file): """Retrieve the raw value of for this tag from the Mutagen file object. """ try: return mutagen_file[self.key][0] except (KeyError, IndexError): return None def deserialize(self, mutagen_value): """Given a raw value stored on a Mutagen object, decode and return the represented value. """ if self.suffix and isinstance(mutagen_value, unicode) \ and mutagen_value.endswith(self.suffix): return mutagen_value[:-len(self.suffix)] else: return mutagen_value # Setter. def set(self, mutagen_file, value): """Assign the value for the field using this style. """ self.store(mutagen_file, self.serialize(value)) def store(self, mutagen_file, value): """Store a serialized value in the Mutagen file object. """ mutagen_file[self.key] = [value] def serialize(self, value): """Convert the external Python value to a type that is suitable for storing in a Mutagen file object. """ if isinstance(value, float) and self.as_type is unicode: value = u'{0:.{1}f}'.format(value, self.float_places) value = self.as_type(value) elif self.as_type is unicode: if isinstance(value, bool): # Store bools as 1/0 instead of True/False. value = unicode(int(bool(value))) elif isinstance(value, str): value = value.decode('utf8', 'ignore') else: value = unicode(value) else: value = self.as_type(value) if self.suffix: value += self.suffix return value def delete(self, mutagen_file): """Remove the tag from the file. """ if self.key in mutagen_file: del mutagen_file[self.key] class ListStorageStyle(StorageStyle): """Abstract storage style that provides access to lists. The ListMediaField descriptor uses a ListStorageStyle via two methods: ``get_list()`` and ``set_list()``. It passes a Mutagen file object to each. Subclasses may overwrite ``fetch`` and ``store``. ``fetch`` must return a (possibly empty) list and ``store`` receives a serialized list of values as the second argument. The `serialize` and `deserialize` methods (from the base `StorageStyle`) are still called with individual values. This class handles packing and unpacking the values into lists. """ def get(self, mutagen_file): """Get the first value in the field's value list. """ try: return self.get_list(mutagen_file)[0] except IndexError: return None def get_list(self, mutagen_file): """Get a list of all values for the field using this style. """ return [self.deserialize(item) for item in self.fetch(mutagen_file)] def fetch(self, mutagen_file): """Get the list of raw (serialized) values. """ try: return mutagen_file[self.key] except KeyError: return [] def set(self, mutagen_file, value): """Set an individual value as the only value for the field using this style. """ self.set_list(mutagen_file, [value]) def set_list(self, mutagen_file, values): """Set all values for the field using this style. `values` should be an iterable. """ self.store(mutagen_file, [self.serialize(value) for value in values]) def store(self, mutagen_file, values): """Set the list of all raw (serialized) values for this field. """ mutagen_file[self.key] = values class SoundCheckStorageStyleMixin(object): """A mixin for storage styles that read and write iTunes SoundCheck analysis values. The object must have an `index` field that indicates which half of the gain/peak pair---0 or 1---the field represents. """ def get(self, mutagen_file): data = self.fetch(mutagen_file) if data is not None: return _sc_decode(data)[self.index] def set(self, mutagen_file, value): data = self.fetch(mutagen_file) if data is None: gain_peak = [0, 0] else: gain_peak = list(_sc_decode(data)) gain_peak[self.index] = value or 0 data = self.serialize(_sc_encode(*gain_peak)) self.store(mutagen_file, data) class ASFStorageStyle(ListStorageStyle): """A general storage style for Windows Media/ASF files. """ formats = ['ASF'] def deserialize(self, data): if isinstance(data, mutagen.asf.ASFBaseAttribute): data = data.value return data class MP4StorageStyle(StorageStyle): """A general storage style for MPEG-4 tags. """ formats = ['MP4'] def serialize(self, value): value = super(MP4StorageStyle, self).serialize(value) if self.key.startswith('----:') and isinstance(value, unicode): value = value.encode('utf8') return value class MP4TupleStorageStyle(MP4StorageStyle): """A style for storing values as part of a pair of numbers in an MPEG-4 file. """ def __init__(self, key, index=0, **kwargs): super(MP4TupleStorageStyle, self).__init__(key, **kwargs) self.index = index def deserialize(self, mutagen_value): items = mutagen_value or [] packing_length = 2 return list(items) + [0] * (packing_length - len(items)) def get(self, mutagen_file): value = super(MP4TupleStorageStyle, self).get(mutagen_file)[self.index] if value == 0: # The values are always present and saved as integers. So we # assume that "0" indicates it is not set. return None else: return value def set(self, mutagen_file, value): if value is None: value = 0 items = self.deserialize(self.fetch(mutagen_file)) items[self.index] = int(value) self.store(mutagen_file, items) def delete(self, mutagen_file): if self.index == 0: super(MP4TupleStorageStyle, self).delete(mutagen_file) else: self.set(mutagen_file, None) class MP4ListStorageStyle(ListStorageStyle, MP4StorageStyle): pass class MP4SoundCheckStorageStyle(SoundCheckStorageStyleMixin, MP4StorageStyle): def __init__(self, key, index=0, **kwargs): super(MP4SoundCheckStorageStyle, self).__init__(key, **kwargs) self.index = index class MP4BoolStorageStyle(MP4StorageStyle): """A style for booleans in MPEG-4 files. (MPEG-4 has an atom type specifically for representing booleans.) """ def get(self, mutagen_file): try: return mutagen_file[self.key] except KeyError: return None def get_list(self, mutagen_file): raise NotImplementedError('MP4 bool storage does not support lists') def set(self, mutagen_file, value): mutagen_file[self.key] = value def set_list(self, mutagen_file, values): raise NotImplementedError('MP4 bool storage does not support lists') class MP4ImageStorageStyle(MP4ListStorageStyle): """Store images as MPEG-4 image atoms. Values are `Image` objects. """ def __init__(self, **kwargs): super(MP4ImageStorageStyle, self).__init__(key='covr', **kwargs) def deserialize(self, data): return Image(data) def serialize(self, image): if image.mime_type == 'image/png': kind = mutagen.mp4.MP4Cover.FORMAT_PNG elif image.mime_type == 'image/jpeg': kind = mutagen.mp4.MP4Cover.FORMAT_JPEG else: raise ValueError('MP4 files only supports PNG and JPEG images') return mutagen.mp4.MP4Cover(image.data, kind) class MP3StorageStyle(StorageStyle): """Store data in ID3 frames. """ formats = ['MP3', 'AIFF'] def __init__(self, key, id3_lang=None, **kwargs): """Create a new ID3 storage style. `id3_lang` is the value for the language field of newly created frames. """ self.id3_lang = id3_lang super(MP3StorageStyle, self).__init__(key, **kwargs) def fetch(self, mutagen_file): try: return mutagen_file[self.key].text[0] except (KeyError, IndexError): return None def store(self, mutagen_file, value): frame = mutagen.id3.Frames[self.key](encoding=3, text=[value]) mutagen_file.tags.setall(self.key, [frame]) class MP3ListStorageStyle(ListStorageStyle, MP3StorageStyle): """Store lists of data in multiple ID3 frames. """ def fetch(self, mutagen_file): try: return mutagen_file[self.key].text except KeyError: return [] def store(self, mutagen_file, values): frame = mutagen.id3.Frames[self.key](encoding=3, text=values) mutagen_file.tags.setall(self.key, [frame]) class MP3UFIDStorageStyle(MP3StorageStyle): """Store data in a UFID ID3 frame with a particular owner. """ def __init__(self, owner, **kwargs): self.owner = owner super(MP3UFIDStorageStyle, self).__init__('UFID:' + owner, **kwargs) def fetch(self, mutagen_file): try: return mutagen_file[self.key].data except KeyError: return None def store(self, mutagen_file, value): frames = mutagen_file.tags.getall(self.key) for frame in frames: # Replace existing frame data. if frame.owner == self.owner: frame.data = value else: # New frame. frame = mutagen.id3.UFID(owner=self.owner, data=value) mutagen_file.tags.setall(self.key, [frame]) class MP3DescStorageStyle(MP3StorageStyle): """Store data in a TXXX (or similar) ID3 frame. The frame is selected based its ``desc`` field. """ def __init__(self, desc=u'', key='TXXX', **kwargs): self.description = desc super(MP3DescStorageStyle, self).__init__(key=key, **kwargs) def store(self, mutagen_file, value): frames = mutagen_file.tags.getall(self.key) if self.key != 'USLT': value = [value] # try modifying in place found = False for frame in frames: if frame.desc.lower() == self.description.lower(): frame.text = value found = True # need to make a new frame? if not found: frame = mutagen.id3.Frames[self.key]( desc=str(self.description), text=value, encoding=3 ) if self.id3_lang: frame.lang = self.id3_lang mutagen_file.tags.add(frame) def fetch(self, mutagen_file): for frame in mutagen_file.tags.getall(self.key): if frame.desc.lower() == self.description.lower(): if self.key == 'USLT': return frame.text try: return frame.text[0] except IndexError: return None def delete(self, mutagen_file): found_frame = None for frame in mutagen_file.tags.getall(self.key): if frame.desc.lower() == self.description.lower(): found_frame = frame break if found_frame is not None: del mutagen_file[frame.HashKey] class MP3SlashPackStorageStyle(MP3StorageStyle): """Store value as part of pair that is serialized as a slash- separated string. """ def __init__(self, key, pack_pos=0, **kwargs): super(MP3SlashPackStorageStyle, self).__init__(key, **kwargs) self.pack_pos = pack_pos def _fetch_unpacked(self, mutagen_file): data = self.fetch(mutagen_file) if data: items = unicode(data).split('/') else: items = [] packing_length = 2 return list(items) + [None] * (packing_length - len(items)) def get(self, mutagen_file): return self._fetch_unpacked(mutagen_file)[self.pack_pos] def set(self, mutagen_file, value): items = self._fetch_unpacked(mutagen_file) items[self.pack_pos] = value if items[0] is None: items[0] = '' if items[1] is None: items.pop() # Do not store last value self.store(mutagen_file, '/'.join(map(unicode, items))) def delete(self, mutagen_file): if self.pack_pos == 0: super(MP3SlashPackStorageStyle, self).delete(mutagen_file) else: self.set(mutagen_file, None) class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): """Converts between APIC frames and ``Image`` instances. The `get_list` method inherited from ``ListStorageStyle`` returns a list of ``Image``s. Similarly, the `set_list` method accepts a list of ``Image``s as its ``values`` arguemnt. """ def __init__(self): super(MP3ImageStorageStyle, self).__init__(key='APIC') self.as_type = str def deserialize(self, apic_frame): """Convert APIC frame into Image.""" return Image(data=apic_frame.data, desc=apic_frame.desc, type=apic_frame.type) def fetch(self, mutagen_file): return mutagen_file.tags.getall(self.key) def store(self, mutagen_file, frames): mutagen_file.tags.setall(self.key, frames) def serialize(self, image): """Return an APIC frame populated with data from ``image``. """ assert isinstance(image, Image) frame = mutagen.id3.Frames[self.key]() frame.data = image.data frame.mime = image.mime_type frame.desc = (image.desc or u'').encode('utf8') frame.encoding = 3 # UTF-8 encoding of desc frame.type = image.type_index return frame class MP3SoundCheckStorageStyle(SoundCheckStorageStyleMixin, MP3DescStorageStyle): def __init__(self, index=0, **kwargs): super(MP3SoundCheckStorageStyle, self).__init__(**kwargs) self.index = index class ASFImageStorageStyle(ListStorageStyle): """Store images packed into Windows Media/ASF byte array attributes. Values are `Image` objects. """ formats = ['ASF'] def __init__(self): super(ASFImageStorageStyle, self).__init__(key='WM/Picture') def deserialize(self, asf_picture): mime, data, type, desc = _unpack_asf_image(asf_picture.value) return Image(data, desc=desc, type=type) def serialize(self, image): pic = mutagen.asf.ASFByteArrayAttribute() pic.value = _pack_asf_image(image.mime_type, image.data, type=image.type_index, description=image.desc or u'') return pic class VorbisImageStorageStyle(ListStorageStyle): """Store images in Vorbis comments. Both legacy COVERART fields and modern METADATA_BLOCK_PICTURE tags are supported. Data is base64-encoded. Values are `Image` objects. """ formats = ['OggOpus', 'OggTheora', 'OggSpeex', 'OggVorbis', 'OggFlac', 'APEv2File', 'WavPack', 'Musepack', 'MonkeysAudio'] def __init__(self): super(VorbisImageStorageStyle, self).__init__( key='metadata_block_picture' ) self.as_type = str def fetch(self, mutagen_file): images = [] if 'metadata_block_picture' not in mutagen_file: # Try legacy COVERART tags. if 'coverart' in mutagen_file: for data in mutagen_file['coverart']: images.append(Image(base64.b64decode(data))) return images for data in mutagen_file["metadata_block_picture"]: try: pic = mutagen.flac.Picture(base64.b64decode(data)) except (TypeError, AttributeError): continue images.append(Image(data=pic.data, desc=pic.desc, type=pic.type)) return images def store(self, mutagen_file, image_data): # Strip all art, including legacy COVERART. if 'coverart' in mutagen_file: del mutagen_file['coverart'] if 'coverartmime' in mutagen_file: del mutagen_file['coverartmime'] super(VorbisImageStorageStyle, self).store(mutagen_file, image_data) def serialize(self, image): """Turn a Image into a base64 encoded FLAC picture block. """ pic = mutagen.flac.Picture() pic.data = image.data pic.type = image.type_index pic.mime = image.mime_type pic.desc = image.desc or u'' return base64.b64encode(pic.write()) class FlacImageStorageStyle(ListStorageStyle): """Converts between ``mutagen.flac.Picture`` and ``Image`` instances. """ formats = ['FLAC'] def __init__(self): super(FlacImageStorageStyle, self).__init__(key='') def fetch(self, mutagen_file): return mutagen_file.pictures def deserialize(self, flac_picture): return Image(data=flac_picture.data, desc=flac_picture.desc, type=flac_picture.type) def store(self, mutagen_file, pictures): """``pictures`` is a list of mutagen.flac.Picture instances. """ mutagen_file.clear_pictures() for pic in pictures: mutagen_file.add_picture(pic) def serialize(self, image): """Turn a Image into a mutagen.flac.Picture. """ pic = mutagen.flac.Picture() pic.data = image.data pic.type = image.type_index pic.mime = image.mime_type pic.desc = image.desc or u'' return pic def delete(self, mutagen_file): """Remove all images from the file. """ mutagen_file.clear_pictures() # MediaField is a descriptor that represents a single logical field. It # aggregates several StorageStyles describing how to access the data for # each file type. class MediaField(object): """A descriptor providing access to a particular (abstract) metadata field. """ def __init__(self, *styles, **kwargs): """Creates a new MediaField. :param styles: `StorageStyle` instances that describe the strategy for reading and writing the field in particular formats. There must be at least one style for each possible file format. :param out_type: the type of the value that should be returned when getting this property. """ self.out_type = kwargs.get('out_type', unicode) self._styles = styles def styles(self, mutagen_file): """Yields the list of storage styles of this field that can handle the MediaFile's format. """ for style in self._styles: if mutagen_file.__class__.__name__ in style.formats: yield style def __get__(self, mediafile, owner=None): out = None for style in self.styles(mediafile.mgfile): out = style.get(mediafile.mgfile) if out: break return _safe_cast(self.out_type, out) def __set__(self, mediafile, value): if value is None: value = self._none_value() for style in self.styles(mediafile.mgfile): style.set(mediafile.mgfile, value) def __delete__(self, mediafile): for style in self.styles(mediafile.mgfile): style.delete(mediafile.mgfile) def _none_value(self): """Get an appropriate "null" value for this field's type. This is used internally when setting the field to None. """ if self.out_type == int: return 0 elif self.out_type == float: return 0.0 elif self.out_type == bool: return False elif self.out_type == unicode: return u'' class ListMediaField(MediaField): """Property descriptor that retrieves a list of multiple values from a tag. Uses ``get_list`` and set_list`` methods of its ``StorageStyle`` strategies to do the actual work. """ def __get__(self, mediafile, _): values = [] for style in self.styles(mediafile.mgfile): values.extend(style.get_list(mediafile.mgfile)) return [_safe_cast(self.out_type, value) for value in values] def __set__(self, mediafile, values): for style in self.styles(mediafile.mgfile): style.set_list(mediafile.mgfile, values) def single_field(self): """Returns a ``MediaField`` descriptor that gets and sets the first item. """ options = {'out_type': self.out_type} return MediaField(*self._styles, **options) class DateField(MediaField): """Descriptor that handles serializing and deserializing dates The getter parses value from tags into a ``datetime.date`` instance and setter serializes such an instance into a string. For granular access to year, month, and day, use the ``*_field`` methods to create corresponding `DateItemField`s. """ def __init__(self, *date_styles, **kwargs): """``date_styles`` is a list of ``StorageStyle``s to store and retrieve the whole date from. The ``year`` option is an additional list of fallback styles for the year. The year is always set on this style, but is only retrieved if the main storage styles do not return a value. """ super(DateField, self).__init__(*date_styles) year_style = kwargs.get('year', None) if year_style: self._year_field = MediaField(*year_style) def __get__(self, mediafile, owner=None): year, month, day = self._get_date_tuple(mediafile) if not year: return None try: return datetime.date( year, month or 1, day or 1 ) except ValueError: # Out of range values. return None def __set__(self, mediafile, date): if date is None: self._set_date_tuple(mediafile, None, None, None) else: self._set_date_tuple(mediafile, date.year, date.month, date.day) def __delete__(self, mediafile): super(DateField, self).__delete__(mediafile) if hasattr(self, '_year_field'): self._year_field.__delete__(mediafile) def _get_date_tuple(self, mediafile): """Get a 3-item sequence representing the date consisting of a year, month, and day number. Each number is either an integer or None. """ # Get the underlying data and split on hyphens. datestring = super(DateField, self).__get__(mediafile, None) if isinstance(datestring, basestring): datestring = re.sub(r'[Tt ].*$', '', unicode(datestring)) items = unicode(datestring).split('-') else: items = [] # Ensure that we have exactly 3 components, possibly by # truncating or padding. items = items[:3] if len(items) < 3: items += [None] * (3 - len(items)) # Use year field if year is missing. if not items[0] and hasattr(self, '_year_field'): items[0] = self._year_field.__get__(mediafile) # Convert each component to an integer if possible. items_ = [] for item in items: try: items_.append(int(item)) except: items_.append(None) return items_ def _set_date_tuple(self, mediafile, year, month=None, day=None): """Set the value of the field given a year, month, and day number. Each number can be an integer or None to indicate an unset component. """ if year is None: self.__delete__(mediafile) return date = [u'{0:04d}'.format(int(year))] if month: date.append(u'{0:02d}'.format(int(month))) if month and day: date.append(u'{0:02d}'.format(int(day))) date = map(unicode, date) super(DateField, self).__set__(mediafile, u'-'.join(date)) if hasattr(self, '_year_field'): self._year_field.__set__(mediafile, year) def year_field(self): return DateItemField(self, 0) def month_field(self): return DateItemField(self, 1) def day_field(self): return DateItemField(self, 2) class DateItemField(MediaField): """Descriptor that gets and sets constituent parts of a `DateField`: the month, day, or year. """ def __init__(self, date_field, item_pos): self.date_field = date_field self.item_pos = item_pos def __get__(self, mediafile, _): return self.date_field._get_date_tuple(mediafile)[self.item_pos] def __set__(self, mediafile, value): items = self.date_field._get_date_tuple(mediafile) items[self.item_pos] = value self.date_field._set_date_tuple(mediafile, *items) def __delete__(self, mediafile): self.__set__(mediafile, None) class CoverArtField(MediaField): """A descriptor that provides access to the *raw image data* for the first image on a file. This is used for backwards compatibility: the full `ImageListField` provides richer `Image` objects. """ def __init__(self): pass def __get__(self, mediafile, _): try: return mediafile.images[0].data except IndexError: return None def __set__(self, mediafile, data): if data: mediafile.images = [Image(data=data)] else: mediafile.images = [] def __delete__(self, mediafile): delattr(mediafile, 'images') class ImageListField(MediaField): """Descriptor to access the list of images embedded in tags. The getter returns a list of `Image` instances obtained from the tags. The setter accepts a list of `Image` instances to be written to the tags. """ def __init__(self): # The storage styles used here must implement the # `ListStorageStyle` interface and get and set lists of # `Image`s. super(ImageListField, self).__init__( MP3ImageStorageStyle(), MP4ImageStorageStyle(), ASFImageStorageStyle(), VorbisImageStorageStyle(), FlacImageStorageStyle(), ) def __get__(self, mediafile, _): images = [] for style in self.styles(mediafile.mgfile): images.extend(style.get_list(mediafile.mgfile)) return images def __set__(self, mediafile, images): for style in self.styles(mediafile.mgfile): style.set_list(mediafile.mgfile, images) # MediaFile is a collection of fields. class MediaFile(object): """Represents a multimedia file on disk and provides access to its metadata. """ def __init__(self, path, id3v23=False): """Constructs a new `MediaFile` reflecting the file at path. May throw `UnreadableFileError`. By default, MP3 files are saved with ID3v2.4 tags. You can use the older ID3v2.3 standard by specifying the `id3v23` option. """ self.path = path unreadable_exc = ( mutagen.mp3.error, mutagen.id3.error, mutagen.flac.error, mutagen.monkeysaudio.MonkeysAudioHeaderError, mutagen.mp4.error, mutagen.oggopus.error, mutagen.oggvorbis.error, mutagen.ogg.error, mutagen.asf.error, mutagen.apev2.error, mutagen.aiff.error, ) try: self.mgfile = mutagen.File(path) except unreadable_exc as exc: log.debug(u'header parsing failed: {0}'.format(unicode(exc))) raise UnreadableFileError(path) except IOError as exc: if type(exc) == IOError: # This is a base IOError, not a subclass from Mutagen or # anywhere else. raise else: log.debug(traceback.format_exc()) raise MutagenError(path, exc) except Exception as exc: # Isolate bugs in Mutagen. log.debug(traceback.format_exc()) log.error(u'uncaught Mutagen exception in open: {0}'.format(exc)) raise MutagenError(path, exc) if self.mgfile is None: # Mutagen couldn't guess the type raise FileTypeError(path) elif (type(self.mgfile).__name__ == 'M4A' or type(self.mgfile).__name__ == 'MP4'): # This hack differentiates AAC and ALAC until we find a more # deterministic approach. Mutagen only sets the sample rate # for AAC files. See: # https://github.com/sampsyo/beets/pull/295 if hasattr(self.mgfile.info, 'sample_rate') and \ self.mgfile.info.sample_rate > 0: self.type = 'aac' else: self.type = 'alac' elif (type(self.mgfile).__name__ == 'ID3' or type(self.mgfile).__name__ == 'MP3'): self.type = 'mp3' elif type(self.mgfile).__name__ == 'FLAC': self.type = 'flac' elif type(self.mgfile).__name__ == 'OggOpus': self.type = 'opus' elif type(self.mgfile).__name__ == 'OggVorbis': self.type = 'ogg' elif type(self.mgfile).__name__ == 'MonkeysAudio': self.type = 'ape' elif type(self.mgfile).__name__ == 'WavPack': self.type = 'wv' elif type(self.mgfile).__name__ == 'Musepack': self.type = 'mpc' elif type(self.mgfile).__name__ == 'ASF': self.type = 'asf' elif type(self.mgfile).__name__ == 'AIFF': self.type = 'aiff' else: raise FileTypeError(path, type(self.mgfile).__name__) # Add a set of tags if it's missing. if self.mgfile.tags is None: self.mgfile.add_tags() # Set the ID3v2.3 flag only for MP3s. self.id3v23 = id3v23 and self.type == 'mp3' def save(self): """Write the object's tags back to the file. """ # Possibly save the tags to ID3v2.3. kwargs = {} if self.id3v23: id3 = self.mgfile if hasattr(id3, 'tags'): # In case this is an MP3 object, not an ID3 object. id3 = id3.tags id3.update_to_v23() kwargs['v2_version'] = 3 # Isolate bugs in Mutagen. try: self.mgfile.save(**kwargs) except (IOError, OSError): # Propagate these through: they don't represent Mutagen bugs. raise except Exception as exc: log.debug(traceback.format_exc()) log.error(u'uncaught Mutagen exception in save: {0}'.format(exc)) raise MutagenError(self.path, exc) def delete(self): """Remove the current metadata tag from the file. """ try: self.mgfile.delete() except NotImplementedError: # For Mutagen types that don't support deletion (notably, # ASF), just delete each tag individually. for tag in self.mgfile.keys(): del self.mgfile[tag] # Convenient access to the set of available fields. @classmethod def fields(cls): """Get the names of all writable properties that reflect metadata tags (i.e., those that are instances of :class:`MediaField`). """ for property, descriptor in cls.__dict__.items(): if isinstance(descriptor, MediaField): yield property @classmethod def readable_fields(cls): """Get all metadata fields: the writable ones from :meth:`fields` and also other audio properties. """ for property in cls.fields(): yield property for property in ('length', 'samplerate', 'bitdepth', 'bitrate', 'channels', 'format'): yield property @classmethod def add_field(cls, name, descriptor): """Add a field to store custom tags. :param name: the name of the property the field is accessed through. It must not already exist on this class. :param descriptor: an instance of :class:`MediaField`. """ if not isinstance(descriptor, MediaField): raise ValueError( u'{0} must be an instance of MediaField'.format(descriptor)) if name in cls.__dict__: raise ValueError( u'property "{0}" already exists on MediaField'.format(name)) setattr(cls, name, descriptor) def update(self, dict): """Set all field values from a dictionary. For any key in `dict` that is also a field to store tags the method retrieves the corresponding value from `dict` and updates the `MediaFile`. If a key has the value `None`, the corresponding property is deleted from the `MediaFile`. """ for field in self.fields(): if field in dict: if dict[field] is None: delattr(self, field) else: setattr(self, field, dict[field]) # Field definitions. title = MediaField( MP3StorageStyle('TIT2'), MP4StorageStyle("\xa9nam"), StorageStyle('TITLE'), ASFStorageStyle('Title'), ) artist = MediaField( MP3StorageStyle('TPE1'), MP4StorageStyle("\xa9ART"), StorageStyle('ARTIST'), ASFStorageStyle('Author'), ) album = MediaField( MP3StorageStyle('TALB'), MP4StorageStyle("\xa9alb"), StorageStyle('ALBUM'), ASFStorageStyle('WM/AlbumTitle'), ) genres = ListMediaField( MP3ListStorageStyle('TCON'), MP4ListStorageStyle("\xa9gen"), ListStorageStyle('GENRE'), ASFStorageStyle('WM/Genre'), ) genre = genres.single_field() composer = MediaField( MP3StorageStyle('TCOM'), MP4StorageStyle("\xa9wrt"), StorageStyle('COMPOSER'), ASFStorageStyle('WM/Composer'), ) grouping = MediaField( MP3StorageStyle('TIT1'), MP4StorageStyle("\xa9grp"), StorageStyle('GROUPING'), ASFStorageStyle('WM/ContentGroupDescription'), ) track = MediaField( MP3SlashPackStorageStyle('TRCK', pack_pos=0), MP4TupleStorageStyle('trkn', index=0), StorageStyle('TRACK'), StorageStyle('TRACKNUMBER'), ASFStorageStyle('WM/TrackNumber'), out_type=int, ) tracktotal = MediaField( MP3SlashPackStorageStyle('TRCK', pack_pos=1), MP4TupleStorageStyle('trkn', index=1), StorageStyle('TRACKTOTAL'), StorageStyle('TRACKC'), StorageStyle('TOTALTRACKS'), ASFStorageStyle('TotalTracks'), out_type=int, ) disc = MediaField( MP3SlashPackStorageStyle('TPOS', pack_pos=0), MP4TupleStorageStyle('disk', index=0), StorageStyle('DISC'), StorageStyle('DISCNUMBER'), ASFStorageStyle('WM/PartOfSet'), out_type=int, ) disctotal = MediaField( MP3SlashPackStorageStyle('TPOS', pack_pos=1), MP4TupleStorageStyle('disk', index=1), StorageStyle('DISCTOTAL'), StorageStyle('DISCC'), StorageStyle('TOTALDISCS'), ASFStorageStyle('TotalDiscs'), out_type=int, ) lyrics = MediaField( MP3DescStorageStyle(key='USLT'), MP4StorageStyle("\xa9lyr"), StorageStyle('LYRICS'), ASFStorageStyle('WM/Lyrics'), ) comments = MediaField( MP3DescStorageStyle(key='COMM'), MP4StorageStyle("\xa9cmt"), StorageStyle('DESCRIPTION'), StorageStyle('COMMENT'), ASFStorageStyle('WM/Comments'), ) bpm = MediaField( MP3StorageStyle('TBPM'), MP4StorageStyle('tmpo', as_type=int), StorageStyle('BPM'), ASFStorageStyle('WM/BeatsPerMinute'), out_type=int, ) comp = MediaField( MP3StorageStyle('TCMP'), MP4BoolStorageStyle('cpil'), StorageStyle('COMPILATION'), ASFStorageStyle('WM/IsCompilation', as_type=bool), out_type=bool, ) albumartist = MediaField( MP3StorageStyle('TPE2'), MP4StorageStyle('aART'), StorageStyle('ALBUM ARTIST'), StorageStyle('ALBUMARTIST'), ASFStorageStyle('WM/AlbumArtist'), ) albumtype = MediaField( MP3DescStorageStyle(u'MusicBrainz Album Type'), MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Type'), StorageStyle('MUSICBRAINZ_ALBUMTYPE'), ASFStorageStyle('MusicBrainz/Album Type'), ) label = MediaField( MP3StorageStyle('TPUB'), MP4StorageStyle('----:com.apple.iTunes:Label'), MP4StorageStyle('----:com.apple.iTunes:publisher'), StorageStyle('LABEL'), StorageStyle('PUBLISHER'), # Traktor ASFStorageStyle('WM/Publisher'), ) artist_sort = MediaField( MP3StorageStyle('TSOP'), MP4StorageStyle("soar"), StorageStyle('ARTISTSORT'), ASFStorageStyle('WM/ArtistSortOrder'), ) albumartist_sort = MediaField( MP3DescStorageStyle(u'ALBUMARTISTSORT'), MP4StorageStyle("soaa"), StorageStyle('ALBUMARTISTSORT'), ASFStorageStyle('WM/AlbumArtistSortOrder'), ) asin = MediaField( MP3DescStorageStyle(u'ASIN'), MP4StorageStyle("----:com.apple.iTunes:ASIN"), StorageStyle('ASIN'), ASFStorageStyle('MusicBrainz/ASIN'), ) catalognum = MediaField( MP3DescStorageStyle(u'CATALOGNUMBER'), MP4StorageStyle("----:com.apple.iTunes:CATALOGNUMBER"), StorageStyle('CATALOGNUMBER'), ASFStorageStyle('WM/CatalogNo'), ) disctitle = MediaField( MP3StorageStyle('TSST'), MP4StorageStyle("----:com.apple.iTunes:DISCSUBTITLE"), StorageStyle('DISCSUBTITLE'), ASFStorageStyle('WM/SetSubTitle'), ) encoder = MediaField( MP3StorageStyle('TENC'), MP4StorageStyle("\xa9too"), StorageStyle('ENCODEDBY'), StorageStyle('ENCODER'), ASFStorageStyle('WM/EncodedBy'), ) script = MediaField( MP3DescStorageStyle(u'Script'), MP4StorageStyle("----:com.apple.iTunes:SCRIPT"), StorageStyle('SCRIPT'), ASFStorageStyle('WM/Script'), ) language = MediaField( MP3StorageStyle('TLAN'), MP4StorageStyle("----:com.apple.iTunes:LANGUAGE"), StorageStyle('LANGUAGE'), ASFStorageStyle('WM/Language'), ) country = MediaField( MP3DescStorageStyle('MusicBrainz Album Release Country'), MP4StorageStyle("----:com.apple.iTunes:MusicBrainz " "Album Release Country"), StorageStyle('RELEASECOUNTRY'), ASFStorageStyle('MusicBrainz/Album Release Country'), ) albumstatus = MediaField( MP3DescStorageStyle(u'MusicBrainz Album Status'), MP4StorageStyle("----:com.apple.iTunes:MusicBrainz Album Status"), StorageStyle('MUSICBRAINZ_ALBUMSTATUS'), ASFStorageStyle('MusicBrainz/Album Status'), ) media = MediaField( MP3StorageStyle('TMED'), MP4StorageStyle("----:com.apple.iTunes:MEDIA"), StorageStyle('MEDIA'), ASFStorageStyle('WM/Media'), ) albumdisambig = MediaField( # This tag mapping was invented for beets (not used by Picard, etc). MP3DescStorageStyle(u'MusicBrainz Album Comment'), MP4StorageStyle("----:com.apple.iTunes:MusicBrainz Album Comment"), StorageStyle('MUSICBRAINZ_ALBUMCOMMENT'), ASFStorageStyle('MusicBrainz/Album Comment'), ) # Release date. date = DateField( MP3StorageStyle('TDRC'), MP4StorageStyle("\xa9day"), StorageStyle('DATE'), ASFStorageStyle('WM/Year'), year=(StorageStyle('YEAR'),)) year = date.year_field() month = date.month_field() day = date.day_field() # *Original* release date. original_date = DateField( MP3StorageStyle('TDOR'), MP4StorageStyle('----:com.apple.iTunes:ORIGINAL YEAR'), StorageStyle('ORIGINALDATE'), ASFStorageStyle('WM/OriginalReleaseYear')) original_year = original_date.year_field() original_month = original_date.month_field() original_day = original_date.day_field() # Nonstandard metadata. artist_credit = MediaField( MP3DescStorageStyle(u'Artist Credit'), MP4StorageStyle("----:com.apple.iTunes:Artist Credit"), StorageStyle('ARTIST_CREDIT'), ASFStorageStyle('beets/Artist Credit'), ) albumartist_credit = MediaField( MP3DescStorageStyle(u'Album Artist Credit'), MP4StorageStyle("----:com.apple.iTunes:Album Artist Credit"), StorageStyle('ALBUMARTIST_CREDIT'), ASFStorageStyle('beets/Album Artist Credit'), ) # Legacy album art field art = CoverArtField() # Image list images = ImageListField() # MusicBrainz IDs. mb_trackid = MediaField( MP3UFIDStorageStyle(owner='http://musicbrainz.org'), MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Track Id'), StorageStyle('MUSICBRAINZ_TRACKID'), ASFStorageStyle('MusicBrainz/Track Id'), ) mb_albumid = MediaField( MP3DescStorageStyle(u'MusicBrainz Album Id'), MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Id'), StorageStyle('MUSICBRAINZ_ALBUMID'), ASFStorageStyle('MusicBrainz/Album Id'), ) mb_artistid = MediaField( MP3DescStorageStyle(u'MusicBrainz Artist Id'), MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Artist Id'), StorageStyle('MUSICBRAINZ_ARTISTID'), ASFStorageStyle('MusicBrainz/Artist Id'), ) mb_albumartistid = MediaField( MP3DescStorageStyle(u'MusicBrainz Album Artist Id'), MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Artist Id'), StorageStyle('MUSICBRAINZ_ALBUMARTISTID'), ASFStorageStyle('MusicBrainz/Album Artist Id'), ) mb_releasegroupid = MediaField( MP3DescStorageStyle(u'MusicBrainz Release Group Id'), MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Release Group Id'), StorageStyle('MUSICBRAINZ_RELEASEGROUPID'), ASFStorageStyle('MusicBrainz/Release Group Id'), ) # Acoustid fields. acoustid_fingerprint = MediaField( MP3DescStorageStyle(u'Acoustid Fingerprint'), MP4StorageStyle('----:com.apple.iTunes:Acoustid Fingerprint'), StorageStyle('ACOUSTID_FINGERPRINT'), ASFStorageStyle('Acoustid/Fingerprint'), ) acoustid_id = MediaField( MP3DescStorageStyle(u'Acoustid Id'), MP4StorageStyle('----:com.apple.iTunes:Acoustid Id'), StorageStyle('ACOUSTID_ID'), ASFStorageStyle('Acoustid/Id'), ) # ReplayGain fields. rg_track_gain = MediaField( MP3DescStorageStyle( u'REPLAYGAIN_TRACK_GAIN', float_places=2, suffix=u' dB' ), MP3DescStorageStyle( u'replaygain_track_gain', float_places=2, suffix=u' dB' ), MP3SoundCheckStorageStyle( key='COMM', index=0, desc=u'iTunNORM', id3_lang='eng' ), MP4StorageStyle( '----:com.apple.iTunes:replaygain_track_gain', float_places=2, suffix=b' dB' ), MP4SoundCheckStorageStyle( '----:com.apple.iTunes:iTunNORM', index=0 ), StorageStyle( u'REPLAYGAIN_TRACK_GAIN', float_places=2, suffix=u' dB' ), ASFStorageStyle( u'replaygain_track_gain', float_places=2, suffix=u' dB' ), out_type=float ) rg_album_gain = MediaField( MP3DescStorageStyle( u'REPLAYGAIN_ALBUM_GAIN', float_places=2, suffix=u' dB' ), MP3DescStorageStyle( u'replaygain_album_gain', float_places=2, suffix=u' dB' ), MP4SoundCheckStorageStyle( '----:com.apple.iTunes:iTunNORM', index=1 ), StorageStyle( u'REPLAYGAIN_ALBUM_GAIN', float_places=2, suffix=u' dB' ), ASFStorageStyle( u'replaygain_album_gain', float_places=2, suffix=u' dB' ), out_type=float ) rg_track_peak = MediaField( MP3DescStorageStyle( u'REPLAYGAIN_TRACK_PEAK', float_places=6 ), MP3DescStorageStyle( u'replaygain_track_peak', float_places=6 ), MP3SoundCheckStorageStyle( key=u'COMM', index=1, desc=u'iTunNORM', id3_lang='eng' ), MP4StorageStyle( '----:com.apple.iTunes:replaygain_track_peak', float_places=6 ), MP4SoundCheckStorageStyle( '----:com.apple.iTunes:iTunNORM', index=1 ), StorageStyle(u'REPLAYGAIN_TRACK_PEAK', float_places=6), ASFStorageStyle(u'replaygain_track_peak', float_places=6), out_type=float, ) rg_album_peak = MediaField( MP3DescStorageStyle( u'REPLAYGAIN_ALBUM_PEAK', float_places=6 ), MP3DescStorageStyle( u'replaygain_album_peak', float_places=6 ), MP4StorageStyle( '----:com.apple.iTunes:replaygain_album_peak', float_places=6 ), StorageStyle(u'REPLAYGAIN_ALBUM_PEAK', float_places=6), ASFStorageStyle(u'replaygain_album_peak', float_places=6), out_type=float, ) initial_key = MediaField( MP3StorageStyle('TKEY'), MP4StorageStyle('----:com.apple.iTunes:initialkey'), StorageStyle('INITIALKEY'), ASFStorageStyle('INITIALKEY'), ) @property def length(self): """The duration of the audio in seconds (a float).""" return self.mgfile.info.length @property def samplerate(self): """The audio's sample rate (an int).""" if hasattr(self.mgfile.info, 'sample_rate'): return self.mgfile.info.sample_rate elif self.type == 'opus': # Opus is always 48kHz internally. return 48000 return 0 @property def bitdepth(self): """The number of bits per sample in the audio encoding (an int). Only available for certain file formats (zero where unavailable). """ if hasattr(self.mgfile.info, 'bits_per_sample'): return self.mgfile.info.bits_per_sample return 0 @property def channels(self): """The number of channels in the audio (an int).""" if isinstance(self.mgfile.info, mutagen.mp3.MPEGInfo): return { mutagen.mp3.STEREO: 2, mutagen.mp3.JOINTSTEREO: 2, mutagen.mp3.DUALCHANNEL: 2, mutagen.mp3.MONO: 1, }[self.mgfile.info.mode] if hasattr(self.mgfile.info, 'channels'): return self.mgfile.info.channels return 0 @property def bitrate(self): """The number of bits per seconds used in the audio coding (an int). If this is provided explicitly by the compressed file format, this is a precise reflection of the encoding. Otherwise, it is estimated from the on-disk file size. In this case, some imprecision is possible because the file header is incorporated in the file size. """ if hasattr(self.mgfile.info, 'bitrate') and self.mgfile.info.bitrate: # Many formats provide it explicitly. return self.mgfile.info.bitrate else: # Otherwise, we calculate bitrate from the file size. (This # is the case for all of the lossless formats.) if not self.length: # Avoid division by zero if length is not available. return 0 size = os.path.getsize(self.path) return int(size * 8 / self.length) @property def format(self): """A string describing the file format/codec.""" return TYPES[self.type] beets-1.3.8/beets/plugins.py0000755000076500000240000002775512405373453016757 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Support for beets plugins.""" import logging import traceback from collections import defaultdict import inspect import beets from beets import mediafile PLUGIN_NAMESPACE = 'beetsplug' # Plugins using the Last.fm API can share the same API key. LASTFM_KEY = '2dc3914abf35f0d9c92d97d8f8e42b43' # Global logger. log = logging.getLogger('beets') class PluginConflictException(Exception): """Indicates that the services provided by one plugin conflict with those of another. For example two plugins may define different types for flexible fields. """ # Managing the plugins themselves. class BeetsPlugin(object): """The base class for all beets plugins. Plugins provide functionality by defining a subclass of BeetsPlugin and overriding the abstract methods defined here. """ def __init__(self, name=None): """Perform one-time plugin setup. """ self.import_stages = [] self.name = name or self.__module__.split('.')[-1] self.config = beets.config[self.name] if not self.template_funcs: self.template_funcs = {} if not self.template_fields: self.template_fields = {} if not self.album_template_fields: self.album_template_fields = {} def commands(self): """Should return a list of beets.ui.Subcommand objects for commands that should be added to beets' CLI. """ return () def queries(self): """Should return a dict mapping prefixes to Query subclasses. """ return {} def track_distance(self, item, info): """Should return a Distance object to be added to the distance for every track comparison. """ return beets.autotag.hooks.Distance() def album_distance(self, items, album_info, mapping): """Should return a Distance object to be added to the distance for every album-level comparison. """ return beets.autotag.hooks.Distance() def candidates(self, items, artist, album, va_likely): """Should return a sequence of AlbumInfo objects that match the album whose items are provided. """ return () def item_candidates(self, item, artist, title): """Should return a sequence of TrackInfo objects that match the item provided. """ return () def album_for_id(self, album_id): """Return an AlbumInfo object or None if no matching release was found. """ return None def track_for_id(self, track_id): """Return a TrackInfo object or None if no matching release was found. """ return None def add_media_field(self, name, descriptor): """Add a field that is synchronized between media files and items. When a media field is added ``item.write()`` will set the name property of the item's MediaFile to ``item[name]`` and save the changes. Similarly ``item.read()`` will set ``item[name]`` to the value of the name property of the media file. ``descriptor`` must be an instance of ``mediafile.MediaField``. """ # Defer impor to prevent circular dependency from beets import library mediafile.MediaFile.add_field(name, descriptor) library.Item._media_fields.add(name) listeners = None @classmethod def register_listener(cls, event, func): """Add a function as a listener for the specified event. (An imperative alternative to the @listen decorator.) """ if cls.listeners is None: cls.listeners = defaultdict(list) cls.listeners[event].append(func) @classmethod def listen(cls, event): """Decorator that adds a function as an event handler for the specified event (as a string). The parameters passed to function will vary depending on what event occurred. The function should respond to named parameters. function(**kwargs) will trap all arguments in a dictionary. Example: >>> @MyPlugin.listen("imported") >>> def importListener(**kwargs): ... pass """ def helper(func): if cls.listeners is None: cls.listeners = defaultdict(list) cls.listeners[event].append(func) return func return helper template_funcs = None template_fields = None album_template_fields = None @classmethod def template_func(cls, name): """Decorator that registers a path template function. The function will be invoked as ``%name{}`` from path format strings. """ def helper(func): if cls.template_funcs is None: cls.template_funcs = {} cls.template_funcs[name] = func return func return helper @classmethod def template_field(cls, name): """Decorator that registers a path template field computation. The value will be referenced as ``$name`` from path format strings. The function must accept a single parameter, the Item being formatted. """ def helper(func): if cls.template_fields is None: cls.template_fields = {} cls.template_fields[name] = func return func return helper _classes = set() def load_plugins(names=()): """Imports the modules for a sequence of plugin names. Each name must be the name of a Python module under the "beetsplug" namespace package in sys.path; the module indicated should contain the BeetsPlugin subclasses desired. """ for name in names: modname = '%s.%s' % (PLUGIN_NAMESPACE, name) try: try: namespace = __import__(modname, None, None) except ImportError as exc: # Again, this is hacky: if exc.args[0].endswith(' ' + name): log.warn(u'** plugin {0} not found'.format(name)) else: raise else: for obj in getattr(namespace, name).__dict__.values(): if isinstance(obj, type) and issubclass(obj, BeetsPlugin) \ and obj != BeetsPlugin and obj not in _classes: _classes.add(obj) except: log.warn(u'** error loading plugin {0}'.format(name)) log.warn(traceback.format_exc()) _instances = {} def find_plugins(): """Returns a list of BeetsPlugin subclass instances from all currently loaded beets plugins. Loads the default plugin set first. """ load_plugins() plugins = [] for cls in _classes: # Only instantiate each plugin class once. if cls not in _instances: _instances[cls] = cls() plugins.append(_instances[cls]) return plugins # Communication with plugins. def commands(): """Returns a list of Subcommand objects from all loaded plugins. """ out = [] for plugin in find_plugins(): out += plugin.commands() return out def queries(): """Returns a dict mapping prefix strings to Query subclasses all loaded plugins. """ out = {} for plugin in find_plugins(): out.update(plugin.queries()) return out def types(model_cls): # Gives us `item_types` and `album_types` attr_name = '{0}_types'.format(model_cls.__name__.lower()) types = {} for plugin in find_plugins(): plugin_types = getattr(plugin, attr_name, {}) for field in plugin_types: if field in types: raise PluginConflictException( u'Plugin {0} defines flexible field {1} ' 'which has already been defined.' .format(plugin.name,)) types.update(plugin_types) return types def track_distance(item, info): """Gets the track distance calculated by all loaded plugins. Returns a Distance object. """ from beets.autotag.hooks import Distance dist = Distance() for plugin in find_plugins(): dist.update(plugin.track_distance(item, info)) return dist def album_distance(items, album_info, mapping): """Returns the album distance calculated by plugins.""" from beets.autotag.hooks import Distance dist = Distance() for plugin in find_plugins(): dist.update(plugin.album_distance(items, album_info, mapping)) return dist def candidates(items, artist, album, va_likely): """Gets MusicBrainz candidates for an album from each plugin. """ out = [] for plugin in find_plugins(): out.extend(plugin.candidates(items, artist, album, va_likely)) return out def item_candidates(item, artist, title): """Gets MusicBrainz candidates for an item from the plugins. """ out = [] for plugin in find_plugins(): out.extend(plugin.item_candidates(item, artist, title)) return out def album_for_id(album_id): """Get AlbumInfo objects for a given ID string. """ out = [] for plugin in find_plugins(): res = plugin.album_for_id(album_id) if res: out.append(res) return out def track_for_id(track_id): """Get TrackInfo objects for a given ID string. """ out = [] for plugin in find_plugins(): res = plugin.track_for_id(track_id) if res: out.append(res) return out def template_funcs(): """Get all the template functions declared by plugins as a dictionary. """ funcs = {} for plugin in find_plugins(): if plugin.template_funcs: funcs.update(plugin.template_funcs) return funcs def import_stages(): """Get a list of import stage functions defined by plugins.""" stages = [] for plugin in find_plugins(): if hasattr(plugin, 'import_stages'): stages += plugin.import_stages return stages # New-style (lazy) plugin-provided fields. def item_field_getters(): """Get a dictionary mapping field names to unary functions that compute the field's value. """ funcs = {} for plugin in find_plugins(): if plugin.template_fields: funcs.update(plugin.template_fields) return funcs def album_field_getters(): """As above, for album fields. """ funcs = {} for plugin in find_plugins(): if plugin.album_template_fields: funcs.update(plugin.album_template_fields) return funcs # Event dispatch. def event_handlers(): """Find all event handlers from plugins as a dictionary mapping event names to sequences of callables. """ all_handlers = defaultdict(list) for plugin in find_plugins(): if plugin.listeners: for event, handlers in plugin.listeners.items(): all_handlers[event] += handlers return all_handlers def send(event, **arguments): """Sends an event to all assigned event listeners. Event is the name of the event to send, all other named arguments go to the event handler(s). Returns a list of return values from the handlers. """ log.debug(u'Sending event: {0}'.format(event)) for handler in event_handlers()[event]: # Don't break legacy plugins if we want to pass more arguments argspec = inspect.getargspec(handler).args args = dict((k, v) for k, v in arguments.items() if k in argspec) handler(**args) beets-1.3.8/beets/ui/0000755000076500000240000000000012406440351015307 5ustar asampsonstaff00000000000000beets-1.3.8/beets/ui/__init__.py0000644000076500000240000007651112405373453017441 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """This module contains all of the core logic for beets' command-line interface. To invoke the CLI, just call beets.ui.main(). The actual CLI commands are implemented in the ui.commands module. """ from __future__ import print_function import locale import optparse import textwrap import sys from difflib import SequenceMatcher import logging import sqlite3 import errno import re import struct import traceback import os.path from beets import library from beets import plugins from beets import util from beets.util.functemplate import Template from beets import config from beets.util import confit from beets.autotag import mb # On Windows platforms, use colorama to support "ANSI" terminal colors. if sys.platform == 'win32': try: import colorama except ImportError: pass else: colorama.init() log = logging.getLogger('beets') if not log.handlers: log.addHandler(logging.StreamHandler()) log.propagate = False # Don't propagate to root handler. PF_KEY_QUERIES = { 'comp': 'comp:true', 'singleton': 'singleton:true', } class UserError(Exception): """UI exception. Commands should throw this in order to display nonrecoverable errors to the user. """ # Utilities. def _encoding(): """Tries to guess the encoding used by the terminal.""" # Configured override? encoding = config['terminal_encoding'].get() if encoding: return encoding # Determine from locale settings. try: return locale.getdefaultlocale()[1] or 'utf8' except ValueError: # Invalid locale environment variable setting. To avoid # failing entirely for no good reason, assume UTF-8. return 'utf8' def decargs(arglist): """Given a list of command-line argument bytestrings, attempts to decode them to Unicode strings. """ return [s.decode(_encoding()) for s in arglist] def print_(*strings): """Like print, but rather than raising an error when a character is not in the terminal's encoding's character set, just silently replaces it. """ if strings: if isinstance(strings[0], unicode): txt = u' '.join(strings) else: txt = ' '.join(strings) else: txt = u'' if isinstance(txt, unicode): txt = txt.encode(_encoding(), 'replace') print(txt) def input_(prompt=None): """Like `raw_input`, but decodes the result to a Unicode string. Raises a UserError if stdin is not available. The prompt is sent to stdout rather than stderr. A printed between the prompt and the input cursor. """ # raw_input incorrectly sends prompts to stderr, not stdout, so we # use print() explicitly to display prompts. # http://bugs.python.org/issue1927 if prompt: if isinstance(prompt, unicode): prompt = prompt.encode(_encoding(), 'replace') print(prompt, end=' ') try: resp = raw_input() except EOFError: raise UserError('stdin stream ended while input required') return resp.decode(sys.stdin.encoding or 'utf8', 'ignore') def input_options(options, require=False, prompt=None, fallback_prompt=None, numrange=None, default=None, max_width=72): """Prompts a user for input. The sequence of `options` defines the choices the user has. A single-letter shortcut is inferred for each option; the user's choice is returned as that single, lower-case letter. The options should be provided as lower-case strings unless a particular shortcut is desired; in that case, only that letter should be capitalized. By default, the first option is the default. `default` can be provided to override this. If `require` is provided, then there is no default. The prompt and fallback prompt are also inferred but can be overridden. If numrange is provided, it is a pair of `(high, low)` (both ints) indicating that, in addition to `options`, the user may enter an integer in that inclusive range. `max_width` specifies the maximum number of columns in the automatically generated prompt string. """ # Assign single letters to each option. Also capitalize the options # to indicate the letter. letters = {} display_letters = [] capitalized = [] first = True for option in options: # Is a letter already capitalized? for letter in option: if letter.isalpha() and letter.upper() == letter: found_letter = letter break else: # Infer a letter. for letter in option: if not letter.isalpha(): continue # Don't use punctuation. if letter not in letters: found_letter = letter break else: raise ValueError('no unambiguous lettering found') letters[found_letter.lower()] = option index = option.index(found_letter) # Mark the option's shortcut letter for display. if not require and ( (default is None and not numrange and first) or (isinstance(default, basestring) and found_letter.lower() == default.lower())): # The first option is the default; mark it. show_letter = '[%s]' % found_letter.upper() is_default = True else: show_letter = found_letter.upper() is_default = False # Colorize the letter shortcut. show_letter = colorize('turquoise' if is_default else 'blue', show_letter) # Insert the highlighted letter back into the word. capitalized.append( option[:index] + show_letter + option[index + 1:] ) display_letters.append(found_letter.upper()) first = False # The default is just the first option if unspecified. if require: default = None elif default is None: if numrange: default = numrange[0] else: default = display_letters[0].lower() # Make a prompt if one is not provided. if not prompt: prompt_parts = [] prompt_part_lengths = [] if numrange: if isinstance(default, int): default_name = str(default) default_name = colorize('turquoise', default_name) tmpl = '# selection (default %s)' prompt_parts.append(tmpl % default_name) prompt_part_lengths.append(len(tmpl % str(default))) else: prompt_parts.append('# selection') prompt_part_lengths.append(len(prompt_parts[-1])) prompt_parts += capitalized prompt_part_lengths += [len(s) for s in options] # Wrap the query text. prompt = '' line_length = 0 for i, (part, length) in enumerate(zip(prompt_parts, prompt_part_lengths)): # Add punctuation. if i == len(prompt_parts) - 1: part += '?' else: part += ',' length += 1 # Choose either the current line or the beginning of the next. if line_length + length + 1 > max_width: prompt += '\n' line_length = 0 if line_length != 0: # Not the beginning of the line; need a space. part = ' ' + part length += 1 prompt += part line_length += length # Make a fallback prompt too. This is displayed if the user enters # something that is not recognized. if not fallback_prompt: fallback_prompt = 'Enter one of ' if numrange: fallback_prompt += '%i-%i, ' % numrange fallback_prompt += ', '.join(display_letters) + ':' resp = 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: resp = int(resp) except ValueError: pass else: low, high = numrange if low <= resp <= high: 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 = input_(fallback_prompt) def input_yn(prompt, require=False): """Prompts the user for a "yes" or "no" response. The default is "yes" unless `require` is `True`, in which case there is no default. """ sel = input_options( ('y', 'n'), require, prompt, 'Enter Y or N:' ) return sel == 'y' def human_bytes(size): """Formats size, a number of bytes, in a human-readable way.""" suffices = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'HB'] for suffix in suffices: if size < 1024: return "%3.1f %s" % (size, suffix) size /= 1024.0 return "big" def human_seconds(interval): """Formats interval, a number of seconds, as a human-readable time interval using English words. """ units = [ (1, 'second'), (60, 'minute'), (60, 'hour'), (24, 'day'), (7, 'week'), (52, 'year'), (10, 'decade'), ] for i in range(len(units) - 1): increment, suffix = units[i] next_increment, _ = units[i + 1] interval /= float(increment) if interval < next_increment: break else: # Last unit. increment, suffix = units[-1] interval /= float(increment) return "%3.1f %ss" % (interval, suffix) def human_seconds_short(interval): """Formats a number of seconds as a short human-readable M:SS string. """ interval = int(interval) return u'%i:%02i' % (interval // 60, interval % 60) # ANSI terminal colorization code heavily inspired by pygments: # http://dev.pocoo.org/hg/pygments-main/file/b2deea5b5030/pygments/console.py # (pygments is by Tim Hatch, Armin Ronacher, et al.) COLOR_ESCAPE = "\x1b[" DARK_COLORS = ["black", "darkred", "darkgreen", "brown", "darkblue", "purple", "teal", "lightgray"] LIGHT_COLORS = ["darkgray", "red", "green", "yellow", "blue", "fuchsia", "turquoise", "white"] RESET_COLOR = COLOR_ESCAPE + "39;49;00m" def _colorize(color, text): """Returns a string that prints the given text in the given color in a terminal that is ANSI color-aware. The color must be something in DARK_COLORS or LIGHT_COLORS. """ if color in DARK_COLORS: escape = COLOR_ESCAPE + "%im" % (DARK_COLORS.index(color) + 30) elif color in LIGHT_COLORS: escape = COLOR_ESCAPE + "%i;01m" % (LIGHT_COLORS.index(color) + 30) else: raise ValueError('no such color %s', color) return escape + text + RESET_COLOR def colorize(color, text): """Colorize text if colored output is enabled. (Like _colorize but conditional.) """ if config['color']: return _colorize(color, text) else: return text def _colordiff(a, b, highlight='red', minor_highlight='lightgray'): """Given two values, return the same pair of strings except with their differences highlighted in the specified color. Strings are highlighted intelligently to show differences; other values are stringified and highlighted in their entirety. """ if not isinstance(a, basestring) or not isinstance(b, basestring): # Non-strings: use ordinary equality. a = unicode(a) b = unicode(b) if a == b: return a, b else: return colorize(highlight, a), colorize(highlight, b) if isinstance(a, bytes) or isinstance(b, bytes): # A path field. a = util.displayable_path(a) b = util.displayable_path(b) 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': # In both strings. a_out.append(a[a_start:a_end]) b_out.append(b[b_start:b_end]) elif op == 'insert': # Right only. b_out.append(colorize(highlight, b[b_start:b_end])) elif op == 'delete': # Left only. a_out.append(colorize(highlight, a[a_start:a_end])) elif op == 'replace': # Right and left differ. Colorise with second highlight if # it's just a case change. if a[a_start:a_end].lower() != b[b_start:b_end].lower(): color = highlight else: color = minor_highlight a_out.append(colorize(color, a[a_start:a_end])) b_out.append(colorize(color, b[b_start:b_end])) else: assert(False) return u''.join(a_out), u''.join(b_out) def colordiff(a, b, highlight='red'): """Colorize differences between two values if color is enabled. (Like _colordiff but conditional.) """ if config['color']: return _colordiff(a, b, highlight) else: return unicode(a), unicode(b) def color_diff_suffix(a, b, highlight='red'): """Colorize the differing suffix between two strings.""" a, b = unicode(a), unicode(b) if not config['color']: return a, b # Fast path. if a == b: return a, b # Find the longest common prefix. first_diff = None for i in range(min(len(a), len(b))): if a[i] != b[i]: first_diff = i break else: first_diff = min(len(a), len(b)) # Colorize from the first difference on. return (a[:first_diff] + colorize(highlight, a[first_diff:]), b[:first_diff] + colorize(highlight, b[first_diff:])) def get_path_formats(subview=None): """Get the configuration's path formats as a list of query/template pairs. """ path_formats = [] subview = subview or config['paths'] for query, view in subview.items(): query = PF_KEY_QUERIES.get(query, query) # Expand common queries. path_formats.append((query, Template(view.get(unicode)))) return path_formats def get_replacements(): """Confit validation function that reads regex/string pairs. """ replacements = [] for pattern, repl in config['replace'].get(dict).items(): repl = repl or '' try: replacements.append((re.compile(pattern), repl)) except re.error: raise UserError( u'malformed regular expression in replace: {0}'.format( pattern ) ) return replacements def _pick_format(album, fmt=None): """Pick a format string for printing Album or Item objects, falling back to config options and defaults. """ if fmt: return fmt if album: return config['list_format_album'].get(unicode) else: return config['list_format_item'].get(unicode) def print_obj(obj, lib, fmt=None): """Print an Album or Item object. If `fmt` is specified, use that format string. Otherwise, use the configured template. """ album = isinstance(obj, library.Album) fmt = _pick_format(album, fmt) if isinstance(fmt, Template): template = fmt else: template = Template(fmt) print_(obj.evaluate_template(template)) def term_width(): """Get the width (columns) of the terminal.""" fallback = config['ui']['terminal_width'].get(int) # The fcntl and termios modules are not available on non-Unix # platforms, so we fall back to a constant. try: import fcntl import termios except ImportError: return fallback try: buf = fcntl.ioctl(0, termios.TIOCGWINSZ, ' ' * 4) except IOError: return fallback try: height, width = struct.unpack('hh', buf) except struct.error: return fallback return width FLOAT_EPSILON = 0.01 def _field_diff(field, old, new): """Given two Model objects, format their values for `field` and highlight changes among them. Return a human-readable string. If the value has not changed, return None instead. """ oldval = old.get(field) newval = new.get(field) # If no change, abort. if isinstance(oldval, float) and isinstance(newval, float) and \ abs(oldval - newval) < FLOAT_EPSILON: return None elif oldval == newval: return None # Get formatted values for output. oldstr = old.formatted().get(field, u'') newstr = new.formatted().get(field, u'') # For strings, highlight changes. For others, colorize the whole # thing. if isinstance(oldval, basestring): oldstr, newstr = colordiff(oldval, newstr) else: oldstr, newstr = colorize('red', oldstr), colorize('red', newstr) return u'{0} -> {1}'.format(oldstr, newstr) def show_model_changes(new, old=None, fields=None, always=False): """Given a Model object, print a list of changes from its pristine version stored in the database. Return a boolean indicating whether any changes were found. `old` may be the "original" object to avoid using the pristine version from the database. `fields` may be a list of fields to restrict the detection to. `always` indicates whether the object is always identified, regardless of whether any changes are present. """ old = old or new._db._get(type(new), new.id) # Build up lines showing changed fields. changes = [] for field in old: # Subset of the fields. Never show mtime. if field == 'mtime' or (fields and field not in fields): continue # Detect and show difference for this field. line = _field_diff(field, old, new) if line: changes.append(u' {0}: {1}'.format(field, line)) # New fields. for field in set(new) - set(old): if fields and field not in fields: continue changes.append(u' {0}: {1}'.format( field, colorize('red', new.formatted()[field]) )) # Print changes. if changes or always: print_obj(old, old._db) if changes: print_(u'\n'.join(changes)) return bool(changes) # Subcommand parsing infrastructure. # # This is a fairly generic subcommand parser for optparse. It is # maintained externally here: # http://gist.github.com/462717 # There you will also find a better description of the code and a more # succinct example program. class Subcommand(object): """A subcommand of a root command-line application that may be invoked by a SubcommandOptionParser. """ def __init__(self, name, parser=None, help='', aliases=(), hide=False): """Creates a new subcommand. name is the primary way to invoke the subcommand; aliases are alternate names. parser is an OptionParser responsible for parsing the subcommand's options. help is a short description of the command. If no parser is given, it defaults to a new, empty OptionParser. """ self.name = name self.parser = parser or optparse.OptionParser() self.aliases = aliases self.help = help self.hide = hide self._root_parser = None def print_help(self): self.parser.print_help() def parse_args(self, args): return self.parser.parse_args(args) @property def root_parser(self): return self._root_parser @root_parser.setter def root_parser(self, root_parser): self._root_parser = root_parser self.parser.prog = '{0} {1}'.format(root_parser.get_prog_name(), self.name) class SubcommandsOptionParser(optparse.OptionParser): """A variant of OptionParser that parses subcommands and their arguments. """ def __init__(self, *args, **kwargs): """Create a new subcommand-aware option parser. All of the options to OptionParser.__init__ are supported in addition to subcommands, a sequence of Subcommand objects. """ # A more helpful default usage. if 'usage' not in kwargs: kwargs['usage'] = """ %prog COMMAND [ARGS...] %prog help COMMAND""" kwargs['add_help_option'] = False # Super constructor. optparse.OptionParser.__init__(self, *args, **kwargs) # Our root parser needs to stop on the first unrecognized argument. self.disable_interspersed_args() self.subcommands = [] def add_subcommand(self, *cmds): """Adds a Subcommand object to the parser's list of commands. """ for cmd in cmds: cmd.root_parser = self 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 = [] help_position = 0 subcommands = [c for c in self.subcommands if not c.hide] subcommands.sort(key=lambda c: c.name) for subcommand in subcommands: name = subcommand.name 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) # Add each subcommand to the output. for subcommand, name in zip(subcommands, disp_names): # Lifted directly from optparse.py. name_width = help_position - formatter.current_indent - 2 if len(name) > name_width: name = "%*s%s\n" % (formatter.current_indent, "", name) indent_first = help_position else: name = "%*s%-*s " % (formatter.current_indent, "", name_width, name) indent_first = 0 result.append(name) help_width = formatter.width - help_position help_lines = textwrap.wrap(subcommand.help, help_width) result.append("%*s%s\n" % (indent_first, "", help_lines[0])) 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 an alias. If no subcommand matches, returns None. """ for subcommand in self.subcommands: if name == subcommand.name or \ name in subcommand.aliases: return subcommand return None def parse_global_options(self, args): """Parse options up to the subcommand argument. Returns a tuple of the options object and the remaining arguments. """ options, subargs = self.parse_args(args) # Force the help command if options.help: subargs = ['help'] elif options.version: subargs = ['version'] return options, subargs def parse_subcommand(self, args): """Given the `args` left unused by a `parse_global_options`, return the invoked subcommand, the subcommand options, and the subcommand arguments. """ # Help is default command if not args: args = ['help'] cmdname = args.pop(0) subcommand = self._subcommand_for_name(cmdname) if not subcommand: raise UserError("unknown command '{0}'".format(cmdname)) suboptions, subargs = subcommand.parse_args(args) return subcommand, suboptions, subargs optparse.Option.ALWAYS_TYPED_ACTIONS += ('callback',) def vararg_callback(option, opt_str, value, parser): """Callback for an option with variable arguments. Manually collect arguments right of a callback-action option (ie. with action="callback"), and add the resulting list to the destination var. Usage: parser.add_option("-c", "--callback", dest="vararg_attr", action="callback", callback=vararg_callback) Details: http://docs.python.org/2/library/optparse.html#callback-example-6-variable -arguments """ value = [value] def floatable(str): try: float(str) return True except ValueError: return False for arg in parser.rargs: # stop on --foo like options if arg[:2] == "--" and len(arg) > 2: break # stop on -a, but not on -3 or -3.0 if arg[:1] == "-" and len(arg) > 1 and not floatable(arg): break value.append(arg) del parser.rargs[:len(value) - 1] setattr(parser.values, option.dest, value) # The main entry point and bootstrapping. def _load_plugins(config): """Load the plugins specified in the configuration. """ paths = config['pluginpath'].get(confit.StrSeq(split=False)) paths = map(util.normpath, paths) import beetsplug beetsplug.__path__ = paths + beetsplug.__path__ # For backwards compatibility. sys.path += paths plugins.load_plugins(config['plugins'].as_str_seq()) plugins.send("pluginload") return plugins def _setup(options, lib=None): """Prepare and global state and updates it with command line options. Returns a list of subcommands, a list of plugins, and a library instance. """ # Configure the MusicBrainz API. mb.configure() config = _configure(options) plugins = _load_plugins(config) # Temporary: Migrate from 1.0-style configuration. from beets.ui import migrate migrate.automigrate() # Get the default subcommands. from beets.ui.commands import default_commands subcommands = list(default_commands) subcommands.append(migrate.migrate_cmd) subcommands.extend(plugins.commands()) if lib is None: lib = _open_library(config) plugins.send("library_opened", lib=lib) library.Item._types = plugins.types(library.Item) library.Album._types = plugins.types(library.Album) return subcommands, plugins, lib def _configure(options): """Amend the global configuration object with command line options. """ # Add any additional config files specified with --config. This # special handling lets specified plugins get loaded before we # finish parsing the command line. if getattr(options, 'config', None) is not None: config_path = options.config del options.config config.set_file(config_path) config.set_args(options) # Configure the logger. if config['verbose'].get(bool): log.setLevel(logging.DEBUG) else: log.setLevel(logging.INFO) config_path = config.user_config_path() if os.path.isfile(config_path): log.debug(u'user configuration: {0}'.format( util.displayable_path(config_path))) else: log.debug(u'no user configuration found at {0}'.format( util.displayable_path(config_path))) log.debug(u'data directory: {0}' .format(util.displayable_path(config.config_dir()))) return config def _open_library(config): """Create a new library instance from the configuration. """ dbpath = config['library'].as_filename() try: lib = library.Library( dbpath, config['directory'].as_filename(), get_path_formats(), get_replacements(), ) except sqlite3.OperationalError: raise UserError(u"database file {0} could not be opened".format( util.displayable_path(dbpath) )) log.debug(u'library database: {0}\n' u'library directory: {1}' .format(util.displayable_path(lib.path), util.displayable_path(lib.directory))) return lib def _raw_main(args, lib=None): """A helper function for `main` without top-level exception handling. """ parser = SubcommandsOptionParser() parser.add_option('-l', '--library', dest='library', help='library database file to use') parser.add_option('-d', '--directory', dest='directory', help="destination music directory") parser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='print debugging information') parser.add_option('-c', '--config', dest='config', help='path to configuration file') parser.add_option('-h', '--help', dest='help', action='store_true', help='how this help message and exit') parser.add_option('--version', dest='version', action='store_true', help=optparse.SUPPRESS_HELP) options, subargs = parser.parse_global_options(args) subcommands, plugins, lib = _setup(options, lib) parser.add_subcommand(*subcommands) subcommand, suboptions, subargs = parser.parse_subcommand(subargs) subcommand.func(lib, suboptions, subargs) plugins.send('cli_exit', lib=lib) def main(args=None): """Run the main command-line interface for beets. Includes top-level exception handlers that print friendly error messages. """ try: _raw_main(args) except UserError as exc: message = exc.args[0] if exc.args else None log.error(u'error: {0}'.format(message)) sys.exit(1) except util.HumanReadableException as exc: exc.log(log) sys.exit(1) except library.FileOperationError as exc: # These errors have reasonable human-readable descriptions, but # we still want to log their tracebacks for debugging. log.debug(traceback.format_exc()) log.error(exc) sys.exit(1) except confit.ConfigError as exc: log.error(u'configuration error: {0}'.format(exc)) sys.exit(1) except IOError as exc: if exc.errno == errno.EPIPE: # "Broken pipe". End silently. pass else: raise except KeyboardInterrupt: # Silently ignore ^C except in verbose mode. log.debug(traceback.format_exc()) beets-1.3.8/beets/ui/commands.py0000644000076500000240000015164012406126245017474 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """This module provides the default commands for beets' command-line interface. """ from __future__ import print_function import logging import os import time import codecs import platform import re import beets from beets import ui from beets.ui import print_, input_, decargs from beets import autotag from beets.autotag import Recommendation from beets.autotag import hooks from beets import plugins from beets import importer from beets import util from beets.util import syspath, normpath, ancestry, displayable_path from beets.util.functemplate import Template from beets import library from beets import config from beets.util.confit import _package_path VARIOUS_ARTISTS = u'Various Artists' # Global logger. log = logging.getLogger('beets') # The list of default subcommands. This is populated with Subcommand # objects that can be fed to a SubcommandsOptionParser. default_commands = [] # Utilities. def _do_query(lib, query, album, also_items=True): """For commands that operate on matched items, performs a query and returns a list of matching items and a list of matching albums. (The latter is only nonempty when album is True.) Raises a UserError if no items match. also_items controls whether, when fetching albums, the associated items should be fetched also. """ if album: albums = list(lib.albums(query)) items = [] if also_items: for al in albums: items += al.items() else: albums = [] items = list(lib.items(query)) if album and not albums: raise ui.UserError('No matching albums found.') elif not album and not items: raise ui.UserError('No matching items found.') return items, albums # fields: Shows a list of available fields for queries and format strings. def fields_func(lib, opts, args): def _print_rows(names): print(" " + "\n ".join(names)) def _show_plugin_fields(album): plugin_fields = [] for plugin in plugins.find_plugins(): if album: fdict = plugin.album_template_fields else: fdict = plugin.template_fields plugin_fields += fdict.keys() if plugin_fields: print("Template fields from plugins:") _print_rows(plugin_fields) print("Item fields:") _print_rows(library.Item._fields.keys()) _show_plugin_fields(False) print("\nAlbum fields:") _print_rows(library.Album._fields.keys()) _show_plugin_fields(True) fields_cmd = ui.Subcommand( 'fields', help='show fields available for queries and format strings' ) fields_cmd.func = fields_func default_commands.append(fields_cmd) # help: Print help text for commands class HelpCommand(ui.Subcommand): def __init__(self): super(HelpCommand, self).__init__( 'help', aliases=('?',), help='give detailed help on a specific sub-command', ) def func(self, lib, opts, args): if args: cmdname = args[0] helpcommand = self.root_parser._subcommand_for_name(cmdname) if not helpcommand: raise ui.UserError("unknown command '{0}'".format(cmdname)) helpcommand.print_help() else: self.root_parser.print_help() default_commands.append(HelpCommand()) # import: Autotagger and importer. # Importer utilities and support. def disambig_string(info): """Generate a string for an AlbumInfo or TrackInfo object that provides context that helps disambiguate similar-looking albums and tracks. """ disambig = [] if info.data_source and info.data_source != 'MusicBrainz': disambig.append(info.data_source) if isinstance(info, hooks.AlbumInfo): if info.media: if info.mediums > 1: disambig.append(u'{0}x{1}'.format( info.mediums, info.media )) else: disambig.append(info.media) if info.year: disambig.append(unicode(info.year)) if info.country: disambig.append(info.country) if info.label: disambig.append(info.label) if info.albumdisambig: disambig.append(info.albumdisambig) if disambig: return u', '.join(disambig) def dist_string(dist): """Formats a distance (a float) as a colorized similarity percentage string. """ out = '%.1f%%' % ((1 - dist) * 100) if dist <= config['match']['strong_rec_thresh'].as_number(): out = ui.colorize('green', out) elif dist <= config['match']['medium_rec_thresh'].as_number(): out = ui.colorize('yellow', out) else: out = ui.colorize('red', out) return out def penalty_string(distance, limit=None): """Returns a colorized string that indicates all the penalties applied to a distance object. """ penalties = [] for key in distance.keys(): key = key.replace('album_', '') key = key.replace('track_', '') key = key.replace('_', ' ') penalties.append(key) if penalties: if limit and len(penalties) > limit: penalties = penalties[:limit] + ['...'] return ui.colorize('yellow', '(%s)' % ', '.join(penalties)) def show_change(cur_artist, cur_album, match): """Print out a representation of the changes that will be made if an album's tags are changed according to `match`, which must be an AlbumMatch object. """ def show_album(artist, album): if artist: album_description = u' %s - %s' % (artist, album) elif album: album_description = u' %s' % album else: album_description = u' (unknown album)' print_(album_description) def format_index(track_info): """Return a string representing the track index of the given TrackInfo or Item object. """ if isinstance(track_info, hooks.TrackInfo): index = track_info.index medium_index = track_info.medium_index medium = track_info.medium mediums = match.info.mediums else: index = medium_index = track_info.track medium = track_info.disc mediums = track_info.disctotal if config['per_disc_numbering']: if mediums > 1: return u'{0}-{1}'.format(medium, medium_index) else: return unicode(medium_index) else: return unicode(index) # Identify the album in question. if cur_artist != match.info.artist or \ (cur_album != match.info.album and match.info.album != VARIOUS_ARTISTS): artist_l, artist_r = cur_artist or '', match.info.artist album_l, album_r = cur_album or '', match.info.album if artist_r == VARIOUS_ARTISTS: # Hide artists for VA releases. artist_l, artist_r = u'', u'' artist_l, artist_r = ui.colordiff(artist_l, artist_r) album_l, album_r = ui.colordiff(album_l, album_r) print_("Correcting tags from:") show_album(artist_l, album_l) print_("To:") show_album(artist_r, album_r) else: print_(u"Tagging:\n {0.artist} - {0.album}".format(match.info)) # Data URL. if match.info.data_url: print_('URL:\n %s' % match.info.data_url) # Info line. info = [] # Similarity. info.append('(Similarity: %s)' % dist_string(match.distance)) # Penalties. penalties = penalty_string(match.distance) if penalties: info.append(penalties) # Disambiguation. disambig = disambig_string(match.info) if disambig: info.append(ui.colorize('lightgray', '(%s)' % disambig)) print_(' '.join(info)) # Tracks. pairs = match.mapping.items() pairs.sort(key=lambda (_, track_info): track_info.index) # Build up LHS and RHS for track difference display. The `lines` list # contains ``(lhs, rhs, width)`` tuples where `width` is the length (in # characters) of the uncolorized LHS. lines = [] medium = disctitle = None for item, track_info in pairs: # Medium number and title. if medium != track_info.medium or disctitle != track_info.disctitle: media = match.info.media or 'Media' if match.info.mediums > 1 and track_info.disctitle: lhs = '%s %s: %s' % (media, track_info.medium, track_info.disctitle) elif match.info.mediums > 1: lhs = '%s %s' % (media, track_info.medium) elif track_info.disctitle: lhs = '%s: %s' % (media, track_info.disctitle) else: lhs = None if lhs: lines.append((lhs, '', 0)) medium, disctitle = track_info.medium, track_info.disctitle # Titles. new_title = track_info.title if not item.title.strip(): # If there's no title, we use the filename. cur_title = displayable_path(os.path.basename(item.path)) lhs, rhs = cur_title, new_title else: cur_title = item.title.strip() lhs, rhs = ui.colordiff(cur_title, new_title) lhs_width = len(cur_title) # Track number change. cur_track, new_track = format_index(item), format_index(track_info) if cur_track != new_track: if item.track in (track_info.index, track_info.medium_index): color = 'lightgray' else: color = 'red' if (cur_track + new_track).count('-') == 1: lhs_track, rhs_track = (ui.colorize(color, cur_track), ui.colorize(color, new_track)) else: color = 'red' lhs_track, rhs_track = ui.color_diff_suffix(cur_track, new_track) templ = (ui.colorize(color, u' (#') + u'{0}' + ui.colorize(color, u')')) lhs += templ.format(lhs_track) rhs += templ.format(rhs_track) lhs_width += len(cur_track) + 4 # Length change. if item.length and track_info.length and \ abs(item.length - track_info.length) > \ config['ui']['length_diff_thresh'].as_number(): cur_length = ui.human_seconds_short(item.length) new_length = ui.human_seconds_short(track_info.length) lhs_length, rhs_length = ui.color_diff_suffix(cur_length, new_length) templ = (ui.colorize('red', u' (') + u'{0}' + ui.colorize('red', u')')) lhs += templ.format(lhs_length) rhs += templ.format(rhs_length) lhs_width += len(cur_length) + 3 # Penalties. penalties = penalty_string(match.distance.tracks[track_info]) if penalties: rhs += ' %s' % penalties if lhs != rhs: lines.append((' * %s' % lhs, rhs, lhs_width)) elif config['import']['detail']: lines.append((' * %s' % lhs, '', lhs_width)) # Print each track in two columns, or across two lines. col_width = (ui.term_width() - len(''.join([' * ', ' -> ']))) // 2 if lines: max_width = max(w for _, _, w in lines) for lhs, rhs, lhs_width in lines: if not rhs: print_(lhs) elif max_width > col_width: print_(u'%s ->\n %s' % (lhs, rhs)) else: pad = max_width - lhs_width print_(u'%s%s -> %s' % (lhs, ' ' * pad, rhs)) # Missing and unmatched tracks. if match.extra_tracks: print_('Missing tracks:') for track_info in match.extra_tracks: line = ' ! %s (#%s)' % (track_info.title, format_index(track_info)) if track_info.length: line += ' (%s)' % ui.human_seconds_short(track_info.length) print_(ui.colorize('yellow', line)) if match.extra_items: print_('Unmatched tracks:') for item in match.extra_items: line = ' ! %s (#%s)' % (item.title, format_index(item)) if item.length: line += ' (%s)' % ui.human_seconds_short(item.length) print_(ui.colorize('yellow', line)) def show_item_change(item, match): """Print out the change that would occur by tagging `item` with the metadata from `match`, a TrackMatch object. """ cur_artist, new_artist = item.artist, match.info.artist cur_title, new_title = item.title, match.info.title if cur_artist != new_artist or cur_title != new_title: cur_artist, new_artist = ui.colordiff(cur_artist, new_artist) cur_title, new_title = ui.colordiff(cur_title, new_title) print_("Correcting track tags from:") print_(" %s - %s" % (cur_artist, cur_title)) print_("To:") print_(" %s - %s" % (new_artist, new_title)) else: print_("Tagging track: %s - %s" % (cur_artist, cur_title)) # Data URL. if match.info.data_url: print_('URL:\n %s' % match.info.data_url) # Info line. info = [] # Similarity. info.append('(Similarity: %s)' % dist_string(match.distance)) # Penalties. penalties = penalty_string(match.distance) if penalties: info.append(penalties) # Disambiguation. disambig = disambig_string(match.info) if disambig: info.append(ui.colorize('lightgray', '(%s)' % disambig)) print_(' '.join(info)) def summarize_items(items, singleton): """Produces a brief summary line describing a set of items. Used for manually resolving duplicates during import. `items` is a list of `Item` objects. `singleton` indicates whether this is an album or single-item import (if the latter, them `items` should only have one element). """ summary_parts = [] if not singleton: summary_parts.append("{0} items".format(len(items))) format_counts = {} for item in items: format_counts[item.format] = format_counts.get(item.format, 0) + 1 if len(format_counts) == 1: # A single format. summary_parts.append(items[0].format) else: # Enumerate all the formats. for format, count in format_counts.iteritems(): summary_parts.append('{0} {1}'.format(format, count)) average_bitrate = sum([item.bitrate for item in items]) / len(items) total_duration = sum([item.length for item in items]) summary_parts.append('{0}kbps'.format(int(average_bitrate / 1000))) summary_parts.append(ui.human_seconds_short(total_duration)) return ', '.join(summary_parts) def _summary_judment(rec): """Determines whether a decision should be made without even asking the user. This occurs in quiet mode and when an action is chosen for NONE recommendations. Return an action or None if the user should be queried. May also print to the console if a summary judgment is made. """ if config['import']['quiet']: if rec == Recommendation.strong: return importer.action.APPLY else: action = config['import']['quiet_fallback'].as_choice({ 'skip': importer.action.SKIP, 'asis': importer.action.ASIS, }) elif rec == Recommendation.none: action = config['import']['none_rec_action'].as_choice({ 'skip': importer.action.SKIP, 'asis': importer.action.ASIS, 'ask': None, }) else: return None if action == importer.action.SKIP: print_('Skipping.') elif action == importer.action.ASIS: print_('Importing as-is.') return action def choose_candidate(candidates, singleton, rec, cur_artist=None, cur_album=None, item=None, itemcount=None): """Given a sorted list of candidates, ask the user for a selection of which candidate to use. Applies to both full albums and singletons (tracks). Candidates are either AlbumMatch or TrackMatch objects depending on `singleton`. for albums, `cur_artist`, `cur_album`, and `itemcount` must be provided. For singletons, `item` must be provided. Returns the result of the choice, which may SKIP, ASIS, TRACKS, or MANUAL or a candidate (an AlbumMatch/TrackMatch object). """ # Sanity check. if singleton: assert item is not None else: assert cur_artist is not None assert cur_album is not None # Zero candidates. if not candidates: if singleton: print_("No matching recordings found.") opts = ('Use as-is', 'Skip', 'Enter search', 'enter Id', 'aBort') else: print_("No matching release found for {0} tracks." .format(itemcount)) print_('For help, see: ' 'http://beets.readthedocs.org/en/latest/faq.html#nomatch') opts = ('Use as-is', 'as Tracks', 'Group albums', 'Skip', 'Enter search', 'enter Id', 'aBort') sel = ui.input_options(opts) if sel == 'u': return importer.action.ASIS elif sel == 't': assert not singleton return importer.action.TRACKS elif sel == 'e': return importer.action.MANUAL elif sel == 's': return importer.action.SKIP elif sel == 'b': raise importer.ImportAbort() elif sel == 'i': return importer.action.MANUAL_ID elif sel == 'g': return importer.action.ALBUMS else: assert False # Is the change good enough? bypass_candidates = False if rec != Recommendation.none: match = candidates[0] bypass_candidates = True while True: # Display and choose from candidates. require = rec <= Recommendation.low if not bypass_candidates: # Display list of candidates. print_(u'Finding tags for {0} "{1} - {2}".'.format( u'track' if singleton else u'album', item.artist if singleton else cur_artist, item.title if singleton else cur_album, )) print_(u'Candidates:') for i, match in enumerate(candidates): # Index, metadata, and distance. line = [ u'{0}.'.format(i + 1), u'{0} - {1}'.format( match.info.artist, match.info.title if singleton else match.info.album, ), u'({0})'.format(dist_string(match.distance)), ] # Penalties. penalties = penalty_string(match.distance, 3) if penalties: line.append(penalties) # Disambiguation disambig = disambig_string(match.info) if disambig: line.append(ui.colorize('lightgray', '(%s)' % disambig)) print_(' '.join(line)) # Ask the user for a choice. if singleton: opts = ('Skip', 'Use as-is', 'Enter search', 'enter Id', 'aBort') else: opts = ('Skip', 'Use as-is', 'as Tracks', 'Group albums', 'Enter search', 'enter Id', 'aBort') sel = ui.input_options(opts, numrange=(1, len(candidates))) if sel == 's': return importer.action.SKIP elif sel == 'u': return importer.action.ASIS elif sel == 'm': pass elif sel == 'e': return importer.action.MANUAL elif sel == 't': assert not singleton return importer.action.TRACKS elif sel == 'b': raise importer.ImportAbort() elif sel == 'i': return importer.action.MANUAL_ID elif sel == 'g': return importer.action.ALBUMS else: # Numerical selection. match = candidates[sel - 1] if sel != 1: # When choosing anything but the first match, # disable the default action. require = True bypass_candidates = False # Show what we're about to do. if singleton: show_item_change(item, match) else: show_change(cur_artist, cur_album, match) # Exact match => tag automatically if we're not in timid mode. if rec == Recommendation.strong and not config['import']['timid']: return match # Ask for confirmation. if singleton: opts = ('Apply', 'More candidates', 'Skip', 'Use as-is', 'Enter search', 'enter Id', 'aBort') else: opts = ('Apply', 'More candidates', 'Skip', 'Use as-is', 'as Tracks', 'Group albums', 'Enter search', 'enter Id', 'aBort') default = config['import']['default_action'].as_choice({ 'apply': 'a', 'skip': 's', 'asis': 'u', 'none': None, }) if default is None: require = True sel = ui.input_options(opts, require=require, default=default) if sel == 'a': return match elif sel == 'g': return importer.action.ALBUMS elif sel == 's': return importer.action.SKIP elif sel == 'u': return importer.action.ASIS elif sel == 't': assert not singleton return importer.action.TRACKS elif sel == 'e': return importer.action.MANUAL elif sel == 'b': raise importer.ImportAbort() elif sel == 'i': return importer.action.MANUAL_ID def manual_search(singleton): """Input either an artist and album (for full albums) or artist and track name (for singletons) for manual search. """ artist = input_('Artist:') name = input_('Track:' if singleton else 'Album:') return artist.strip(), name.strip() def manual_id(singleton): """Input an ID, either for an album ("release") or a track ("recording"). """ prompt = u'Enter {0} ID:'.format('recording' if singleton else 'release') return input_(prompt).strip() class TerminalImportSession(importer.ImportSession): """An import session that runs in a terminal. """ def choose_match(self, task): """Given an initial autotagging of items, go through an interactive dance with the user to ask for a choice of metadata. Returns an AlbumMatch object, ASIS, or SKIP. """ # Show what we're tagging. print_() print_(displayable_path(task.paths, u'\n') + u' ({0} items)'.format(len(task.items))) # Take immediate action if appropriate. action = _summary_judment(task.rec) if action == importer.action.APPLY: match = task.candidates[0] show_change(task.cur_artist, task.cur_album, match) return match elif action is not None: return action # Loop until we have a choice. candidates, rec = task.candidates, task.rec while True: # Ask for a choice from the user. choice = choose_candidate( candidates, False, rec, task.cur_artist, task.cur_album, itemcount=len(task.items) ) # Choose which tags to use. if choice in (importer.action.SKIP, importer.action.ASIS, importer.action.TRACKS, importer.action.ALBUMS): # Pass selection to main control flow. return choice elif choice is importer.action.MANUAL: # Try again with manual search terms. search_artist, search_album = manual_search(False) _, _, candidates, rec = autotag.tag_album( task.items, search_artist, search_album ) elif choice is importer.action.MANUAL_ID: # Try a manually-entered ID. search_id = manual_id(False) if search_id: _, _, candidates, rec = autotag.tag_album( task.items, search_id=search_id ) else: # We have a candidate! Finish tagging. Here, choice is an # AlbumMatch object. assert isinstance(choice, autotag.AlbumMatch) return choice def choose_item(self, task): """Ask the user for a choice about tagging a single item. Returns either an action constant or a TrackMatch object. """ print_() print_(task.item.path) candidates, rec = task.candidates, task.rec # Take immediate action if appropriate. action = _summary_judment(task.rec) if action == importer.action.APPLY: match = candidates[0] show_item_change(task.item, match) return match elif action is not None: return action while True: # Ask for a choice. choice = choose_candidate(candidates, True, rec, item=task.item) if choice in (importer.action.SKIP, importer.action.ASIS): return choice elif choice == importer.action.TRACKS: assert False # TRACKS is only legal for albums. elif choice == importer.action.MANUAL: # Continue in the loop with a new set of candidates. search_artist, search_title = manual_search(True) candidates, rec = autotag.tag_item(task.item, search_artist, search_title) elif choice == importer.action.MANUAL_ID: # Ask for a track ID. search_id = manual_id(True) if search_id: candidates, rec = autotag.tag_item(task.item, search_id=search_id) else: # Chose a candidate. assert isinstance(choice, autotag.TrackMatch) return choice def resolve_duplicate(self, task, found_duplicates): """Decide what to do when a new album or item seems similar to one that's already in the library. """ log.warn(u"This {0} is already in the library!" .format("album" if task.is_album else "item")) if config['import']['quiet']: # In quiet mode, don't prompt -- just skip. log.info(u'Skipping.') sel = 's' else: # Print some detail about the existing and new items so the # user can make an informed decision. for duplicate in found_duplicates: print("Old: " + summarize_items( list(duplicate.items()) if task.is_album else [duplicate], not task.is_album, )) print("New: " + summarize_items( task.imported_items(), not task.is_album, )) sel = ui.input_options( ('Skip new', 'Keep both', 'Remove old') ) if sel == 's': # Skip new. task.set_choice(importer.action.SKIP) elif sel == 'k': # Keep both. Do nothing; leave the choice intact. pass elif sel == 'r': # Remove old. task.should_remove_duplicates = True else: assert False def should_resume(self, path): return ui.input_yn(u"Import of the directory:\n{0}\n" "was interrupted. Resume (Y/n)?" .format(displayable_path(path))) # The import command. def import_files(lib, paths, query): """Import the files in the given list of paths or matching the query. """ # Check the user-specified directories. for path in paths: if not os.path.exists(syspath(normpath(path))): raise ui.UserError(u'no such file or directory: {0}'.format( displayable_path(path))) # Check parameter consistency. if config['import']['quiet'] and config['import']['timid']: raise ui.UserError("can't be both quiet and timid") # Open the log. if config['import']['log'].get() is not None: logpath = config['import']['log'].as_filename() try: logfile = codecs.open(syspath(logpath), 'a', 'utf8') except IOError: raise ui.UserError(u"could not open log file for writing: %s" % displayable_path(logpath)) print(u'import started', time.asctime(), file=logfile) else: logfile = None # Never ask for input in quiet mode. if config['import']['resume'].get() == 'ask' and \ config['import']['quiet']: config['import']['resume'] = False session = TerminalImportSession(lib, logfile, paths, query) try: session.run() finally: # If we were logging, close the file. if logfile: print(u'', file=logfile) logfile.close() # Emit event. plugins.send('import', lib=lib, paths=paths) def import_func(lib, opts, args): config['import'].set_args(opts) # Special case: --copy flag suppresses import_move (which would # otherwise take precedence). if opts.copy: config['import']['move'] = False if opts.library: query = decargs(args) paths = [] else: query = None paths = args if not paths: raise ui.UserError('no path specified') import_files(lib, paths, query) import_cmd = ui.Subcommand( 'import', help='import new music', aliases=('imp', 'im') ) import_cmd.parser.add_option( '-c', '--copy', action='store_true', default=None, help="copy tracks into library directory (default)" ) import_cmd.parser.add_option( '-C', '--nocopy', action='store_false', dest='copy', help="don't copy tracks (opposite of -c)" ) import_cmd.parser.add_option( '-w', '--write', action='store_true', default=None, help="write new metadata to files' tags (default)" ) import_cmd.parser.add_option( '-W', '--nowrite', action='store_false', dest='write', help="don't write metadata (opposite of -w)" ) import_cmd.parser.add_option( '-a', '--autotag', action='store_true', dest='autotag', help="infer tags for imported files (default)" ) import_cmd.parser.add_option( '-A', '--noautotag', action='store_false', dest='autotag', help="don't infer tags for imported files (opposite of -a)" ) import_cmd.parser.add_option( '-p', '--resume', action='store_true', default=None, help="resume importing if interrupted" ) import_cmd.parser.add_option( '-P', '--noresume', action='store_false', dest='resume', help="do not try to resume importing" ) import_cmd.parser.add_option( '-q', '--quiet', action='store_true', dest='quiet', help="never prompt for input: skip albums instead" ) import_cmd.parser.add_option( '-l', '--log', dest='log', help='file to log untaggable albums for later review' ) import_cmd.parser.add_option( '-s', '--singletons', action='store_true', help='import individual tracks instead of full albums' ) import_cmd.parser.add_option( '-t', '--timid', dest='timid', action='store_true', help='always confirm all actions' ) import_cmd.parser.add_option( '-L', '--library', dest='library', action='store_true', help='retag items matching a query' ) import_cmd.parser.add_option( '-i', '--incremental', dest='incremental', action='store_true', help='skip already-imported directories' ) import_cmd.parser.add_option( '-I', '--noincremental', dest='incremental', action='store_false', help='do not skip already-imported directories' ) import_cmd.parser.add_option( '--flat', dest='flat', action='store_true', help='import an entire tree as a single album' ) import_cmd.parser.add_option( '-g', '--group-albums', dest='group_albums', action='store_true', help='group tracks in a folder into separate albums' ) import_cmd.func = import_func default_commands.append(import_cmd) # list: Query and show library contents. def list_items(lib, query, album, fmt): """Print out items in lib matching query. If album, then search for albums instead of single items. """ tmpl = Template(ui._pick_format(album, fmt)) if album: for album in lib.albums(query): ui.print_obj(album, lib, tmpl) else: for item in lib.items(query): ui.print_obj(item, lib, tmpl) def list_func(lib, opts, args): if opts.path: fmt = '$path' else: fmt = opts.format list_items(lib, decargs(args), opts.album, fmt) list_cmd = ui.Subcommand('list', help='query the library', aliases=('ls',)) list_cmd.parser.add_option( '-a', '--album', action='store_true', help='show matching albums instead of tracks' ) list_cmd.parser.add_option( '-p', '--path', action='store_true', help='print paths for matched items or albums' ) list_cmd.parser.add_option( '-f', '--format', action='store', help='print with custom format', default=None ) list_cmd.func = list_func default_commands.append(list_cmd) # update: Update library contents according to on-disk tags. def update_items(lib, query, album, move, pretend): """For all the items matched by the query, update the library to reflect the item's embedded tags. """ with lib.transaction(): items, _ = _do_query(lib, query, album) # Walk through the items and pick up their changes. affected_albums = set() for item in items: # Item deleted? if not os.path.exists(syspath(item.path)): ui.print_obj(item, lib) ui.print_(ui.colorize('red', u' deleted')) if not pretend: item.remove(True) affected_albums.add(item.album_id) continue # Did the item change since last checked? if item.current_mtime() <= item.mtime: log.debug(u'skipping {0} because mtime is up to date ({1})' .format(displayable_path(item.path), item.mtime)) continue # Read new data. try: item.read() except library.ReadError as exc: log.error(u'error reading {0}: {1}'.format( displayable_path(item.path), exc)) continue # Special-case album artist when it matches track artist. (Hacky # but necessary for preserving album-level metadata for non- # autotagged imports.) if not item.albumartist: old_item = lib.get_item(item.id) if old_item.albumartist == old_item.artist == item.artist: item.albumartist = old_item.albumartist item._dirty.discard('albumartist') # Check for and display changes. changed = ui.show_model_changes(item, fields=library.Item._media_fields) # Save changes. if not pretend: if changed: # Move the item if it's in the library. if move and lib.directory in ancestry(item.path): item.move() item.store() affected_albums.add(item.album_id) else: # The file's mtime was different, but there were no # changes to the metadata. Store the new mtime, # which is set in the call to read(), so we don't # check this again in the future. item.store() # Skip album changes while pretending. if pretend: return # Modify affected albums to reflect changes in their items. for album_id in affected_albums: if album_id is None: # Singletons. continue album = lib.get_album(album_id) if not album: # Empty albums have already been removed. log.debug(u'emptied album {0}'.format(album_id)) continue first_item = album.items().get() # Update album structure to reflect an item in it. for key in library.Album.item_keys: album[key] = first_item[key] album.store() # Move album art (and any inconsistent items). if move and lib.directory in ancestry(first_item.path): log.debug(u'moving album {0}'.format(album_id)) album.move() def update_func(lib, opts, args): update_items(lib, decargs(args), opts.album, opts.move, opts.pretend) update_cmd = ui.Subcommand( 'update', help='update the library', aliases=('upd', 'up',) ) update_cmd.parser.add_option( '-a', '--album', action='store_true', help='match albums instead of tracks' ) update_cmd.parser.add_option( '-M', '--nomove', action='store_false', default=True, dest='move', help="don't move files in library" ) update_cmd.parser.add_option( '-p', '--pretend', action='store_true', help="show all changes but do nothing" ) update_cmd.parser.add_option( '-f', '--format', action='store', help='print with custom format', default=None ) update_cmd.func = update_func default_commands.append(update_cmd) # remove: Remove items from library, delete files. def remove_items(lib, query, album, delete): """Remove items matching query from lib. If album, then match and remove whole albums. If delete, also remove files from disk. """ # Get the matching items. items, albums = _do_query(lib, query, album) # Prepare confirmation with user. print_() if delete: fmt = u'$path - $title' prompt = 'Really DELETE %i files (y/n)?' % len(items) else: fmt = None prompt = 'Really remove %i items from the library (y/n)?' % \ len(items) # Show all the items. for item in items: ui.print_obj(item, lib, fmt) # Confirm with user. if not ui.input_yn(prompt, True): return # Remove (and possibly delete) items. with lib.transaction(): for obj in (albums if album else items): obj.remove(delete) def remove_func(lib, opts, args): remove_items(lib, decargs(args), opts.album, opts.delete) remove_cmd = ui.Subcommand( 'remove', help='remove matching items from the library', aliases=('rm',) ) remove_cmd.parser.add_option( "-d", "--delete", action="store_true", help="also remove files from disk" ) remove_cmd.parser.add_option( '-a', '--album', action='store_true', help='match albums instead of tracks' ) remove_cmd.func = remove_func default_commands.append(remove_cmd) # stats: Show library/query statistics. def show_stats(lib, query, exact): """Shows some statistics about the matched items.""" items = lib.items(query) total_size = 0 total_time = 0.0 total_items = 0 artists = set() albums = set() album_artists = set() for item in items: if exact: total_size += os.path.getsize(item.path) else: total_size += int(item.length * item.bitrate / 8) total_time += item.length total_items += 1 artists.add(item.artist) album_artists.add(item.albumartist) if item.album_id: albums.add(item.album_id) size_str = '' + ui.human_bytes(total_size) if exact: size_str += ' ({0} bytes)'.format(total_size) print_("""Tracks: {0} Total time: {1} ({2:.2f} seconds) Total size: {3} Artists: {4} Albums: {5} Album artists: {6}""".format(total_items, ui.human_seconds(total_time), total_time, size_str, len(artists), len(albums), len(album_artists))) def stats_func(lib, opts, args): show_stats(lib, decargs(args), opts.exact) stats_cmd = ui.Subcommand( 'stats', help='show statistics about the library or a query' ) stats_cmd.parser.add_option( '-e', '--exact', action='store_true', help='get exact file sizes' ) stats_cmd.func = stats_func default_commands.append(stats_cmd) # version: Show current beets version. def show_version(lib, opts, args): print_('beets version %s' % beets.__version__) # Show plugins. names = [p.name for p in plugins.find_plugins()] if names: print_('plugins:', ', '.join(names)) else: print_('no plugins loaded') version_cmd = ui.Subcommand( 'version', help='output version information' ) version_cmd.func = show_version default_commands.append(version_cmd) # modify: Declaratively change metadata. def modify_items(lib, mods, dels, query, write, move, album, confirm): """Modifies matching items according to user-specified assignments and deletions. `mods` is a dictionary of field and value pairse indicating assignments. `dels` is a list of fields to be deleted. """ # Parse key=value specifications into a dictionary. model_cls = library.Album if album else library.Item for key, value in mods.items(): mods[key] = model_cls._parse(key, value) # Get the items to modify. items, albums = _do_query(lib, query, album, False) objs = albums if album else items # Apply changes *temporarily*, preview them, and collect modified # objects. print_('Modifying {0} {1}s.' .format(len(objs), 'album' if album else 'item')) changed = set() for obj in objs: obj.update(mods) for field in dels: del obj[field] if ui.show_model_changes(obj): changed.add(obj) # Still something to do? if not changed: print_('No changes to make.') return # Confirm action. if confirm: if write and move: extra = ', move and write tags' elif write: extra = ' and write tags' elif move: extra = ' and move' else: extra = '' if not ui.input_yn('Really modify%s (Y/n)?' % extra): return # Apply changes to database and files with lib.transaction(): for obj in changed: if move: cur_path = obj.path if lib.directory in ancestry(cur_path): # In library? log.debug(u'moving object {0}' .format(displayable_path(cur_path))) obj.move() obj.try_sync(write) def modify_parse_args(args): """Split the arguments for the modify subcommand into query parts, assignments (field=value), and deletions (field!). Returns the result as a three-tuple in that order. """ mods = {} dels = [] query = [] for arg in args: if arg.endswith('!') and '=' not in arg and ':' not in arg: dels.append(arg[:-1]) # Strip trailing !. elif '=' in arg and ':' not in arg.split('=', 1)[0]: key, val = arg.split('=', 1) mods[key] = val else: query.append(arg) return query, mods, dels def modify_func(lib, opts, args): query, mods, dels = modify_parse_args(decargs(args)) if not mods and not dels: raise ui.UserError('no modifications specified') write = opts.write if opts.write is not None else \ config['import']['write'].get(bool) modify_items(lib, mods, dels, query, write, opts.move, opts.album, not opts.yes) modify_cmd = ui.Subcommand( 'modify', help='change metadata fields', aliases=('mod',) ) modify_cmd.parser.add_option( '-M', '--nomove', action='store_false', default=True, dest='move', help="don't move files in library" ) modify_cmd.parser.add_option( '-w', '--write', action='store_true', default=None, help="write new metadata to files' tags (default)" ) modify_cmd.parser.add_option( '-W', '--nowrite', action='store_false', dest='write', help="don't write metadata (opposite of -w)" ) modify_cmd.parser.add_option( '-a', '--album', action='store_true', help='modify whole albums instead of tracks' ) modify_cmd.parser.add_option( '-y', '--yes', action='store_true', help='skip confirmation' ) modify_cmd.parser.add_option( '-f', '--format', action='store', help='print with custom format', default=None ) modify_cmd.func = modify_func default_commands.append(modify_cmd) # move: Move/copy files to the library or a new base directory. def move_items(lib, dest, query, copy, album): """Moves or copies items to a new base directory, given by dest. If dest is None, then the library's base directory is used, making the command "consolidate" files. """ items, albums = _do_query(lib, query, album, False) objs = albums if album else items action = 'Copying' if copy else 'Moving' entity = 'album' if album else 'item' log.info(u'{0} {1} {2}s.'.format(action, len(objs), entity)) for obj in objs: log.debug(u'moving: {0}'.format(util.displayable_path(obj.path))) obj.move(copy, basedir=dest) obj.store() def move_func(lib, opts, args): dest = opts.dest if dest is not None: dest = normpath(dest) if not os.path.isdir(dest): raise ui.UserError('no such directory: %s' % dest) move_items(lib, dest, decargs(args), opts.copy, opts.album) move_cmd = ui.Subcommand( 'move', help='move or copy items', aliases=('mv',) ) move_cmd.parser.add_option( '-d', '--dest', metavar='DIR', dest='dest', help='destination directory' ) move_cmd.parser.add_option( '-c', '--copy', default=False, action='store_true', help='copy instead of moving' ) move_cmd.parser.add_option( '-a', '--album', default=False, action='store_true', help='match whole albums instead of tracks' ) move_cmd.func = move_func default_commands.append(move_cmd) # write: Write tags into files. def write_items(lib, query, pretend, force): """Write tag information from the database to the respective files in the filesystem. """ items, albums = _do_query(lib, query, False, False) for item in items: # Item deleted? if not os.path.exists(syspath(item.path)): log.info(u'missing file: {0}'.format( util.displayable_path(item.path) )) continue # Get an Item object reflecting the "clean" (on-disk) state. try: clean_item = library.Item.from_path(item.path) except library.ReadError as exc: log.error(u'error reading {0}: {1}'.format( displayable_path(item.path), exc )) continue # Check for and display changes. changed = ui.show_model_changes(item, clean_item, library.Item._media_fields, force) if (changed or force) and not pretend: item.try_sync() def write_func(lib, opts, args): write_items(lib, decargs(args), opts.pretend, opts.force) write_cmd = ui.Subcommand('write', help='write tag information to files') write_cmd.parser.add_option( '-p', '--pretend', action='store_true', help="show all changes but do nothing" ) write_cmd.parser.add_option( '-f', '--force', action='store_true', help="write tags even if the existing tags match the database" ) write_cmd.func = write_func default_commands.append(write_cmd) # config: Show and edit user configuration. def config_func(lib, opts, args): # Make sure lazy configuration is loaded config.resolve() # Print paths. if opts.paths: filenames = [] for source in config.sources: if not opts.defaults and source.default: continue if source.filename: filenames.append(source.filename) # In case the user config file does not exist, prepend it to the # list. user_path = config.user_config_path() if user_path not in filenames: filenames.insert(0, user_path) for filename in filenames: print(filename) # Open in editor. elif opts.edit: path = config.user_config_path() if 'EDITOR' in os.environ: editor = os.environ['EDITOR'] args = [editor, editor, path] elif platform.system() == 'Darwin': args = ['open', 'open', '-n', path] elif platform.system() == 'Windows': # On windows we can execute arbitrary files. The os will # take care of starting an appropriate application args = [path, path] else: # Assume Unix args = ['xdg-open', 'xdg-open', path] try: os.execlp(*args) except OSError: raise ui.UserError("Could not edit configuration. Please" "set the EDITOR environment variable.") # Dump configuration. else: print(config.dump(full=opts.defaults)) config_cmd = ui.Subcommand('config', help='show or edit the user configuration') config_cmd.parser.add_option( '-p', '--paths', action='store_true', help='show files that configuration was loaded from' ) config_cmd.parser.add_option( '-e', '--edit', action='store_true', help='edit user configuration with $EDITOR' ) config_cmd.parser.add_option( '-d', '--defaults', action='store_true', help='include the default configuration' ) config_cmd.func = config_func default_commands.append(config_cmd) # completion: print completion script def print_completion(*args): for line in completion_script(default_commands + plugins.commands()): print(line, end='') if not any(map(os.path.isfile, BASH_COMPLETION_PATHS)): log.warn(u'Warning: Unable to find the bash-completion package. ' u'Command line completion might not work.') BASH_COMPLETION_PATHS = map(syspath, [ u'/etc/bash_completion', u'/usr/share/bash-completion/bash_completion', u'/usr/share/local/bash-completion/bash_completion', u'/opt/local/share/bash-completion/bash_completion', # SmartOS u'/usr/local/etc/bash_completion', # Homebrew ]) def completion_script(commands): """Yield the full completion shell script as strings. ``commands`` is alist of ``ui.Subcommand`` instances to generate completion data for. """ base_script = os.path.join(_package_path('beets.ui'), 'completion_base.sh') with open(base_script, 'r') as base_script: yield base_script.read() options = {} aliases = {} command_names = [] # Collect subcommands for cmd in commands: name = cmd.name command_names.append(name) for alias in cmd.aliases: if re.match(r'^\w+$', alias): aliases[alias] = name options[name] = {'flags': [], 'opts': []} for opts in cmd.parser._get_all_options()[1:]: if opts.action in ('store_true', 'store_false'): option_type = 'flags' else: option_type = 'opts' options[name][option_type].extend( opts._short_opts + opts._long_opts ) # Add global options options['_global'] = { 'flags': ['-v', '--verbose'], 'opts': '-l --library -c --config -d --directory -h --help'.split(' ') } # Add flags common to all commands options['_common'] = { 'flags': ['-h', '--help'] } # Start generating the script yield "_beet() {\n" # Command names yield " local commands='%s'\n" % ' '.join(command_names) yield "\n" # Command aliases yield " local aliases='%s'\n" % ' '.join(aliases.keys()) for alias, cmd in aliases.items(): yield " local alias__%s=%s\n" % (alias, cmd) yield '\n' # Fields yield " fields='%s'\n" % ' '.join( set(library.Item._fields.keys() + library.Album._fields.keys()) ) # Command options for cmd, opts in options.items(): for option_type, option_list in opts.items(): if option_list: option_list = ' '.join(option_list) yield " local %s__%s='%s'\n" % (option_type, cmd, option_list) yield ' _beet_dispatch\n' yield '}\n' completion_cmd = ui.Subcommand( 'completion', help='print shell script that provides command line completion' ) completion_cmd.func = print_completion completion_cmd.hide = True default_commands.append(completion_cmd) beets-1.3.8/beets/ui/completion_base.sh0000644000076500000240000001050312317325515021012 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright (c) 2014, Thomas Scholtes. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. # Completion for the `beet` command # ================================= # # Load this script to complete beets subcommands, options, and # queries. # # If a beets command is found on the command line it completes filenames and # the subcommand's options. Otherwise it will complete global options and # subcommands. If the previous option on the command line expects an argument, # it also completes filenames or directories. Options are only # completed if '-' has already been typed on the command line. # # Note that completion of plugin commands only works for those plugins # that were enabled when running `beet completion`. It does not check # plugins dynamically # # Currently, only Bash 3.2 and newer is supported and the # `bash-completion` package is requied. # # TODO # ---- # # * There are some issues with arguments that are quoted on the command line. # # * Complete arguments for the `--format` option by expanding field variables. # # beet ls -f "$tit[TAB] # beet ls -f "$title # # * Support long options with `=`, e.g. `--config=file`. Debian's bash # completion package can handle this. # # Determines the beets subcommand and dispatches the completion # accordingly. _beet_dispatch() { local cur prev cmd= COMPREPLY=() _get_comp_words_by_ref -n : cur prev # Look for the beets subcommand local arg for (( i=1; i < COMP_CWORD; i++ )); do arg="${COMP_WORDS[i]}" if _list_include_item "${opts___global}" $arg; then ((i++)) elif [[ "$arg" != -* ]]; then cmd="$arg" break fi done # Replace command shortcuts if [[ -n $cmd ]] && _list_include_item "$aliases" "$cmd"; then eval "cmd=\$alias__$cmd" fi case $cmd in help) COMPREPLY+=( $(compgen -W "$commands" -- $cur) ) ;; list|remove|move|update|write|stats) _beet_complete_query ;; "") _beet_complete_global ;; *) _beet_complete ;; esac } # Adds option and file completion to COMPREPLY for the subcommand $cmd _beet_complete() { if [[ $cur == -* ]]; then local opts flags completions eval "opts=\$opts__$cmd" eval "flags=\$flags__$cmd" completions="${flags___common} ${opts} ${flags}" COMPREPLY+=( $(compgen -W "$completions" -- $cur) ) else _filedir fi } # Add global options and subcommands to the completion _beet_complete_global() { case $prev in -h|--help) # Complete commands COMPREPLY+=( $(compgen -W "$commands" -- $cur) ) return ;; -l|--library|-c|--config) # Filename completion _filedir return ;; -d|--directory) # Directory completion _filedir -d return ;; esac if [[ $cur == -* ]]; then local completions="$opts___global $flags___global" COMPREPLY+=( $(compgen -W "$completions" -- $cur) ) elif [[ -n $cur ]] && _list_include_item "$aliases" "$cur"; then local cmd eval "cmd=\$alias__$cur" COMPREPLY+=( "$cmd" ) else COMPREPLY+=( $(compgen -W "$commands" -- $cur) ) fi } _beet_complete_query() { local opts eval "opts=\$opts__$cmd" if [[ $cur == -* ]] || _list_include_item "$opts" "$prev"; then _beet_complete elif [[ $cur != \'* && $cur != \"* && $cur != *:* ]]; then # Do not complete quoted queries or those who already have a field # set. compopt -o nospace COMPREPLY+=( $(compgen -S : -W "$fields" -- $cur) ) return 0 fi } # Returns true if the space separated list $1 includes $2 _list_include_item() { [[ " $1 " == *[[:space:]]$2[[:space:]]* ]] } # This is where beets dynamically adds the _beet function. This # function sets the variables $flags, $opts, $commands, and $aliases. complete -o filenames -F _beet beet beets-1.3.8/beets/ui/migrate.py0000644000076500000240000003222212322123716017312 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Conversion from legacy (pre-1.1) configuration to Confit/YAML configuration. """ import os import ConfigParser import codecs import yaml import logging import time import itertools import re import beets from beets import util from beets import ui from beets.util import confit CONFIG_PATH_VAR = 'BEETSCONFIG' DEFAULT_CONFIG_FILENAME_UNIX = '.beetsconfig' DEFAULT_CONFIG_FILENAME_WINDOWS = 'beetsconfig.ini' DEFAULT_LIBRARY_FILENAME_UNIX = '.beetsmusic.blb' DEFAULT_LIBRARY_FILENAME_WINDOWS = 'beetsmusic.blb' WINDOWS_BASEDIR = os.environ.get('APPDATA') or '~' OLD_CONFIG_SUFFIX = '.old' PLUGIN_NAMES = { 'rdm': 'random', 'fuzzy_search': 'fuzzy', } AUTO_KEYS = ('automatic', 'autofetch', 'autoembed', 'autoscrub') IMPORTFEEDS_PREFIX = 'feeds_' CONFIG_MIGRATED_MESSAGE = u""" You appear to be upgrading from beets 1.0 (or earlier) to 1.1. Your configuration file has been migrated automatically to: {newconfig} Edit this file to configure beets. You might want to remove your old-style ".beetsconfig" file now. See the documentation for more details on the new configuration system: http://beets.readthedocs.org/page/reference/config.html """.strip() DB_MIGRATED_MESSAGE = u'Your database file has also been copied to:\n{newdb}' YAML_COMMENT = '# Automatically migrated from legacy .beetsconfig.\n\n' log = logging.getLogger('beets') # An itertools recipe. def grouper(n, iterable): args = [iter(iterable)] * n return itertools.izip_longest(*args) def _displace(fn): """Move a file aside using a timestamp suffix so a new file can be put in its place. """ util.move( fn, u'{0}.old.{1}'.format(fn, int(time.time())), True ) def default_paths(): """Produces the appropriate default config and library database paths for the current system. On Unix, this is always in ~. On Windows, tries ~ first and then $APPDATA for the config and library files (for backwards compatibility). """ windows = os.path.__name__ == 'ntpath' if windows: windata = os.environ.get('APPDATA') or '~' # Shorthand for joining paths. def exp(*vals): return os.path.expanduser(os.path.join(*vals)) config = exp('~', DEFAULT_CONFIG_FILENAME_UNIX) if windows and not os.path.exists(config): config = exp(windata, DEFAULT_CONFIG_FILENAME_WINDOWS) libpath = exp('~', DEFAULT_LIBRARY_FILENAME_UNIX) if windows and not os.path.exists(libpath): libpath = exp(windata, DEFAULT_LIBRARY_FILENAME_WINDOWS) return config, libpath def get_config(): """Using the same logic as beets 1.0, locate and read the .beetsconfig file. Return a ConfigParser instance or None if no config is found. """ default_config, default_libpath = default_paths() if CONFIG_PATH_VAR in os.environ: configpath = os.path.expanduser(os.environ[CONFIG_PATH_VAR]) else: configpath = default_config config = ConfigParser.SafeConfigParser() if os.path.exists(util.syspath(configpath)): with codecs.open(configpath, 'r', encoding='utf-8') as f: config.readfp(f) return config, configpath else: return None, configpath def flatten_config(config): """Given a ConfigParser, flatten the values into a dict-of-dicts representation where each section gets its own dictionary of values. """ out = confit.OrderedDict() for section in config.sections(): sec_dict = out[section] = confit.OrderedDict() for option in config.options(section): sec_dict[option] = config.get(section, option, True) return out def transform_value(value): """Given a string read as the value of a config option, return a massaged version of that value (possibly with a different type). """ # Booleans. if value.lower() in ('false', 'no', 'off'): return False elif value.lower() in ('true', 'yes', 'on'): return True # Integers. try: return int(value) except ValueError: pass # Floats. try: return float(value) except ValueError: pass return value def transform_data(data): """Given a dict-of-dicts representation of legacy config data, tweak the data into a new form. This new form is suitable for dumping as YAML. """ out = confit.OrderedDict() for section, pairs in data.items(): if section == 'beets': # The "main" section. In the new config system, these values # are in the "root": no section at all. for key, value in pairs.items(): value = transform_value(value) if key.startswith('import_'): # Importer config is now under an "import:" key. if 'import' not in out: out['import'] = confit.OrderedDict() out['import'][key[7:]] = value elif key == 'plugins': # Renamed plugins. plugins = value.split() new_plugins = [PLUGIN_NAMES.get(p, p) for p in plugins] out['plugins'] = ' '.join(new_plugins) elif key == 'replace': # YAMLy representation for character replacements. replacements = confit.OrderedDict() for pat, repl in grouper(2, value.split()): if repl == '': repl = '' replacements[pat] = repl out['replace'] = replacements elif key == 'pluginpath': # Used to be a colon-separated string. Now a list. out['pluginpath'] = value.split(':') else: out[key] = value elif pairs: # Other sections (plugins, etc). sec_out = out[section] = confit.OrderedDict() for key, value in pairs.items(): # Standardized "auto" option. if key in AUTO_KEYS: key = 'auto' # Unnecessary : hack in queries. if section == 'paths': key = key.replace('_', ':') # Changed option names for importfeeds plugin. if section == 'importfeeds': if key.startswith(IMPORTFEEDS_PREFIX): key = key[len(IMPORTFEEDS_PREFIX):] sec_out[key] = transform_value(value) return out class Dumper(yaml.SafeDumper): """A PyYAML Dumper that represents OrderedDicts as ordinary mappings (in order, of course). """ # From http://pyyaml.org/attachment/ticket/161/use_ordered_dict.py def represent_mapping(self, tag, mapping, flow_style=None): value = [] node = yaml.MappingNode(tag, value, flow_style=flow_style) if self.alias_key is not None: self.represented_objects[self.alias_key] = node best_style = True if hasattr(mapping, 'items'): mapping = list(mapping.items()) for item_key, item_value in mapping: node_key = self.represent_data(item_key) node_value = self.represent_data(item_value) if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style): best_style = False if not (isinstance(node_value, yaml.ScalarNode) and not node_value.style): best_style = False value.append((node_key, node_value)) if flow_style is None: if self.default_flow_style is not None: node.flow_style = self.default_flow_style else: node.flow_style = best_style return node Dumper.add_representer(confit.OrderedDict, Dumper.represent_dict) def migrate_config(replace=False): """Migrate a legacy beetsconfig file to a new-style config.yaml file in an appropriate place. If `replace` is enabled, then any existing config.yaml will be moved aside. Otherwise, the process is aborted when the file exists. """ # Load legacy configuration data, if any. config, configpath = get_config() if not config: log.debug(u'no config file found at {0}'.format( util.displayable_path(configpath) )) return # Get the new configuration file path and possibly move it out of # the way. destfn = os.path.join(beets.config.config_dir(), confit.CONFIG_FILENAME) if os.path.exists(destfn): if replace: log.debug(u'moving old config aside: {0}'.format( util.displayable_path(destfn) )) _displace(destfn) else: # File exists and we won't replace it. We're done. return log.debug(u'migrating config file {0}'.format( util.displayable_path(configpath) )) # Convert the configuration to a data structure ready to be dumped # as the new Confit file. data = transform_data(flatten_config(config)) # Encode result as YAML. yaml_out = yaml.dump( data, Dumper=Dumper, default_flow_style=False, indent=4, width=1000, ) # A ridiculous little hack to add some whitespace between "sections" # in the YAML output. I hope this doesn't break any YAML syntax. yaml_out = re.sub(r'(\n\w+:\n [^-\s])', '\n\\1', yaml_out) yaml_out = YAML_COMMENT + yaml_out # Write the data to the new config destination. log.debug(u'writing migrated config to {0}'.format( util.displayable_path(destfn) )) with open(destfn, 'w') as f: f.write(yaml_out) return destfn def migrate_db(replace=False): """Copy the beets library database file to the new location (e.g., from ~/.beetsmusic.blb to ~/.config/beets/library.db). """ _, srcfn = default_paths() destfn = beets.config['library'].as_filename() if not os.path.exists(srcfn) or srcfn == destfn: # Old DB does not exist or we're configured to point to the same # database. Do nothing. return if os.path.exists(destfn): if replace: log.debug(u'moving old database aside: {0}'.format( util.displayable_path(destfn) )) _displace(destfn) else: return log.debug(u'copying database from {0} to {1}'.format( util.displayable_path(srcfn), util.displayable_path(destfn) )) util.copy(srcfn, destfn) return destfn def migrate_state(replace=False): """Copy the beets runtime state file from the old path (i.e., ~/.beetsstate) to the new path (i.e., ~/.config/beets/state.pickle). """ srcfn = os.path.expanduser(os.path.join('~', '.beetsstate')) if not os.path.exists(srcfn): return destfn = beets.config['statefile'].as_filename() if os.path.exists(destfn): if replace: _displace(destfn) else: return log.debug(u'copying state file from {0} to {1}'.format( util.displayable_path(srcfn), util.displayable_path(destfn) )) util.copy(srcfn, destfn) return destfn # Automatic migration when beets starts. def automigrate(): """Migrate the configuration, database, and state files. If any migration occurs, print out a notice with some helpful next steps. """ config_fn = migrate_config() db_fn = migrate_db() migrate_state() if config_fn: ui.print_(ui.colorize('fuchsia', u'MIGRATED CONFIGURATION')) ui.print_(CONFIG_MIGRATED_MESSAGE.format( newconfig=util.displayable_path(config_fn)) ) if db_fn: ui.print_(DB_MIGRATED_MESSAGE.format( newdb=util.displayable_path(db_fn) )) ui.input_(ui.colorize('fuchsia', u'Press ENTER to continue:')) ui.print_() # CLI command for explicit migration. migrate_cmd = ui.Subcommand('migrate', help='convert legacy config') def migrate_func(lib, opts, args): """Explicit command for migrating files. Existing files in each destination are moved aside. """ config_fn = migrate_config(replace=True) if config_fn: log.info(u'Migrated configuration to: {0}'.format( util.displayable_path(config_fn) )) db_fn = migrate_db(replace=True) if db_fn: log.info(u'Migrated library database to: {0}'.format( util.displayable_path(db_fn) )) state_fn = migrate_state(replace=True) if state_fn: log.info(u'Migrated state file to: {0}'.format( util.displayable_path(state_fn) )) migrate_cmd.func = migrate_func beets-1.3.8/beets/util/0000755000076500000240000000000012406440351015647 5ustar asampsonstaff00000000000000beets-1.3.8/beets/util/__init__.py0000644000076500000240000005236112374550673020004 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Miscellaneous utility functions.""" from __future__ import division import os import sys import re import shutil import fnmatch from collections import defaultdict import traceback import subprocess import platform MAX_FILENAME_LENGTH = 200 WINDOWS_MAGIC_PREFIX = u'\\\\?\\' class HumanReadableException(Exception): """An Exception that can include a human-readable error message to be logged without a traceback. Can preserve a traceback for debugging purposes as well. Has at least two fields: `reason`, the underlying exception or a string describing the problem; and `verb`, the action being performed during the error. If `tb` is provided, it is a string containing a traceback for the associated exception. (Note that this is not necessary in Python 3.x and should be removed when we make the transition.) """ error_kind = 'Error' # Human-readable description of error type. def __init__(self, reason, verb, tb=None): self.reason = reason self.verb = verb self.tb = tb super(HumanReadableException, self).__init__(self.get_message()) def _gerund(self): """Generate a (likely) gerund form of the English verb. """ if ' ' in self.verb: return self.verb gerund = self.verb[:-1] if self.verb.endswith('e') else self.verb gerund += 'ing' return gerund def _reasonstr(self): """Get the reason as a string.""" if isinstance(self.reason, unicode): return self.reason elif isinstance(self.reason, basestring): # Byte string. return self.reason.decode('utf8', 'ignore') elif hasattr(self.reason, 'strerror'): # i.e., EnvironmentError return self.reason.strerror else: return u'"{0}"'.format(unicode(self.reason)) def get_message(self): """Create the human-readable description of the error, sans introduction. """ raise NotImplementedError def log(self, logger): """Log to the provided `logger` a human-readable message as an error and a verbose traceback as a debug message. """ if self.tb: logger.debug(self.tb) logger.error(u'{0}: {1}'.format(self.error_kind, self.args[0])) class FilesystemError(HumanReadableException): """An error that occurred while performing a filesystem manipulation via a function in this module. The `paths` field is a sequence of pathnames involved in the operation. """ def __init__(self, reason, verb, paths, tb=None): self.paths = paths super(FilesystemError, self).__init__(reason, verb, tb) def get_message(self): # Use a nicer English phrasing for some specific verbs. if self.verb in ('move', 'copy', 'rename'): clause = u'while {0} {1} to {2}'.format( self._gerund(), displayable_path(self.paths[0]), displayable_path(self.paths[1]) ) elif self.verb in ('delete', 'write', 'create', 'read'): clause = u'while {0} {1}'.format( self._gerund(), displayable_path(self.paths[0]) ) else: clause = u'during {0} of paths {1}'.format( self.verb, u', '.join(displayable_path(p) for p in self.paths) ) return u'{0} {1}'.format(self._reasonstr(), clause) def normpath(path): """Provide the canonical form of the path suitable for storing in the database. """ path = syspath(path, prefix=False) path = os.path.normpath(os.path.abspath(os.path.expanduser(path))) return bytestring_path(path) def ancestry(path): """Return a list consisting of path's parent directory, its grandparent, and so on. For instance: >>> ancestry('/a/b/c') ['/', '/a', '/a/b'] The argument should *not* be the result of a call to `syspath`. """ out = [] last_path = None while path: path = os.path.dirname(path) if path == last_path: break last_path = path if path: # don't yield '' out.insert(0, path) return out def sorted_walk(path, ignore=(), logger=None): """Like `os.walk`, but yields things in case-insensitive sorted, breadth-first order. Directory and file names matching any glob pattern in `ignore` are skipped. If `logger` is provided, then warning messages are logged there when a directory cannot be listed. """ # Make sure the path isn't a Unicode string. path = bytestring_path(path) # Get all the directories and files at this level. try: contents = os.listdir(syspath(path)) except OSError as exc: if logger: logger.warn(u'could not list directory {0}: {1}'.format( displayable_path(path), exc.strerror )) return dirs = [] files = [] for base in contents: base = bytestring_path(base) # Skip ignored filenames. skip = False for pat in ignore: if fnmatch.fnmatch(base, pat): skip = True break if skip: continue # Add to output as either a file or a directory. cur = os.path.join(path, base) if os.path.isdir(syspath(cur)): dirs.append(base) else: files.append(base) # Sort lists (case-insensitive) and yield the current level. dirs.sort(key=bytes.lower) files.sort(key=bytes.lower) yield (path, dirs, files) # Recurse into directories. for base in dirs: cur = os.path.join(path, base) # yield from sorted_walk(...) for res in sorted_walk(cur, ignore, logger): yield res def mkdirall(path): """Make all the enclosing directories of path (like mkdir -p on the parent). """ for ancestor in ancestry(path): if not os.path.isdir(syspath(ancestor)): try: os.mkdir(syspath(ancestor)) except (OSError, IOError) as exc: raise FilesystemError(exc, 'create', (ancestor,), traceback.format_exc()) def fnmatch_all(names, patterns): """Determine whether all strings in `names` match at least one of the `patterns`, which should be shell glob expressions. """ for name in names: matches = False for pattern in patterns: matches = fnmatch.fnmatch(name, pattern) if matches: break if not matches: return False return True def prune_dirs(path, root=None, clutter=('.DS_Store', 'Thumbs.db')): """If path is an empty directory, then remove it. Recursively remove path's ancestry up to root (which is never removed) where there are empty directories. If path is not contained in root, then nothing is removed. Glob patterns in clutter are ignored when determining emptiness. If root is not provided, then only path may be removed (i.e., no recursive removal). """ path = normpath(path) if root is not None: root = normpath(root) ancestors = ancestry(path) if root is None: # Only remove the top directory. ancestors = [] elif root in ancestors: # Only remove directories below the root. ancestors = ancestors[ancestors.index(root) + 1:] else: # Remove nothing. return # Traverse upward from path. ancestors.append(path) ancestors.reverse() for directory in ancestors: directory = syspath(directory) if not os.path.exists(directory): # Directory gone already. continue if fnmatch_all(os.listdir(directory), clutter): # Directory contains only clutter (or nothing). try: shutil.rmtree(directory) except OSError: break else: break def components(path): """Return a list of the path components in path. For instance: >>> components('/a/b/c') ['a', 'b', 'c'] The argument should *not* be the result of a call to `syspath`. """ comps = [] ances = ancestry(path) for anc in ances: comp = os.path.basename(anc) if comp: comps.append(comp) else: # root comps.append(anc) last = os.path.basename(path) if last: comps.append(last) return comps def _fsencoding(): """Get the system's filesystem encoding. On Windows, this is always UTF-8 (not MBCS). """ encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() if encoding == 'mbcs': # On Windows, a broken encoding known to Python as "MBCS" is # used for the filesystem. However, we only use the Unicode API # for Windows paths, so the encoding is actually immaterial so # we can avoid dealing with this nastiness. We arbitrarily # choose UTF-8. encoding = 'utf8' return encoding def bytestring_path(path): """Given a path, which is either a str or a unicode, returns a str path (ensuring that we never deal with Unicode pathnames). """ # Pass through bytestrings. if isinstance(path, str): return path # On Windows, remove the magic prefix added by `syspath`. This makes # ``bytestring_path(syspath(X)) == X``, i.e., we can safely # round-trip through `syspath`. if os.path.__name__ == 'ntpath' and path.startswith(WINDOWS_MAGIC_PREFIX): path = path[len(WINDOWS_MAGIC_PREFIX):] # Try to encode with default encodings, but fall back to UTF8. try: return path.encode(_fsencoding()) except (UnicodeError, LookupError): return path.encode('utf8') def displayable_path(path, separator=u'; '): """Attempts to decode a bytestring path to a unicode object for the purpose of displaying it to the user. If the `path` argument is a list or a tuple, the elements are joined with `separator`. """ if isinstance(path, (list, tuple)): return separator.join(displayable_path(p) for p in path) elif isinstance(path, unicode): return path elif not isinstance(path, str): # A non-string object: just get its unicode representation. return unicode(path) try: return path.decode(_fsencoding(), 'ignore') except (UnicodeError, LookupError): return path.decode('utf8', 'ignore') def syspath(path, prefix=True): """Convert a path for use by the operating system. In particular, paths on Windows must receive a magic prefix and must be converted to Unicode before they are sent to the OS. To disable the magic prefix on Windows, set `prefix` to False---but only do this if you *really* know what you're doing. """ # Don't do anything if we're not on windows if os.path.__name__ != 'ntpath': return path if not isinstance(path, unicode): # Beets currently represents Windows paths internally with UTF-8 # arbitrarily. But earlier versions used MBCS because it is # reported as the FS encoding by Windows. Try both. try: path = path.decode('utf8') except UnicodeError: # The encoding should always be MBCS, Windows' broken # Unicode representation. encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() path = path.decode(encoding, 'replace') # Add the magic prefix if it isn't already there. # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx if prefix and not path.startswith(WINDOWS_MAGIC_PREFIX): if path.startswith(u'\\\\'): # UNC path. Final path should look like \\?\UNC\... path = u'UNC' + path[1:] path = WINDOWS_MAGIC_PREFIX + path return path def samefile(p1, p2): """Safer equality for paths.""" return shutil._samefile(syspath(p1), syspath(p2)) def remove(path, soft=True): """Remove the file. If `soft`, then no error will be raised if the file does not exist. """ path = syspath(path) if soft and not os.path.exists(path): return try: os.remove(path) except (OSError, IOError) as exc: raise FilesystemError(exc, 'delete', (path,), traceback.format_exc()) def copy(path, dest, replace=False): """Copy a plain file. Permissions are not copied. If `dest` already exists, raises a FilesystemError unless `replace` is True. Has no effect if `path` is the same as `dest`. Paths are translated to system paths before the syscall. """ if samefile(path, dest): return path = syspath(path) dest = syspath(dest) if not replace and os.path.exists(dest): raise FilesystemError('file exists', 'copy', (path, dest)) try: shutil.copyfile(path, dest) except (OSError, IOError) as exc: raise FilesystemError(exc, 'copy', (path, dest), traceback.format_exc()) def move(path, dest, replace=False): """Rename a file. `dest` may not be a directory. If `dest` already exists, raises an OSError unless `replace` is True. Has no effect if `path` is the same as `dest`. If the paths are on different filesystems (or the rename otherwise fails), a copy is attempted instead, in which case metadata will *not* be preserved. Paths are translated to system paths. """ if samefile(path, dest): return path = syspath(path) dest = syspath(dest) if os.path.exists(dest) and not replace: raise FilesystemError('file exists', 'rename', (path, dest), traceback.format_exc()) # First, try renaming the file. try: os.rename(path, dest) except OSError: # Otherwise, copy and delete the original. try: shutil.copyfile(path, dest) os.remove(path) except (OSError, IOError) as exc: raise FilesystemError(exc, 'move', (path, dest), traceback.format_exc()) def unique_path(path): """Returns a version of ``path`` that does not exist on the filesystem. Specifically, if ``path` itself already exists, then something unique is appended to the path. """ if not os.path.exists(syspath(path)): return path base, ext = os.path.splitext(path) match = re.search(r'\.(\d)+$', base) if match: num = int(match.group(1)) base = base[:match.start()] else: num = 0 while True: num += 1 new_path = '%s.%i%s' % (base, num, ext) if not os.path.exists(new_path): return new_path # Note: The Windows "reserved characters" are, of course, allowed on # Unix. They are forbidden here because they cause problems on Samba # shares, which are sufficiently common as to cause frequent problems. # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx CHAR_REPLACE = [ (re.compile(ur'[\\/]'), u'_'), # / and \ -- forbidden everywhere. (re.compile(ur'^\.'), u'_'), # Leading dot (hidden files on Unix). (re.compile(ur'[\x00-\x1f]'), u''), # Control characters. (re.compile(ur'[<>:"\?\*\|]'), u'_'), # Windows "reserved characters". (re.compile(ur'\.$'), u'_'), # Trailing dots. (re.compile(ur'\s+$'), u''), # Trailing whitespace. ] def sanitize_path(path, replacements=None): """Takes a path (as a Unicode string) and makes sure that it is legal. Returns a new path. Only works with fragments; won't work reliably on Windows when a path begins with a drive letter. Path separators (including altsep!) should already be cleaned from the path components. If replacements is specified, it is used *instead* of the default set of replacements; it must be a list of (compiled regex, replacement string) pairs. """ replacements = replacements or CHAR_REPLACE comps = components(path) if not comps: return '' for i, comp in enumerate(comps): for regex, repl in replacements: comp = regex.sub(repl, comp) comps[i] = comp return os.path.join(*comps) def truncate_path(path, length=MAX_FILENAME_LENGTH): """Given a bytestring path or a Unicode path fragment, truncate the components to a legal length. In the last component, the extension is preserved. """ comps = components(path) out = [c[:length] for c in comps] base, ext = os.path.splitext(comps[-1]) if ext: # Last component has an extension. base = base[:length - len(ext)] out[-1] = base + ext return os.path.join(*out) def str2bool(value): """Returns a boolean reflecting a human-entered string.""" if value.lower() in ('yes', '1', 'true', 't', 'y'): return True else: return False def as_string(value): """Convert a value to a Unicode object for matching with a query. None becomes the empty string. Bytestrings are silently decoded. """ if value is None: return u'' elif isinstance(value, buffer): return str(value).decode('utf8', 'ignore') elif isinstance(value, str): return value.decode('utf8', 'ignore') else: return unicode(value) def levenshtein(s1, s2): """A nice DP edit distance implementation from Wikibooks: http://en.wikibooks.org/wiki/Algorithm_implementation/Strings/ Levenshtein_distance#Python """ if len(s1) < len(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] for j, c2 in enumerate(s2): insertions = previous_row[j + 1] + 1 deletions = current_row[j] + 1 substitutions = previous_row[j] + (c1 != c2) current_row.append(min(insertions, deletions, substitutions)) previous_row = current_row return previous_row[-1] def plurality(objs): """Given a sequence of comparable objects, returns the object that is most common in the set and the frequency of that object. The sequence must contain at least one object. """ # Calculate frequencies. freqs = defaultdict(int) for obj in objs: freqs[obj] += 1 if not freqs: raise ValueError('sequence must be non-empty') # Find object with maximum frequency. max_freq = 0 res = None for obj, freq in freqs.items(): if freq > max_freq: max_freq = freq res = obj return res, max_freq def cpu_count(): """Return the number of hardware thread contexts (cores or SMT threads) in the system. """ # Adapted from the soundconverter project: # https://github.com/kassoulet/soundconverter if sys.platform == 'win32': try: num = int(os.environ['NUMBER_OF_PROCESSORS']) except (ValueError, KeyError): num = 0 elif sys.platform == 'darwin': try: num = int(command_output(['sysctl', '-n', 'hw.ncpu'])) except ValueError: num = 0 else: try: num = os.sysconf('SC_NPROCESSORS_ONLN') except (ValueError, OSError, AttributeError): num = 0 if num >= 1: return num else: return 1 def command_output(cmd, shell=False): """Runs the command and returns its output after it has exited. ``cmd`` is a list of arguments starting with the command names. If ``shell`` is true, ``cmd`` is assumed to be a string and passed to a shell to execute. If the process exits with a non-zero return code ``subprocess.CalledProcessError`` is raised. May also raise ``OSError``. This replaces `subprocess.check_output`, which isn't available in Python 2.6 and which can have problems if lots of output is sent to stderr. """ with open(os.devnull, 'wb') as devnull: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=devnull, close_fds=platform.system() != 'Windows', shell=shell) stdout, _ = proc.communicate() if proc.returncode: raise subprocess.CalledProcessError(proc.returncode, cmd) return stdout def max_filename_length(path, limit=MAX_FILENAME_LENGTH): """Attempt to determine the maximum filename length for the filesystem containing `path`. If the value is greater than `limit`, then `limit` is used instead (to prevent errors when a filesystem misreports its capacity). If it cannot be determined (e.g., on Windows), return `limit`. """ if hasattr(os, 'statvfs'): try: res = os.statvfs(path) except OSError: return limit return min(res[9], limit) else: return limit beets-1.3.8/beets/util/artresizer.py0000644000076500000240000001354412321560145020422 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Fabrice Laporte # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Abstraction layer to resize images using PIL, ImageMagick, or a public resizing proxy if neither is available. """ import urllib import subprocess import os from tempfile import NamedTemporaryFile import logging from beets import util # Resizing methods PIL = 1 IMAGEMAGICK = 2 WEBPROXY = 3 PROXY_URL = 'http://images.weserv.nl/' log = logging.getLogger('beets') def resize_url(url, maxwidth): """Return a proxied image URL that resizes the original image to maxwidth (preserving aspect ratio). """ return '{0}?{1}'.format(PROXY_URL, urllib.urlencode({ 'url': url.replace('http://', ''), 'w': str(maxwidth), })) def temp_file_for(path): """Return an unused filename with the same extension as the specified path. """ ext = os.path.splitext(path)[1] with NamedTemporaryFile(suffix=ext, delete=False) as f: return f.name def pil_resize(maxwidth, path_in, path_out=None): """Resize using Python Imaging Library (PIL). Return the output path of resized image. """ path_out = path_out or temp_file_for(path_in) from PIL import Image log.debug(u'artresizer: PIL resizing {0} to {1}'.format( util.displayable_path(path_in), util.displayable_path(path_out) )) try: im = Image.open(util.syspath(path_in)) size = maxwidth, maxwidth im.thumbnail(size, Image.ANTIALIAS) im.save(path_out) return path_out except IOError: log.error(u"PIL cannot create thumbnail for '{0}'".format( util.displayable_path(path_in) )) return path_in def im_resize(maxwidth, path_in, path_out=None): """Resize using ImageMagick's ``convert`` tool. tool. Return the output path of resized image. """ path_out = path_out or temp_file_for(path_in) log.debug(u'artresizer: ImageMagick resizing {0} to {1}'.format( util.displayable_path(path_in), util.displayable_path(path_out) )) # "-resize widthxheight>" shrinks images with dimension(s) larger # than the corresponding width and/or height dimension(s). The > # "only shrink" flag is prefixed by ^ escape char for Windows # compatibility. try: util.command_output([ 'convert', util.syspath(path_in), '-resize', '{0}x^>'.format(maxwidth), path_out ]) except subprocess.CalledProcessError: log.warn(u'artresizer: IM convert failed for {0}'.format( util.displayable_path(path_in) )) return path_in return path_out BACKEND_FUNCS = { PIL: pil_resize, IMAGEMAGICK: im_resize, } class Shareable(type): """A pseudo-singleton metaclass that allows both shared and non-shared instances. The ``MyClass.shared`` property holds a lazily-created shared instance of ``MyClass`` while calling ``MyClass()`` to construct a new object works as usual. """ def __init__(cls, name, bases, dict): super(Shareable, cls).__init__(name, bases, dict) cls._instance = None @property def shared(cls): if cls._instance is None: cls._instance = cls() return cls._instance class ArtResizer(object): """A singleton class that performs image resizes. """ __metaclass__ = Shareable def __init__(self, method=None): """Create a resizer object for the given method or, if none is specified, with an inferred method. """ self.method = method or self._guess_method() log.debug(u"artresizer: method is {0}".format(self.method)) def resize(self, maxwidth, path_in, path_out=None): """Manipulate an image file according to the method, returning a new path. For PIL or IMAGEMAGIC methods, resizes the image to a temporary file. For WEBPROXY, returns `path_in` unmodified. """ if self.local: func = BACKEND_FUNCS[self.method] return func(maxwidth, path_in, path_out) else: return path_in def proxy_url(self, maxwidth, url): """Modifies an image URL according the method, returning a new URL. For WEBPROXY, a URL on the proxy server is returned. Otherwise, the URL is returned unmodified. """ if self.local: return url else: return resize_url(url, maxwidth) @property def local(self): """A boolean indicating whether the resizing method is performed locally (i.e., PIL or IMAGEMAGICK). """ return self.method in BACKEND_FUNCS @staticmethod def _guess_method(): """Determine which resizing method to use. Returns PIL, IMAGEMAGICK, or WEBPROXY depending on available dependencies. """ # Try importing PIL. try: __import__('PIL', fromlist=['Image']) return PIL except ImportError: pass # Try invoking ImageMagick's "convert". try: out = util.command_output(['convert', '--version']) if 'imagemagick' in out.lower(): # system32/convert.exe may be interfering return IMAGEMAGICK except (subprocess.CalledProcessError, OSError): pass # Fall back to Web proxy method. return WEBPROXY beets-1.3.8/beets/util/bluelet.py0000644000076500000240000004701712321560145017666 0ustar asampsonstaff00000000000000"""Extremely simple pure-Python implementation of coroutine-style asynchronous socket I/O. Inspired by, but inferior to, Eventlet. Bluelet can also be thought of as a less-terrible replacement for asyncore. Bluelet: easy concurrency without all the messy parallelism. """ import socket import select import sys import types import errno import traceback import time import collections # A little bit of "six" (Python 2/3 compatibility): cope with PEP 3109 syntax # changes. PY3 = sys.version_info[0] == 3 if PY3: def _reraise(typ, exc, tb): raise exc.with_traceback(tb) else: exec(""" def _reraise(typ, exc, tb): raise typ, exc, tb """) # Basic events used for thread scheduling. class Event(object): """Just a base class identifying Bluelet events. An event is an object yielded from a Bluelet thread coroutine to suspend operation and communicate with the scheduler. """ pass class WaitableEvent(Event): """A waitable event is one encapsulating an action that can be waited for using a select() call. That is, it's an event with an associated file descriptor. """ def waitables(self): """Return "waitable" objects to pass to select(). Should return three iterables for input readiness, output readiness, and exceptional conditions (i.e., the three lists passed to select()). """ return (), (), () def fire(self): """Called when an associated file descriptor becomes ready (i.e., is returned from a select() call). """ pass class ValueEvent(Event): """An event that does nothing but return a fixed value.""" def __init__(self, value): self.value = value class ExceptionEvent(Event): """Raise an exception at the yield point. Used internally.""" def __init__(self, exc_info): self.exc_info = exc_info class SpawnEvent(Event): """Add a new coroutine thread to the scheduler.""" def __init__(self, coro): self.spawned = coro class JoinEvent(Event): """Suspend the thread until the specified child thread has completed. """ def __init__(self, child): self.child = child class KillEvent(Event): """Unschedule a child thread.""" def __init__(self, child): self.child = child class DelegationEvent(Event): """Suspend execution of the current thread, start a new thread and, once the child thread finished, return control to the parent thread. """ def __init__(self, coro): self.spawned = coro class ReturnEvent(Event): """Return a value the current thread's delegator at the point of delegation. Ends the current (delegate) thread. """ def __init__(self, value): self.value = value class SleepEvent(WaitableEvent): """Suspend the thread for a given duration. """ def __init__(self, duration): self.wakeup_time = time.time() + duration def time_left(self): return max(self.wakeup_time - time.time(), 0.0) class ReadEvent(WaitableEvent): """Reads from a file-like object.""" def __init__(self, fd, bufsize): self.fd = fd self.bufsize = bufsize def waitables(self): return (self.fd,), (), () def fire(self): return self.fd.read(self.bufsize) class WriteEvent(WaitableEvent): """Writes to a file-like object.""" def __init__(self, fd, data): self.fd = fd self.data = data def waitable(self): return (), (self.fd,), () def fire(self): self.fd.write(self.data) # Core logic for executing and scheduling threads. def _event_select(events): """Perform a select() over all the Events provided, returning the ones ready to be fired. Only WaitableEvents (including SleepEvents) matter here; all other events are ignored (and thus postponed). """ # Gather waitables and wakeup times. waitable_to_event = {} rlist, wlist, xlist = [], [], [] earliest_wakeup = None for event in events: if isinstance(event, SleepEvent): if not earliest_wakeup: earliest_wakeup = event.wakeup_time else: earliest_wakeup = min(earliest_wakeup, event.wakeup_time) elif isinstance(event, WaitableEvent): r, w, x = event.waitables() rlist += r wlist += w xlist += x for waitable in r: waitable_to_event[('r', waitable)] = event for waitable in w: waitable_to_event[('w', waitable)] = event for waitable in x: waitable_to_event[('x', waitable)] = event # If we have a any sleeping threads, determine how long to sleep. if earliest_wakeup: timeout = max(earliest_wakeup - time.time(), 0.0) else: timeout = None # Perform select() if we have any waitables. if rlist or wlist or xlist: rready, wready, xready = select.select(rlist, wlist, xlist, timeout) else: rready, wready, xready = (), (), () if timeout: time.sleep(timeout) # Gather ready events corresponding to the ready waitables. ready_events = set() for ready in rready: ready_events.add(waitable_to_event[('r', ready)]) for ready in wready: ready_events.add(waitable_to_event[('w', ready)]) for ready in xready: ready_events.add(waitable_to_event[('x', ready)]) # Gather any finished sleeps. for event in events: if isinstance(event, SleepEvent) and event.time_left() == 0.0: ready_events.add(event) return ready_events class ThreadException(Exception): def __init__(self, coro, exc_info): self.coro = coro self.exc_info = exc_info def reraise(self): _reraise(self.exc_info[0], self.exc_info[1], self.exc_info[2]) SUSPENDED = Event() # Special sentinel placeholder for suspended threads. class Delegated(Event): """Placeholder indicating that a thread has delegated execution to a different thread. """ def __init__(self, child): self.child = child def run(root_coro): """Schedules a coroutine, running it to completion. This encapsulates the Bluelet scheduler, which the root coroutine can add to by spawning new coroutines. """ # The "threads" dictionary keeps track of all the currently- # executing and suspended coroutines. It maps coroutines to their # currently "blocking" event. The event value may be SUSPENDED if # the coroutine is waiting on some other condition: namely, a # delegated coroutine or a joined coroutine. In this case, the # coroutine should *also* appear as a value in one of the below # dictionaries `delegators` or `joiners`. threads = {root_coro: ValueEvent(None)} # Maps child coroutines to delegating parents. delegators = {} # Maps child coroutines to joining (exit-waiting) parents. joiners = collections.defaultdict(list) def complete_thread(coro, return_value): """Remove a coroutine from the scheduling pool, awaking delegators and joiners as necessary and returning the specified value to any delegating parent. """ del threads[coro] # Resume delegator. if coro in delegators: threads[delegators[coro]] = ValueEvent(return_value) del delegators[coro] # Resume joiners. if coro in joiners: for parent in joiners[coro]: threads[parent] = ValueEvent(None) del joiners[coro] 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 exits, then the thread is removed from the pool. If the coroutine raises an exception, it is reraised in a ThreadException. If is_exc is True, then the value must be an exc_info tuple and the exception is thrown into the coroutine. """ try: if is_exc: next_event = coro.throw(*value) else: next_event = coro.send(value) except StopIteration: # Thread is done. complete_thread(coro, None) except: # Thread raised some other exception. del threads[coro] raise ThreadException(coro, sys.exc_info()) else: if isinstance(next_event, types.GeneratorType): # Automatically invoke sub-coroutines. (Shorthand for # explicit bluelet.call().) next_event = DelegationEvent(next_event) threads[coro] = next_event def kill_thread(coro): """Unschedule this thread and its (recursive) delegates. """ # Collect all coroutines in the delegation stack. coros = [coro] while isinstance(threads[coro], Delegated): coro = threads[coro].child coros.append(coro) # Complete each coroutine from the top to the bottom of the # stack. for coro in reversed(coros): complete_thread(coro, None) # Continue advancing threads until root thread exits. exit_te = None while threads: try: # Look for events that can be run immediately. Continue # running immediate events until nothing is ready. while True: have_ready = False for coro, event in list(threads.items()): if isinstance(event, SpawnEvent): threads[event.spawned] = ValueEvent(None) # Spawn. advance_thread(coro, None) have_ready = True elif isinstance(event, ValueEvent): advance_thread(coro, event.value) have_ready = True elif isinstance(event, ExceptionEvent): advance_thread(coro, event.exc_info, True) have_ready = True elif isinstance(event, DelegationEvent): threads[coro] = Delegated(event.spawned) # Suspend. threads[event.spawned] = ValueEvent(None) # Spawn. delegators[event.spawned] = coro have_ready = True elif isinstance(event, ReturnEvent): # Thread is done. complete_thread(coro, event.value) have_ready = True elif isinstance(event, JoinEvent): threads[coro] = SUSPENDED # Suspend. joiners[event.child].append(coro) have_ready = True elif isinstance(event, KillEvent): threads[coro] = ValueEvent(None) kill_thread(event.child) have_ready = True # Only start the select when nothing else is ready. if not have_ready: break # Wait and fire. event2coro = dict((v, k) for k, v in threads.items()) for event in _event_select(threads.values()): # Run the IO operation, but catch socket errors. try: value = event.fire() except socket.error as exc: if isinstance(exc.args, tuple) and \ exc.args[0] == errno.EPIPE: # Broken pipe. Remote host disconnected. pass else: traceback.print_exc() # Abort the coroutine. threads[event2coro[event]] = ReturnEvent(None) else: advance_thread(event2coro[event], value) except ThreadException as te: # Exception raised from inside a thread. event = ExceptionEvent(te.exc_info) if te.coro in delegators: # The thread is a delegate. Raise exception in its # delegator. threads[delegators[te.coro]] = event del delegators[te.coro] else: # 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. threads = {root_coro: ExceptionEvent(sys.exc_info())} # If any threads still remain, kill them. for coro in threads: coro.close() # If we're exiting with an exception, raise it in the client. if exit_te: exit_te.reraise() # Sockets and their associated events. class SocketClosedError(Exception): pass class Listener(object): """A socket wrapper object for listening sockets. """ def __init__(self, host, port): """Create a listening socket on the given hostname and port. """ self._closed = False self.host = host self.port = port self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((host, port)) self.sock.listen(5) def accept(self): """An event that waits for a connection on the listening socket. When a connection is made, the event returns a Connection object. """ if self._closed: raise SocketClosedError() return AcceptEvent(self) def close(self): """Immediately close the listening socket. (Not an event.) """ self._closed = True self.sock.close() class Connection(object): """A socket wrapper object for connected sockets. """ def __init__(self, sock, addr): self.sock = sock self.addr = addr self._buf = b'' self._closed = False def close(self): """Close the connection.""" self._closed = True self.sock.close() def recv(self, size): """Read at most size bytes of data from the socket.""" if self._closed: raise SocketClosedError() if self._buf: # We already have data read previously. out = self._buf[:size] self._buf = self._buf[size:] return ValueEvent(out) else: return ReceiveEvent(self, size) def send(self, data): """Sends data on the socket, returning the number of bytes successfully sent. """ if self._closed: raise SocketClosedError() return SendEvent(self, data) def sendall(self, data): """Send all of data on the socket.""" if self._closed: raise SocketClosedError() return SendEvent(self, data, True) def readline(self, terminator=b"\n", bufsize=1024): """Reads a line (delimited by terminator) from the socket.""" if self._closed: raise SocketClosedError() while True: if terminator in self._buf: line, self._buf = self._buf.split(terminator, 1) line += terminator yield ReturnEvent(line) break data = yield ReceiveEvent(self, bufsize) if data: self._buf += data else: line = self._buf self._buf = b'' yield ReturnEvent(line) break class AcceptEvent(WaitableEvent): """An event for Listener objects (listening sockets) that suspends execution until the socket gets a connection. """ def __init__(self, listener): self.listener = listener def waitables(self): return (self.listener.sock,), (), () def fire(self): sock, addr = self.listener.sock.accept() return Connection(sock, addr) class ReceiveEvent(WaitableEvent): """An event for Connection objects (connected sockets) for asynchronously reading data. """ def __init__(self, conn, bufsize): self.conn = conn self.bufsize = bufsize def waitables(self): return (self.conn.sock,), (), () def fire(self): return self.conn.sock.recv(self.bufsize) class SendEvent(WaitableEvent): """An event for Connection objects (connected sockets) for asynchronously writing data. """ def __init__(self, conn, data, sendall=False): self.conn = conn self.data = data self.sendall = sendall def waitables(self): return (), (self.conn.sock,), () def fire(self): if self.sendall: return self.conn.sock.sendall(self.data) else: return self.conn.sock.send(self.data) # Public interface for threads; each returns an event object that # can immediately be "yield"ed. def null(): """Event: yield to the scheduler without doing anything special. """ return ValueEvent(None) def spawn(coro): """Event: add another coroutine to the scheduler. Both the parent and child coroutines run concurrently. """ if not isinstance(coro, types.GeneratorType): raise ValueError('%s is not a coroutine' % str(coro)) return SpawnEvent(coro) def call(coro): """Event: delegate to another coroutine. The current coroutine is resumed once the sub-coroutine finishes. If the sub-coroutine returns a value using end(), then this event returns that value. """ if not isinstance(coro, types.GeneratorType): raise ValueError('%s is not a coroutine' % str(coro)) return DelegationEvent(coro) def end(value=None): """Event: ends the coroutine and returns a value to its delegator. """ return ReturnEvent(value) def read(fd, bufsize=None): """Event: read from a file descriptor asynchronously.""" if bufsize is None: # Read all. def reader(): buf = [] while True: data = yield read(fd, 1024) if not data: break buf.append(data) yield ReturnEvent(''.join(buf)) return DelegationEvent(reader()) else: return ReadEvent(fd, bufsize) def write(fd, data): """Event: write to a file descriptor asynchronously.""" return WriteEvent(fd, data) def connect(host, port): """Event: connect to a network address and return a Connection object for communicating on the socket. """ addr = (host, port) sock = socket.create_connection(addr) return ValueEvent(Connection(sock, addr)) def sleep(duration): """Event: suspend the thread for ``duration`` seconds. """ return SleepEvent(duration) def join(coro): """Suspend the thread until another, previously `spawn`ed thread completes. """ return JoinEvent(coro) def kill(coro): """Halt the execution of a different `spawn`ed thread. """ return KillEvent(coro) # Convenience function for running socket servers. def server(host, port, func): """A coroutine that runs a network server. Host and port specify the listening address. func should be a coroutine that takes a single parameter, a Connection object. The coroutine is invoked for every incoming connection on the listening socket. """ def handler(conn): try: yield func(conn) finally: conn.close() listener = Listener(host, port) try: while True: conn = yield listener.accept() yield spawn(handler(conn)) except KeyboardInterrupt: pass finally: listener.close() beets-1.3.8/beets/util/confit.py0000644000076500000240000012336412405157370017521 0ustar asampsonstaff00000000000000# This file is part of Confit. # Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Worry-free YAML configuration files. """ from __future__ import unicode_literals import platform import os import pkgutil import sys import yaml import types import collections import re try: from collections import OrderedDict except ImportError: from ordereddict import OrderedDict UNIX_DIR_VAR = 'XDG_CONFIG_HOME' UNIX_DIR_FALLBACK = '~/.config' WINDOWS_DIR_VAR = 'APPDATA' WINDOWS_DIR_FALLBACK = '~\\AppData\\Roaming' MAC_DIR = '~/Library/Application Support' CONFIG_FILENAME = 'config.yaml' DEFAULT_FILENAME = 'config_default.yaml' ROOT_NAME = 'root' YAML_TAB_PROBLEM = "found character '\\t' that cannot start any token" # Utilities. PY3 = sys.version_info[0] == 3 STRING = str if PY3 else unicode BASESTRING = str if PY3 else basestring NUMERIC_TYPES = (int, float) if PY3 else (int, float, long) TYPE_TYPES = (type,) if PY3 else (type, types.ClassType) def iter_first(sequence): """Get the first element from an iterable or raise a ValueError if the iterator generates no values. """ it = iter(sequence) try: if PY3: return next(it) else: return it.next() except StopIteration: raise ValueError() # Exceptions. class ConfigError(Exception): """Base class for exceptions raised when querying a configuration. """ class NotFoundError(ConfigError): """A requested value could not be found in the configuration trees. """ class ConfigValueError(ConfigError): """The value in the configuration is illegal.""" class ConfigTypeError(ConfigValueError): """The value in the configuration did not match the expected type. """ class ConfigTemplateError(ConfigError): """Base class for exceptions raised because of an invalid template. """ class ConfigReadError(ConfigError): """A configuration file could not be read.""" def __init__(self, filename, reason=None): self.filename = filename self.reason = reason message = 'file {0} could not be read'.format(filename) if isinstance(reason, yaml.scanner.ScannerError) and \ reason.problem == YAML_TAB_PROBLEM: # Special-case error message for tab indentation in YAML markup. message += ': found tab character at line {0}, column {1}'.format( reason.problem_mark.line + 1, reason.problem_mark.column + 1, ) elif reason: # Generic error message uses exception's message. message += ': {0}'.format(reason) super(ConfigReadError, self).__init__(message) # Views and sources. class ConfigSource(dict): """A dictionary augmented with metadata about the source of the configuration. """ def __init__(self, value, filename=None, default=False): super(ConfigSource, self).__init__(value) if filename is not None and not isinstance(filename, BASESTRING): raise TypeError('filename must be a string or None') self.filename = filename self.default = default def __repr__(self): return 'ConfigSource({0}, {1}, {2})'.format( super(ConfigSource, self).__repr__(), repr(self.filename), repr(self.default) ) @classmethod def of(self, value): """Given either a dictionary or a `ConfigSource` object, return a `ConfigSource` object. This lets a function accept either type of object as an argument. """ if isinstance(value, ConfigSource): return value elif isinstance(value, dict): return ConfigSource(value) else: raise TypeError('source value must be a dict') class ConfigView(object): """A configuration "view" is a query into a program's configuration data. A view represents a hypothetical location in the configuration tree; to extract the data from the location, a client typically calls the ``view.get()`` method. The client can access children in the tree (subviews) by subscripting the parent view (i.e., ``view[key]``). """ name = None """The name of the view, depicting the path taken through the configuration in Python-like syntax (e.g., ``foo['bar'][42]``). """ def resolve(self): """The core (internal) data retrieval method. Generates (value, source) pairs for each source that contains a value for this view. May raise ConfigTypeError if a type error occurs while traversing a source. """ raise NotImplementedError def first(self): """Return a (value, source) pair for the first object found for this view. This amounts to the first element returned by `resolve`. If no values are available, a NotFoundError is raised. """ pairs = self.resolve() try: return iter_first(pairs) except ValueError: raise NotFoundError("{0} not found".format(self.name)) def exists(self): """Determine whether the view has a setting in any source. """ try: self.first() except NotFoundError: return False return True def add(self, value): """Set the *default* value for this configuration view. The specified value is added as the lowest-priority configuration data source. """ raise NotImplementedError def set(self, value): """*Override* the value for this configuration view. The specified value is added as the highest-priority configuration data source. """ raise NotImplementedError def root(self): """The RootView object from which this view is descended. """ raise NotImplementedError def __repr__(self): return '' % self.name def __getitem__(self, key): """Get a subview of this view.""" return Subview(self, key) def __setitem__(self, key, value): """Create an overlay source to assign a given key under this view. """ self.set({key: value}) def __contains__(self, key): return self[key].exists() def set_args(self, namespace): """Overlay parsed command-line arguments, generated by a library like argparse or optparse, onto this view's value. """ args = {} for key, value in namespace.__dict__.items(): if value is not None: # Avoid unset options. args[key] = value self.set(args) # Magical conversions. These special methods make it possible to use # View objects somewhat transparently in certain circumstances. For # example, rather than using ``view.get(bool)``, it's possible to # just say ``bool(view)`` or use ``view`` in a conditional. def __str__(self): """Gets the value for this view as a byte string.""" return str(self.get()) def __unicode__(self): """Gets the value for this view as a unicode string. (Python 2 only.) """ return unicode(self.get()) def __nonzero__(self): """Gets the value for this view as a boolean. (Python 2 only.) """ return self.__bool__() def __bool__(self): """Gets the value for this view as a boolean. (Python 3 only.) """ return bool(self.get()) # Dictionary emulation methods. def keys(self): """Returns a list containing all the keys available as subviews of the current views. This enumerates all the keys in *all* dictionaries matching the current view, in contrast to ``view.get(dict).keys()``, which gets all the keys for the *first* dict matching the view. If the object for this view in any source is not a dict, then a ConfigTypeError is raised. The keys are ordered according to how they appear in each source. """ keys = [] for dic, _ in self.resolve(): try: cur_keys = dic.keys() except AttributeError: raise ConfigTypeError( '{0} must be a dict, not {1}'.format( self.name, type(dic).__name__ ) ) for key in cur_keys: if key not in keys: keys.append(key) return keys def items(self): """Iterates over (key, subview) pairs contained in dictionaries from *all* sources at this view. If the object for this view in any source is not a dict, then a ConfigTypeError is raised. """ for key in self.keys(): yield key, self[key] def values(self): """Iterates over all the subviews contained in dictionaries from *all* sources at this view. If the object for this view in any source is not a dict, then a ConfigTypeError is raised. """ for key in self.keys(): yield self[key] # List/sequence emulation. def all_contents(self): """Iterates over all subviews from collections at this view from *all* sources. If the object for this view in any source is not iterable, then a ConfigTypeError is raised. This method is intended to be used when the view indicates a list; this method will concatenate the contents of the list from all sources. """ for collection, _ in self.resolve(): try: it = iter(collection) except TypeError: raise ConfigTypeError( '{0} must be an iterable, not {1}'.format( self.name, type(collection).__name__ ) ) for value in it: yield value # Validation and conversion. def flatten(self): """Create a hierarchy of OrderedDicts containing the data from this view, recursively reifying all views to get their represented values. """ od = OrderedDict() for key, view in self.items(): try: od[key] = view.flatten() except ConfigTypeError: od[key] = view.get() return od def get(self, template=None): """Retrieve the value for this view according to the template. The `template` against which the values are checked can be anything convertible to a `Template` using `as_template`. This means you can pass in a default integer or string value, for example, or a type to just check that something matches the type you expect. May raise a `ConfigValueError` (or its subclass, `ConfigTypeError`) or a `NotFoundError` when the configuration doesn't satisfy the template. """ return as_template(template).value(self, template) # Old validation methods (deprecated). def as_filename(self): return self.get(Filename()) def as_choice(self, choices): return self.get(Choice(choices)) def as_number(self): return self.get(Number()) def as_str_seq(self): return self.get(StrSeq()) class RootView(ConfigView): """The base of a view hierarchy. This view keeps track of the sources that may be accessed by subviews. """ def __init__(self, sources): """Create a configuration hierarchy for a list of sources. At least one source must be provided. The first source in the list has the highest priority. """ self.sources = list(sources) self.name = ROOT_NAME def add(self, obj): self.sources.append(ConfigSource.of(obj)) def set(self, value): self.sources.insert(0, ConfigSource.of(value)) def resolve(self): return ((dict(s), s) for s in self.sources) def clear(self): """Remove all sources from this configuration.""" del self.sources[:] def root(self): return self class Subview(ConfigView): """A subview accessed via a subscript of a parent view.""" def __init__(self, parent, key): """Make a subview of a parent view for a given subscript key. """ self.parent = parent self.key = key # Choose a human-readable name for this view. if isinstance(self.parent, RootView): self.name = '' else: self.name = self.parent.name if not isinstance(self.key, int): self.name += '.' if isinstance(self.key, int): self.name += '#{0}'.format(self.key) elif isinstance(self.key, BASESTRING): self.name += '{0}'.format(self.key) else: self.name += '{0}'.format(repr(self.key)) def resolve(self): for collection, source in self.parent.resolve(): try: value = collection[self.key] except IndexError: # List index out of bounds. continue except KeyError: # Dict key does not exist. continue except TypeError: # Not subscriptable. raise ConfigTypeError( "{0} must be a collection, not {1}".format( self.parent.name, type(collection).__name__ ) ) yield value, source def set(self, value): self.parent.set({self.key: value}) def add(self, value): self.parent.add({self.key: value}) def root(self): return self.parent.root() # Config file paths, including platform-specific paths and in-package # defaults. # Based on get_root_path from Flask by Armin Ronacher. def _package_path(name): """Returns the path to the package containing the named module or None if the path could not be identified (e.g., if ``name == "__main__"``). """ loader = pkgutil.get_loader(name) if loader is None or name == '__main__': return None if hasattr(loader, 'get_filename'): filepath = loader.get_filename(name) else: # Fall back to importing the specified module. __import__(name) filepath = sys.modules[name].__file__ return os.path.dirname(os.path.abspath(filepath)) def config_dirs(): """Return a platform-specific list of candidates for user configuration directories on the system. The candidates are in order of priority, from highest to lowest. The last element is the "fallback" location to be used when no higher-priority config file exists. """ paths = [] if platform.system() == 'Darwin': paths.append(MAC_DIR) paths.append(UNIX_DIR_FALLBACK) if UNIX_DIR_VAR in os.environ: paths.append(os.environ[UNIX_DIR_VAR]) elif platform.system() == 'Windows': paths.append(WINDOWS_DIR_FALLBACK) if WINDOWS_DIR_VAR in os.environ: paths.append(os.environ[WINDOWS_DIR_VAR]) else: # Assume Unix. paths.append(UNIX_DIR_FALLBACK) if UNIX_DIR_VAR in os.environ: paths.append(os.environ[UNIX_DIR_VAR]) # Expand and deduplicate paths. out = [] for path in paths: path = os.path.abspath(os.path.expanduser(path)) if path not in out: out.append(path) return out # YAML loading. class Loader(yaml.SafeLoader): """A customized YAML loader. This loader deviates from the official YAML spec in a few convenient ways: - All strings as are Unicode objects. - All maps are OrderedDicts. - Strings can begin with % without quotation. """ # All strings should be Unicode objects, regardless of contents. def _construct_unicode(self, node): return self.construct_scalar(node) # Use ordered dictionaries for every YAML map. # From https://gist.github.com/844388 def construct_yaml_map(self, node): data = OrderedDict() yield data value = self.construct_mapping(node) data.update(value) def construct_mapping(self, node, deep=False): if isinstance(node, yaml.MappingNode): self.flatten_mapping(node) else: raise yaml.constructor.ConstructorError( None, None, 'expected a mapping node, but found %s' % node.id, node.start_mark ) mapping = OrderedDict() for key_node, value_node in node.value: key = self.construct_object(key_node, deep=deep) try: hash(key) except TypeError as exc: raise yaml.constructor.ConstructorError( 'while constructing a mapping', node.start_mark, 'found unacceptable key (%s)' % exc, key_node.start_mark ) value = self.construct_object(value_node, deep=deep) mapping[key] = value return mapping # Allow bare strings to begin with %. Directives are still detected. def check_plain(self): plain = super(Loader, self).check_plain() return plain or self.peek() == '%' Loader.add_constructor('tag:yaml.org,2002:str', Loader._construct_unicode) Loader.add_constructor('tag:yaml.org,2002:map', Loader.construct_yaml_map) Loader.add_constructor('tag:yaml.org,2002:omap', Loader.construct_yaml_map) def load_yaml(filename): """Read a YAML document from a file. If the file cannot be read or parsed, a ConfigReadError is raised. """ try: with open(filename, 'r') as f: return yaml.load(f, Loader=Loader) except (IOError, yaml.error.YAMLError) as exc: raise ConfigReadError(filename, exc) # YAML dumping. class Dumper(yaml.SafeDumper): """A PyYAML Dumper that represents OrderedDicts as ordinary mappings (in order, of course). """ # From http://pyyaml.org/attachment/ticket/161/use_ordered_dict.py def represent_mapping(self, tag, mapping, flow_style=None): value = [] node = yaml.MappingNode(tag, value, flow_style=flow_style) if self.alias_key is not None: self.represented_objects[self.alias_key] = node best_style = False if hasattr(mapping, 'items'): mapping = list(mapping.items()) for item_key, item_value in mapping: node_key = self.represent_data(item_key) node_value = self.represent_data(item_value) if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style): best_style = False if not (isinstance(node_value, yaml.ScalarNode) and not node_value.style): best_style = False value.append((node_key, node_value)) if flow_style is None: if self.default_flow_style is not None: node.flow_style = self.default_flow_style else: node.flow_style = best_style return node def represent_list(self, data): """If a list has less than 4 items, represent it in inline style (i.e. comma separated, within square brackets). """ node = super(Dumper, self).represent_list(data) length = len(data) if self.default_flow_style is None and length < 4: node.flow_style = True elif self.default_flow_style is None: node.flow_style = False return node def represent_bool(self, data): """Represent bool as 'yes' or 'no' instead of 'true' or 'false'. """ if data: value = 'yes' else: value = 'no' return self.represent_scalar('tag:yaml.org,2002:bool', value) def represent_none(self, data): """Represent a None value with nothing instead of 'none'. """ return self.represent_scalar('tag:yaml.org,2002:null', '') Dumper.add_representer(OrderedDict, Dumper.represent_dict) Dumper.add_representer(bool, Dumper.represent_bool) Dumper.add_representer(type(None), Dumper.represent_none) Dumper.add_representer(list, Dumper.represent_list) def restore_yaml_comments(data, default_data): """Scan default_data for comments (we include empty lines in our definition of comments) and place them before the same keys in data. Only works with comments that are on one or more own lines, i.e. not next to a yaml mapping. """ comment_map = dict() default_lines = iter(default_data.splitlines()) for line in default_lines: if not line: comment = "\n" elif line.startswith("#"): comment = "{0}\n".format(line) else: continue while True: line = next(default_lines) if line and not line.startswith("#"): break comment += "{0}\n".format(line) key = line.split(':')[0].strip() comment_map[key] = comment out_lines = iter(data.splitlines()) out_data = "" for line in out_lines: key = line.split(':')[0].strip() if key in comment_map: out_data += comment_map[key] out_data += "{0}\n".format(line) return out_data # Main interface. class Configuration(RootView): def __init__(self, appname, modname=None, read=True): """Create a configuration object by reading the automatically-discovered config files for the application for a given name. If `modname` is specified, it should be the import name of a module whose package will be searched for a default config file. (Otherwise, no defaults are used.) Pass `False` for `read` to disable automatic reading of all discovered configuration files. Use this when creating a configuration object at module load time and then call the `read` method later. """ super(Configuration, self).__init__([]) self.appname = appname self.modname = modname self._env_var = '{0}DIR'.format(self.appname.upper()) if read: self.read() def user_config_path(self): """Points to the location of the user configuration. The file may not exist. """ return os.path.join(self.config_dir(), CONFIG_FILENAME) def _add_user_source(self): """Add the configuration options from the YAML file in the user's configuration directory (given by `config_dir`) if it exists. """ filename = self.user_config_path() if os.path.isfile(filename): self.add(ConfigSource(load_yaml(filename) or {}, filename)) def _add_default_source(self): """Add the package's default configuration settings. This looks for a YAML file located inside the package for the module `modname` if it was given. """ if self.modname: pkg_path = _package_path(self.modname) if pkg_path: filename = os.path.join(pkg_path, DEFAULT_FILENAME) if os.path.isfile(filename): self.add(ConfigSource(load_yaml(filename), filename, True)) def read(self, user=True, defaults=True): """Find and read the files for this configuration and set them as the sources for this configuration. To disable either discovered user configuration files or the in-package defaults, set `user` or `defaults` to `False`. """ if user: self._add_user_source() if defaults: self._add_default_source() def config_dir(self): """Get the path to the user configuration directory. The directory is guaranteed to exist as a postcondition (one may be created if none exist). If the application's ``...DIR`` environment variable is set, it is used as the configuration directory. Otherwise, platform-specific standard configuration locations are searched for a ``config.yaml`` file. If no configuration file is found, a fallback path is used. """ # If environment variable is set, use it. if self._env_var in os.environ: appdir = os.environ[self._env_var] appdir = os.path.abspath(os.path.expanduser(appdir)) if os.path.isfile(appdir): raise ConfigError('{0} must be a directory'.format( self._env_var )) else: # Search platform-specific locations. If no config file is # found, fall back to the final directory in the list. for confdir in config_dirs(): appdir = os.path.join(confdir, self.appname) if os.path.isfile(os.path.join(appdir, CONFIG_FILENAME)): break # Ensure that the directory exists. if not os.path.isdir(appdir): os.makedirs(appdir) return appdir def set_file(self, filename): """Parses the file as YAML and inserts it into the configuration sources with highest priority. """ filename = os.path.abspath(filename) self.set(ConfigSource(load_yaml(filename), filename)) def dump(self, full=True): """Dump the Configuration object to a YAML file. The order of the keys is determined from the default configuration file. All keys not in the default configuration will be appended to the end of the file. :param filename: The file to dump the configuration to, or None if the YAML string should be returned instead :type filename: unicode :param full: Dump settings that don't differ from the defaults as well """ if full: out_dict = self.flatten() else: # Exclude defaults when flattening. sources = [s for s in self.sources if not s.default] out_dict = RootView(sources).flatten() yaml_out = yaml.dump(out_dict, Dumper=Dumper, default_flow_style=None, indent=4, width=1000) # Restore comments to the YAML text. default_source = None for source in self.sources: if source.default: default_source = source break if default_source: with open(default_source.filename, 'r') as fp: default_data = fp.read() yaml_out = restore_yaml_comments(yaml_out, default_data) return yaml_out class LazyConfig(Configuration): """A Configuration at reads files on demand when it is first accessed. This is appropriate for using as a global config object at the module level. """ def __init__(self, appname, modname=None): super(LazyConfig, self).__init__(appname, modname, False) self._materialized = False # Have we read the files yet? self._lazy_prefix = [] # Pre-materialization calls to set(). self._lazy_suffix = [] # Calls to add(). def read(self, user=True, defaults=True): self._materialized = True super(LazyConfig, self).read(user, defaults) def resolve(self): if not self._materialized: # Read files and unspool buffers. self.read() self.sources += self._lazy_suffix self.sources[:0] = self._lazy_prefix return super(LazyConfig, self).resolve() def add(self, value): super(LazyConfig, self).add(value) if not self._materialized: # Buffer additions to end. self._lazy_suffix += self.sources del self.sources[:] def set(self, value): super(LazyConfig, self).set(value) if not self._materialized: # Buffer additions to beginning. self._lazy_prefix[:0] = self.sources del self.sources[:] def clear(self): """Remove all sources from this configuration.""" del self.sources[:] self._lazy_suffix = [] self._lazy_prefix = [] # "Validated" configuration views: experimental! REQUIRED = object() """A sentinel indicating that there is no default value and an exception should be raised when the value is missing. """ class Template(object): """A value template for configuration fields. The template works like a type and instructs Confit about how to interpret a deserialized YAML value. This includes type conversions, providing a default value, and validating for errors. For example, a filepath type might expand tildes and check that the file exists. """ def __init__(self, default=REQUIRED): """Create a template with a given default value. If `default` is the sentinel `REQUIRED` (as it is by default), then an error will be raised when a value is missing. Otherwise, missing values will instead return `default`. """ self.default = default def __call__(self, view): """Invoking a template on a view gets the view's value according to the template. """ return self.value(view, self) def value(self, view, template=None): """Get the value for a `ConfigView`. May raise a `NotFoundError` if the value is missing (and the template requires it) or a `ConfigValueError` for invalid values. """ if view.exists(): value, _ = view.first() return self.convert(value, view) elif self.default is REQUIRED: # Missing required value. This is an error. raise NotFoundError("{0} not found".format(view.name)) else: # Missing value, but not required. return self.default def convert(self, value, view): """Convert the YAML-deserialized value to a value of the desired type. Subclasses should override this to provide useful conversions. May raise a `ConfigValueError` when the configuration is wrong. """ # Default implementation does no conversion. return value def fail(self, message, view, type_error=False): """Raise an exception indicating that a value cannot be accepted. `type_error` indicates whether the error is due to a type mismatch rather than a malformed value. In this case, a more specific exception is raised. """ exc_class = ConfigTypeError if type_error else ConfigValueError raise exc_class( '{0}: {1}'.format(view.name, message) ) def __repr__(self): return '{0}({1})'.format( type(self).__name__, '' if self.default is REQUIRED else repr(self.default), ) class Integer(Template): """An integer configuration value template. """ def convert(self, value, view): """Check that the value is an integer. Floats are rounded. """ if isinstance(value, int): return value elif isinstance(value, float): return int(value) else: self.fail('must be a number', view, True) class Number(Template): """A numeric type: either an integer or a floating-point number. """ def convert(self, value, view): """Check that the value is an int or a float. """ if isinstance(value, NUMERIC_TYPES): return value else: self.fail( 'must be numeric, not {0}'.format(type(value).__name__), view, True ) class MappingTemplate(Template): """A template that uses a dictionary to specify other types for the values for a set of keys and produce a validated `AttrDict`. """ def __init__(self, mapping): """Create a template according to a dict (mapping). The mapping's values should themselves either be Types or convertible to Types. """ subtemplates = {} for key, typ in mapping.items(): subtemplates[key] = as_template(typ) self.subtemplates = subtemplates def value(self, view, template=None): """Get a dict with the same keys as the template and values validated according to the value types. """ out = AttrDict() for key, typ in self.subtemplates.items(): out[key] = typ.value(view[key], self) return out def __repr__(self): return 'MappingTemplate({0})'.format(repr(self.subtemplates)) class String(Template): """A string configuration value template. """ def __init__(self, default=REQUIRED, pattern=None): """Create a template with the added optional `pattern` argument, a regular expression string that the value should match. """ super(String, self).__init__(default) self.pattern = pattern if pattern: self.regex = re.compile(pattern) def convert(self, value, view): """Check that the value is a string and matches the pattern. """ if isinstance(value, BASESTRING): if self.pattern and not self.regex.match(value): self.fail( "must match the pattern {0}".format(self.pattern), view ) return value else: self.fail('must be a string', view, True) class Choice(Template): """A template that permits values from a sequence of choices. """ def __init__(self, choices): """Create a template that validates any of the values from the iterable `choices`. If `choices` is a map, then the corresponding value is emitted. Otherwise, the value itself is emitted. """ self.choices = choices def convert(self, value, view): """Ensure that the value is among the choices (and remap if the choices are a mapping). """ if value not in self.choices: self.fail( 'must be one of {0}, not {1}'.format( repr(list(self.choices)), repr(value) ), view ) if isinstance(self.choices, collections.Mapping): return self.choices[value] else: return value def __repr__(self): return 'Choice({0!r})'.format(self.choices) class StrSeq(Template): """A template for values that are lists of strings. Validates both actual YAML string lists and single strings. Strings can optionally be split on whitespace. """ def __init__(self, split=True): """Create a new template. `split` indicates whether, when the underlying value is a single string, it should be split on whitespace. Otherwise, the resulting value is a list containing a single string. """ super(StrSeq, self).__init__() self.split = split def convert(self, value, view): if isinstance(value, bytes): value = value.decode('utf8', 'ignore') if isinstance(value, STRING): if self.split: return value.split() else: return [value] try: value = list(value) except TypeError: self.fail('must be a whitespace-separated string or a list', view, True) def convert(x): if isinstance(x, unicode): return x elif isinstance(x, BASESTRING): return x.decode('utf8', 'ignore') else: self.fail('must be a list of strings', view, True) return map(convert, value) class Filename(Template): """A template that validates strings as filenames. Filenames are returned as absolute, tilde-free paths. Relative paths are relative to the template's `cwd` argument when it is specified, then the configuration directory (see the `config_dir` method) if they come from a file. Otherwise, they are relative to the current working directory. This helps attain the expected behavior when using command-line options. """ def __init__(self, default=REQUIRED, cwd=None, relative_to=None): """ `relative_to` is the name of a sibling value that is being validated at the same time. """ super(Filename, self).__init__(default) self.cwd, self.relative_to = cwd, relative_to def __repr__(self): args = [] if self.default is not REQUIRED: args.append(repr(self.default)) if self.cwd is not None: args.append('cwd=' + repr(self.cwd)) if self.relative_to is not None: args.append('relative_to=' + repr(self.relative_to)) return 'Filename({0})'.format(', '.join(args)) def resolve_relative_to(self, view, template): if not isinstance(template, (collections.Mapping, MappingTemplate)): # disallow config.get(Filename(relative_to='foo')) raise ConfigTemplateError( 'relative_to may only be used when getting multiple values.' ) elif self.relative_to == view.key: raise ConfigTemplateError( '{0} is relative to itself'.format(view.name) ) elif self.relative_to not in view.parent.keys(): # self.relative_to is not in the config self.fail( ( 'needs sibling value "{0}" to expand relative path' ).format(self.relative_to), view ) old_template = {} old_template.update(template.subtemplates) # save time by skipping MappingTemplate's init loop next_template = MappingTemplate({}) next_relative = self.relative_to # gather all the needed templates and nothing else while next_relative is not None: try: # pop to avoid infinite loop because of recursive # relative paths rel_to_template = old_template.pop(next_relative) except KeyError: if next_relative in template.subtemplates: # we encountered this config key previously raise ConfigTemplateError(( '{0} and {1} are recursively relative' ).format(view.name, self.relative_to)) else: raise ConfigTemplateError(( 'missing template for {0}, needed to expand {1}\'s' + 'relative path' ).format(self.relative_to, view.name)) next_template.subtemplates[next_relative] = rel_to_template next_relative = rel_to_template.relative_to return view.parent.get(next_template)[self.relative_to] def value(self, view, template=None): path, source = view.first() if not isinstance(path, BASESTRING): self.fail( 'must be a filename, not {0}'.format(type(path).__name__), view, True ) path = os.path.expanduser(STRING(path)) if not os.path.isabs(path): if self.cwd is not None: # relative to the template's argument path = os.path.join(self.cwd, path) elif self.relative_to is not None: path = os.path.join( self.resolve_relative_to(view, template), path, ) elif source.filename: # From defaults: relative to the app's directory. path = os.path.join(view.root().config_dir(), path) return os.path.abspath(path) class TypeTemplate(Template): """A simple template that checks that a value is an instance of a desired Python type. """ def __init__(self, typ, default=REQUIRED): """Create a template that checks that the value is an instance of `typ`. """ super(TypeTemplate, self).__init__(default) self.typ = typ def convert(self, value, view): if not isinstance(value, self.typ): self.fail( 'must be a {0}, not {1}'.format( self.typ.__name__, type(value).__name__, ), view, True ) return value class AttrDict(dict): """A `dict` subclass that can be accessed via attributes (dot notation) for convenience. """ def __getattr__(self, key): if key in self: return self[key] else: raise AttributeError(key) def as_template(value): """Convert a simple "shorthand" Python value to a `Template`. """ if isinstance(value, Template): # If it's already a Template, pass it through. return value elif isinstance(value, collections.Mapping): # Dictionaries work as templates. return MappingTemplate(value) elif value is int: return Integer() elif isinstance(value, int): return Integer(value) elif isinstance(value, type) and issubclass(value, BASESTRING): return String() elif isinstance(value, BASESTRING): return String(value) elif value is float: return Number() elif value is None: return Template() elif value is dict: return TypeTemplate(collections.Mapping) elif value is list: return TypeTemplate(collections.Sequence) elif isinstance(value, type): return TypeTemplate(value) else: raise ValueError('cannot convert to template: {0!r}'.format(value)) beets-1.3.8/beets/util/enumeration.py0000644000076500000240000000253112321560145020550 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. from enum import Enum class OrderedEnum(Enum): """ An Enum subclass that allows comparison of members. """ def __ge__(self, other): if self.__class__ is other.__class__: return self.value >= other.value return NotImplemented def __gt__(self, other): if self.__class__ is other.__class__: return self.value > other.value return NotImplemented def __le__(self, other): if self.__class__ is other.__class__: return self.value <= other.value return NotImplemented def __lt__(self, other): if self.__class__ is other.__class__: return self.value < other.value return NotImplemented beets-1.3.8/beets/util/functemplate.py0000644000076500000240000004545512322654070020727 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """This module implements a string formatter based on the standard PEP 292 string.Template class extended with function calls. Variables, as with string.Template, are indicated with $ and functions are delimited with %. This module assumes that everything is Unicode: the template and the substitution values. Bytestrings are not supported. Also, the templates always behave like the ``safe_substitute`` method in the standard library: unknown symbols are left intact. This is sort of like a tiny, horrible degeneration of a real templating engine like Jinja2 or Mustache. """ from __future__ import print_function import re import ast import dis import types SYMBOL_DELIM = u'$' FUNC_DELIM = u'%' GROUP_OPEN = u'{' GROUP_CLOSE = u'}' ARG_SEP = u',' ESCAPE_CHAR = u'$' VARIABLE_PREFIX = '__var_' FUNCTION_PREFIX = '__func_' class Environment(object): """Contains the values and functions to be substituted into a template. """ def __init__(self, values, functions): self.values = values self.functions = functions # Code generation helpers. def ex_lvalue(name): """A variable load expression.""" return ast.Name(name, ast.Store()) def ex_rvalue(name): """A variable store expression.""" return ast.Name(name, ast.Load()) def ex_literal(val): """An int, float, long, bool, string, or None literal with the given value. """ if val is None: return ast.Name('None', ast.Load()) elif isinstance(val, (int, float, long)): return ast.Num(val) elif isinstance(val, bool): return ast.Name(str(val), ast.Load()) elif isinstance(val, basestring): return ast.Str(val) raise TypeError('no literal for {0}'.format(type(val))) def ex_varassign(name, expr): """Assign an expression into a single variable. The expression may either be an `ast.expr` object or a value to be used as a literal. """ if not isinstance(expr, ast.expr): expr = ex_literal(expr) return ast.Assign([ex_lvalue(name)], expr) def ex_call(func, args): """A function-call expression with only positional parameters. The function may be an expression or the name of a function. Each argument may be an expression or a value to be used as a literal. """ if isinstance(func, basestring): func = ex_rvalue(func) args = list(args) for i in range(len(args)): if not isinstance(args[i], ast.expr): args[i] = ex_literal(args[i]) return ast.Call(func, args, [], None, None) def compile_func(arg_names, statements, name='_the_func', debug=False): """Compile a list of statements as the body of a function and return the resulting Python function. If `debug`, then print out the bytecode of the compiled function. """ func_def = ast.FunctionDef( name, ast.arguments( [ast.Name(n, ast.Param()) for n in arg_names], None, None, [ex_literal(None) for _ in arg_names], ), statements, [], ) mod = ast.Module([func_def]) ast.fix_missing_locations(mod) prog = compile(mod, '', 'exec') # Debug: show bytecode. if debug: dis.dis(prog) for const in prog.co_consts: if isinstance(const, types.CodeType): dis.dis(const) the_locals = {} exec prog in {}, the_locals return the_locals[name] # AST nodes for the template language. class Symbol(object): """A variable-substitution symbol in a template.""" def __init__(self, ident, original): self.ident = ident self.original = original def __repr__(self): return u'Symbol(%s)' % repr(self.ident) def evaluate(self, env): """Evaluate the symbol in the environment, returning a Unicode string. """ if self.ident in env.values: # Substitute for a value. return env.values[self.ident] else: # Keep original text. return self.original def translate(self): """Compile the variable lookup.""" expr = ex_rvalue(VARIABLE_PREFIX + self.ident.encode('utf8')) return [expr], set([self.ident.encode('utf8')]), set() class Call(object): """A function call in a template.""" def __init__(self, ident, args, original): self.ident = ident self.args = args self.original = original def __repr__(self): return u'Call(%s, %s, %s)' % (repr(self.ident), repr(self.args), repr(self.original)) def evaluate(self, env): """Evaluate the function call in the environment, returning a Unicode string. """ if self.ident in env.functions: arg_vals = [expr.evaluate(env) for expr in self.args] try: out = env.functions[self.ident](*arg_vals) except Exception as exc: # Function raised exception! Maybe inlining the name of # the exception will help debug. return u'<%s>' % unicode(exc) return unicode(out) else: return self.original def translate(self): """Compile the function call.""" varnames = set() funcnames = set([self.ident.encode('utf8')]) arg_exprs = [] for arg in self.args: subexprs, subvars, subfuncs = arg.translate() varnames.update(subvars) funcnames.update(subfuncs) # Create a subexpression that joins the result components of # the arguments. arg_exprs.append(ex_call( ast.Attribute(ex_literal(u''), 'join', ast.Load()), [ex_call( 'map', [ ex_rvalue('unicode'), ast.List(subexprs, ast.Load()), ] )], )) subexpr_call = ex_call( FUNCTION_PREFIX + self.ident.encode('utf8'), arg_exprs ) return [subexpr_call], varnames, funcnames class Expression(object): """Top-level template construct: contains a list of text blobs, Symbols, and Calls. """ def __init__(self, parts): self.parts = parts def __repr__(self): return u'Expression(%s)' % (repr(self.parts)) def evaluate(self, env): """Evaluate the entire expression in the environment, returning a Unicode string. """ out = [] for part in self.parts: if isinstance(part, basestring): out.append(part) else: out.append(part.evaluate(env)) return u''.join(map(unicode, out)) def translate(self): """Compile the expression to a list of Python AST expressions, a set of variable names used, and a set of function names. """ expressions = [] varnames = set() funcnames = set() for part in self.parts: if isinstance(part, basestring): expressions.append(ex_literal(part)) else: e, v, f = part.translate() expressions.extend(e) varnames.update(v) funcnames.update(f) return expressions, varnames, funcnames # Parser. class ParseError(Exception): pass class Parser(object): """Parses a template expression string. Instantiate the class with the template source and call ``parse_expression``. The ``pos`` field will indicate the character after the expression finished and ``parts`` will contain a list of Unicode strings, Symbols, and Calls reflecting the concatenated portions of the expression. This is a terrible, ad-hoc parser implementation based on a left-to-right scan with no lexing step to speak of; it's probably both inefficient and incorrect. Maybe this should eventually be replaced with a real, accepted parsing technique (PEG, parser generator, etc.). """ def __init__(self, string): self.string = string self.pos = 0 self.parts = [] # Common parsing resources. special_chars = (SYMBOL_DELIM, FUNC_DELIM, GROUP_OPEN, GROUP_CLOSE, ARG_SEP, ESCAPE_CHAR) special_char_re = re.compile(ur'[%s]|$' % u''.join(re.escape(c) for c in special_chars)) def parse_expression(self): """Parse a template expression starting at ``pos``. Resulting components (Unicode strings, Symbols, and Calls) are added to the ``parts`` field, a list. The ``pos`` field is updated to be the next character after the expression. """ text_parts = [] while self.pos < len(self.string): char = self.string[self.pos] if char not in self.special_chars: # A non-special character. Skip to the next special # character, treating the interstice as literal text. next_pos = ( self.special_char_re.search(self.string[self.pos:]).start() + self.pos ) text_parts.append(self.string[self.pos:next_pos]) self.pos = next_pos continue if self.pos == len(self.string) - 1: # The last character can never begin a structure, so we # just interpret it as a literal character (unless it # terminates the expression, as with , and }). if char not in (GROUP_CLOSE, ARG_SEP): text_parts.append(char) self.pos += 1 break next_char = self.string[self.pos + 1] if char == ESCAPE_CHAR and next_char in \ (SYMBOL_DELIM, FUNC_DELIM, GROUP_CLOSE, ARG_SEP): # An escaped special character ($$, $}, etc.). Note that # ${ is not an escape sequence: this is ambiguous with # the start of a symbol and it's not necessary (just # using { suffices in all cases). text_parts.append(next_char) self.pos += 2 # Skip the next character. continue # Shift all characters collected so far into a single string. if text_parts: self.parts.append(u''.join(text_parts)) text_parts = [] if char == SYMBOL_DELIM: # Parse a symbol. self.parse_symbol() elif char == FUNC_DELIM: # Parse a function call. self.parse_call() elif char in (GROUP_CLOSE, ARG_SEP): # Template terminated. break elif char == GROUP_OPEN: # Start of a group has no meaning hear; just pass # through the character. text_parts.append(char) self.pos += 1 else: assert False # If any parsed characters remain, shift them into a string. if text_parts: self.parts.append(u''.join(text_parts)) def parse_symbol(self): """Parse a variable reference (like ``$foo`` or ``${foo}``) starting at ``pos``. Possibly appends a Symbol object (or, failing that, text) to the ``parts`` field and updates ``pos``. The character at ``pos`` must, as a precondition, be ``$``. """ assert self.pos < len(self.string) assert self.string[self.pos] == SYMBOL_DELIM if self.pos == len(self.string) - 1: # Last character. self.parts.append(SYMBOL_DELIM) self.pos += 1 return next_char = self.string[self.pos + 1] start_pos = self.pos self.pos += 1 if next_char == GROUP_OPEN: # A symbol like ${this}. self.pos += 1 # Skip opening. closer = self.string.find(GROUP_CLOSE, self.pos) if closer == -1 or closer == self.pos: # No closing brace found or identifier is empty. self.parts.append(self.string[start_pos:self.pos]) else: # Closer found. ident = self.string[self.pos:closer] self.pos = closer + 1 self.parts.append(Symbol(ident, self.string[start_pos:self.pos])) else: # A bare-word symbol. ident = self._parse_ident() if ident: # Found a real symbol. self.parts.append(Symbol(ident, self.string[start_pos:self.pos])) else: # A standalone $. self.parts.append(SYMBOL_DELIM) def parse_call(self): """Parse a function call (like ``%foo{bar,baz}``) starting at ``pos``. Possibly appends a Call object to ``parts`` and update ``pos``. The character at ``pos`` must be ``%``. """ assert self.pos < len(self.string) assert self.string[self.pos] == FUNC_DELIM start_pos = self.pos self.pos += 1 ident = self._parse_ident() if not ident: # 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]) return if self.string[self.pos] != GROUP_OPEN: # Argument list not opened. self.parts.append(self.string[start_pos:self.pos]) return # Skip past opening brace and try to parse an argument list. self.pos += 1 args = self.parse_argument_list() if self.pos >= len(self.string) or \ self.string[self.pos] != GROUP_CLOSE: # Arguments unclosed. self.parts.append(self.string[start_pos:self.pos]) return self.pos += 1 # Move past closing brace. self.parts.append(Call(ident, args, self.string[start_pos:self.pos])) def parse_argument_list(self): """Parse a list of arguments starting at ``pos``, returning a list of Expression objects. Does not modify ``parts``. Should leave ``pos`` pointing to a } character or the end of the string. """ # Try to parse a subexpression in a subparser. expressions = [] while self.pos < len(self.string): subparser = Parser(self.string[self.pos:]) subparser.parse_expression() # Extract and advance past the parsed expression. expressions.append(Expression(subparser.parts)) self.pos += subparser.pos if self.pos >= len(self.string) or \ self.string[self.pos] == GROUP_CLOSE: # Argument list terminated by EOF or closing brace. break # Only other way to terminate an expression is with ,. # Continue to the next argument. assert self.string[self.pos] == ARG_SEP self.pos += 1 return expressions def _parse_ident(self): """Parse an identifier and return it (possibly an empty string). Updates ``pos``. """ remainder = self.string[self.pos:] ident = re.match(ur'\w*', remainder).group(0) self.pos += len(ident) return ident def _parse(template): """Parse a top-level template string Expression. Any extraneous text is considered literal text. """ parser = Parser(template) parser.parse_expression() parts = parser.parts remainder = parser.string[parser.pos:] if remainder: parts.append(remainder) return Expression(parts) # External interface. class Template(object): """A string template, including text, Symbols, and Calls. """ def __init__(self, template): self.expr = _parse(template) self.original = template self.compiled = self.translate() def __eq__(self, other): return self.original == other.original def interpret(self, values={}, functions={}): """Like `substitute`, but forces the interpreter (rather than the compiled version) to be used. The interpreter includes exception-handling code for missing variables and buggy template functions but is much slower. """ return self.expr.evaluate(Environment(values, functions)) def substitute(self, values={}, functions={}): """Evaluate the template given the values and functions. """ try: res = self.compiled(values, functions) except: # Handle any exceptions thrown by compiled version. res = self.interpret(values, functions) return res def translate(self): """Compile the template to a Python function.""" expressions, varnames, funcnames = self.expr.translate() argnames = [] for varname in varnames: argnames.append(VARIABLE_PREFIX.encode('utf8') + varname) for funcname in funcnames: argnames.append(FUNCTION_PREFIX.encode('utf8') + funcname) func = compile_func( argnames, [ast.Return(ast.List(expressions, ast.Load()))], ) def wrapper_func(values={}, functions={}): args = {} for varname in varnames: args[VARIABLE_PREFIX + varname] = values[varname] for funcname in funcnames: args[FUNCTION_PREFIX + funcname] = functions[funcname] parts = func(**args) return u''.join(parts) return wrapper_func # Performance tests. if __name__ == '__main__': import timeit _tmpl = Template(u'foo $bar %baz{foozle $bar barzle} $bar') _vars = {'bar': 'qux'} _funcs = {'baz': unicode.upper} interp_time = timeit.timeit('_tmpl.interpret(_vars, _funcs)', 'from __main__ import _tmpl, _vars, _funcs', number=10000) print(interp_time) comp_time = timeit.timeit('_tmpl.substitute(_vars, _funcs)', 'from __main__ import _tmpl, _vars, _funcs', number=10000) print(comp_time) print('Speedup:', interp_time / comp_time) beets-1.3.8/beets/util/pipeline.py0000644000076500000240000003663612404675421020052 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Simple but robust implementation of generator/coroutine-based pipelines in Python. The pipelines may be run either sequentially (single-threaded) or in parallel (one thread per pipeline stage). This implementation supports pipeline bubbles (indications that the processing for a certain item should abort). To use them, yield the BUBBLE constant from any stage coroutine except the last. In the parallel case, the implementation transparently handles thread shutdown when the processing is complete and when a stage raises an exception. KeyboardInterrupts (^C) are also handled. When running a parallel pipeline, it is also possible to use multiple coroutines for the same pipeline stage; this lets you speed up a bottleneck stage by dividing its work among multiple threads. To do so, pass an iterable of coroutines to the Pipeline constructor in place of any single coroutine. """ from __future__ import print_function import Queue from threading import Thread, Lock import sys BUBBLE = '__PIPELINE_BUBBLE__' POISON = '__PIPELINE_POISON__' DEFAULT_QUEUE_SIZE = 16 def _invalidate_queue(q, val=None, sync=True): """Breaks a Queue such that it never blocks, always has size 1, and has no maximum size. get()ing from the queue returns `val`, which defaults to None. `sync` controls whether a lock is required (because it's not reentrant!). """ def _qsize(len=len): return 1 def _put(item): pass def _get(): return val if sync: q.mutex.acquire() try: q.maxsize = 0 q._qsize = _qsize q._put = _put q._get = _get q.not_empty.notifyAll() q.not_full.notifyAll() finally: if sync: q.mutex.release() class CountedQueue(Queue.Queue): """A queue that keeps track of the number of threads that are still feeding into it. The queue is poisoned when all threads are finished with the queue. """ def __init__(self, maxsize=0): Queue.Queue.__init__(self, maxsize) self.nthreads = 0 self.poisoned = False def acquire(self): """Indicate that a thread will start putting into this queue. Should not be called after the queue is already poisoned. """ with self.mutex: assert not self.poisoned assert self.nthreads >= 0 self.nthreads += 1 def release(self): """Indicate that a thread that was putting into this queue has exited. If this is the last thread using the queue, the queue is poisoned. """ with self.mutex: self.nthreads -= 1 assert self.nthreads >= 0 if self.nthreads == 0: # All threads are done adding to this queue. Poison it # when it becomes empty. self.poisoned = True # Replacement _get invalidates when no items remain. _old_get = self._get def _get(): out = _old_get() if not self.queue: _invalidate_queue(self, POISON, False) return out if self.queue: # Items remain. self._get = _get else: # No items. Invalidate immediately. _invalidate_queue(self, POISON, False) class MultiMessage(object): """A message yielded by a pipeline stage encapsulating multiple values to be sent to the next stage. """ def __init__(self, messages): self.messages = messages def multiple(messages): """Yield multiple([message, ..]) from a pipeline stage to send multiple values to the next pipeline stage. """ return MultiMessage(messages) def stage(func): """Decorate a function to become a simple stage. >>> @stage ... def add(n, i): ... return i + n >>> pipe = Pipeline([ ... iter([1, 2, 3]), ... add(2), ... ]) >>> list(pipe.pull()) [3, 4, 5] """ def coro(*args): task = None while True: task = yield task task = func(*(args + (task,))) return coro def mutator_stage(func): """Decorate a function that manipulates items in a coroutine to become a simple stage. >>> @mutator_stage ... def setkey(key, item): ... item[key] = True >>> pipe = Pipeline([ ... iter([{'x': False}, {'a': False}]), ... setkey('x'), ... ]) >>> list(pipe.pull()) [{'x': True}, {'a': False, 'x': True}] """ def coro(*args): task = None while True: task = yield task func(*(args + (task,))) return coro def _allmsgs(obj): """Returns a list of all the messages encapsulated in obj. If obj is a MultiMessage, returns its enclosed messages. If obj is BUBBLE, returns an empty list. Otherwise, returns a list containing obj. """ if isinstance(obj, MultiMessage): return obj.messages elif obj == BUBBLE: return [] else: return [obj] class PipelineThread(Thread): """Abstract base class for pipeline-stage threads.""" def __init__(self, all_threads): super(PipelineThread, self).__init__() self.abort_lock = Lock() self.abort_flag = False self.all_threads = all_threads self.exc_info = None def abort(self): """Shut down the thread at the next chance possible. """ with self.abort_lock: self.abort_flag = True # Ensure that we are not blocking on a queue read or write. if hasattr(self, 'in_queue'): _invalidate_queue(self.in_queue, POISON) if hasattr(self, 'out_queue'): _invalidate_queue(self.out_queue, POISON) def abort_all(self, exc_info): """Abort all other threads in the system for an exception. """ self.exc_info = exc_info for thread in self.all_threads: thread.abort() class FirstPipelineThread(PipelineThread): """The thread running the first stage in a parallel pipeline setup. The coroutine should just be a generator. """ def __init__(self, coro, out_queue, all_threads): super(FirstPipelineThread, self).__init__(all_threads) 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: if self.abort_flag: return self.out_queue.put(msg) except: self.abort_all(sys.exc_info()) return # 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. """ def __init__(self, coro, in_queue, out_queue, all_threads): super(MiddlePipelineThread, self).__init__(all_threads) self.coro = coro self.in_queue = in_queue self.out_queue = out_queue self.out_queue.acquire() def run(self): try: # Prime the coroutine. self.coro.next() while True: 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 # Invoke the current stage. out = self.coro.send(msg) # Send messages to next stage. for msg in _allmsgs(out): with self.abort_lock: if self.abort_flag: return self.out_queue.put(msg) except: self.abort_all(sys.exc_info()) return # Pipeline is shutting down normally. self.out_queue.release() class LastPipelineThread(PipelineThread): """A thread running the last stage in a pipeline. The coroutine should yield nothing. """ def __init__(self, coro, in_queue, all_threads): super(LastPipelineThread, self).__init__(all_threads) self.coro = coro self.in_queue = in_queue def run(self): # Prime the coroutine. self.coro.next() try: while True: 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 # Send to consumer. self.coro.send(msg) except: self.abort_all(sys.exc_info()) return class Pipeline(object): """Represents a staged pattern of work. Each stage in the pipeline is a coroutine that receives messages from the previous stage and yields messages to be sent to the next stage. """ def __init__(self, stages): """Makes a new pipeline from a list of coroutines. There must be at least two stages. """ if len(stages) < 2: raise ValueError('pipeline must have at least two stages') self.stages = [] for stage in stages: if isinstance(stage, (list, tuple)): self.stages.append(stage) else: # Default to one thread per stage. 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 in each stage is used. """ list(self.pull()) 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 size. """ queue_count = len(self.stages) - 1 queues = [CountedQueue(queue_size) for i in range(queue_count)] threads = [] # Set up first stage. for coro in self.stages[0]: threads.append(FirstPipelineThread(coro, queues[0], threads)) # Middle stages. for i in range(1, queue_count): for coro in self.stages[i]: threads.append(MiddlePipelineThread( coro, queues[i - 1], queues[i], threads )) # Last stage. for coro in self.stages[-1]: 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 # exceptions during the join(). while threads[-1].isAlive(): threads[-1].join(1) except: # Stop all the threads immediately. 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, # in normal operation, or aborted, in case of an exception. for thread in threads[:-1]: thread.join() for thread in threads: exc_info = thread.exc_info if exc_info: # Make the exception appear as it was raised originally. raise exc_info[0], exc_info[1], exc_info[2] def pull(self): """Yield elements from the end of the pipeline. Runs the stages sequentially until the last yields some messages. Each of the messages is then yielded by ``pulled.next()``. If the pipeline has a consumer, that is the last stage does not yield any messages, then pull will not yield any messages. Only the first coroutine in each stage is used """ coros = [stage[0] for stage in self.stages] # "Prime" the coroutines. for coro in coros[1:]: coro.next() # Begin the pipeline. for out in coros[0]: msgs = _allmsgs(out) for coro in coros[1:]: next_msgs = [] for msg in msgs: out = coro.send(msg) next_msgs.extend(_allmsgs(out)) msgs = next_msgs for msg in msgs: yield msg # Smoke test. if __name__ == '__main__': import time # Test a normally-terminating pipeline both in sequence and # in parallel. def produce(): for i in range(5): print('generating %i' % i) time.sleep(1) yield i def work(): num = yield while True: print('processing %i' % num) time.sleep(2) num = yield num * 2 def consume(): while True: num = yield time.sleep(1) print('received %i' % num) ts_start = time.time() Pipeline([produce(), work(), consume()]).run_sequential() ts_seq = time.time() Pipeline([produce(), work(), consume()]).run_parallel() ts_par = time.time() Pipeline([produce(), (work(), work()), consume()]).run_parallel() ts_end = time.time() print('Sequential time:', ts_seq - ts_start) print('Parallel time:', ts_par - ts_seq) print('Multiply-parallel time:', ts_end - ts_par) print() # Test a pipeline that raises an exception. def exc_produce(): for i in range(10): print('generating %i' % i) time.sleep(1) yield i def exc_work(): num = yield while True: print('processing %i' % num) time.sleep(3) if num == 3: raise Exception() num = yield num * 2 def exc_consume(): while True: num = yield print('received %i' % num) Pipeline([exc_produce(), exc_work(), exc_consume()]).run_parallel(1) beets-1.3.8/beets/vfs.py0000644000076500000240000000332512321560145016045 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """A simple utility for constructing filesystem-like trees from beets libraries. """ from collections import namedtuple from beets import util Node = namedtuple('Node', ['files', 'dirs']) def _insert(node, path, itemid): """Insert an item into a virtual filesystem node.""" if len(path) == 1: # Last component. Insert file. node.files[path[0]] = itemid else: # In a directory. dirname = path[0] rest = path[1:] if dirname not in node.dirs: node.dirs[dirname] = Node({}, {}) _insert(node.dirs[dirname], rest, itemid) def libtree(lib): """Generates a filesystem-like directory tree for the files contained in `lib`. Filesystem nodes are (files, dirs) named tuples in which both components are dictionaries. The first maps filenames to Item ids. The second maps directory names to child node tuples. """ root = Node({}, {}) for item in lib.items(): dest = item.destination(fragment=True) parts = util.components(dest) _insert(root, parts, item.id) return root beets-1.3.8/beets.egg-info/0000755000076500000240000000000012406440351016364 5ustar asampsonstaff00000000000000beets-1.3.8/beets.egg-info/dependency_links.txt0000644000076500000240000000000112406440350022431 0ustar asampsonstaff00000000000000 beets-1.3.8/beets.egg-info/entry_points.txt0000644000076500000240000000005012406440350021654 0ustar asampsonstaff00000000000000[console_scripts] beet = beets.ui:main beets-1.3.8/beets.egg-info/PKG-INFO0000644000076500000240000001224012406440350017457 0ustar asampsonstaff00000000000000Metadata-Version: 1.1 Name: beets Version: 1.3.8 Summary: music tagger and library organizer Home-page: http://beets.radbox.org/ Author: Adrian Sampson Author-email: adrian@radbox.org License: MIT Description: .. image:: https://travis-ci.org/sampsyo/beets.svg?branch=master :target: https://travis-ci.org/sampsyo/beets .. image:: http://img.shields.io/coveralls/sampsyo/beets.svg :target: https://coveralls.io/r/sampsyo/beets .. image:: http://img.shields.io/pypi/v/beets.svg :target: https://pypi.python.org/pypi/beets Beets is the media library management system for obsessive-compulsive music geeks. The purpose of beets is to get your music collection right once and for all. It catalogs your collection, automatically improving its metadata as it goes. It then provides a bouquet of tools for manipulating and accessing your music. Here's an example of beets' brainy tag corrector doing its thing:: $ beet import ~/music/ladytron Tagging: Ladytron - Witching Hour (Similarity: 98.4%) * Last One Standing -> The Last One Standing * Beauty -> Beauty*2 * White Light Generation -> Whitelightgenerator * All the Way -> All the Way... Because beets is designed as a library, it can do almost anything you can imagine for your music collection. Via `plugins`_, beets becomes a panacea: - Fetch or calculate all the metadata you could possibly need: `album art`_, `lyrics`_, `genres`_, `tempos`_, `ReplayGain`_ levels, or `acoustic fingerprints`_. - Get metadata from `MusicBrainz`_, `Discogs`_, or `Beatport`_. Or guess metadata using songs' filenames or their acoustic fingerprints. - `Transcode audio`_ to any format you like. - Check your library for `duplicate tracks and albums`_ or for `albums that are missing tracks`_. - Clean up crufty tags left behind by other, less-awesome tools. - Embed and extract album art from files' metadata. - Browse your music library graphically through a Web browser and play it in any browser that supports `HTML5 Audio`_. - Analyze music files' metadata from the command line. - Listen to your library with a music player that speaks the `MPD`_ protocol and works with a staggering variety of interfaces. If beets doesn't do what you want yet, `writing your own plugin`_ is shockingly simple if you know a little Python. .. _plugins: http://beets.readthedocs.org/page/plugins/ .. _MPD: http://www.musicpd.org/ .. _MusicBrainz music collection: http://musicbrainz.org/doc/Collections/ .. _writing your own plugin: http://beets.readthedocs.org/page/dev/plugins.html .. _HTML5 Audio: http://www.w3.org/TR/html-markup/audio.html .. _albums that are missing tracks: http://beets.readthedocs.org/page/plugins/missing.html .. _duplicate tracks and albums: http://beets.readthedocs.org/page/plugins/duplicates.html .. _Transcode audio: http://beets.readthedocs.org/page/plugins/convert.html .. _Beatport: http://www.beatport.com/ .. _Discogs: http://www.discogs.com/ .. _acoustic fingerprints: http://beets.readthedocs.org/page/plugins/chroma.html .. _ReplayGain: http://beets.readthedocs.org/page/plugins/replaygain.html .. _tempos: http://beets.readthedocs.org/page/plugins/echonest_tempo.html .. _genres: http://beets.readthedocs.org/page/plugins/lastgenre.html .. _album art: http://beets.readthedocs.org/page/plugins/fetchart.html .. _lyrics: http://beets.readthedocs.org/page/plugins/lyrics.html .. _MusicBrainz: http://musicbrainz.org/ Read More --------- Learn more about beets at `its Web site`_. Follow `@b33ts`_ on Twitter for news and updates. You can install beets by typing ``pip install beets``. Then check out the `Getting Started`_ guide. .. _its Web site: http://beets.radbox.org/ .. _Getting Started: http://beets.readthedocs.org/page/guides/main.html .. _@b33ts: http://twitter.com/b33ts/ Authors ------- Beets is by `Adrian Sampson`_ with a supporting cast of thousands. For help, please contact the `mailing list`_. .. _mailing list: https://groups.google.com/forum/#!forum/beets-users .. _Adrian Sampson: http://homes.cs.washington.edu/~asampson/ Platform: ALL Classifier: Topic :: Multimedia :: Sound/Audio Classifier: Topic :: Multimedia :: Sound/Audio :: Players :: MP3 Classifier: License :: OSI Approved :: MIT License Classifier: Environment :: Console Classifier: Environment :: Web Environment Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 beets-1.3.8/beets.egg-info/requires.txt0000644000076500000240000000044012406440350020761 0ustar asampsonstaff00000000000000enum34 mutagen>=1.23 munkres unidecode musicbrainzngs>=0.4 pyyaml [beatport] requests [chroma] pyacoustid [discogs] discogs-client>=2.0.0 [echonest] pyechonest [echonest_tempo] pyechonest [fetchart] requests [import] rarfile [lastgenre] pylast [mpdstats] python-mpd [web] flask beets-1.3.8/beets.egg-info/SOURCES.txt0000644000076500000240000001520712406440351020255 0ustar asampsonstaff00000000000000LICENSE MANIFEST.in README.rst setup.cfg setup.py beets/__init__.py beets/config_default.yaml beets/importer.py beets/library.py beets/mediafile.py beets/plugins.py beets/vfs.py beets.egg-info/PKG-INFO beets.egg-info/SOURCES.txt beets.egg-info/dependency_links.txt beets.egg-info/entry_points.txt beets.egg-info/requires.txt beets.egg-info/top_level.txt beets/autotag/__init__.py beets/autotag/hooks.py beets/autotag/match.py beets/autotag/mb.py beets/dbcore/__init__.py beets/dbcore/db.py beets/dbcore/query.py beets/dbcore/queryparse.py beets/dbcore/types.py beets/ui/__init__.py beets/ui/commands.py beets/ui/completion_base.sh beets/ui/migrate.py beets/util/__init__.py beets/util/artresizer.py beets/util/bluelet.py beets/util/confit.py beets/util/enumeration.py beets/util/functemplate.py beets/util/pipeline.py beetsplug/__init__.py beetsplug/beatport.py beetsplug/bench.py beetsplug/bpm.py beetsplug/bucket.py beetsplug/chroma.py beetsplug/convert.py beetsplug/discogs.py beetsplug/duplicates.py beetsplug/echonest.py beetsplug/echonest_tempo.py beetsplug/embedart.py beetsplug/fetchart.py beetsplug/fromfilename.py beetsplug/ftintitle.py beetsplug/fuzzy.py beetsplug/ihate.py beetsplug/importadded.py beetsplug/importfeeds.py beetsplug/info.py beetsplug/inline.py beetsplug/keyfinder.py beetsplug/lyrics.py beetsplug/mbcollection.py beetsplug/mbsync.py beetsplug/missing.py beetsplug/mpdstats.py beetsplug/mpdupdate.py beetsplug/play.py beetsplug/random.py beetsplug/replaygain.py beetsplug/rewrite.py beetsplug/scrub.py beetsplug/smartplaylist.py beetsplug/spotify.py beetsplug/the.py beetsplug/types.py beetsplug/zero.py beetsplug/bpd/__init__.py beetsplug/bpd/gstplayer.py beetsplug/lastgenre/__init__.py beetsplug/lastgenre/genres-tree.yaml beetsplug/lastgenre/genres.txt beetsplug/web/__init__.py beetsplug/web/static/backbone.js beetsplug/web/static/beets.css beetsplug/web/static/beets.js beetsplug/web/static/jquery.js beetsplug/web/static/underscore.js beetsplug/web/templates/index.html docs/Makefile docs/changelog.rst docs/conf.py docs/faq.rst docs/index.rst docs/dev/api.rst docs/dev/index.rst docs/dev/media_file.rst docs/dev/plugins.rst docs/guides/advanced.rst docs/guides/index.rst docs/guides/main.rst docs/guides/migration.rst docs/guides/tagger.rst docs/plugins/beatport.rst docs/plugins/beetsweb.png docs/plugins/bpd.rst docs/plugins/bpm.rst docs/plugins/bucket.rst docs/plugins/chroma.rst docs/plugins/convert.rst docs/plugins/discogs.rst docs/plugins/duplicates.rst docs/plugins/echonest.rst docs/plugins/echonest_tempo.rst docs/plugins/embedart.rst docs/plugins/fetchart.rst docs/plugins/fromfilename.rst docs/plugins/ftintitle.rst docs/plugins/fuzzy.rst docs/plugins/ihate.rst docs/plugins/importadded.rst docs/plugins/importfeeds.rst docs/plugins/index.rst docs/plugins/info.rst docs/plugins/inline.rst docs/plugins/keyfinder.rst docs/plugins/lastgenre.rst docs/plugins/lyrics.rst docs/plugins/mbcollection.rst docs/plugins/mbsync.rst docs/plugins/missing.rst docs/plugins/mpdstats.rst docs/plugins/mpdupdate.rst docs/plugins/play.rst docs/plugins/random.rst docs/plugins/replaygain.rst docs/plugins/rewrite.rst docs/plugins/scrub.rst docs/plugins/smartplaylist.rst docs/plugins/spotify.rst docs/plugins/the.rst docs/plugins/types.rst docs/plugins/web.rst docs/plugins/zero.rst docs/reference/cli.rst docs/reference/config.rst docs/reference/index.rst docs/reference/pathformat.rst docs/reference/query.rst man/beet.1 man/beetsconfig.5 test/__init__.py test/_common.py test/helper.py test/lyrics_sources.py test/test_art.py test/test_autotag.py test/test_bucket.py test/test_config_command.py test/test_convert.py test/test_datequery.py test/test_dbcore.py test/test_echonest.py test/test_embedart.py test/test_fetchart.py test/test_files.py test/test_ihate.py test/test_importer.py test/test_importfeeds.py test/test_info.py test/test_keyfinder.py test/test_lastgenre.py test/test_library.py test/test_lyrics.py test/test_mb.py test/test_mbsync.py test/test_mediafile.py test/test_mediafile_edge.py test/test_pipeline.py test/test_player.py test/test_plugins.py test/test_query.py test/test_replaygain.py test/test_sort.py test/test_spotify.py test/test_template.py test/test_the.py test/test_types_plugin.py test/test_ui.py test/test_ui_importer.py test/test_vfs.py test/test_web.py test/test_zero.py test/testall.py test/rsrc/archive.rar test/rsrc/bpm.mp3 test/rsrc/coverart.ogg test/rsrc/date.mp3 test/rsrc/discc.ogg test/rsrc/empty.aiff test/rsrc/empty.alac.m4a test/rsrc/empty.ape test/rsrc/empty.flac test/rsrc/empty.m4a test/rsrc/empty.mp3 test/rsrc/empty.mpc test/rsrc/empty.ogg test/rsrc/empty.opus test/rsrc/empty.wma test/rsrc/empty.wv test/rsrc/emptylist.mp3 test/rsrc/full.aiff test/rsrc/full.alac.m4a test/rsrc/full.ape test/rsrc/full.flac test/rsrc/full.m4a test/rsrc/full.mp3 test/rsrc/full.mpc test/rsrc/full.ogg test/rsrc/full.opus test/rsrc/full.wma test/rsrc/full.wv test/rsrc/image-2x3.jpg test/rsrc/image-2x3.png test/rsrc/image-2x3.tiff test/rsrc/image.flac test/rsrc/image.m4a test/rsrc/image.mp3 test/rsrc/image.ogg test/rsrc/image.wma test/rsrc/lyricstext.yaml test/rsrc/min.flac test/rsrc/min.m4a test/rsrc/min.mp3 test/rsrc/oldape.ape test/rsrc/partial.flac test/rsrc/partial.m4a test/rsrc/partial.mp3 test/rsrc/space_time.mp3 test/rsrc/t_time.m4a test/rsrc/unicode’d.mp3 test/rsrc/unparseable.aiff test/rsrc/unparseable.alac.m4a test/rsrc/unparseable.ape test/rsrc/unparseable.flac test/rsrc/unparseable.m4a test/rsrc/unparseable.mp3 test/rsrc/unparseable.mpc test/rsrc/unparseable.ogg test/rsrc/unparseable.opus test/rsrc/unparseable.wma test/rsrc/unparseable.wv test/rsrc/year.ogg test/rsrc/beetsplug/test.py test/rsrc/beetsplug/test.pyc test/rsrc/lyrics/absolutelyricscom/ladymadonna.txt test/rsrc/lyrics/azlyricscom/ladymadonnahtml.txt test/rsrc/lyrics/chartlyricscom/LadyMadonnaaspx.txt test/rsrc/lyrics/elyricsworldcom/ladymadonnalyricsbeatleshtml.txt test/rsrc/lyrics/lacoccinellenet/550512html.txt test/rsrc/lyrics/lyrics007com/Lady20Madonna20Lyricshtml.txt test/rsrc/lyrics/lyricscom/ladymadonnalyricsthebeatleshtml.txt test/rsrc/lyrics/lyricsmaniacom/heyitsoklyricslillywoodandtheprickhtml.txt test/rsrc/lyrics/lyricsontopcom/jazznblueslyricshtml.txt test/rsrc/lyrics/lyricswikiacom/TheBeatlesLadyMadonna.txt test/rsrc/lyrics/metrolyricscom/bestforlastlyricsadelehtml.txt test/rsrc/lyrics/parolesnet/parolesheyitsok.txt test/rsrc/lyrics/reggaelyricsinfo/icouldbeatmyself.txt test/rsrc/lyrics/releaselyricscom/thebeatlesladymadonna.txt test/rsrc/lyrics/smartlyricscom/Song18148TheBeatlesLadyMadonnalyricsaspx.txt test/rsrc/lyrics/songlyricscom/ladymadonnalyrics.txt test/rsrc/lyrics/sweetslyricscom/761696The20Beatles2020Lady20Madonnahtml.txt test/rsrc/lyrics/sweetslyricscom/best-for-last-lyrics-adelehtml.txt.htmlbeets-1.3.8/beets.egg-info/top_level.txt0000644000076500000240000000002012406440350021105 0ustar asampsonstaff00000000000000beetsplug beets beets-1.3.8/beetsplug/0000755000076500000240000000000012406440351015562 5ustar asampsonstaff00000000000000beets-1.3.8/beetsplug/__init__.py0000644000076500000240000000144112313347401017672 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """A namespace package for beets plugins.""" # Make this a namespace package. from pkgutil import extend_path __path__ = extend_path(__path__, __name__) beets-1.3.8/beetsplug/beatport.py0000644000076500000240000002705012405373453017767 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Adds Beatport release and track search support to the autotagger """ import logging import re from datetime import datetime, timedelta import requests from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance from beets.plugins import BeetsPlugin log = logging.getLogger('beets') class BeatportAPIError(Exception): pass class BeatportObject(object): def __init__(self, data): self.beatport_id = data['id'] self.name = unicode(data['name']) if 'releaseDate' in data: self.release_date = datetime.strptime(data['releaseDate'], '%Y-%m-%d') if 'artists' in data: self.artists = [(x['id'], unicode(x['name'])) for x in data['artists']] if 'genres' in data: self.genres = [unicode(x['name']) for x in data['genres']] class BeatportAPI(object): API_BASE = 'http://api.beatport.com/' @classmethod def get(cls, endpoint, **kwargs): try: response = requests.get(cls.API_BASE + endpoint, params=kwargs) except Exception as e: raise BeatportAPIError("Error connection to Beatport API: {}" .format(e.message)) if not response: raise BeatportAPIError( "Error {0.status_code} for '{0.request.path_url}" .format(response)) return response.json()['results'] class BeatportSearch(object): query = None release_type = None def __unicode__(self): return u''.format( self.release_type, self.query, len(self.results)) def __init__(self, query, release_type='release', details=True): self.results = [] self.query = query self.release_type = release_type response = BeatportAPI.get('catalog/3/search', query=query, facets=['fieldType:{0}' .format(release_type)], perPage=5) for item in response: if release_type == 'release': release = BeatportRelease(item) if details: release.get_tracks() self.results.append(release) elif release_type == 'track': self.results.append(BeatportTrack(item)) class BeatportRelease(BeatportObject): API_ENDPOINT = 'catalog/3/beatport/release' def __unicode__(self): if len(self.artists) < 4: artist_str = ", ".join(x[1] for x in self.artists) else: artist_str = "Various Artists" return u"".format( artist_str, self.name, self.catalog_number, ) def __init__(self, data): BeatportObject.__init__(self, data) if 'catalogNumber' in data: self.catalog_number = data['catalogNumber'] if 'label' in data: self.label_name = data['label']['name'] if 'category' in data: self.category = data['category'] if 'slug' in data: self.url = "http://beatport.com/release/{0}/{1}".format( data['slug'], data['id']) @classmethod def from_id(cls, beatport_id): response = BeatportAPI.get(cls.API_ENDPOINT, id=beatport_id) release = BeatportRelease(response['release']) release.tracks = [BeatportTrack(x) for x in response['tracks']] return release def get_tracks(self): response = BeatportAPI.get(self.API_ENDPOINT, id=self.beatport_id) self.tracks = [BeatportTrack(x) for x in response['tracks']] class BeatportTrack(BeatportObject): API_ENDPOINT = 'catalog/3/beatport/track' def __unicode__(self): artist_str = ", ".join(x[1] for x in self.artists) return u"".format(artist_str, self.name, self.mix_name) def __init__(self, data): BeatportObject.__init__(self, data) if 'title' in data: self.title = unicode(data['title']) if 'mixName' in data: self.mix_name = unicode(data['mixName']) self.length = timedelta(milliseconds=data.get('lengthMs', 0) or 0) if not self.length: try: min, sec = data.get('length', '0:0').split(':') self.length = timedelta(minutes=int(min), seconds=int(sec)) except ValueError: pass if 'slug' in data: self.url = "http://beatport.com/track/{0}/{1}".format(data['slug'], data['id']) @classmethod def from_id(cls, beatport_id): response = BeatportAPI.get(cls.API_ENDPOINT, id=beatport_id) return BeatportTrack(response['track']) class BeatportPlugin(BeetsPlugin): def __init__(self): super(BeatportPlugin, self).__init__() self.config.add({ 'source_weight': 0.5, }) def album_distance(self, items, album_info, mapping): """Returns the beatport source weight and the maximum source weight for albums. """ dist = Distance() if album_info.data_source == 'Beatport': dist.add('source', self.config['source_weight'].as_number()) return dist def track_distance(self, item, track_info): """Returns the beatport source weight and the maximum source weight for individual tracks. """ dist = Distance() if track_info.data_source == 'Beatport': dist.add('source', self.config['source_weight'].as_number()) return dist def candidates(self, items, artist, release, va_likely): """Returns a list of AlbumInfo objects for beatport search results matching release and artist (if not various). """ if va_likely: query = release else: query = '%s %s' % (artist, release) try: return self._get_releases(query) except BeatportAPIError as e: log.debug(u'Beatport API Error: {0} (query: {1})'.format(e, query)) return [] def item_candidates(self, item, artist, title): """Returns a list of TrackInfo objects for beatport search results matching title and artist. """ query = '%s %s' % (artist, title) try: return self._get_tracks(query) except BeatportAPIError as e: log.debug(u'Beatport API Error: {0} (query: {1})'.format(e, query)) return [] def album_for_id(self, release_id): """Fetches a release by its Beatport ID and returns an AlbumInfo object or None if the release is not found. """ log.debug(u'Searching Beatport for release {0}'.format(release_id)) match = re.search(r'(^|beatport\.com/release/.+/)(\d+)$', release_id) if not match: return None release = BeatportRelease.from_id(match.group(2)) album = self._get_album_info(release) return album def track_for_id(self, track_id): """Fetches a track by its Beatport ID and returns a TrackInfo object or None if the track is not found. """ log.debug(u'Searching Beatport for track {0}'.format(str(track_id))) match = re.search(r'(^|beatport\.com/track/.+/)(\d+)$', track_id) if not match: return None bp_track = BeatportTrack.from_id(match.group(2)) track = self._get_track_info(bp_track) return track def _get_releases(self, query): """Returns a list of AlbumInfo objects for a beatport search query. """ # Strip non-word characters from query. Things like "!" and "-" can # cause a query to return no results, even if they match the artist or # album title. Use `re.UNICODE` flag to avoid stripping non-english # word characters. query = re.sub(r'\W+', ' ', query, re.UNICODE) # Strip medium information from query, Things like "CD1" and "disk 1" # can also negate an otherwise positive result. query = re.sub(r'\b(CD|disc)\s*\d+', '', query, re.I) albums = [self._get_album_info(x) for x in BeatportSearch(query).results] return albums def _get_album_info(self, release): """Returns an AlbumInfo object for a Beatport Release object. """ va = len(release.artists) > 3 artist, artist_id = self._get_artist(release.artists) if va: artist = u"Various Artists" tracks = [self._get_track_info(x, index=idx) for idx, x in enumerate(release.tracks, 1)] return AlbumInfo(album=release.name, album_id=release.beatport_id, artist=artist, artist_id=artist_id, tracks=tracks, albumtype=release.category, va=va, year=release.release_date.year, month=release.release_date.month, day=release.release_date.day, label=release.label_name, catalognum=release.catalog_number, media=u'Digital', data_source=u'Beatport', data_url=release.url) def _get_track_info(self, track, index=None): """Returns a TrackInfo object for a Beatport Track object. """ title = track.name if track.mix_name != u"Original Mix": title += u" ({0})".format(track.mix_name) artist, artist_id = self._get_artist(track.artists) length = track.length.total_seconds() return TrackInfo(title=title, track_id=track.beatport_id, artist=artist, artist_id=artist_id, length=length, index=index, data_source=u'Beatport', data_url=track.url) def _get_artist(self, artists): """Returns an artist string (all artists) and an artist_id (the main artist) for a list of Beatport release or track artists. """ artist_id = None bits = [] for artist in artists: if not artist_id: artist_id = artist[0] name = artist[1] # Strip disambiguation number. name = re.sub(r' \(\d+\)$', '', name) # Move articles to the front. name = re.sub(r'^(.*?), (a|an|the)$', r'\2 \1', name, flags=re.I) bits.append(name) artist = ', '.join(bits).replace(' ,', ',') or None return artist, artist_id def _get_tracks(self, query): """Returns a list of TrackInfo objects for a Beatport query. """ bp_tracks = BeatportSearch(query, release_type='track').results tracks = [self._get_track_info(x) for x in bp_tracks] return tracks beets-1.3.8/beetsplug/bench.py0000644000076500000240000001006112322627645017223 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Some simple performance benchmarks for beets. """ from __future__ import print_function from beets.plugins import BeetsPlugin from beets import ui from beets import vfs from beets import library from beets.util.functemplate import Template from beets.autotag import match from beets import plugins from beets import importer import cProfile import timeit def aunique_benchmark(lib, prof): def _build_tree(): vfs.libtree(lib) # Measure path generation performance with %aunique{} included. lib.path_formats = [ (library.PF_KEY_DEFAULT, Template('$albumartist/$album%aunique{}/$track $title')), ] if prof: cProfile.runctx('_build_tree()', {}, {'_build_tree': _build_tree}, 'paths.withaunique.prof') else: interval = timeit.timeit(_build_tree, number=1) print('With %aunique:', interval) # And with %aunique replaceed with a "cheap" no-op function. lib.path_formats = [ (library.PF_KEY_DEFAULT, Template('$albumartist/$album%lower{}/$track $title')), ] if prof: cProfile.runctx('_build_tree()', {}, {'_build_tree': _build_tree}, 'paths.withoutaunique.prof') else: interval = timeit.timeit(_build_tree, number=1) print('Without %aunique:', interval) def match_benchmark(lib, prof, query=None, album_id=None): # If no album ID is provided, we'll match against a suitably huge # album. if not album_id: album_id = '9c5c043e-bc69-4edb-81a4-1aaf9c81e6dc' # Get an album from the library to use as the source for the match. items = lib.albums(query).get().items() # Ensure fingerprinting is invoked (if enabled). plugins.send('import_task_start', task=importer.ImportTask(None, None, items), session=importer.ImportSession(lib, None, None, None)) # Run the match. def _run_match(): match.tag_album(items, search_id=album_id) if prof: cProfile.runctx('_run_match()', {}, {'_run_match': _run_match}, 'match.prof') else: interval = timeit.timeit(_run_match, number=1) print('match duration:', interval) class BenchmarkPlugin(BeetsPlugin): """A plugin for performing some simple performance benchmarks. """ def commands(self): aunique_bench_cmd = ui.Subcommand('bench_aunique', help='benchmark for %aunique{}') aunique_bench_cmd.parser.add_option('-p', '--profile', action='store_true', default=False, help='performance profiling') aunique_bench_cmd.func = lambda lib, opts, args: \ aunique_benchmark(lib, opts.profile) match_bench_cmd = ui.Subcommand('bench_match', help='benchmark for track matching') match_bench_cmd.parser.add_option('-p', '--profile', action='store_true', default=False, help='performance profiling') match_bench_cmd.parser.add_option('-i', '--id', default=None, help='album ID to match against') match_bench_cmd.func = lambda lib, opts, args: \ match_benchmark(lib, opts.profile, ui.decargs(args), opts.id) return [aunique_bench_cmd, match_bench_cmd] beets-1.3.8/beetsplug/bpd/0000755000076500000240000000000012406440351016327 5ustar asampsonstaff00000000000000beets-1.3.8/beetsplug/bpd/__init__.py0000644000076500000240000011601212405373453020450 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """A clone of the Music Player Daemon (MPD) that plays music from a Beets library. Attempts to implement a compatible protocol to allow use of the wide range of MPD clients. """ from __future__ import print_function import re from string import Template import traceback import logging import random import time import beets from beets.plugins import BeetsPlugin import beets.ui from beets import vfs from beets.util import bluelet from beets.library import Item from beets import dbcore from beets.mediafile import MediaFile PROTOCOL_VERSION = '0.13.0' BUFSIZE = 1024 HELLO = 'OK MPD %s' % PROTOCOL_VERSION CLIST_BEGIN = 'command_list_begin' CLIST_VERBOSE_BEGIN = 'command_list_ok_begin' CLIST_END = 'command_list_end' RESP_OK = 'OK' RESP_CLIST_VERBOSE = 'list_OK' RESP_ERR = 'ACK' NEWLINE = u"\n" ERROR_NOT_LIST = 1 ERROR_ARG = 2 ERROR_PASSWORD = 3 ERROR_PERMISSION = 4 ERROR_UNKNOWN = 5 ERROR_NO_EXIST = 50 ERROR_PLAYLIST_MAX = 51 ERROR_SYSTEM = 52 ERROR_PLAYLIST_LOAD = 53 ERROR_UPDATE_ALREADY = 54 ERROR_PLAYER_SYNC = 55 ERROR_EXIST = 56 VOLUME_MIN = 0 VOLUME_MAX = 100 SAFE_COMMANDS = ( # Commands that are available when unauthenticated. u'close', u'commands', u'notcommands', u'password', u'ping', ) ITEM_KEYS_WRITABLE = set(MediaFile.fields()).intersection(Item._fields.keys()) # Loggers. log = logging.getLogger('beets.bpd') global_log = logging.getLogger('beets') # Gstreamer import error. class NoGstreamerError(Exception): pass # Error-handling, exceptions, parameter parsing. class BPDError(Exception): """An error that should be exposed to the client to the BPD server. """ def __init__(self, code, message, cmd_name='', index=0): self.code = code 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 erring command. """ return self.template.substitute({ 'resp': RESP_ERR, 'code': self.code, 'index': self.index, 'cmd_name': self.cmd_name, 'message': self.message, }) def make_bpd_error(s_code, s_message): """Create a BPDError subclass for a static code and message. """ class NewBPDError(BPDError): code = s_code message = s_message cmd_name = '' index = 0 def __init__(self): pass return NewBPDError ArgumentTypeError = make_bpd_error(ERROR_ARG, 'invalid type for argument') ArgumentIndexError = make_bpd_error(ERROR_ARG, 'argument out of range') ArgumentNotFoundError = make_bpd_error(ERROR_NO_EXIST, 'argument not found') def cast_arg(t, val): """Attempts to call t on val, raising a ArgumentTypeError 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). """ if t == 'intbool': return cast_arg(bool, cast_arg(int, val)) else: try: return t(val) except ValueError: raise ArgumentTypeError() class BPDClose(Exception): """Raised by a command invocation to indicate that the connection should be closed. """ # Generic server infrastructure, implementing the basic protocol. 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, port, password): """Create a new server bound to address `host` and listening on port `port`. If `password` is given, it is required to do anything significant on the server. """ self.host, self.port, self.password = host, port, password # Default server values. self.random = False self.repeat = False self.volume = VOLUME_MAX self.crossfade = 0 self.playlist = [] self.playlist_version = 0 self.current_index = -1 self.paused = False self.error = None # 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. """ self.startup_time = time.time() bluelet.run(bluelet.server(self.host, self.port, Connection.handler(self))) def _item_info(self, item): """An abstract method that should response lines containing a single song's metadata. """ raise NotImplementedError def _item_id(self, item): """An abstract method returning the integer id for an item. """ raise NotImplementedError def _id_to_index(self, track_id): """Searches the playlist for a song with the given id and returns its index in the playlist. """ track_id = cast_arg(int, track_id) for index, track in enumerate(self.playlist): if self._item_id(track) == track_id: return index # Loop finished with no track found. raise ArgumentNotFoundError() def _random_idx(self): """Returns a random index different from the current one. If there are no songs in the playlist it returns -1. If there is only one song in the playlist it returns 0. """ if len(self.playlist) < 2: return len(self.playlist) - 1 new_index = self.random_obj.randint(0, len(self.playlist) - 1) while new_index == self.current_index: new_index = self.random_obj.randint(0, len(self.playlist) - 1) return new_index def _succ_idx(self): """Returns the index for the next song to play. It also considers random and repeat flags. No boundaries are checked. """ if self.repeat: return self.current_index if self.random: return self._random_idx() return self.current_index + 1 def _prev_idx(self): """Returns the index for the previous song to play. It also considers random and repeat flags. No boundaries are checked. """ if self.repeat: return self.current_index if self.random: return self._random_idx() return self.current_index - 1 def cmd_ping(self, conn): """Succeeds.""" pass def cmd_kill(self, conn): """Exits the server process.""" 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: conn.authenticated = True 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: # Not authenticated. Show privileged commands. for func in dir(self): if func.startswith('cmd_'): 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. """ yield ( u'volume: ' + unicode(self.volume), u'repeat: ' + unicode(int(self.repeat)), u'random: ' + unicode(int(self.random)), u'playlist: ' + unicode(self.playlist_version), u'playlistlength: ' + unicode(len(self.playlist)), u'xfade: ' + unicode(self.crossfade), ) if self.current_index == -1: state = u'stop' elif self.paused: state = u'pause' 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) yield u'songid: ' + unicode(current_id) if self.error: yield u'error: ' + self.error def cmd_clearerror(self, conn): """Removes the persistent error state of the server. This error is set when a problem arises not in response to a 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: 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) try: del(self.playlist[index]) except IndexError: raise ArgumentIndexError() self.playlist_version += 1 if self.current_index == index: # Deleted playing song. self.cmd_stop(conn) elif index < self.current_index: # Deleted before playing. # Shift playing index down. self.current_index -= 1 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) idx_to = cast_arg(int, idx_to) try: track = self.playlist.pop(idx_from) 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 elif idx_from < self.current_index <= idx_to: 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, idx_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) j = cast_arg(int, j) try: track_i = self.playlist[i] 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. """ index = cast_arg(int, index) if index == -1: for track in self.playlist: yield self._item_info(track) else: try: track = self.playlist[index] except IndexError: raise ArgumentIndexError() 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. """ if self.current_index != -1: # -1 means stopped. track = self.playlist[self.current_index] yield self._item_info(track) def cmd_next(self, conn): """Advance to the next song in the playlist.""" self.current_index = self._succ_idx() if self.current_index >= len(self.playlist): # Fallen off the end. Just move to stopped state. 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() if self.current_index < 0: 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: index = -1 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) if index < 0 or index >= len(self.playlist): raise ArgumentIndexError() self.current_index = index def cmd_seekid(self, conn, track_id, pos): index = self._id_to_index(track_id) return self.cmd_seek(conn, index, pos) def cmd_profile(self, conn): """Memory profiling for debugging.""" from guppy import hpy heap = hpy().heap() print(heap) class Connection(object): """A connection between a client and the server. Handles input and output from and to the client. """ def __init__(self, server, sock): """Create a new connection for the accepted socket `client`. """ 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 added after every string. Returns a Bluelet event that sends the data. """ if isinstance(lines, basestring): lines = [lines] out = NEWLINE.join(lines) + NEWLINE log.debug(out[:-1]) # Don't log trailing newline. 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.""" try: yield bluelet.call(command.run(self)) except BPDError as e: # Send the error. yield self.send(e.response()) 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() if not line: break line = line.strip() if not line: break log.debug(line) if clist is not None: # Command list already opened. if line == CLIST_END: yield bluelet.call(self.do_command(clist)) 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: yield bluelet.call(self.do_command(Command(line))) except BPDClose: # Command indicates that the conn should close. self.sock.close() return @classmethod def handler(cls, server): def _handle(sock): """Creates a new `Connection` and runs it. """ return cls(server, sock).run() return _handle 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. """ command_match = self.command_re.match(s) self.name = command_match.group(1) self.args = [] arg_matches = self.arg_re.findall(s[command_match.end():]) for match in arg_matches: if match[0]: # Quoted argument. arg = match[0] arg = arg.replace('\\"', '"').replace('\\\\', '\\') else: # Unquoted argument. 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. """ # Attempt to get correct command function. func_name = 'cmd_' + self.name 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 as 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 as 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 server. May be verbose, in which case the response is delimited, or not. Should be a list of `Command` objects. """ def __init__(self, sequence=None, verbose=False): """Create a new `CommandList` from the given sequence of `Command`s. If `verbose`, this is a verbose command list. """ if sequence: for item in sequence: self.append(item) self.verbose = verbose def run(self, conn): """Coroutine executing all the commands in this list. """ for i, command in enumerate(self): try: yield bluelet.call(command.run(conn)) except BPDError as e: # If the command failed, stop executing. e.index = i # Give the error the correct index. raise e # Otherwise, possibly send the output delimeter if we're in a # verbose ("OK") command list. if self.verbose: yield conn.send(RESP_CLIST_VERBOSE) # A subclass of the basic, protocol-handling server that actually plays # music. class Server(BaseServer): """An MPD-compatible server using GStreamer to play audio and beets to store its library. """ def __init__(self, library, host, port, password): try: from beetsplug.bpd import gstplayer except ImportError as e: # This is a little hacky, but it's the best I know for now. if e.args[0].endswith(' gst'): raise NoGstreamerError() else: raise super(Server, self).__init__(host, port, password) self.lib = library self.player = gstplayer.GstPlayer(self.play_finished) self.cmd_update(None) def run(self): self.player.run() super(Server, self).run() def play_finished(self): """A callback invoked every time our player finishes a track. """ self.cmd_next(None) # Metadata helper functions. def _item_info(self, item): info_lines = [ u'file: ' + item.destination(fragment=True), u'Time: ' + unicode(int(item.length)), u'Title: ' + item.title, u'Artist: ' + item.artist, 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 # Database updating. def cmd_update(self, conn, path=u'/'): """Updates the catalog to reflect the current database state. """ # Path is ignored. Also, the real MPD does this asynchronously; # this is done inline. print('Building directory tree...') self.tree = vfs.libtree(self.lib) print('... done.') self.updated_time = time.time() # Path (directory tree) browsing. 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 """ components = path.split(u'/') node = self.tree for component in components: if not component: continue if isinstance(node, int): # We're trying to descend into a file node. raise ArgumentNotFoundError() if component in node.files: node = node.files[component] elif component in node.dirs: node = node.dirs[component] else: 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) if isinstance(node, int): # Trying to list a track. raise BPDError(ERROR_ARG, 'this is not a directory') else: for name, itemid in iter(sorted(node.files.items())): item = self.lib.get_item(itemid) yield self._item_info(item) for name, _ in iter(sorted(node.dirs.iteritems())): dirpath = self._path_join(path, name) if dirpath.startswith(u"/"): # 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. """ if isinstance(node, int): # List a single file. if info: item = self.lib.get_item(node) yield self._item_info(item) else: yield u'file: ' + basepath else: # List a directory. Recurse into both directories and files. for name, itemid in sorted(node.files.iteritems()): newpath = self._path_join(basepath, name) # "yield from" for v in self._listall(newpath, itemid, info): yield v for name, subdir in sorted(node.dirs.iteritems()): 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): """Generator yielding all items under a VFS node. """ if isinstance(node, int): # Could be more efficient if we built up all the IDs and # then issued a single SELECT. yield self.lib.get_item(node) else: # Recurse into a directory. for name, itemid in sorted(node.files.iteritems()): # "yield from" 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. """ for item in self._all_items(self._resolve_path(path)): self.playlist.append(item) 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) # Server info. def cmd_status(self, conn): for line in super(Server, self).cmd_status(conn): yield line if self.current_index > -1: item = self.playlist[self.current_index] yield u'bitrate: ' + unicode(item.bitrate / 1000) # Missing 'audio'. (pos, total) = self.player.time() yield u'time: ' + unicode(pos) + u':' + unicode(total) # Also missing 'updating_db'. def cmd_stats(self, conn): """Sends some statistics about the library.""" with self.lib.transaction() as tx: statement = 'SELECT COUNT(DISTINCT artist), ' \ 'COUNT(DISTINCT album), ' \ 'COUNT(id), ' \ 'SUM(length) ' \ 'FROM items' artists, albums, songs, totaltime = tx.query(statement)[0] yield ( u'artists: ' + unicode(artists), u'albums: ' + unicode(albums), u'songs: ' + unicode(songs), u'uptime: ' + unicode(int(time.time() - self.startup_time)), u'playtime: ' + u'0', # Missing. u'db_playtime: ' + unicode(int(totaltime)), u'db_update: ' + unicode(int(self.updated_time)), ) # Searching. tagtype_map = { u'Artist': u'artist', u'Album': u'album', u'Title': u'title', u'Track': u'track', u'AlbumArtist': u'albumartist', u'AlbumArtistSort': u'albumartist_sort', # Name? u'Genre': u'genre', u'Date': u'year', u'Composer': u'composer', # Performer? u'Disc': u'disc', u'filename': u'path', # Suspect. } def cmd_tagtypes(self, conn): """Returns a list of the metadata (tag) fields available for searching. """ 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 the canonical name of the MPD tagtype and the beets column name. """ for test_tag, key in self.tagtype_map.items(): # Match case-insensitively. if test_tag.lower() == tag.lower(): return test_tag, key raise BPDError(ERROR_UNKNOWN, u'no such tagtype') def _metadata_query(self, query_type, any_query_type, kv): """Helper function returns a query object that will find items according to the library query type provided and the key-value pairs specified. The any_query_type is used for queries of type "any"; if None, then an error is thrown. """ if kv: # At least one key-value pair. queries = [] # Iterate pairwise over the arguments. it = iter(kv) for tag, value in zip(it, it): if tag.lower() == u'any': if any_query_type: queries.append(any_query_type(value, ITEM_KEYS_WRITABLE, query_type)) else: raise BPDError(ERROR_UNKNOWN, u'no such tagtype') else: _, key = self._tagtype_lookup(tag) queries.append(query_type(key, value)) return dbcore.query.AndQuery(queries) else: # No key-value pairs. return dbcore.query.TrueQuery() def cmd_search(self, conn, *kv): """Perform a substring match for items.""" query = self._metadata_query(dbcore.query.SubstringQuery, dbcore.query.AnyFieldQuery, 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(dbcore.query.MatchQuery, None, 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(dbcore.query.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. """ _, key = self._tagtype_lookup(tag) songs = 0 playtime = 0.0 for item in self.lib.items(dbcore.query.MatchQuery(key, value)): songs += 1 playtime += item.length 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. def cmd_play(self, conn, index=-1): 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. self.player.play() else: self.player.play_file(self.playlist[self.current_index].path) def cmd_pause(self, conn, state=None): super(Server, self).cmd_pause(conn, state) if self.paused: self.player.pause() elif self.player.playing: self.player.play() 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) pos = cast_arg(int, pos) super(Server, self).cmd_seek(conn, index, pos) self.player.seek(pos) # Volume control. def cmd_setvol(self, conn, vol): vol = cast_arg(int, vol) super(Server, self).cmd_setvol(conn, vol) self.player.volume = float(vol) / 100 # Beets plugin hooks. class BPDPlugin(BeetsPlugin): """Provides the "beet bpd" command for running a music player server. """ def __init__(self): super(BPDPlugin, self).__init__() self.config.add({ 'host': u'', 'port': 6600, 'password': u'', }) def start_bpd(self, lib, host, port, password, debug): """Starts a BPD server.""" if debug: log.setLevel(logging.DEBUG) else: log.setLevel(logging.WARNING) try: Server(lib, host, port, password).run() except NoGstreamerError: global_log.error(u'Gstreamer Python bindings not found.') global_log.error(u'Install "python-gst0.10", "py27-gst-python", ' u'or similar package to use BPD.') def commands(self): cmd = beets.ui.Subcommand( 'bpd', help='run an MPD-compatible music player server' ) cmd.parser.add_option( '-d', '--debug', action='store_true', help='dump all MPD traffic to stdout' ) def func(lib, opts, args): host = args.pop(0) if args else self.config['host'].get(unicode) port = args.pop(0) if args else self.config['port'].get(int) if args: raise beets.ui.UserError('too many arguments') password = self.config['password'].get(unicode) debug = opts.debug or False self.start_bpd(lib, host, int(port), password, debug) cmd.func = func return [cmd] beets-1.3.8/beetsplug/bpd/gstplayer.py0000644000076500000240000001605312322627125020723 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """A wrapper for the GStreamer Python bindings that exposes a simple music player. """ from __future__ import print_function import sys import time import gobject import thread import os import copy import urllib import pygst pygst.require('0.10') import gst 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") fakesink = gst.element_factory_make("fakesink", "fakesink") self.player.set_property("video-sink", fakesink) 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 self.cached_time = None self._volume = 1.0 def _get_state(self): """Returns the current state flag of the playbin.""" # 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: # file finished playing self.player.set_state(gst.STATE_NULL) self.playing = False self.cached_time = None if self.finished_callback: self.finished_callback() elif message.type == gst.MESSAGE_ERROR: # error self.player.set_state(gst.STATE_NULL) err, debug = message.parse_error() print("Error: " + str(err)) self.playing = False def _set_volume(self, volume): """Set the volume level to a value in the range [0, 1.5].""" # And the volume for the playbin. self._volume = volume self.player.set_property("volume", volume) def _get_volume(self): """Get the volume as a float in the range [0, 1.5].""" return self._volume volume = property(_get_volume, _set_volume) def play_file(self, path): """Immediately begin playing the audio file at the given path. """ self.player.set_state(gst.STATE_NULL) if isinstance(path, unicode): path = path.encode('utf8') uri = 'file://' + urllib.quote(path) self.player.set_property("uri", uri) self.player.set_state(gst.STATE_PLAYING) self.playing = True def play(self): """If paused, resume playback.""" 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) def stop(self): """Halt playback.""" self.player.set_state(gst.STATE_NULL) self.playing = False self.cached_time = None def run(self): """Start a new thread for the player. Call this function before trying to play any music with play_file() or play(). """ # If we don't use the MainLoop, messages are never sent. gobject.threads_init() def start(): loop = gobject.MainLoop() loop.run() thread.start_new_thread(start, ()) def time(self): """Returns a tuple containing (position, length) where both values are integers in seconds. If no stream is available, returns (0, 0). """ fmt = gst.Format(gst.FORMAT_TIME) try: pos = self.player.query_position(fmt, None)[0] / (10 ** 9) 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 # reason, we cache recent. if self.playing and self.cached_time: return self.cached_time else: return (0, 0) def seek(self, position): """Seeks to position (in seconds).""" cur_pos, cur_len = self.time() 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: time.sleep(1) def play_simple(paths): """Play the files in paths in a straightforward way, without using the player's callback function. """ p = GstPlayer() p.run() for path in paths: p.play_file(path) p.block() def play_complicated(paths): """Play the files in the path one after the other by using the callback function to advance to the next song. """ my_paths = copy.copy(paths) def next_song(): my_paths.pop(0) p.play_file(my_paths[0]) p = GstPlayer(next_song) p.run() p.play_file(my_paths[0]) while my_paths: time.sleep(1) if __name__ == '__main__': # A very simple command-line player. Just give it names of audio # files on the command line; these are all played in sequence. paths = [os.path.abspath(os.path.expanduser(p)) for p in sys.argv[1:]] # play_simple(paths) play_complicated(paths) beets-1.3.8/beetsplug/bpm.py0000644000076500000240000000513512405373453016725 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2014, aroquen # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Determine BPM by pressing a key to the rhythm.""" import time import logging from beets import ui from beets.plugins import BeetsPlugin log = logging.getLogger('beets') def bpm(max_strokes): """Returns average BPM (possibly of a playing song) listening to Enter keystrokes. """ t0 = None dt = [] for i in range(max_strokes): # Press enter to the rhythm... s = raw_input() if s == '': t1 = time.time() # Only start measuring at the second stroke if t0: dt.append(t1 - t0) t0 = t1 else: break # Return average BPM # bpm = (max_strokes-1) / sum(dt) * 60 ave = sum([1.0 / dti * 60 for dti in dt]) / len(dt) return ave class BPMPlugin(BeetsPlugin): def __init__(self): super(BPMPlugin, self).__init__() self.config.add({ u'max_strokes': 3, u'overwrite': True, }) def commands(self): cmd = ui.Subcommand('bpm', help='determine bpm of a song by pressing \ a key to the rhythm') cmd.func = self.command return [cmd] def command(self, lib, opts, args): self.get_bpm(lib.items(ui.decargs(args))) def get_bpm(self, items, write=False): overwrite = self.config['overwrite'].get(bool) if len(items) > 1: raise ValueError('Can only get bpm of one song at time') item = items[0] if item['bpm']: log.info(u'Found bpm {0}'.format(item['bpm'])) if not overwrite: return log.info(u'Press Enter {0} times to the rhythm or Ctrl-D ' u'to exit'.format(self.config['max_strokes'].get(int))) new_bpm = bpm(self.config['max_strokes'].get(int)) item['bpm'] = int(new_bpm) if write: item.try_write() item.store() log.info(u'Added new bpm {0}'.format(item['bpm'])) beets-1.3.8/beetsplug/bucket.py0000644000076500000240000001773312404675421017432 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2014, Fabrice Laporte. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Provides the %bucket{} function for path formatting. """ from datetime import datetime import logging import re import string from itertools import tee, izip from beets import plugins, ui log = logging.getLogger('beets') class BucketError(Exception): pass def pairwise(iterable): "s -> (s0,s1), (s1,s2), (s2, s3), ..." a, b = tee(iterable) next(b, None) return izip(a, b) def span_from_str(span_str): """Build a span dict from the span string representation. """ def normalize_year(d, yearfrom): """Convert string to a 4 digits year """ if yearfrom < 100: raise BucketError("%d must be expressed on 4 digits" % yearfrom) # if two digits only, pick closest year that ends by these two # digits starting from yearfrom if d < 100: if (d % 100) < (yearfrom % 100): d = (yearfrom - yearfrom % 100) + 100 + d else: d = (yearfrom - yearfrom % 100) + d return d years = [int(x) for x in re.findall('\d+', span_str)] if not years: raise ui.UserError("invalid range defined for year bucket '%s': no " "year found" % span_str) try: years = [normalize_year(x, years[0]) for x in years] except BucketError as exc: raise ui.UserError("invalid range defined for year bucket '%s': %s" % (span_str, exc)) res = {'from': years[0], 'str': span_str} if len(years) > 1: res['to'] = years[-1] return res def complete_year_spans(spans): """Set the `to` value of spans if empty and sort them chronologically. """ spans.sort(key=lambda x: x['from']) for (x, y) in pairwise(spans): if 'to' not in x: x['to'] = y['from'] - 1 if spans and 'to' not in spans[-1]: spans[-1]['to'] = datetime.now().year def extend_year_spans(spans, spanlen, start=1900, end=2014): """Add new spans to given spans list so that every year of [start,end] belongs to a span. """ extended_spans = spans[:] for (x, y) in pairwise(spans): # if a gap between two spans, fill the gap with as much spans of # spanlen length as necessary for span_from in range(x['to'] + 1, y['from'], spanlen): extended_spans.append({'from': span_from}) # Create spans prior to declared ones for span_from in range(spans[0]['from'] - spanlen, start, -spanlen): extended_spans.append({'from': span_from}) # Create spans after the declared ones for span_from in range(spans[-1]['to'] + 1, end, spanlen): extended_spans.append({'from': span_from}) complete_year_spans(extended_spans) return extended_spans def build_year_spans(year_spans_str): """Build a chronologically ordered list of spans dict from unordered spans stringlist. """ spans = [] for elem in year_spans_str: spans.append(span_from_str(elem)) complete_year_spans(spans) return spans def str2fmt(s): """Deduces formatting syntax from a span string. """ regex = re.compile("(?P\D*)(?P\d+)(?P\D*)" "(?P\d*)(?P\D*)") m = re.match(regex, s) def year_format(year): return '%%0%dd' % len(year) res = {'fromnchars': len(m.group('fromyear')), 'tonchars': len(m.group('toyear'))} res['fmt'] = "%s%%s%s%s%s" % (m.group('bef'), m.group('sep'), '%s' if res['tonchars'] else '', m.group('after')) return res def format_span(fmt, yearfrom, yearto, fromnchars, tonchars): """Return a span string representation. """ args = (str(yearfrom)[-fromnchars:]) if tonchars: args = (str(yearfrom)[-fromnchars:], str(yearto)[-tonchars:]) return fmt % args def extract_modes(spans): """Extract the most common spans lengths and representation formats """ rangelen = sorted([x['to'] - x['from'] + 1 for x in spans]) deflen = sorted(rangelen, key=rangelen.count)[-1] reprs = [str2fmt(x['str']) for x in spans] deffmt = sorted(reprs, key=reprs.count)[-1] return deflen, deffmt def build_alpha_spans(alpha_spans_str, alpha_regexs): """Extract alphanumerics from string and return sorted list of chars [from...to] """ spans = [] ASCII_DIGITS = string.digits + string.ascii_lowercase for elem in alpha_spans_str: if elem in alpha_regexs: spans.append(re.compile(alpha_regexs[elem])) else: bucket = sorted([x for x in elem.lower() if x.isalnum()]) if bucket: beginIdx = ASCII_DIGITS.index(bucket[0]) endIdx = ASCII_DIGITS.index(bucket[-1]) else: raise ui.UserError("invalid range defined for alpha bucket " "'%s': no alphanumeric character found" % elem) spans.append( re.compile( "^[" + ASCII_DIGITS[beginIdx:endIdx + 1] + ASCII_DIGITS[beginIdx:endIdx + 1].upper() + "]" ) ) return spans class BucketPlugin(plugins.BeetsPlugin): def __init__(self): super(BucketPlugin, self).__init__() self.template_funcs['bucket'] = self._tmpl_bucket self.config.add({ 'bucket_year': [], 'bucket_alpha': [], 'bucket_alpha_regex': {}, 'extrapolate': False }) self.setup() def setup(self): """Setup plugin from config options """ self.year_spans = build_year_spans(self.config['bucket_year'].get()) if self.year_spans and self.config['extrapolate']: [self.ys_len_mode, self.ys_repr_mode] = extract_modes(self.year_spans) self.year_spans = extend_year_spans(self.year_spans, self.ys_len_mode) self.alpha_spans = build_alpha_spans( self.config['bucket_alpha'].get(), self.config['bucket_alpha_regex'].get() ) def find_bucket_year(self, year): """Return bucket that matches given year or return the year if no matching bucket. """ for ys in self.year_spans: if ys['from'] <= int(year) <= ys['to']: if 'str' in ys: return ys['str'] else: return format_span(self.ys_repr_mode['fmt'], ys['from'], ys['to'], self.ys_repr_mode['fromnchars'], self.ys_repr_mode['tonchars']) return year def find_bucket_alpha(self, s): """Return alpha-range bucket that matches given string or return the string initial if no matching bucket. """ for (i, span) in enumerate(self.alpha_spans): if span.match(s): return self.config['bucket_alpha'].get()[i] return s[0].upper() def _tmpl_bucket(self, text, field=None): if not field and len(text) == 4 and text.isdigit(): field = 'year' if field == 'year': func = self.find_bucket_year else: func = self.find_bucket_alpha return func(text) beets-1.3.8/beetsplug/chroma.py0000644000076500000240000002375612405373453017431 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Adds Chromaprint/Acoustid acoustic fingerprinting support to the autotagger. Requires the pyacoustid library. """ from beets import plugins from beets import ui from beets import util from beets import config from beets.util import confit from beets.autotag import hooks import acoustid import logging from collections import defaultdict API_KEY = '1vOwZtEn' SCORE_THRESH = 0.5 TRACK_ID_WEIGHT = 10.0 COMMON_REL_THRESH = 0.6 # How many tracks must have an album in common? log = logging.getLogger('beets') # Stores the Acoustid match information for each track. This is # populated when an import task begins and then used when searching for # candidates. It maps audio file paths to (recording_ids, release_ids) # pairs. If a given path is not present in the mapping, then no match # was found. _matches = {} # Stores the fingerprint and Acoustid ID for each track. This is stored # as metadata for each track for later use but is not relevant for # autotagging. _fingerprints = {} _acoustids = {} def acoustid_match(path): """Gets metadata for a file from Acoustid and populates the _matches, _fingerprints, and _acoustids dictionaries accordingly. """ try: duration, fp = acoustid.fingerprint_file(util.syspath(path)) except acoustid.FingerprintGenerationError as exc: log.error(u'fingerprinting of {0} failed: {1}' .format(util.displayable_path(repr(path)), str(exc))) return None _fingerprints[path] = fp try: res = acoustid.lookup(API_KEY, fp, duration, meta='recordings releases') except acoustid.AcoustidError as exc: log.debug(u'fingerprint matching {0} failed: {1}' .format(util.displayable_path(repr(path)), str(exc))) return None log.debug(u'chroma: fingerprinted {0}' .format(util.displayable_path(repr(path)))) # Ensure the response is usable and parse it. if res['status'] != 'ok' or not res.get('results'): log.debug(u'chroma: no match found') return None result = res['results'][0] # Best match. if result['score'] < SCORE_THRESH: log.debug(u'chroma: no results above threshold') return None _acoustids[path] = result['id'] # Get recording and releases from the result. if not result.get('recordings'): log.debug(u'chroma: no recordings found') return None recording_ids = [] release_ids = [] for recording in result['recordings']: recording_ids.append(recording['id']) if 'releases' in recording: release_ids += [rel['id'] for rel in recording['releases']] log.debug(u'chroma: matched recordings {0}'.format(recording_ids)) _matches[path] = recording_ids, release_ids # Plugin structure and autotagging logic. def _all_releases(items): """Given an iterable of Items, determines (according to Acoustid) which releases the items have in common. Generates release IDs. """ # Count the number of "hits" for each release. relcounts = defaultdict(int) for item in items: if item.path not in _matches: continue _, release_ids = _matches[item.path] for release_id in release_ids: relcounts[release_id] += 1 for release_id, count in relcounts.iteritems(): if float(count) / len(items) > COMMON_REL_THRESH: yield release_id class AcoustidPlugin(plugins.BeetsPlugin): def __init__(self): super(AcoustidPlugin, self).__init__() self.config.add({ 'auto': True, }) if self.config['auto']: self.register_listener('import_task_start', fingerprint_task) def track_distance(self, item, info): dist = hooks.Distance() if item.path not in _matches or not info.track_id: # Match failed or no track ID. return dist recording_ids, _ = _matches[item.path] dist.add_expr('track_id', info.track_id not in recording_ids) return dist def candidates(self, items, artist, album, va_likely): albums = [] for relid in _all_releases(items): album = hooks.album_for_mbid(relid) if album: albums.append(album) log.debug(u'acoustid album candidates: {0}'.format(len(albums))) return albums def item_candidates(self, item, artist, title): if item.path not in _matches: return [] recording_ids, _ = _matches[item.path] tracks = [] for recording_id in recording_ids: track = hooks.track_for_mbid(recording_id) if track: tracks.append(track) log.debug(u'acoustid item candidates: {0}'.format(len(tracks))) return tracks def commands(self): submit_cmd = ui.Subcommand('submit', help='submit Acoustid fingerprints') def submit_cmd_func(lib, opts, args): try: apikey = config['acoustid']['apikey'].get(unicode) except confit.NotFoundError: raise ui.UserError('no Acoustid user API key provided') submit_items(apikey, lib.items(ui.decargs(args))) submit_cmd.func = submit_cmd_func fingerprint_cmd = ui.Subcommand( 'fingerprint', help='generate fingerprints for items without them' ) def fingerprint_cmd_func(lib, opts, args): for item in lib.items(ui.decargs(args)): fingerprint_item(item, write=config['import']['write'].get(bool)) fingerprint_cmd.func = fingerprint_cmd_func return [submit_cmd, fingerprint_cmd] # Hooks into import process. def fingerprint_task(task, session): """Fingerprint each item in the task for later use during the autotagging candidate search. """ items = task.items if task.is_album else [task.item] for item in items: acoustid_match(item.path) @AcoustidPlugin.listen('import_task_apply') def apply_acoustid_metadata(task, session): """Apply Acoustid metadata (fingerprint and ID) to the task's items. """ for item in task.imported_items(): if item.path in _fingerprints: item.acoustid_fingerprint = _fingerprints[item.path] if item.path in _acoustids: item.acoustid_id = _acoustids[item.path] # UI commands. def submit_items(userkey, items, chunksize=64): """Submit fingerprints for the items to the Acoustid server. """ data = [] # The running list of dictionaries to submit. def submit_chunk(): """Submit the current accumulated fingerprint data.""" log.info(u'submitting {0} fingerprints'.format(len(data))) try: acoustid.submit(API_KEY, userkey, data) except acoustid.AcoustidError as exc: log.warn(u'acoustid submission error: {0}'.format(exc)) del data[:] for item in items: fp = fingerprint_item(item) # Construct a submission dictionary for this item. item_data = { 'duration': int(item.length), 'fingerprint': fp, } if item.mb_trackid: item_data['mbid'] = item.mb_trackid log.debug(u'submitting MBID') else: item_data.update({ 'track': item.title, 'artist': item.artist, 'album': item.album, 'albumartist': item.albumartist, 'year': item.year, 'trackno': item.track, 'discno': item.disc, }) log.debug(u'submitting textual metadata') data.append(item_data) # If we have enough data, submit a chunk. if len(data) >= chunksize: submit_chunk() # Submit remaining data in a final chunk. if data: submit_chunk() def fingerprint_item(item, write=False): """Get the fingerprint for an Item. If the item already has a fingerprint, it is not regenerated. If fingerprint generation fails, return None. If the items are associated with a library, they are saved to the database. If `write` is set, then the new fingerprints are also written to files' metadata. """ # Get a fingerprint and length for this track. if not item.length: log.info(u'{0}: no duration available'.format( util.displayable_path(item.path) )) elif item.acoustid_fingerprint: if write: log.info(u'{0}: fingerprint exists, skipping'.format( util.displayable_path(item.path) )) else: log.info(u'{0}: using existing fingerprint'.format( util.displayable_path(item.path) )) return item.acoustid_fingerprint else: log.info(u'{0}: fingerprinting'.format( util.displayable_path(item.path) )) try: _, fp = acoustid.fingerprint_file(item.path) item.acoustid_fingerprint = fp if write: log.info(u'{0}: writing fingerprint'.format( util.displayable_path(item.path) )) item.try_write() if item._db: item.store() return item.acoustid_fingerprint except acoustid.FingerprintGenerationError as exc: log.info(u'fingerprint generation failed: {0}' .format(exc)) beets-1.3.8/beetsplug/convert.py0000644000076500000240000003054712405676404017636 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Jakob Schnitzer. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Converts tracks or albums to external directory """ import logging import os import threading import subprocess import tempfile from string import Template import pipes from beets import ui, util, plugins, config from beets.plugins import BeetsPlugin from beetsplug.embedart import embed_item from beets.util.confit import ConfigTypeError log = logging.getLogger('beets') _fs_lock = threading.Lock() _temp_files = [] # Keep track of temporary transcoded files for deletion. # Some convenient alternate names for formats. ALIASES = { u'wma': u'windows media', u'vorbis': u'ogg', } LOSSLESS_FORMATS = ['ape', 'flac', 'alac', 'wav'] def replace_ext(path, ext): """Return the path with its extension replaced by `ext`. The new extension must not contain a leading dot. """ return os.path.splitext(path)[0] + '.' + ext def get_format(format=None): """Return the command tempate and the extension from the config. """ if not format: format = config['convert']['format'].get(unicode).lower() format = ALIASES.get(format, format) try: format_info = config['convert']['formats'][format].get(dict) command = format_info['command'] extension = format_info['extension'] except KeyError: raise ui.UserError( u'convert: format {0} needs "command" and "extension" fields' .format(format) ) except ConfigTypeError: command = config['convert']['formats'][format].get(str) extension = format # Convenience and backwards-compatibility shortcuts. keys = config['convert'].keys() if 'command' in keys: command = config['convert']['command'].get(unicode) elif 'opts' in keys: # Undocumented option for backwards compatibility with < 1.3.1. command = u'ffmpeg -i $source -y {0} $dest'.format( config['convert']['opts'].get(unicode) ) if 'extension' in keys: extension = config['convert']['extension'].get(unicode) return (command.encode('utf8'), extension.encode('utf8')) def encode(command, source, dest, pretend=False): """Encode `source` to `dest` using command template `command`. Raises `subprocess.CalledProcessError` if the command exited with a non-zero status code. """ quiet = config['convert']['quiet'].get() if not quiet and not pretend: log.info(u'Encoding {0}'.format(util.displayable_path(source))) command = Template(command).safe_substitute({ 'source': pipes.quote(source), 'dest': pipes.quote(dest), }) log.debug(u'convert: executing: {0}' .format(util.displayable_path(command))) if pretend: log.info(command) return try: util.command_output(command, shell=True) except subprocess.CalledProcessError: # Something went wrong (probably Ctrl+C), remove temporary files log.info(u'Encoding {0} failed. Cleaning up...' .format(util.displayable_path(source))) util.remove(dest) util.prune_dirs(os.path.dirname(dest)) raise except OSError as exc: raise ui.UserError( u"convert: could invoke '{0}': {0}".format(command, exc) ) if not quiet and not pretend: log.info(u'Finished encoding {0}'.format( util.displayable_path(source)) ) def should_transcode(item, format): """Determine whether the item should be transcoded as part of conversion (i.e., its bitrate is high or it has the wrong format). """ if config['convert']['never_convert_lossy_files'] and \ not (item.format.lower() in LOSSLESS_FORMATS): return False maxbr = config['convert']['max_bitrate'].get(int) return format.lower() != item.format.lower() or \ item.bitrate >= 1000 * maxbr def convert_item(dest_dir, keep_new, path_formats, format, pretend=False): command, ext = get_format(format) item, original, converted = None, None, None while True: item = yield (item, original, converted) dest = item.destination(basedir=dest_dir, path_formats=path_formats) # When keeping the new file in the library, we first move the # current (pristine) file to the destination. We'll then copy it # back to its old path or transcode it to a new path. if keep_new: original = dest converted = item.path else: original = item.path converted = dest # Ensure that only one thread tries to create directories at a # time. (The existence check is not atomic with the directory # creation inside this function.) if not pretend: with _fs_lock: util.mkdirall(dest) if os.path.exists(util.syspath(dest)): log.info(u'Skipping {0} (target file exists)'.format( util.displayable_path(item.path) )) continue if keep_new: if pretend: log.info(u'mv {0} {1}'.format( util.displayable_path(item.path), util.displayable_path(original), )) else: log.info(u'Moving to {0}'.format( util.displayable_path(original)) ) util.move(item.path, original) if should_transcode(item, format): converted = replace_ext(converted, ext) try: encode(command, original, converted, pretend) except subprocess.CalledProcessError: continue else: if pretend: log.info(u'cp {0} {1}'.format( util.displayable_path(original), util.displayable_path(converted), )) else: # No transcoding necessary. log.info(u'Copying {0}'.format( util.displayable_path(item.path)) ) util.copy(original, converted) if pretend: continue # Write tags from the database to the converted file. item.try_write(path=converted) if keep_new: # If we're keeping the transcoded file, read it again (after # writing) to get new bitrate, duration, etc. item.path = converted item.read() item.store() # Store new path and audio data. if config['convert']['embed']: album = item.get_album() if album and album.artpath: embed_item(item, album.artpath, itempath=converted) if keep_new: plugins.send('after_convert', item=item, dest=dest, keepnew=True) else: plugins.send('after_convert', item=item, dest=converted, keepnew=False) def convert_on_import(lib, item): """Transcode a file automatically after it is imported into the library. """ format = config['convert']['format'].get(unicode).lower() if should_transcode(item, format): command, ext = get_format() fd, dest = tempfile.mkstemp('.' + ext) os.close(fd) _temp_files.append(dest) # Delete the transcode later. try: encode(command, item.path, dest) except subprocess.CalledProcessError: return item.path = dest item.write() item.read() # Load new audio information data. item.store() def convert_func(lib, opts, args): if not opts.dest: opts.dest = config['convert']['dest'].get() if not opts.dest: raise ui.UserError('no convert destination set') opts.dest = util.bytestring_path(opts.dest) if not opts.threads: opts.threads = config['convert']['threads'].get(int) if config['convert']['paths']: path_formats = ui.get_path_formats(config['convert']['paths']) else: path_formats = ui.get_path_formats() if not opts.format: opts.format = config['convert']['format'].get(unicode).lower() pretend = opts.pretend if opts.pretend is not None else \ config['convert']['pretend'].get(bool) if not pretend: ui.commands.list_items(lib, ui.decargs(args), opts.album, None) if not (opts.yes or ui.input_yn("Convert? (Y/n)")): return if opts.album: items = (i for a in lib.albums(ui.decargs(args)) for i in a.items()) else: items = iter(lib.items(ui.decargs(args))) convert = [convert_item(opts.dest, opts.keep_new, path_formats, opts.format, pretend) for _ in range(opts.threads)] pipe = util.pipeline.Pipeline([items, convert]) pipe.run_parallel() class ConvertPlugin(BeetsPlugin): def __init__(self): super(ConvertPlugin, self).__init__() self.config.add({ u'dest': None, u'pretend': False, u'threads': util.cpu_count(), u'format': u'mp3', u'formats': { u'aac': { u'command': u'ffmpeg -i $source -y -vn -acodec libfaac ' u'-aq 100 $dest', u'extension': u'm4a', }, u'alac': { u'command': u'ffmpeg -i $source -y -vn -acodec alac $dest', u'extension': u'm4a', }, u'flac': u'ffmpeg -i $source -y -vn -acodec flac $dest', u'mp3': u'ffmpeg -i $source -y -vn -aq 2 $dest', u'opus': u'ffmpeg -i $source -y -vn -acodec libopus -ab 96k $dest', u'ogg': u'ffmpeg -i $source -y -vn -acodec libvorbis -aq 2 $dest', u'wma': u'ffmpeg -i $source -y -vn -acodec wmav2 -vn $dest', }, u'max_bitrate': 500, u'auto': False, u'quiet': False, u'embed': True, u'paths': {}, u'never_convert_lossy_files': False, }) self.import_stages = [self.auto_convert] def commands(self): cmd = ui.Subcommand('convert', help='convert to external location') cmd.parser.add_option('-p', '--pretend', action='store_true', help='show actions but do nothing') cmd.parser.add_option('-a', '--album', action='store_true', help='choose albums instead of tracks') cmd.parser.add_option('-t', '--threads', action='store', type='int', help='change the number of threads, \ defaults to maximum available processors') cmd.parser.add_option('-k', '--keep-new', action='store_true', dest='keep_new', help='keep only the converted \ and move the old files') cmd.parser.add_option('-d', '--dest', action='store', help='set the destination directory') cmd.parser.add_option('-f', '--format', action='store', dest='format', help='set the destination directory') cmd.parser.add_option('-y', '--yes', action='store', dest='yes', help='do not ask for confirmation') cmd.func = convert_func return [cmd] def auto_convert(self, config, task): if self.config['auto']: for item in task.imported_items(): convert_on_import(config.lib, item) @ConvertPlugin.listen('import_task_files') def _cleanup(task, session): for path in task.old_paths: if path in _temp_files: if os.path.isfile(path): util.remove(path) _temp_files.remove(path) beets-1.3.8/beetsplug/discogs.py0000644000076500000240000002406712405373453017607 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Adds Discogs album search support to the autotagger. Requires the discogs-client library. """ from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance from beets.plugins import BeetsPlugin from discogs_client import Release, Client from discogs_client.exceptions import DiscogsAPIError import beets import logging import re import time log = logging.getLogger('beets') # Silence spurious INFO log lines generated by urllib3. urllib3_logger = logging.getLogger('requests.packages.urllib3') urllib3_logger.setLevel(logging.CRITICAL) class DiscogsPlugin(BeetsPlugin): def __init__(self): super(DiscogsPlugin, self).__init__() self.config.add({ 'source_weight': 0.5, }) self.discogs_client = Client('beets/%s +http://beets.radbox.org/' % beets.__version__) def album_distance(self, items, album_info, mapping): """Returns the album distance. """ dist = Distance() if album_info.data_source == 'Discogs': dist.add('source', self.config['source_weight'].as_number()) return dist def candidates(self, items, artist, album, va_likely): """Returns a list of AlbumInfo objects for discogs search results matching an album and artist (if not various). """ if va_likely: query = album else: query = '%s %s' % (artist, album) try: return self.get_albums(query) except DiscogsAPIError as e: log.debug(u'Discogs API Error: {0} (query: {1})'.format(e, query)) return [] def album_for_id(self, album_id): """Fetches an album by its Discogs ID and returns an AlbumInfo object or None if the album is not found. """ log.debug(u'Searching Discogs for release {0}'.format(str(album_id))) # Discogs-IDs are simple integers. We only look for those at the end # of an input string as to avoid confusion with other metadata plugins. # An optional bracket can follow the integer, as this is how discogs # displays the release ID on its webpage. match = re.search(r'(^|\[*r|discogs\.com/.+/release/)(\d+)($|\])', album_id) if not match: return None result = Release(self.discogs_client, {'id': int(match.group(2))}) # Try to obtain title to verify that we indeed have a valid Release try: getattr(result, 'title') except DiscogsAPIError as e: if e.message != '404 Not Found': log.debug(u'Discogs API Error: {0} (query: {1})' .format(e, result._uri)) return None return self.get_album_info(result) def get_albums(self, query): """Returns a list of AlbumInfo objects for a discogs search query. """ # Strip non-word characters from query. Things like "!" and "-" can # cause a query to return no results, even if they match the artist or # album title. Use `re.UNICODE` flag to avoid stripping non-english # word characters. query = re.sub(r'(?u)\W+', ' ', query).encode('utf8') # Strip medium information from query, Things like "CD1" and "disk 1" # can also negate an otherwise positive result. query = re.sub(r'(?i)\b(CD|disc)\s*\d+', '', query) releases = self.discogs_client.search(query, type='release').page(1) return [self.get_album_info(release) for release in releases[:5]] def get_album_info(self, result): """Returns an AlbumInfo object for a discogs Release object. """ artist, artist_id = self.get_artist([a.data for a in result.artists]) album = re.sub(r' +', ' ', result.title) album_id = result.data['id'] # Use `.data` to access the tracklist directly instead of the # convenient `.tracklist` property, which will strip out useful artist # information and leave us with skeleton `Artist` objects that will # each make an API call just to get the same data back. tracks = self.get_tracks(result.data['tracklist']) albumtype = ', '.join( result.data['formats'][0].get('descriptions', [])) or None va = result.data['artists'][0]['name'].lower() == 'various' year = result.data['year'] label = result.data['labels'][0]['name'] mediums = len(set(t.medium for t in tracks)) catalogno = result.data['labels'][0]['catno'] if catalogno == 'none': catalogno = None country = result.data.get('country') media = result.data['formats'][0]['name'] data_url = result.data['uri'] return AlbumInfo(album, album_id, artist, artist_id, tracks, asin=None, albumtype=albumtype, va=va, year=year, month=None, day=None, label=label, mediums=mediums, artist_sort=None, releasegroup_id=None, catalognum=catalogno, script=None, language=None, country=country, albumstatus=None, media=media, albumdisambig=None, artist_credit=None, original_year=None, original_month=None, original_day=None, data_source='Discogs', data_url=data_url) def get_artist(self, artists): """Returns an artist string (all artists) and an artist_id (the main artist) for a list of discogs album or track artists. """ artist_id = None bits = [] for artist in artists: if not artist_id: artist_id = artist['id'] name = artist['name'] # Strip disambiguation number. name = re.sub(r' \(\d+\)$', '', name) # Move articles to the front. name = re.sub(r'(?i)^(.*?), (a|an|the)$', r'\2 \1', name) bits.append(name) if artist['join']: bits.append(artist['join']) artist = ' '.join(bits).replace(' ,', ',') or None return artist, artist_id def get_tracks(self, tracklist): """Returns a list of TrackInfo objects for a discogs tracklist. """ tracks = [] index_tracks = {} index = 0 for track in tracklist: # Only real tracks have `position`. Otherwise, it's an index track. if track['position']: index += 1 tracks.append(self.get_track_info(track, index)) else: index_tracks[index + 1] = track['title'] # Fix up medium and medium_index for each track. Discogs position is # unreliable, but tracks are in order. medium = None medium_count, index_count = 0, 0 for track in tracks: # Handle special case where a different medium does not indicate a # new disc, when there is no medium_index and the ordinal of medium # is not sequential. For example, I, II, III, IV, V. Assume these # are the track index, not the medium. medium_is_index = track.medium and not track.medium_index and ( len(track.medium) != 1 or ord(track.medium) - 64 != medium_count + 1 ) if not medium_is_index and medium != track.medium: # Increment medium_count and reset index_count when medium # changes. medium = track.medium medium_count += 1 index_count = 0 index_count += 1 track.medium, track.medium_index = medium_count, index_count # Get `disctitle` from Discogs index tracks. Assume that an index track # before the first track of each medium is a disc title. for track in tracks: if track.medium_index == 1: if track.index in index_tracks: disctitle = index_tracks[track.index] else: disctitle = None track.disctitle = disctitle return tracks def get_track_info(self, track, index): """Returns a TrackInfo object for a discogs track. """ title = track['title'] track_id = None medium, medium_index = self.get_track_index(track['position']) artist, artist_id = self.get_artist(track.get('artists', [])) length = self.get_track_length(track['duration']) return TrackInfo(title, track_id, artist, artist_id, length, index, medium, medium_index, artist_sort=None, disctitle=None, artist_credit=None) def get_track_index(self, position): """Returns the medium and medium index for a discogs track position. """ # medium_index is a number at the end of position. medium is everything # else. E.g. (A)(1), (Side A, Track )(1), (A)(), ()(1), etc. match = re.match(r'^(.*?)(\d*)$', position.upper()) if match: medium, index = match.groups() else: log.debug(u'Invalid Discogs position: {0}'.format(position)) medium = index = None return medium or None, index or None def get_track_length(self, duration): """Returns the track length in seconds for a discogs duration. """ try: length = time.strptime(duration, '%M:%S') except ValueError: return None return length.tm_min * 60 + length.tm_sec beets-1.3.8/beetsplug/duplicates.py0000644000076500000240000002133012405373453020277 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Pedro Silva. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """List duplicate tracks or albums. """ import shlex import logging from beets.plugins import BeetsPlugin from beets.ui import decargs, print_obj, vararg_callback, Subcommand, UserError from beets.util import command_output, displayable_path, subprocess PLUGIN = 'duplicates' log = logging.getLogger('beets') def _process_item(item, lib, copy=False, move=False, delete=False, tag=False, format=None): """Process Item `item` in `lib`. """ if copy: item.move(basedir=copy, copy=True) item.store() if move: item.move(basedir=move, copy=False) item.store() if delete: item.remove(delete=True) if tag: try: k, v = tag.split('=') except: raise UserError('%s: can\'t parse k=v tag: %s' % (PLUGIN, tag)) setattr(k, v) item.store() print_obj(item, lib, fmt=format) def _checksum(item, prog): """Run external `prog` on file path associated with `item`, cache output as flexattr on a key that is the name of the program, and return the key, checksum tuple. """ args = [p.format(file=item.path) for p in shlex.split(prog)] key = args[0] checksum = getattr(item, key, False) if not checksum: log.debug(u'{0}: key {1} on item {2} not cached: computing checksum' .format(PLUGIN, key, displayable_path(item.path))) try: checksum = command_output(args) setattr(item, key, checksum) item.store() log.debug(u'{)}: computed checksum for {1} using {2}' .format(PLUGIN, item.title, key)) except subprocess.CalledProcessError as e: log.debug(u'{0}: failed to checksum {1}: {2}' .format(PLUGIN, displayable_path(item.path), e)) else: log.debug(u'{0}: key {1} on item {2} cached: not computing checksum' .format(PLUGIN, key, displayable_path(item.path))) return key, checksum def _group_by(objs, keys): """Return a dictionary with keys arbitrary concatenations of attributes and values lists of objects (Albums or Items) with those keys. """ import collections counts = collections.defaultdict(list) for obj in objs: values = [getattr(obj, k, None) for k in keys] values = [v for v in values if v not in (None, '')] if values: key = '\001'.join(values) counts[key].append(obj) else: log.debug(u'{0}: all keys {1} on item {2} are null: skipping' .format(PLUGIN, str(keys), displayable_path(obj.path))) return counts def _duplicates(objs, keys, full): """Generate triples of keys, duplicate counts, and constituent objects. """ offset = 0 if full else 1 for k, objs in _group_by(objs, keys).iteritems(): if len(objs) > 1: yield (k, len(objs) - offset, objs[offset:]) class DuplicatesPlugin(BeetsPlugin): """List duplicate tracks or albums """ def __init__(self): super(DuplicatesPlugin, self).__init__() self.config.add({ 'format': '', 'count': False, 'album': False, 'full': False, 'path': False, 'keys': ['mb_trackid', 'mb_albumid'], 'checksum': None, 'copy': False, 'move': False, 'delete': False, 'tag': False, }) self._command = Subcommand('duplicates', help=__doc__, aliases=['dup']) self._command.parser.add_option('-f', '--format', dest='format', action='store', type='string', help='print with custom format', metavar='FMT') self._command.parser.add_option('-a', '--album', dest='album', action='store_true', help='show duplicate albums instead of' ' tracks') self._command.parser.add_option('-c', '--count', dest='count', action='store_true', help='show duplicate counts') self._command.parser.add_option('-C', '--checksum', dest='checksum', action='store', metavar='PROG', help='report duplicates based on' ' arbitrary command') self._command.parser.add_option('-d', '--delete', dest='delete', action='store_true', help='delete items from library and ' 'disk') self._command.parser.add_option('-F', '--full', dest='full', action='store_true', help='show all versions of duplicate' ' tracks or albums') self._command.parser.add_option('-k', '--keys', dest='keys', action='callback', metavar='KEY1 KEY2', callback=vararg_callback, help='report duplicates based on keys') self._command.parser.add_option('-m', '--move', dest='move', action='store', metavar='DEST', help='move items to dest') self._command.parser.add_option('-o', '--copy', dest='copy', action='store', metavar='DEST', help='copy items to dest') self._command.parser.add_option('-p', '--path', dest='path', action='store_true', help='print paths for matched items or' ' albums') self._command.parser.add_option('-t', '--tag', dest='tag', action='store', help='tag matched items with \'k=v\'' ' attribute') def commands(self): def _dup(lib, opts, args): self.config.set_args(opts) fmt = self.config['format'].get() album = self.config['album'].get(bool) full = self.config['full'].get(bool) keys = self.config['keys'].get() checksum = self.config['checksum'].get() copy = self.config['copy'].get() move = self.config['move'].get() delete = self.config['delete'].get(bool) tag = self.config['tag'].get() if album: keys = ['mb_albumid'] items = lib.albums(decargs(args)) else: items = lib.items(decargs(args)) if self.config['path']: fmt = '$path' # Default format string for count mode. if self.config['count'] and not fmt: if album: fmt = '$albumartist - $album' else: fmt = '$albumartist - $album - $title' fmt += ': {0}' if checksum: for i in items: k, _ = _checksum(i, checksum) keys = [k] for obj_id, obj_count, objs in _duplicates(items, keys=keys, full=full): if obj_id: # Skip empty IDs. for o in objs: _process_item(o, lib, copy=copy, move=move, delete=delete, tag=tag, format=fmt.format(obj_count)) self._command.func = _dup return [self._command] beets-1.3.8/beetsplug/echonest.py0000644000076500000240000004472312405357732017767 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Fetch a variety of acoustic metrics from The Echo Nest. """ import time import logging import socket import os import tempfile from string import Template import subprocess from beets import util, config, plugins, ui from beets.dbcore import types import pyechonest import pyechonest.song import pyechonest.track log = logging.getLogger('beets') # If a request at the EchoNest fails, we want to retry the request RETRIES # times and wait between retries for RETRY_INTERVAL seconds. RETRIES = 10 RETRY_INTERVAL = 10 DEVNULL = open(os.devnull, 'wb') ALLOWED_FORMATS = ('MP3', 'OGG', 'AAC') UPLOAD_MAX_SIZE = 50 * 1024 * 1024 # Maps attribute names from echonest to their field names in beets. # The attributes are retrieved from a songs `audio_summary`. See: # http://echonest.github.io/pyechonest/song.html#pyechonest.song.profile ATTRIBUTES = { 'energy': 'energy', 'liveness': 'liveness', 'speechiness': 'speechiness', 'acousticness': 'acousticness', 'danceability': 'danceability', 'valence': 'valence', 'tempo': 'bpm', } # Types for the flexible fields added by `ATTRIBUTES` FIELD_TYPES = { 'energy': types.FLOAT, 'liveness': types.FLOAT, 'speechiness': types.FLOAT, 'acousticness': types.FLOAT, 'danceability': types.FLOAT, 'valence': types.FLOAT, } MUSICAL_SCALE = ['C', 'C#', 'D', 'D#', 'E' 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] # We also use echonest_id (song_id) and echonest_fingerprint to speed up # lookups. ID_KEY = 'echonest_id' FINGERPRINT_KEY = 'echonest_fingerprint' def _splitstrip(string, delim=u','): """Split string (at commas by default) and strip whitespace from the pieces. """ return [s.strip() for s in string.split(delim)] def diff(item1, item2): """Score two Item objects according to the Echo Nest numerical fields. """ result = 0.0 for attr in ATTRIBUTES.values(): if attr == 'bpm': # BPM (tempo) is handled specially to normalize. continue try: result += abs( float(item1.get(attr, None)) - float(item2.get(attr, None)) ) except TypeError: result += 1.0 try: bpm1 = float(item1.get('bpm', None)) bpm2 = float(item2.get('bpm', None)) result += abs(bpm1 - bpm2) / max(bpm1, bpm2, 1) except TypeError: result += 1.0 return result def similar(lib, src_item, threshold=0.15, fmt='${difference}: ${path}'): for item in lib.items(): if item.path != src_item.path: d = diff(item, src_item) if d < threshold: s = fmt.replace('${difference}', '{:2.2f}'.format(d)) ui.print_obj(item, lib, s) class EchonestMetadataPlugin(plugins.BeetsPlugin): item_types = FIELD_TYPES def __init__(self): super(EchonestMetadataPlugin, self).__init__() self.config.add({ 'auto': True, 'apikey': u'NY2KTZHQ0QDSHBAP6', 'codegen': None, 'upload': True, 'convert': True, 'truncate': True, }) self.config.add(ATTRIBUTES) pyechonest.config.ECHO_NEST_API_KEY = \ config['echonest']['apikey'].get(unicode) if config['echonest']['codegen']: pyechonest.config.CODEGEN_BINARY_OVERRIDE = \ config['echonest']['codegen'].get(unicode) if self.config['auto']: self.import_stages = [self.imported] def _echofun(self, func, **kwargs): """Wrapper for requests to the EchoNest API. Will retry up to RETRIES times and wait between retries for RETRY_INTERVAL seconds. """ for i in range(RETRIES): try: result = func(**kwargs) except pyechonest.util.EchoNestAPIError as e: if e.code == 3: # reached access limit per minute log.debug(u'echonest: rate-limited on try {0}; ' u'waiting {1} seconds' .format(i + 1, RETRY_INTERVAL)) time.sleep(RETRY_INTERVAL) elif e.code == 5: # specified identifier does not exist # no use in trying again. log.debug(u'echonest: {0}'.format(e)) return None else: log.error(u'echonest: {0}'.format(e.args[0][0])) return None except (pyechonest.util.EchoNestIOError, socket.error) as e: log.warn(u'echonest: IO error: {0}'.format(e)) time.sleep(RETRY_INTERVAL) except Exception as e: # there was an error analyzing the track, status: error log.debug(u'echonest: {0}'.format(e)) return None else: break else: # If we exited the loop without breaking, then we used up all # our allotted retries. raise ui.UserError(u'echonest request failed repeatedly') return None return result def _pick_song(self, songs, item): """Helper method to pick the best matching song from a list of songs returned by the EchoNest. Compares artist, title and duration. If the artist and title match and the duration difference is <= 1.0 seconds, it's considered a match. """ if not songs: log.debug(u'echonest: no songs found') return pick = None min_dist = item.length for song in songs: if song.artist_name.lower() == item.artist.lower() \ and song.title.lower() == item.title.lower(): dist = abs(item.length - song.audio_summary['duration']) if dist < min_dist: min_dist = dist pick = song if min_dist > 2.5: return None return pick def _flatten_song(self, song): """Given an Echo Nest song object, return a flat dict containing attributes we care about. If song is None, return None. """ if not song: return values = dict(song.audio_summary) values['id'] = song.id return values # "Profile" (ID-based) lookup. def profile(self, item): """Do a lookup on the EchoNest by MusicBrainz ID. """ # Use an existing Echo Nest ID. if ID_KEY in item: enid = item[ID_KEY] # Look up the Echo Nest ID based on the MBID. else: if not item.mb_trackid: log.debug(u'echonest: no ID available') return mbid = 'musicbrainz:track:{0}'.format(item.mb_trackid) track = self._echofun(pyechonest.track.track_from_id, identifier=mbid) if not track: log.debug(u'echonest: lookup by MBID failed') return enid = track.song_id # Use the Echo Nest ID to look up the song. songs = self._echofun(pyechonest.song.profile, ids=enid, buckets=['id:musicbrainz', 'audio_summary']) return self._flatten_song(self._pick_song(songs, item)) # "Search" (metadata-based) lookup. def search(self, item): """Search the item at the EchoNest by artist and title. """ songs = self._echofun(pyechonest.song.search, title=item.title, results=100, artist=item.artist, buckets=['id:musicbrainz', 'tracks', 'audio_summary']) return self._flatten_song(self._pick_song(songs, item)) # "Identify" (fingerprinting) lookup. def fingerprint(self, item): """Get the fingerprint for this item from the EchoNest. If we already have a fingerprint, return it and don't calculate it again. """ if FINGERPRINT_KEY in item: return item[FINGERPRINT_KEY] try: res = self._echofun(pyechonest.util.codegen, filename=item.path.decode('utf-8')) except Exception as e: # Frustratingly, the pyechonest library raises a plain Exception # when the command is not found. log.debug(u'echonest: codegen failed: {0}'.format(e)) return if not res or 'code' not in res[0] or not res[0]['code']: log.debug(u'echonest: no fingerprint returned') return code = res[0]['code'] log.debug(u'echonest: calculated fingerprint') item[FINGERPRINT_KEY] = code return code def identify(self, item): """Try to identify the song at the EchoNest. """ code = self.fingerprint(item) if not code: return songs = self._echofun(pyechonest.song.identify, code=code) if not songs: log.debug(u'echonest: no songs found for fingerprint') return return self._flatten_song(max(songs, key=lambda s: s.score)) # "Analyze" (upload the audio itself) method. def convert(self, item): """Converts an item in an unsupported media format to ogg. Config pending. This is stolen from Jakob Schnitzers convert plugin. """ fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) source = item.path log.info(u'echonest: encoding {0} to {1}'.format( util.displayable_path(source), util.displayable_path(dest), )) # Build up the FFmpeg command line. # FIXME: use avconv? command = u'ffmpeg -i $source -y -acodec libvorbis -vn -aq 2 $dest' opts = [] for arg in command.split(): arg = arg.encode('utf-8') opts.append(Template(arg).substitute(source=source, dest=dest)) # Run the command. try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: log.debug(u'echonest: encode failed: {0}'.format(exc)) util.remove(dest) return log.info(u'echonest: finished encoding {0}'.format( util.displayable_path(source)) ) return dest def truncate(self, item): """Truncates an item to a size less than UPLOAD_MAX_SIZE.""" fd, dest = tempfile.mkstemp(u'.ogg') os.close(fd) source = item.path log.info(u'echonest: truncating {0} to {1}'.format( util.displayable_path(source), util.displayable_path(dest), )) command = u'ffmpeg -t 300 -i $source -y -acodec copy $dest' opts = [] for arg in command.split(): arg = arg.encode('utf-8') opts.append(Template(arg).substitute(source=source, dest=dest)) # Run the command. try: util.command_output(opts) except (OSError, subprocess.CalledProcessError) as exc: log.debug(u'echonest: truncate failed: {0}'.format(exc)) util.remove(dest) return log.info(u'echonest: truncate encoding {0}'.format( util.displayable_path(source)) ) return dest def analyze(self, item): """Upload the item to the EchoNest for analysis. May require to convert the item to a supported media format. """ # Get the file to upload (either by using the file directly or by # transcoding it first). source = item.path if item.format not in ALLOWED_FORMATS: if config['echonest']['convert']: source = self.convert(item) if not source: log.debug(u'echonest: failed to convert file') return else: return if os.stat(item.path).st_size > UPLOAD_MAX_SIZE: if config['echonest']['truncate']: source = self.truncate(item) if not source: log.debug(u'echonest: failed to truncate file') return else: return # Upload the audio file. log.info(u'echonest: uploading file, please be patient') track = self._echofun(pyechonest.track.track_from_filename, filename=source) if not track: log.debug(u'echonest: failed to upload file') return # Sometimes we have a track but no song. I guess this happens for # new / unverified songs. We need to "extract" the audio_summary # from the track object manually. I don't know why the # pyechonest API handles tracks (merge audio_summary to __dict__) # and songs (keep audio_summary in an extra attribute) # differently. # Maybe a patch for pyechonest could help? # First get the (limited) metadata from the track in case # there's no associated song. from_track = {} for key in ATTRIBUTES: try: from_track[key] = getattr(track, key) except AttributeError: pass from_track['duration'] = track.duration # Try to look up a song for the full metadata. try: song_id = track.song_id except AttributeError: return from_track songs = self._echofun(pyechonest.song.profile, ids=[song_id], track_ids=[track.id], buckets=['audio_summary']) if songs: pick = self._pick_song(songs, item) if pick: return self._flatten_song(pick) return from_track # Fall back to track metadata. # Shared top-level logic. def fetch_song(self, item): """Try all methods to get a matching song object from the EchoNest. If no method succeeds, return None. """ # There are four different ways to get a song. Each method is a # callable that takes the Item as an argument. methods = [self.profile, self.search] if config['echonest']['codegen']: methods.append(self.identify) if config['echonest']['upload']: methods.append(self.analyze) # Try each method in turn. for method in methods: song = method(item) if song: log.debug( u'echonest: got song through {0}: {1} - {2} [{3}]'.format( method.__name__, item.artist, item.title, song['duration'], ) ) return song def apply_metadata(self, item, values, write=False): """Copy the metadata from the dictionary of song information to the item. """ # Update each field. for k, v in values.iteritems(): if k in ATTRIBUTES: field = ATTRIBUTES[k] log.debug(u'echonest: metadata: {0} = {1}'.format(field, v)) if field == 'bpm': item[field] = int(v) else: item[field] = v if 'key' in values and 'mode' in values: key = MUSICAL_SCALE[values['key'] - 1] if values['mode'] == 0: # Minor key key += 'm' item['initial_key'] = key if 'id' in values: enid = values['id'] log.debug(u'echonest: metadata: {0} = {1}'.format(ID_KEY, enid)) item[ID_KEY] = enid # Write and save. if write: item.try_write() item.store() # Automatic (on-import) metadata fetching. def imported(self, session, task): """Import pipeline stage. """ for item in task.imported_items(): song = self.fetch_song(item) if song: self.apply_metadata(item, song) # Explicit command invocation. def requires_update(self, item): """Check if this item requires an update from the EchoNest (its data is missing). """ for field in ATTRIBUTES.values(): if not item.get(field): return True log.info(u'echonest: no update required') return False def commands(self): fetch_cmd = ui.Subcommand('echonest', help='Fetch metadata from the EchoNest') fetch_cmd.parser.add_option( '-f', '--force', dest='force', action='store_true', default=False, help='(re-)download information from the EchoNest' ) def fetch_func(lib, opts, args): self.config.set_args(opts) write = config['import']['write'].get(bool) for item in lib.items(ui.decargs(args)): log.info(u'echonest: {0} - {1}'.format(item.artist, item.title)) if self.config['force'] or self.requires_update(item): song = self.fetch_song(item) if song: self.apply_metadata(item, song, write) fetch_cmd.func = fetch_func sim_cmd = ui.Subcommand('echosim', help='show related files') sim_cmd.parser.add_option( '-t', '--threshold', dest='threshold', action='store', type='float', default=0.15, help='Set difference threshold' ) sim_cmd.parser.add_option( '-f', '--format', action='store', default='${difference}: ${path}', help='print with custom format' ) def sim_func(lib, opts, args): self.config.set_args(opts) for item in lib.items(ui.decargs(args)): similar(lib, item, opts.threshold, opts.format) sim_cmd.func = sim_func return [fetch_cmd, sim_cmd] beets-1.3.8/beetsplug/echonest_tempo.py0000644000076500000240000001314512405373453021163 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, David Brenner # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Gets tempo (bpm) for imported music from the EchoNest API. Requires the pyechonest library (https://github.com/echonest/pyechonest). """ import time import logging from beets.plugins import BeetsPlugin from beets import ui from beets import config import pyechonest.config import pyechonest.song import socket # Global logger. log = logging.getLogger('beets') RETRY_INTERVAL = 10 # Seconds. RETRIES = 10 def fetch_item_tempo(lib, loglevel, item, write): """Fetch and store tempo for a single item. If ``write``, then the tempo will also be written to the file itself in the bpm field. The ``loglevel`` parameter controls the visibility of the function's status log messages. """ # Skip if the item already has the tempo field. if item.bpm: log.log(loglevel, u'bpm already present: {0} - {1}' .format(item.artist, item.title)) return # Fetch tempo. tempo = get_tempo(item.artist, item.title, item.length) if not tempo: log.log(loglevel, u'tempo not found: {0} - {1}' .format(item.artist, item.title)) return log.log(loglevel, u'fetched tempo: {0} - {1}' .format(item.artist, item.title)) item.bpm = int(tempo) if write: item.try_write() item.store() def get_tempo(artist, title, duration): """Get the tempo for a song.""" # We must have sufficient metadata for the lookup. Otherwise the API # will just complain. artist = artist.replace(u'\n', u' ').strip().lower() title = title.replace(u'\n', u' ').strip().lower() if not artist or not title: return None for i in range(RETRIES): try: # Unfortunately, all we can do is search by artist and title. # EchoNest supports foreign ids from MusicBrainz, but currently # only for artists, not individual tracks/recordings. results = pyechonest.song.search( artist=artist, title=title, results=100, buckets=['audio_summary'] ) except pyechonest.util.EchoNestAPIError as e: if e.code == 3: # Wait and try again. time.sleep(RETRY_INTERVAL) else: log.warn(u'echonest_tempo: {0}'.format(e.args[0][0])) return None except (pyechonest.util.EchoNestIOError, socket.error) as e: log.debug(u'echonest_tempo: IO error: {0}'.format(e)) time.sleep(RETRY_INTERVAL) else: break else: # If we exited the loop without breaking, then we used up all # our allotted retries. log.debug(u'echonest_tempo: exceeded retries') return None # The Echo Nest API can return songs that are not perfect matches. # So we look through the results for songs that have the right # artist and title. The API also doesn't have MusicBrainz track IDs; # otherwise we could use those for a more robust match. min_distance = duration pick = None for result in results: if result.artist_name.lower() == artist and \ result.title.lower() == title: distance = abs(duration - result.audio_summary['duration']) log.debug( u'echonest_tempo: candidate {0:2.2f} ' u'(distance: {1:2.2f}) = {2}'.format( result.audio_summary['duration'], distance, result.audio_summary['tempo'], ) ) if distance < min_distance: min_distance = distance pick = result.audio_summary['tempo'] return pick class EchoNestTempoPlugin(BeetsPlugin): def __init__(self): super(EchoNestTempoPlugin, self).__init__() self.import_stages = [self.imported] self.config.add({ 'apikey': u'NY2KTZHQ0QDSHBAP6', 'auto': True, }) pyechonest.config.ECHO_NEST_API_KEY = \ self.config['apikey'].get(unicode) def commands(self): cmd = ui.Subcommand('tempo', help='fetch song tempo (bpm)') cmd.parser.add_option('-p', '--print', dest='printbpm', action='store_true', default=False, help='print tempo (bpm) to console') def func(lib, opts, args): # The "write to files" option corresponds to the # import_write config value. write = config['import']['write'].get(bool) for item in lib.items(ui.decargs(args)): fetch_item_tempo(lib, logging.INFO, item, write) if opts.printbpm and item.bpm: ui.print_('{0} BPM'.format(item.bpm)) cmd.func = func return [cmd] # Auto-fetch tempo on import. def imported(self, config, task): if self.config['auto']: for item in task.imported_items(): fetch_item_tempo(config.lib, logging.DEBUG, item, False) beets-1.3.8/beetsplug/embedart.py0000644000076500000240000001425312405373453017733 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Allows beets to embed album art into file metadata.""" import os.path import logging import imghdr from beets.plugins import BeetsPlugin from beets import mediafile from beets import ui from beets.ui import decargs from beets.util import syspath, normpath, displayable_path from beets.util.artresizer import ArtResizer from beets import config log = logging.getLogger('beets') class EmbedCoverArtPlugin(BeetsPlugin): """Allows albumart to be embedded into the actual files. """ def __init__(self): super(EmbedCoverArtPlugin, self).__init__() self.config.add({ 'maxwidth': 0, 'auto': True, }) if self.config['maxwidth'].get(int) and \ not ArtResizer.shared.local: self.config['maxwidth'] = 0 log.warn(u"embedart: ImageMagick or PIL not found; " u"'maxwidth' option ignored") def commands(self): # Embed command. embed_cmd = ui.Subcommand( 'embedart', help='embed image files into file metadata' ) embed_cmd.parser.add_option( '-f', '--file', metavar='PATH', help='the image file to embed' ) maxwidth = config['embedart']['maxwidth'].get(int) def embed_func(lib, opts, args): if opts.file: imagepath = normpath(opts.file) for item in lib.items(decargs(args)): embed_item(item, imagepath, maxwidth) else: for album in lib.albums(decargs(args)): embed_album(album, maxwidth) embed_cmd.func = embed_func # Extract command. extract_cmd = ui.Subcommand('extractart', help='extract an image from file metadata') extract_cmd.parser.add_option('-o', dest='outpath', help='image output file') def extract_func(lib, opts, args): outpath = normpath(opts.outpath or 'cover') extract(lib, outpath, decargs(args)) extract_cmd.func = extract_func # Clear command. clear_cmd = ui.Subcommand('clearart', help='remove images from file metadata') def clear_func(lib, opts, args): clear(lib, decargs(args)) clear_cmd.func = clear_func return [embed_cmd, extract_cmd, clear_cmd] @EmbedCoverArtPlugin.listen('album_imported') def album_imported(lib, album): """Automatically embed art into imported albums. """ if album.artpath and config['embedart']['auto']: embed_album(album, config['embedart']['maxwidth'].get(int)) def embed_item(item, imagepath, maxwidth=None, itempath=None): """Embed an image into the item's media file. """ try: item['images'] = [_mediafile_image(imagepath, maxwidth)] item.try_write(itempath) except IOError as exc: log.error(u'embedart: could not read image file: {0}'.format(exc)) finally: # We don't want to store the image in the database del item['images'] def embed_album(album, maxwidth=None): """Embed album art into all of the album's items. """ imagepath = album.artpath if not imagepath: log.info(u'No album art present: {0} - {1}'. format(album.albumartist, album.album)) return if not os.path.isfile(imagepath): log.error(u'Album art not found at {0}' .format(imagepath)) return log.info(u'Embedding album art into {0.albumartist} - {0.album}.' .format(album)) for item in album.items(): embed_item(item, imagepath, maxwidth) def _mediafile_image(image_path, maxwidth=None): """Return a `mediafile.Image` object for the path. If maxwidth is set the image is resized if necessary. """ if maxwidth: image_path = ArtResizer.shared.resize(maxwidth, syspath(image_path)) with open(syspath(image_path), 'rb') as f: data = f.read() return mediafile.Image(data, type=mediafile.ImageType.front) # 'extractart' command. def extract(lib, outpath, query): item = lib.items(query).get() if not item: log.error(u'No item matches query.') return # Extract the art. try: mf = mediafile.MediaFile(syspath(item.path)) except mediafile.UnreadableFileError as exc: log.error(u'Could not extract art from {0}: {1}'.format( displayable_path(item.path), exc )) return art = mf.art if not art: log.error(u'No album art present in {0} - {1}.' .format(item.artist, item.title)) return # Add an extension to the filename. ext = imghdr.what(None, h=art) if not ext: log.error(u'Unknown image type.') return outpath += '.' + ext log.info(u'Extracting album art from: {0.artist} - {0.title}\n' u'To: {1}'.format(item, displayable_path(outpath))) with open(syspath(outpath), 'wb') as f: f.write(art) # 'clearart' command. def clear(lib, query): log.info(u'Clearing album art from items:') for item in lib.items(query): log.info(u'{0} - {1}'.format(item.artist, item.title)) try: mf = mediafile.MediaFile(syspath(item.path), config['id3v23'].get(bool)) except mediafile.UnreadableFileError as exc: log.error(u'Could not clear art from {0}: {1}'.format( displayable_path(item.path), exc )) continue mf.art = None mf.save() beets-1.3.8/beetsplug/fetchart.py0000644000076500000240000002666412405157370017757 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Fetches album art. """ from contextlib import closing import logging import os import re from tempfile import NamedTemporaryFile import requests from beets.plugins import BeetsPlugin from beets.util.artresizer import ArtResizer from beets import importer from beets import ui from beets import util from beets import config IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg'] CONTENT_TYPES = ('image/jpeg',) DOWNLOAD_EXTENSION = '.jpg' log = logging.getLogger('beets') requests_session = requests.Session() requests_session.headers = {'User-Agent': 'beets'} def _fetch_image(url): """Downloads an image from a URL and checks whether it seems to actually be an image. If so, returns a path to the downloaded image. Otherwise, returns None. """ log.debug(u'fetchart: downloading art: {0}'.format(url)) try: with closing(requests_session.get(url, stream=True)) as resp: if 'Content-Type' not in resp.headers \ or resp.headers['Content-Type'] not in CONTENT_TYPES: log.debug(u'fetchart: not an image') return # Generate a temporary file with the correct extension. with NamedTemporaryFile(suffix=DOWNLOAD_EXTENSION, delete=False) \ as fh: for chunk in resp.iter_content(): fh.write(chunk) log.debug(u'fetchart: downloaded art to: {0}'.format( util.displayable_path(fh.name) )) return fh.name except (IOError, requests.RequestException): log.debug(u'fetchart: error fetching art') # ART SOURCES ################################################################ # Cover Art Archive. CAA_URL = 'http://coverartarchive.org/release/{mbid}/front-500.jpg' CAA_GROUP_URL = 'http://coverartarchive.org/release-group/{mbid}/front-500.jpg' def caa_art(release_id): """Return the Cover Art Archive URL given a MusicBrainz release ID. """ return CAA_URL.format(mbid=release_id) def caa_group_art(release_group_id): """Return the Cover Art Archive release group URL given a MusicBrainz release group ID. """ return CAA_GROUP_URL.format(mbid=release_group_id) # Art from Amazon. AMAZON_URL = 'http://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' AMAZON_INDICES = (1, 2) def art_for_asin(asin): """Generate URLs for an Amazon ID (ASIN) string.""" for index in AMAZON_INDICES: yield AMAZON_URL % (asin, index) # AlbumArt.org scraper. AAO_URL = 'http://www.albumart.org/index_detail.php' AAO_PAT = r'href\s*=\s*"([^>"]*)"[^>]*title\s*=\s*"View larger image"' def aao_art(asin): """Return art URL from AlbumArt.org given an ASIN.""" # Get the page from albumart.org. try: resp = requests_session.get(AAO_URL, params={'asin': asin}) log.debug(u'fetchart: scraped art URL: {0}'.format(resp.url)) except requests.RequestException: log.debug(u'fetchart: error scraping art page') return # Search the page for the image URL. m = re.search(AAO_PAT, resp.text) if m: image_url = m.group(1) return image_url else: log.debug(u'fetchart: no image found on page') # Google Images scraper. GOOGLE_URL = 'https://ajax.googleapis.com/ajax/services/search/images' def google_art(album): """Return art URL from google.org given an album title and interpreter. """ search_string = (album.albumartist + ',' + album.album).encode('utf-8') response = requests_session.get(GOOGLE_URL, params={ 'v': '1.0', 'q': search_string, 'start': '0', }) # Get results using JSON. try: results = response.json() data = results['responseData'] dataInfo = data['results'] for myUrl in dataInfo: return myUrl['unescapedUrl'] except: log.debug(u'fetchart: error scraping art page') return # Art from the filesystem. def filename_priority(filename, cover_names): """Sort order for image names. Return indexes of cover names found in the image filename. This means that images with lower-numbered and more keywords will have higher priority. """ return [idx for (idx, x) in enumerate(cover_names) if x in filename] def art_in_path(path, cover_names, cautious): """Look for album art files in a specified directory.""" if not os.path.isdir(path): return # Find all files that look like images in the directory. images = [] for fn in os.listdir(path): for ext in IMAGE_EXTENSIONS: if fn.lower().endswith('.' + ext): images.append(fn) # Look for "preferred" filenames. images = sorted(images, key=lambda x: filename_priority(x, cover_names)) cover_pat = r"(\b|_)({0})(\b|_)".format('|'.join(cover_names)) for fn in images: if re.search(cover_pat, os.path.splitext(fn)[0], re.I): log.debug(u'fetchart: using well-named art file {0}'.format( util.displayable_path(fn) )) return os.path.join(path, fn) # Fall back to any image in the folder. if images and not cautious: log.debug(u'fetchart: using fallback art file {0}'.format( util.displayable_path(images[0]) )) return os.path.join(path, images[0]) # Try each source in turn. def _source_urls(album): """Generate possible source URLs for an album's art. The URLs are not guaranteed to work so they each need to be attempted in turn. This allows the main `art_for_album` function to abort iteration through this sequence early to avoid the cost of scraping when not necessary. """ # Cover Art Archive. if album.mb_albumid: yield caa_art(album.mb_albumid) if album.mb_releasegroupid: yield caa_group_art(album.mb_releasegroupid) # Amazon and AlbumArt.org. if album.asin: for url in art_for_asin(album.asin): yield url url = aao_art(album.asin) if url: yield url if config['fetchart']['google_search']: url = google_art(album) if url: yield url def art_for_album(album, paths, maxwidth=None, local_only=False): """Given an Album object, returns a path to downloaded art for the album (or None if no art is found). If `maxwidth`, then images are resized to this maximum pixel size. If `local_only`, then only local image files from the filesystem are returned; no network requests are made. """ out = None # Local art. cover_names = config['fetchart']['cover_names'].as_str_seq() cover_names = map(util.bytestring_path, cover_names) cautious = config['fetchart']['cautious'].get(bool) if paths: for path in paths: out = art_in_path(path, cover_names, cautious) if out: break # Web art sources. remote_priority = config['fetchart']['remote_priority'].get(bool) if not local_only and (remote_priority or not out): for url in _source_urls(album): if maxwidth: url = ArtResizer.shared.proxy_url(maxwidth, url) candidate = _fetch_image(url) if candidate: out = candidate break if maxwidth and out: out = ArtResizer.shared.resize(maxwidth, out) return out # PLUGIN LOGIC ############################################################### def batch_fetch_art(lib, albums, force, maxwidth=None): """Fetch album art for each of the albums. This implements the manual fetchart CLI command. """ for album in albums: if album.artpath and not force: message = 'has album art' else: # In ordinary invocations, look for images on the # filesystem. When forcing, however, always go to the Web # sources. local_paths = None if force else [album.path] path = art_for_album(album, local_paths, maxwidth) if path: album.set_art(path, False) album.store() message = ui.colorize('green', 'found album art') else: message = ui.colorize('red', 'no art found') log.info(u'{0} - {1}: {2}'.format(album.albumartist, album.album, message)) class FetchArtPlugin(BeetsPlugin): def __init__(self): super(FetchArtPlugin, self).__init__() self.config.add({ 'auto': True, 'maxwidth': 0, 'remote_priority': False, 'cautious': False, 'google_search': False, 'cover_names': ['cover', 'front', 'art', 'album', 'folder'], }) # Holds paths to downloaded images between fetching them and # placing them in the filesystem. self.art_paths = {} self.maxwidth = self.config['maxwidth'].get(int) if self.config['auto']: # Enable two import hooks when fetching is enabled. self.import_stages = [self.fetch_art] self.register_listener('import_task_files', self.assign_art) # Asynchronous; after music is added to the library. def fetch_art(self, session, task): """Find art for the album being imported.""" if task.is_album: # Only fetch art for full albums. if task.choice_flag == importer.action.ASIS: # For as-is imports, don't search Web sources for art. local = True elif task.choice_flag == importer.action.APPLY: # Search everywhere for art. local = False else: # For any other choices (e.g., TRACKS), do nothing. return path = art_for_album(task.album, task.paths, self.maxwidth, local) if path: self.art_paths[task] = path # Synchronous; after music files are put in place. def assign_art(self, session, task): """Place the discovered art in the filesystem.""" if task in self.art_paths: path = self.art_paths.pop(task) album = task.album src_removed = (config['import']['delete'].get(bool) or config['import']['move'].get(bool)) album.set_art(path, not src_removed) album.store() if src_removed: task.prune(path) # Manual album art fetching. def commands(self): cmd = ui.Subcommand('fetchart', help='download album art') cmd.parser.add_option('-f', '--force', dest='force', action='store_true', default=False, help='re-download art when already present') def func(lib, opts, args): batch_fetch_art(lib, lib.albums(ui.decargs(args)), opts.force, self.maxwidth) cmd.func = func return [cmd] beets-1.3.8/beetsplug/fromfilename.py0000644000076500000240000001266312322627125020613 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Jan-Erik Dahlin # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """If the title is empty, try to extract track and title from the filename. """ from beets import plugins from beets.util import displayable_path import os import re # Filename field extraction patterns. PATTERNS = [ # "01 - Track 01" and "01": do nothing ur'^(\d+)\s*-\s*track\s*\d$', ur'^\d+$', # Useful patterns. ur'^(?P.+)-(?P.+)-(?P<tag>.*)$', ur'^(?P<track>\d+)\s*-(?P<artist>.+)-(?P<title>.+)-(?P<tag>.*)$', ur'^(?P<track>\d+)\s(?P<artist>.+)-(?P<title>.+)-(?P<tag>.*)$', ur'^(?P<artist>.+)-(?P<title>.+)$', ur'^(?P<track>\d+)\.\s*(?P<artist>.+)-(?P<title>.+)$', ur'^(?P<track>\d+)\s*-\s*(?P<artist>.+)-(?P<title>.+)$', ur'^(?P<track>\d+)\s*-(?P<artist>.+)-(?P<title>.+)$', ur'^(?P<track>\d+)\s(?P<artist>.+)-(?P<title>.+)$', ur'^(?P<title>.+)$', ur'^(?P<track>\d+)\.\s*(?P<title>.+)$', ur'^(?P<track>\d+)\s*-\s*(?P<title>.+)$', ur'^(?P<track>\d+)\s(?P<title>.+)$', ur'^(?P<title>.+) by (?P<artist>.+)$', ] # Titles considered "empty" and in need of replacement. BAD_TITLE_PATTERNS = [ ur'^$', ur'\d+?\s?-?\s*track\s*\d+', ] def equal(seq): """Determine whether a sequence holds identical elements. """ return len(set(seq)) <= 1 def equal_fields(matchdict, field): """Do all items in `matchdict`, whose values are dictionaries, have the same value for `field`? (If they do, the field is probably not the title.) """ return equal(m[field] for m in matchdict.values()) def all_matches(names, pattern): """If all the filenames in the item/filename mapping match the pattern, return a dictionary mapping the items to dictionaries giving the value for each named subpattern in the match. Otherwise, return None. """ matches = {} for item, name in names.items(): m = re.match(pattern, name, re.IGNORECASE) if m and m.groupdict(): # Only yield a match when the regex applies *and* has # capture groups. Otherwise, no information can be extracted # from the filename. matches[item] = m.groupdict() else: return None return matches def bad_title(title): """Determine whether a given title is "bad" (empty or otherwise meaningless) and in need of replacement. """ for pat in BAD_TITLE_PATTERNS: if re.match(pat, title, re.IGNORECASE): return True return False def apply_matches(d): """Given a mapping from items to field dicts, apply the fields to the objects. """ some_map = d.values()[0] keys = some_map.keys() # Only proceed if the "tag" field is equal across all filenames. if 'tag' in keys and not equal_fields(d, 'tag'): return # Given both an "artist" and "title" field, assume that one is # *actually* the artist, which must be uniform, and use the other # for the title. This, of course, won't work for VA albums. if 'artist' in keys: if equal_fields(d, 'artist'): artist = some_map['artist'] title_field = 'title' elif equal_fields(d, 'title'): artist = some_map['title'] title_field = 'artist' else: # Both vary. Abort. return for item in d: if not item.artist: item.artist = artist # No artist field: remaining field is the title. else: title_field = 'title' # Apply the title and track. for item in d: if bad_title(item.title): item.title = unicode(d[item][title_field]) if 'track' in d[item] and item.track == 0: item.track = int(d[item]['track']) # Plugin structure and hook into import process. class FromFilenamePlugin(plugins.BeetsPlugin): pass @FromFilenamePlugin.listen('import_task_start') def filename_task(task, session): """Examine each item in the task to see if we can extract a title from the filename. Try to match all filenames to a number of regexps, starting with the most complex patterns and successively trying less complex patterns. As soon as all filenames match the same regex we can make an educated guess of which part of the regex that contains the title. """ items = task.items if task.is_album else [task.item] # Look for suspicious (empty or meaningless) titles. missing_titles = sum(bad_title(i.title) for i in items) if missing_titles: # Get the base filenames (no path or extension). names = {} for item in items: path = displayable_path(item.path) name, _ = os.path.splitext(os.path.basename(path)) names[item] = name # Look for useful information in the filenames. for pattern in PATTERNS: d = all_matches(names, pattern) if d: apply_matches(d) �����������������������������������������������������������������������������beets-1.3.8/beetsplug/ftintitle.py������������������������������������������������������������������0000644�0000765�0000024�00000012223�12327077137�020150� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2013, Verrus, <github.com/Verrus/beets-plugin-featInTitle> # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Moves "featured" artists to the title from the artist field. """ from beets.plugins import BeetsPlugin from beets import ui from beets.util import displayable_path from beets import config import re def split_on_feat(artist): """Given an artist string, split the "main" artist from any artist on the right-hand side of a string like "feat". Return the main artist, which is always a string, and the featuring artist, which may be a string or None if none is present. """ parts = re.split( r'[fF]t\.|[fF]eaturing|[fF]eat\.|\b[wW]ith\b|&|vs\.|and', artist, 1, # Only split on the first "feat". ) parts = [s.strip() for s in parts] if len(parts) == 1: return parts[0], None else: return parts def contains_feat(title): """Determine whether the title contains a "featured" marker. """ return bool(re.search( r'[fF]t\.|[fF]eaturing|[fF]eat\.|\b[wW]ith\b|&', title, )) def update_metadata(item, feat_part, drop_feat): """Choose how to add new artists to the title and set the new metadata. Also, print out messages about any changes that are made. If `drop_feat` is set, then do not add the artist to the title; just remove it from the artist field. """ # In all cases, update the artist fields. ui.print_(u'artist: {0} -> {1}'.format(item.artist, item.albumartist)) item.artist = item.albumartist if item.artist_sort: # Just strip the featured artist from the sort name. item.artist_sort, _ = split_on_feat(item.artist_sort) # Only update the title if it does not already contain a featured # artist and if we do not drop featuring information. if not drop_feat and not contains_feat(item.title): new_title = u"{0} feat. {1}".format(item.title, feat_part) ui.print_(u'title: {0} -> {1}'.format(item.title, new_title)) item.title = new_title def ft_in_title(item, drop_feat): """Look for featured artists in the item's artist fields and move them to the title. """ artist = item.artist.strip() albumartist = item.albumartist.strip() # Check whether there is a featured artist on this track and the # artist field does not exactly match the album artist field. In # that case, we attempt to move the featured artist to the title. _, featured = split_on_feat(artist) if featured and albumartist != artist and albumartist: ui.print_(displayable_path(item.path)) feat_part = None # Look for the album artist in the artist field. If it's not # present, give up. albumartist_split = artist.split(albumartist) if len(albumartist_split) <= 1: ui.print_('album artist not present in artist') # If the last element of the split (the right-hand side of the # album artist) is nonempty, then it probably contains the # featured artist. elif albumartist_split[-1] != '': # Extract the featured artist from the right-hand side. _, feat_part = split_on_feat(albumartist_split[-1]) # Otherwise, if there's nothing on the right-hand side, look for a # featuring artist on the left-hand side. else: lhs, rhs = split_on_feat(albumartist_split[0]) if rhs: feat_part = lhs # If we have a featuring artist, move it to the title. if feat_part: update_metadata(item, feat_part, drop_feat) else: ui.print_(u'no featuring artists found') ui.print_() class FtInTitlePlugin(BeetsPlugin): def __init__(self): super(FtInTitlePlugin, self).__init__() self.config.add({ 'drop': False }) self._command = ui.Subcommand( 'ftintitle', help='move featured artists to the title field') self._command.parser.add_option( '-d', '--drop', dest='drop', action='store_true', default=False, help='drop featuring from artists and ignore title update') def commands(self): def func(lib, opts, args): self.config.set_args(opts) drop_feat = self.config['drop'].get(bool) write = config['import']['write'].get(bool) for item in lib.items(ui.decargs(args)): ft_in_title(item, drop_feat) item.store() if write: item.try_write() self._command.func = func return [self._command] �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/fuzzy.py����������������������������������������������������������������������0000644�0000765�0000024�00000002723�12313347401�017326� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2013, Philippe Mongeau. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Provides a fuzzy matching query. """ from beets.plugins import BeetsPlugin from beets.dbcore.query import StringFieldQuery import beets import difflib class FuzzyQuery(StringFieldQuery): @classmethod def string_match(self, pattern, val): # smartcase if pattern.islower(): val = val.lower() queryMatcher = difflib.SequenceMatcher(None, pattern, val) threshold = beets.config['fuzzy']['threshold'].as_number() return queryMatcher.quick_ratio() >= threshold class FuzzyPlugin(BeetsPlugin): def __init__(self): super(FuzzyPlugin, self).__init__() self.config.add({ 'prefix': '~', 'threshold': 0.7, }) def queries(self): prefix = beets.config['fuzzy']['prefix'].get(basestring) return {prefix: FuzzyQuery} ���������������������������������������������beets-1.3.8/beetsplug/ihate.py����������������������������������������������������������������������0000644�0000765�0000024�00000006042�12405710020�017220� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2014, Blemjhoo Tezoulbr <baobab@heresiarch.info>. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Warns you about things you hate (or even blocks import).""" import logging from beets.plugins import BeetsPlugin from beets.importer import action from beets.library import parse_query_string from beets.library import Item from beets.library import Album __author__ = 'baobab@heresiarch.info' __version__ = '2.0' def summary(task): """Given an ImportTask, produce a short string identifying the object. """ if task.is_album: return u'{0} - {1}'.format(task.cur_artist, task.cur_album) else: return u'{0} - {1}'.format(task.item.artist, task.item.title) class IHatePlugin(BeetsPlugin): _log = logging.getLogger('beets') def __init__(self): super(IHatePlugin, self).__init__() self.register_listener('import_task_choice', self.import_task_choice_event) self.config.add({ 'warn': [], 'skip': [], }) @classmethod def do_i_hate_this(cls, task, action_patterns): """Process group of patterns (warn or skip) and returns True if task is hated and not whitelisted. """ if action_patterns: for query_string in action_patterns: query, _ = parse_query_string( query_string, Album if task.is_album else Item, ) if any(query.match(item) for item in task.imported_items()): return True return False def import_task_choice_event(self, session, task): skip_queries = self.config['skip'].as_str_seq() warn_queries = self.config['warn'].as_str_seq() if task.choice_flag == action.APPLY: if skip_queries or warn_queries: self._log.debug(u'[ihate] processing your hate') if self.do_i_hate_this(task, skip_queries): task.choice_flag = action.SKIP self._log.info(u'[ihate] skipped: {0}' .format(summary(task))) return if self.do_i_hate_this(task, warn_queries): self._log.info(u'[ihate] you maybe hate this: {0}' .format(summary(task))) else: self._log.debug(u'[ihate] nothing to do') else: self._log.debug(u'[ihate] user made a decision, nothing to do') ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/importadded.py����������������������������������������������������������������0000644�0000765�0000024�00000005403�12405373453�020441� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Populate an items `added` and `mtime` field by using the file modification time (mtime) of the item's source file before import. """ from __future__ import unicode_literals, absolute_import, print_function import logging import os from beets import config from beets import util from beets.plugins import BeetsPlugin log = logging.getLogger('beets') class ImportAddedPlugin(BeetsPlugin): def __init__(self): super(ImportAddedPlugin, self).__init__() self.config.add({ 'preserve_mtimes': False, }) @ImportAddedPlugin.listen('import_task_start') def check_config(task, session): config['importadded']['preserve_mtimes'].get(bool) def write_file_mtime(path, mtime): """Write the given mtime to the destination path. """ stat = os.stat(util.syspath(path)) os.utime(util.syspath(path), (stat.st_atime, mtime)) # key: item path in the library # value: the file mtime of the file the item was imported from item_mtime = dict() def write_item_mtime(item, mtime): """Write the given mtime to an item's `mtime` field and to the mtime of the item's file. """ if mtime is None: log.warn(u"No mtime to be preserved for item {0}" .format(util.displayable_path(item.path))) return # The file's mtime on disk must be in sync with the item's mtime write_file_mtime(util.syspath(item.path), mtime) item.mtime = mtime @ImportAddedPlugin.listen('before_item_moved') @ImportAddedPlugin.listen('item_copied') def record_import_mtime(item, source, destination): """Record the file mtime of an item's path before import. """ if (source == destination): # Re-import of an existing library item? return mtime = os.stat(util.syspath(source)).st_mtime item_mtime[destination] = mtime log.debug(u"Recorded mtime {0} for item '{1}' imported from '{2}'".format( mtime, util.displayable_path(destination), util.displayable_path(source))) @ImportAddedPlugin.listen('album_imported') def update_album_times(lib, album): album_mtimes = [] for item in album.items(): mtime = item_mtime[item.path] if mtime is not None: album_mtimes.append(mtime) if config['importadded']['preserve_mtimes'].get(bool): write_item_mtime(item, mtime) item.store() del item_mtime[item.path] album.added = min(album_mtimes) album.store() @ImportAddedPlugin.listen('item_imported') def update_item_times(lib, item): mtime = item_mtime[item.path] if mtime is not None: item.added = mtime if config['importadded']['preserve_mtimes'].get(bool): write_item_mtime(item, mtime) item.store() del item_mtime[item.path] �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/importfeeds.py����������������������������������������������������������������0000644�0000765�0000024�00000011520�12405372257�020464� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2013, Fabrice Laporte. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Write paths of imported files in various formats to ease later import in a music player. Also allow printing the new file locations to stdout in case one wants to manually add music to a player by its path. """ import datetime import os import re import logging from beets.plugins import BeetsPlugin from beets.util import normpath, syspath, bytestring_path from beets import config M3U_DEFAULT_NAME = 'imported.m3u' log = logging.getLogger('beets') class ImportFeedsPlugin(BeetsPlugin): def __init__(self): super(ImportFeedsPlugin, self).__init__() self.config.add({ 'formats': [], 'm3u_name': u'imported.m3u', 'dir': None, 'relative_to': None, 'absolute_path': False, }) feeds_dir = self.config['dir'].get() if feeds_dir: feeds_dir = os.path.expanduser(bytestring_path(feeds_dir)) self.config['dir'] = feeds_dir if not os.path.exists(syspath(feeds_dir)): os.makedirs(syspath(feeds_dir)) relative_to = self.config['relative_to'].get() if relative_to: self.config['relative_to'] = normpath(relative_to) else: self.config['relative_to'] = feeds_dir def _get_feeds_dir(lib): """Given a Library object, return the path to the feeds directory to be used (either in the library directory or an explicitly configured path). Ensures that the directory exists. """ # Inside library directory. dirpath = lib.directory # Ensure directory exists. if not os.path.exists(syspath(dirpath)): os.makedirs(syspath(dirpath)) return dirpath def _build_m3u_filename(basename): """Builds unique m3u filename by appending given basename to current date.""" basename = re.sub(r"[\s,/\\'\"]", '_', basename) date = datetime.datetime.now().strftime("%Y%m%d_%Hh%M") path = normpath(os.path.join( config['importfeeds']['dir'].as_filename(), date + '_' + basename + '.m3u' )) return path def _write_m3u(m3u_path, items_paths): """Append relative paths to items into m3u file. """ with open(syspath(m3u_path), 'a') as f: for path in items_paths: f.write(path + '\n') def _record_items(lib, basename, items): """Records relative paths to the given items for each feed format """ feedsdir = bytestring_path(config['importfeeds']['dir'].as_filename()) formats = config['importfeeds']['formats'].as_str_seq() relative_to = config['importfeeds']['relative_to'].get() \ or config['importfeeds']['dir'].as_filename() relative_to = bytestring_path(relative_to) paths = [] for item in items: if config['importfeeds']['absolute_path']: paths.append(item.path) else: try: relpath = os.path.relpath(item.path, relative_to) except ValueError: # On Windows, it is sometimes not possible to construct a # relative path (if the files are on different disks). relpath = item.path paths.append(relpath) if 'm3u' in formats: basename = bytestring_path( config['importfeeds']['m3u_name'].get(unicode) ) m3u_path = os.path.join(feedsdir, basename) _write_m3u(m3u_path, paths) if 'm3u_multi' in formats: m3u_path = _build_m3u_filename(basename) _write_m3u(m3u_path, paths) if 'link' in formats: for path in paths: dest = os.path.join(feedsdir, os.path.basename(path)) if not os.path.exists(syspath(dest)): os.symlink(syspath(path), syspath(dest)) if 'echo' in formats: log.info("Location of imported music:") for path in paths: log.info(" " + path) @ImportFeedsPlugin.listen('library_opened') def library_opened(lib): if config['importfeeds']['dir'].get() is None: config['importfeeds']['dir'] = _get_feeds_dir(lib) @ImportFeedsPlugin.listen('album_imported') def album_imported(lib, album): _record_items(lib, album.album, album.items()) @ImportFeedsPlugin.listen('item_imported') def item_imported(lib, item): _record_items(lib, item.title, [item]) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/info.py�����������������������������������������������������������������������0000644�0000765�0000024�00000010520�12405373453�017074� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Shows file metadata. """ import os import logging from beets.plugins import BeetsPlugin from beets import ui from beets import mediafile from beets.util import displayable_path, normpath, syspath log = logging.getLogger('beets') def run(lib, opts, args): """Print tag info or library data for each file referenced by args. Main entry point for the `beet info ARGS...` command. If an argument is a path pointing to an existing file, then the tags of that file are printed. All other arguments are considered queries, and for each item matching all those queries the tags from the file are printed. If `opts.summarize` is true, the function merges all tags into one dictionary and only prints that. If two files have different values for the same tag, the value is set to '[various]' """ if opts.library: data_collector = library_data else: data_collector = tag_data first = True summary = {} for data_emitter in data_collector(lib, ui.decargs(args)): try: data = data_emitter() except mediafile.UnreadableFileError as ex: log.error(u'cannot read file: {0}'.format(ex.message)) continue if opts.summarize: update_summary(summary, data) else: if not first: ui.print_() print_data(data) first = False if opts.summarize: print_data(summary) def tag_data(lib, args): query = [] for arg in args: path = normpath(arg) if os.path.isfile(syspath(path)): yield tag_data_emitter(path) else: query.append(arg) if query: for item in lib.items(query): yield tag_data_emitter(item.path) def tag_data_emitter(path): def emitter(): fields = list(mediafile.MediaFile.readable_fields()) fields.remove('images') mf = mediafile.MediaFile(syspath(path)) tags = {} for field in fields: tags[field] = getattr(mf, field) tags['art'] = mf.art is not None tags['path'] = displayable_path(path) return tags return emitter def library_data(lib, args): for item in lib.items(args): yield library_data_emitter(item) def library_data_emitter(item): def emitter(): data = dict(item.formatted()) data['path'] = displayable_path(item.path) return data return emitter def update_summary(summary, tags): for key, value in tags.iteritems(): if key not in summary: summary[key] = value elif summary[key] != value: summary[key] = '[various]' return summary def print_data(data): path = data.pop('path') formatted = {} for key, value in data.iteritems(): if isinstance(value, list): formatted[key] = u'; '.join(value) if value is not None: formatted[key] = value maxwidth = max(len(key) for key in formatted) lineformat = u'{{0:>{0}}}: {{1}}'.format(maxwidth) if path: ui.print_(displayable_path(path)) for field in sorted(formatted): value = formatted[field] if isinstance(value, list): value = u'; '.join(value) ui.print_(lineformat.format(field, value)) class InfoPlugin(BeetsPlugin): def commands(self): cmd = ui.Subcommand('info', help='show file metadata') cmd.func = run cmd.parser.add_option('-l', '--library', action='store_true', help='show library fields instead of tags') cmd.parser.add_option('-s', '--summarize', action='store_true', help='summarize the tags of all files') return [cmd] ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/inline.py���������������������������������������������������������������������0000644�0000765�0000024�00000010005�12405373453�017415� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Allows inline path template customization code in the config file. """ import logging import traceback import itertools from beets.plugins import BeetsPlugin from beets import config log = logging.getLogger('beets') FUNC_NAME = u'__INLINE_FUNC__' class InlineError(Exception): """Raised when a runtime error occurs in an inline expression. """ def __init__(self, code, exc): super(InlineError, self).__init__( (u"error in inline path field code:\n" u"%s\n%s: %s") % (code, type(exc).__name__, unicode(exc)) ) def _compile_func(body): """Given Python code for a function body, return a compiled callable that invokes that code. """ body = u'def {0}():\n {1}'.format( FUNC_NAME, body.replace('\n', '\n ') ) code = compile(body, 'inline', 'exec') env = {} eval(code, env) return env[FUNC_NAME] def compile_inline(python_code, album): """Given a Python expression or function body, compile it as a path field function. The returned function takes a single argument, an Item, and returns a Unicode string. If the expression cannot be compiled, then an error is logged and this function returns None. """ # First, try compiling as a single function. try: code = compile(u'({0})'.format(python_code), 'inline', 'eval') except SyntaxError: # Fall back to a function body. try: func = _compile_func(python_code) except SyntaxError: log.error(u'syntax error in inline field definition:\n{0}', traceback.format_exc()) return else: is_expr = False else: is_expr = True def _dict_for(obj): out = dict(obj) if album: out['items'] = list(obj.items()) return out if is_expr: # For expressions, just evaluate and return the result. def _expr_func(obj): values = _dict_for(obj) try: return eval(code, values) except Exception as exc: raise InlineError(python_code, exc) return _expr_func else: # For function bodies, invoke the function with values as global # variables. def _func_func(obj): func.__globals__.update(_dict_for(obj)) try: return func() except Exception as exc: raise InlineError(python_code, exc) return _func_func class InlinePlugin(BeetsPlugin): def __init__(self): super(InlinePlugin, self).__init__() config.add({ 'pathfields': {}, # Legacy name. 'item_fields': {}, 'album_fields': {}, }) # Item fields. for key, view in itertools.chain(config['item_fields'].items(), config['pathfields'].items()): log.debug(u'inline: adding item field {0}'.format(key)) func = compile_inline(view.get(unicode), False) if func is not None: self.template_fields[key] = func # Album fields. for key, view in config['album_fields'].items(): log.debug(u'inline: adding album field {0}'.format(key)) func = compile_inline(view.get(unicode), True) if func is not None: self.album_template_fields[key] = func ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/keyfinder.py������������������������������������������������������������������0000644�0000765�0000024�00000004442�12404675421�020126� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2014, Thomas Scholtes. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Uses the `KeyFinder` program to add the `initial_key` field. """ import logging import subprocess from beets import ui from beets import util from beets.plugins import BeetsPlugin log = logging.getLogger('beets') class KeyFinderPlugin(BeetsPlugin): def __init__(self): super(KeyFinderPlugin, self).__init__() self.config.add({ u'bin': u'KeyFinder', u'auto': True, u'overwrite': False, }) self.config['auto'].get(bool) self.import_stages = [self.imported] def commands(self): cmd = ui.Subcommand('keyfinder', help='detect and add initial key from audio') cmd.func = self.command return [cmd] def command(self, lib, opts, args): self.find_key(lib.items(ui.decargs(args))) def imported(self, session, task): if self.config['auto'].get(bool): self.find_key(task.items) def find_key(self, items): overwrite = self.config['overwrite'].get(bool) bin = util.bytestring_path(self.config['bin'].get(unicode)) for item in items: if item['initial_key'] and not overwrite: continue try: key = util.command_output([bin, '-f', item.path]) except (subprocess.CalledProcessError, OSError) as exc: log.error(u'KeyFinder execution failed: {0}'.format(exc)) continue item['initial_key'] = key log.debug(u'added computed initial key {0} for {1}' .format(key, util.displayable_path(item.path))) item.try_write() item.store() ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/lastgenre/��������������������������������������������������������������������0000755�0000765�0000024�00000000000�12406440351�017546� 5����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/lastgenre/__init__.py���������������������������������������������������������0000644�0000765�0000024�00000032743�12405373453�021677� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Gets genres for imported music based on Last.fm tags. Uses a provided whitelist file to determine which tags are valid genres. The included (default) genre list was originally produced by scraping Wikipedia and has been edited to remove some questionable entries. The scraper script used is available here: https://gist.github.com/1241307 """ import logging import pylast import os import yaml from beets import plugins from beets import ui from beets.util import normpath, plurality from beets import config from beets import library log = logging.getLogger('beets') LASTFM = pylast.LastFMNetwork(api_key=plugins.LASTFM_KEY) PYLAST_EXCEPTIONS = ( pylast.WSError, pylast.MalformedResponseError, pylast.NetworkError, ) def deduplicate(seq): """Remove duplicates from sequence wile preserving order. """ seen = set() return [x for x in seq if x not in seen and not seen.add(x)] # Core genre identification routine. def _tags_for(obj, min_weight=None): """Given a pylast entity (album or track), return a list of tag names for that entity. Return an empty list if the entity is not found or another error occurs. If `min_weight` is specified, tags are filtered by weight. """ try: # Work around an inconsistency in pylast where # Album.get_top_tags() does not return TopItem instances. # https://code.google.com/p/pylast/issues/detail?id=85 if isinstance(obj, pylast.Album): res = super(pylast.Album, obj).get_top_tags() else: res = obj.get_top_tags() except PYLAST_EXCEPTIONS as exc: log.debug(u'last.fm error: {0}'.format(exc)) return [] # Filter by weight (optionally). if min_weight: res = [el for el in res if (el.weight or 0) >= min_weight] # Get strings from tags. res = [el.item.get_name().lower() for el in res] return res # Canonicalization tree processing. def flatten_tree(elem, path, branches): """Flatten nested lists/dictionaries into lists of strings (branches). """ if not path: path = [] if isinstance(elem, dict): for (k, v) in elem.items(): flatten_tree(v, path + [k], branches) elif isinstance(elem, list): for sub in elem: flatten_tree(sub, path, branches) else: branches.append(path + [unicode(elem)]) def find_parents(candidate, branches): """Find parents genre of a given genre, ordered from the closest to the further parent. """ for branch in branches: try: idx = branch.index(candidate.lower()) return list(reversed(branch[:idx + 1])) except ValueError: continue return [candidate] # Main plugin logic. WHITELIST = os.path.join(os.path.dirname(__file__), 'genres.txt') C14N_TREE = os.path.join(os.path.dirname(__file__), 'genres-tree.yaml') class LastGenrePlugin(plugins.BeetsPlugin): def __init__(self): super(LastGenrePlugin, self).__init__() self.config.add({ 'whitelist': True, 'min_weight': 10, 'count': 1, 'fallback': None, 'canonical': False, 'source': 'album', 'force': True, 'auto': True, 'separator': u', ', }) self.setup() def setup(self): """Setup plugin from config options """ if self.config['auto']: self.import_stages = [self.imported] self._genre_cache = {} # Read the whitelist file if enabled. self.whitelist = set() wl_filename = self.config['whitelist'].get() if wl_filename in (True, ''): # Indicates the default whitelist. wl_filename = WHITELIST if wl_filename: wl_filename = normpath(wl_filename) with open(wl_filename, 'r') as f: for line in f: line = line.decode('utf8').strip().lower() if line and not line.startswith(u'#'): self.whitelist.add(line) # Read the genres tree for canonicalization if enabled. self.c14n_branches = [] c14n_filename = self.config['canonical'].get() if c14n_filename in (True, ''): # Default tree. c14n_filename = C14N_TREE if c14n_filename: c14n_filename = normpath(c14n_filename) genres_tree = yaml.load(open(c14n_filename, 'r')) flatten_tree(genres_tree, [], self.c14n_branches) @property def sources(self): """A tuple of allowed genre sources. May contain 'track', 'album', or 'artist.' """ source = self.config['source'].as_choice(('track', 'album', 'artist')) if source == 'track': return 'track', 'album', 'artist' elif source == 'album': return 'album', 'artist' elif source == 'artist': return 'artist', def _resolve_genres(self, tags): """Given a list of strings, return a genre by joining them into a single string and (optionally) canonicalizing each. """ if not tags: return None count = self.config['count'].get(int) if self.c14n_branches: # Extend the list to consider tags parents in the c14n tree tags_all = [] for tag in tags: # Add parents that are in the whitelist, or add the oldest # ancestor if no whitelist if self.whitelist: parents = [x for x in find_parents(tag, self.c14n_branches) if self._is_allowed(x)] else: parents = [find_parents(tag, self.c14n_branches)[-1]] tags_all += parents if len(tags_all) >= count: break tags = tags_all tags = deduplicate(tags) # c14n only adds allowed genres but we may have had forbidden genres in # the original tags list tags = [x.title() for x in tags if self._is_allowed(x)] return self.config['separator'].get(unicode).join( tags[:self.config['count'].get(int)] ) def fetch_genre(self, lastfm_obj): """Return the genre for a pylast entity or None if no suitable genre can be found. Ex. 'Electronic, House, Dance' """ min_weight = self.config['min_weight'].get(int) return self._resolve_genres(_tags_for(lastfm_obj, min_weight)) def _is_allowed(self, genre): """Determine whether the genre is present in the whitelist, returning a boolean. """ if genre is None: return False if not self.whitelist or genre in self.whitelist: return True return False # Cached entity lookups. def _cached_lookup(self, entity, method, *args): """Get a genre based on the named entity using the callable `method` whose arguments are given in the sequence `args`. The genre lookup is cached based on the entity name and the arguments. """ # Shortcut if we're missing metadata. if any(not s for s in args): return None key = u'{0}.{1}'.format(entity, u'-'.join(unicode(a) for a in args)) if key in self._genre_cache: return self._genre_cache[key] else: genre = self.fetch_genre(method(*args)) self._genre_cache[key] = genre return genre def fetch_album_genre(self, obj): """Return the album genre for this Item or Album. """ return self._cached_lookup(u'album', LASTFM.get_album, obj.albumartist, obj.album) def fetch_album_artist_genre(self, obj): """Return the album artist genre for this Item or Album. """ return self._cached_lookup(u'artist', LASTFM.get_artist, obj.albumartist) def fetch_artist_genre(self, item): """Returns the track artist genre for this Item. """ return self._cached_lookup(u'artist', LASTFM.get_artist, item.artist) def fetch_track_genre(self, obj): """Returns the track genre for this Item. """ return self._cached_lookup(u'track', LASTFM.get_track, obj.artist, obj.title) def _get_genre(self, obj): """Get the genre string for an Album or Item object based on self.sources. Return a `(genre, source)` pair. The prioritization order is: - track (for Items only) - album - artist - original - fallback - None """ # Shortcut to existing genre if not forcing. if not self.config['force'] and self._is_allowed(obj.genre): return obj.genre, 'keep' # Track genre (for Items only). if isinstance(obj, library.Item): if 'track' in self.sources: result = self.fetch_track_genre(obj) if result: return result, 'track' # Album genre. if 'album' in self.sources: result = self.fetch_album_genre(obj) if result: return result, 'album' # Artist (or album artist) genre. if 'artist' in self.sources: result = None if isinstance(obj, library.Item): result = self.fetch_artist_genre(obj) elif obj.albumartist != 'Various Artists': result = self.fetch_album_artist_genre(obj) else: # For "Various Artists", pick the most popular track genre. item_genres = [] for item in obj.items(): item_genre = None if 'track' in self.sources: item_genre = self.fetch_track_genre(item) if not item_genre: item_genre = self.fetch_artist_genre(item) if item_genre: item_genres.append(item_genre) if item_genres: result, _ = plurality(item_genres) if result: return result, 'artist' # Filter the existing genre. if obj.genre: result = self._resolve_genres([obj.genre]) if result: return result, 'original' # Fallback string. fallback = self.config['fallback'].get() if fallback: return fallback, 'fallback' return None, None def commands(self): lastgenre_cmd = ui.Subcommand('lastgenre', help='fetch genres') lastgenre_cmd.parser.add_option( '-f', '--force', dest='force', action='store_true', default=False, help='re-download genre when already present' ) lastgenre_cmd.parser.add_option( '-s', '--source', dest='source', type='string', help='genre source: artist, album, or track' ) def lastgenre_func(lib, opts, args): write = config['import']['write'].get(bool) self.config.set_args(opts) for album in lib.albums(ui.decargs(args)): album.genre, src = self._get_genre(album) log.info(u'genre for album {0} - {1} ({2}): {3}'.format( album.albumartist, album.album, src, album.genre )) album.store() for item in album.items(): # If we're using track-level sources, also look up each # track on the album. if 'track' in self.sources: item.genre, src = self._get_genre(item) item.store() log.info(u'genre for track {0} - {1} ({2}): {3}' .format(item.artist, item.title, src, item.genre)) if write: item.try_write() lastgenre_cmd.func = lastgenre_func return [lastgenre_cmd] def imported(self, session, task): """Event hook called when an import task finishes.""" if task.is_album: album = task.album album.genre, src = self._get_genre(album) log.debug(u'added last.fm album genre ({0}): {1}'.format( src, album.genre)) album.store() if 'track' in self.sources: for item in album.items(): item.genre, src = self._get_genre(item) log.debug(u'added last.fm item genre ({0}): {1}'.format( src, item.genre)) item.store() else: item = task.item item.genre, src = self._get_genre(item) log.debug(u'added last.fm item genre ({0}): {1}'.format( src, item.genre)) item.store() �����������������������������beets-1.3.8/beetsplug/lastgenre/genres-tree.yaml����������������������������������������������������0000644�0000765�0000024�00000034450�12404675421�022666� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������- african: - african heavy metal - african hip hop - afrobeat - apala - benga - bikutsi - bongo flava - cape jazz - chimurenga - coupé-décalé - fuji music - genge - highlife - hiplife - isicathamiya - jit - jùjú - kapuka - kizomba - kuduro - kwaito - kwela - makossa - maloya - marrabenta - mbalax - mbaqanga - mbube - morna - museve - palm-wine - raï - sakara - sega - seggae - semba - soukous - taarab - zouglou - asian: - east asian: - anison - c-pop - cantopop - enka - hong kong english pop - j-pop - k-pop - kayÅkyoku - korean pop - mandopop - onkyokei - taiwanese pop - fann at-tanbura - fijiri - khaliji - liwa - sawt - south and southeast asian: - baila - bhangra - bhojpuri - dangdut - filmi - indian pop - lavani - luk thung: - luk krung - manila sound - morlam - pinoy pop - pop sunda - ragini - thai pop - avant-garde: - experimental music - lo-fi - musique concrète - blues: - african blues - blues rock - blues shouter - british blues - canadian blues - chicago blues - classic female blues - contemporary r&b - country blues - delta blues - detroit blues - electric blues - gospel blues - hill country blues - hokum blues - jazz blues - jump blues - kansas city blues - louisiana blues - memphis blues - piano blues - piedmont blues - punk blues - soul blues - st. louis blues - swamp blues - texas blues - west coast blues - caribbean and latin american: - bachata - baithak gana - bolero - brazilian: - axé - bossa nova - brazilian rock - brega - choro - forró - frevo - funk carioca - lambada - maracatu - música popular brasileira - música sertaneja - pagode - samba - samba rock - tecnobrega - tropicalia - zouk-lambada - calypso - chutney - chutney soca - compas - dance hall - mambo - merengue - méringue - other latin: - chicha - criolla - cumbia - huayno - mariachi - ranchera - tejano - punta - punta rock - rasin - reggae - reggaeton - salsa - soca - son - timba - twoubadou - zouk - comedy: - comedy music - comedy rock - parody music - country: - alternative country: - cowpunk - americana - australian country music - bakersfield sound - bluegrass: - progressive bluegrass - reactionary bluegrass - blues country - cajun: - cajun fiddle tunes - christian country music - classic country - close harmony - country pop - country rap - country rock - country soul - cowboy/western music - dansband music - franco-country - gulf and western - hellbilly music - hokum - honky tonk - instrumental country - lubbock sound - nashville sound - neotraditional country - outlaw country - progressive country - psychobilly/punkabilly - red dirt - rockabilly - sertanejo - texas country - traditional country music - truck-driving country - western swing - zydeco - easy listening: - background music - beautiful music - elevator music - furniture music - lounge music - middle of the road - new-age music - electronic: - ambient: - ambient dub - ambient house - ambient techno - dark ambient - drone music - illbient - isolationism - lowercase - asian underground - breakbeat: - 4-beat - acid breaks - baltimore club - big beat - breakbeat hardcore - broken beat - florida breaks - nu skool breaks - chiptune: - bitpop - game boy music - nintendocore - video game music - yorkshire bleeps and bass - disco: - cosmic disco - disco polo - euro disco - italo disco - nu-disco - space disco - downtempo: - acid jazz - balearic beat - chill out - dub music - dubtronica - ethnic electronica - moombahton - nu jazz - trip hop - drum and bass: - darkcore - darkstep - drumfunk - drumstep - hardstep - intelligent drum and bass - jump-up - liquid funk - neurofunk - oldschool jungle: - darkside jungle - ragga-jungle - raggacore - sambass - techstep - electro: - crunk - electro backbeat - electro-grime - electropop - electroacoustic: - acousmatic music - computer music - electroacoustic improvisation - field recording - live coding - live electronics - soundscape composition - tape music - electronic rock: - alternative dance: - baggy - madchester - dance-punk - dance-rock - dark wave - electroclash - electronicore - electropunk - ethereal wave - indietronica - new rave - space rock - synthpop - synthpunk - electronica: - berlin school - chillwave - electronic art music - electronic dance music - folktronica - freestyle music - glitch - idm - laptronica - skweee - sound art - synthcore - eurodance: - bubblegum dance - italo dance - turbofolk - hardcore: - bouncy house - bouncy techno - breakcore - digital hardcore - doomcore - dubstyle - gabber - happy hardcore - hardstyle - jumpstyle - makina - speedcore - terrorcore - uk hardcore - hi-nrg: - eurobeat - hard nrg - new beat - house: - acid house - chicago house - deep house - diva house - dutch house - electro house - freestyle house - french house - funky house - ghetto house - hardbag - hip house - italo house - latin house - minimal house - progressive house - rave music - swing house - tech house - tribal house - uk hard house - us garage - vocal house - industrial: - aggrotech - coldwave - cybergrind - dark electro - death industrial - electro-industrial - electronic body music: - futurepop - industrial metal: - neue deutsche härte - industrial rock - noise: - japanoise - power electronics - power noise - witch house - post-disco: - boogie - dance-pop - progressive: - progressive house/trance: - disco house - dream house - space house - progressive breaks - progressive drum & bass - progressive techno - techno: - acid techno - detroit techno - free tekno - ghettotech - minimal - nortec - schranz - techno-dnb - technopop - tecno brega - toytown techno - trance: - acid trance - classic trance - dream trance - goa trance: - dark psytrance - full on - psybreaks - psyprog - suomisaundi - hard trance - tech trance - uplifting trance: - orchestral uplifting - vocal trance - uk garage: - 2-step - 4x4 - bassline - breakstep - dubstep - funky - grime - speed garage - trap - folk: - american folk revival - anti-folk - british folk revival - celtic music - contemporary folk - filk music - freak folk - indie folk - industrial folk - neofolk - progressive folk - psychedelic folk - sung poetry - techno-folk - hip hop: - alternative hip hop - avant-garde hip hop - chap hop - christian hip hop - conscious hip hop - country-rap - crunkcore - cumbia rap - east coast hip hop: - brick city club - hardcore hip hop - mafioso rap - new jersey hip hop - electro music - freestyle rap - g-funk - gangsta rap - golden age hip hop - hip hop soul - hip pop - hyphy - industrial hip hop - instrumental hip hop - jazz rap - low bap - lyrical hip hop - merenrap - midwest hip hop: - chicago hip hop: - detroit hip hop: - horrorcore - st. louis hip hop - twin cities hip hop - motswako - nerdcore - new jack swing - new school hip hop - old school hip hop - political hip hop - ragga - rap opera - rap rock: - rap metal - rapcore - reggae español/spanish reggae - songo-salsa - southern hip hop: - atlanta hip hop: - snap music - bounce music - houston hip hop: - chopped and screwed - miami bass - turntablism - underground hip hop - urban pasifika - west coast hip hop: - chicano rap - jerkin' - jazz: - asian american jazz - avant-garde jazz - bebop - boogie-woogie - british dance band - chamber jazz - continental jazz - cool jazz - crossover jazz - cubop - dixieland - ethno jazz - european free jazz - free funk - free improvisation - free jazz - gypsy jazz - hard bop - jazz fusion - jazz rock - jazz-funk - kansas city jazz - latin jazz - livetronica - m-base - mainstream jazz - modal jazz - neo-bop jazz - neo-swing - novelty ragtime - orchestral jazz - post-bop - punk jazz - ragtime - shibuya-kei - ska jazz - smooth jazz - soul jazz - straight-ahead jazz - stride jazz - swing - third stream - trad jazz - vocal jazz - west coast gypsy jazz - west coast jazz - other: - worldbeat - pop: - adult contemporary - arab pop - baroque pop - bubblegum pop - chanson - christian pop - classical crossover - europop: - austropop - balkan pop - french pop - latin pop - laïkó - nederpop - russian pop - iranian pop - jangle pop - latin ballad - levenslied - louisiana swamp pop - mexican pop - motorpop - new romanticism - pop rap - popera - psychedelic pop - schlager - soft rock - sophisti-pop - space age pop - sunshine pop - surf pop - teen pop - traditional pop music - turkish pop - vispop - wonky pop - rhythm and blues: - funk: - deep funk - go-go - p-funk - soul: - blue-eyed soul - neo soul - northern soul - rock: - alternative rock: - britpop: - post-britpop - dream pop - grunge: - post-grunge - indie pop: - dunedin sound - twee pop - indie rock - noise pop - nu metal - post-punk revival - post-rock: - post-metal - sadcore - shoegaze - slowcore - art rock - beat music - chinese rock - christian rock - dark cabaret - desert rock - experimental rock - folk rock - garage rock - glam rock - hard rock - heavy metal: - alternative metal: - black metal: - viking metal - christian metal - death metal: - goregrind - melodic death metal - technical death metal - doom metal - drone metal - folk metal: - celtic metal - medieval metal - funk metal - glam metal - gothic metal - metalcore: - deathcore - mathcore: - djent - power metal - progressive metal - sludge metal - speed metal - stoner rock - symphonic metal - thrash metal: - crossover thrash - groove metal - math rock - new wave: - world fusion - paisley underground - pop rock - post-punk: - gothic rock - no wave - noise rock - power pop - progressive rock: - canterbury scene - krautrock - new prog - rock in opposition - psychedelic rock: - acid rock - freakbeat - neo-psychedelia - raga rock - punk rock: - anarcho punk: - crust punk: - d-beat - art punk - christian punk - deathrock - folk punk: - celtic punk - gypsy punk - garage punk - grindcore: - crustgrind - noisegrind - hardcore punk: - post-hardcore: - emo: - screamo - powerviolence - street punk - thrashcore - horror punk - pop punk - psychobilly - riot grrrl - ska punk: - ska-core - skate punk - rock and roll - southern rock - sufi rock - surf rock - visual kei: - nagoya kei - ska: - 2 tone - dancehall - dub - lovers rock - ragga jungle - polish reggae - reggae fusion - rocksteady ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/lastgenre/genres.txt����������������������������������������������������������0000644�0000765�0000024�00000041135�12404675421�021604� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������2 tone 2-step garage 4-beat 4x4 garage 8-bit acapella acid acid breaks acid house acid jazz acid rock acoustic music acousticana adult contemporary music african popular music african rumba afrobeat aleatoric music alternative country alternative dance alternative hip hop alternative metal alternative rock ambient ambient house ambient music americana anarcho punk anti-folk apala ape haters arab pop arabesque arabic pop argentine rock ars antiqua ars nova art punk art rock ashiq asian american jazz australian country music australian hip hop australian pub rock austropop avant-garde avant-garde jazz avant-garde metal avant-garde music axé bac-bal bachata baggy baila baile funk baisha xiyue baithak gana baião bajourou bakersfield sound bakou bakshy bal-musette balakadri balinese gamelan balkan pop ballad ballata ballet bamboo band bambuco banda bangsawan bantowbol barbershop music barndance baroque music baroque pop bass music batcave batucada batuco batá-rumba beach music beat beatboxing beautiful music bebop beiguan bel canto bend-skin benga berlin school of electronic music bhajan bhangra bhangra-wine bhangragga bhangramuffin big band big band music big beat biguine bihu bikutsi biomusic bitcore bitpop black metal blackened death metal blue-eyed soul bluegrass blues blues ballad blues-rock boogie boogie woogie boogie-woogie bossa nova brass band brazilian funk brazilian jazz breakbeat breakbeat hardcore breakcore breton music brill building pop britfunk british blues british invasion britpop broken beat brown-eyed soul brukdown brutal death metal bubblegum dance bubblegum pop bulerias bumba-meu-boi bunraku burger-highlife burgundian school byzantine chant ca din tulnic ca pe lunca ca trù cabaret cadence cadence rampa cadence-lypso café-aman cai luong cajun music cakewalk calenda calentanos calgia calypso calypso jazz calypso-style baila campursari canatronic candombe canon canrock cantata cante chico cante jondo canterbury scene cantiga cantique cantiñas canto livre canto nuevo canto popular cantopop canzone napoletana cape jazz capoeira music caracoles carceleras cardas cardiowave carimbó cariso carnatic music carol cartageneras cassette culture casséy-co cavacha caveman caña celempungan cello rock celtic celtic fusion celtic metal celtic punk celtic reggae celtic rock cha-cha-cha chakacha chalga chamamé chamber jazz chamber music chamber pop champeta changuí chanson chant charanga charanga-vallenata charikawi chastushki chau van chemical breaks chicago blues chicago house chicago soul chicano rap chicha chicken scratch children's music chillout chillwave chimurenga chinese music chinese pop chinese rock chip music cho-kantrum chongak chopera chorinho choro chouval bwa chowtal christian alternative christian black metal christian electronic music christian hardcore christian hip hop christian industrial christian metal christian music christian punk christian r&b christian rock christian ska christmas carol christmas music chumba chut-kai-pang chutney chutney soca chutney-bhangra chutney-hip hop chutney-soca chylandyk chzalni chèo cigányzene classic classic country classic female blues classic rock classical music classical music era clicks n cuts close harmony club music cocobale coimbra fado coladeira colombianas combined rhythm comedy rap comedy rock comic opera comparsa compas direct compas meringue concert overture concerto concerto grosso congo conjunto contemporary christian contemporary christian music contemporary r&b contonbley contradanza cool jazz corrido corsican polyphonic song cothoza mfana country country blues country gospel country music country pop country r&b country rock country-rap countrypolitan couple de sonneurs coupé-décalé cowpunk cretan music crossover jazz crossover music crossover thrash crossover thrash metal crunk crunk&b crunkcore crust punk csárdás cuarteto cuban rumba cuddlecore cueca cumbia cumbia villera cybergrind dabka dadra daina dalauna dance dance music dance-pop dance-punk dance-rock dancehall dangdut danger music dansband danza danzón dark ambient dark cabaret dark pop darkcore darkstep darkwave de ascultat la servici de codru de dragoste de jale de pahar death industrial death metal death rock death/doom deathcore deathgrind deathrock deep funk deep house deep soul degung delta blues dementia desert rock desi detroit blues detroit techno dhamar dhimotiká dhrupad dhun digital hardcore dirge dirty dutch dirty rap dirty rap/pornocore dirty south disco disco house disco polo disney disney hardcore disney pop diva house divine rock dixieland dixieland jazz djambadon djent dodompa doina dombola dondang sayang donegal fiddle tradition dongjing doo wop doom metal doomcore downtempo drag dream pop drone doom drone metal drone music dronology drum and bass dub dub house dubanguthu dubstep dubtronica dunedin sound dunun dutch jazz décima early music east coast blues east coast hip hop easy listening electric blues electric folk electro electro backbeat electro hop electro house electro punk electro-industrial electro-swing electroclash electrofunk electronic electronic art music electronic body music electronic dance electronic luk thung electronic music electronic rock electronica electropop elevator music emo emo pop emo rap emocore emotronic enka eremwu eu ethereal pop ethereal wave euro euro disco eurobeat eurodance europop eurotrance eurourban exotica experimental music experimental noise experimental pop experimental rock extreme metal ezengileer fado falak fandango farruca fife and drum blues filk film score filmi filmi-ghazal finger-style fjatpangarri flamenco flamenco rumba flower power foaie verde fofa folk hop folk metal folk music folk pop folk punk folk rock folktronica forró franco-country freak-folk freakbeat free improvisation free jazz free music freestyle freestyle house freetekno french pop frenchcore frevo fricote fuji fuji music fulia full on funaná funeral doom funk funk metal funk rock funkcore funky house furniture music fusion jazz g-funk gaana gabba gabber gagaku gaikyoku gaita galant gamad gambang kromong gamelan gamelan angklung gamelan bang gamelan bebonangan gamelan buh gamelan degung gamelan gede gamelan kebyar gamelan salendro gamelan selunding gamelan semar pegulingan gamewave gammeldans gandrung gangsta rap gar garage rock garrotin gavotte gelugpa chanting gender wayang gending german folk music gharbi gharnati ghazal ghazal-song ghetto house ghettotech girl group glam metal glam punk glam rock glitch gnawa go-go goa goa trance gong-chime music goombay goregrind goshu ondo gospel music gothic metal gothic rock granadinas grebo gregorian chant grime grindcore groove metal group sounds grunge grupera guaguanbo guajira guasca guitarra baiana guitarradas gumbe gunchei gunka guoyue gwo ka gwo ka moderne gypsy jazz gypsy punk gypsybilly gyu ke habanera hajnali hakka halling hambo hands up hapa haole happy hardcore haqibah hard hard bop hard house hard rock hard trance hardcore hip hop hardcore metal hardcore punk hardcore techno hardstyle harepa harmonica blues hasaposérviko heart attack heartland rock heavy beat heavy metal hesher hi-nrg highlands highlife highlife fusion hillybilly music hindustani classical music hip hop hip hop & rap hip hop soul hip house hiplife hiragasy hiva usu hong kong and cantonese pop hong kong english pop honky tonk honkyoku hora lunga hornpipe horror punk horrorcore horrorcore rap house house music hua'er huasteco huayno hula humppa hunguhungu hyangak hymn hyphy hát chau van hát chèo hát cãi luong hát tuồng ibiza music icaro idm igbo music ijexá ilahije illbient impressionist music improvisational incidental music indian pop indie folk indie music indie pop indie rock indietronica indo jazz indo rock indonesian pop indoyíftika industrial death metal industrial hip-hop industrial metal industrial music industrial musical industrial rock instrumental rock intelligent dance music international latin inuit music iranian pop irish folk irish rebel music iscathamiya isicathamiya isikhwela jo island isolationist italo dance italo disco italo house itsmeños izvorna bosanska muzika j'ouvert j-fusion j-pop j-rock jaipongan jaliscienses jam band jam rock jamana kura jamrieng samai jangle pop japanese pop jarana jariang jarochos jawaiian jazz jazz blues jazz fusion jazz metal jazz rap jazz-funk jazz-rock jegog jenkka jesus music jibaro jig jig punk jing ping jingle jit jitterbug jive joged joged bumbung joik jonnycore joropo jota jtek jug band jujitsu juju juke joint blues jump blues jumpstyle jungle junkanoo juré jùjú k-pop kaba kabuki kachÄshÄ« kadans kagok kagyupa chanting kaiso kalamatianó kalattuut kalinda kamba pop kan ha diskan kansas city blues kantrum kantádhes kargyraa karma kaseko katajjaq kawachi ondo kayÅkyoku ke-kwe kebyar kecak kecapi suling kertok khaleeji khap khelimaski djili khene khoomei khorovodi khplam wai khrung sai khyal kilapanda kinko kirtan kiwi rock kizomba klape klasik klezmer kliningan kléftiko kochare kolomyjka komagaku kompa konpa korean pop koumpaneia kpanlogo krakowiak krautrock kriti kroncong krump krzesany kuduro kulintang kulning kumina kun-borrk kundere kundiman kussundé kutumba wake kveding kvæði kwaito kwassa kwassa kwela käng kélé kÄ©kÅ©yÅ© pop la la latin american latin jazz latin pop latin rap lavway laïko laïkó le leagan legényes lelio letkajenkka levenslied lhamo lieder light music light rock likanos liquid drum&bass liquid funk liquindi llanera llanto lo-fi lo-fi music loki djili long-song louisiana blues louisiana swamp pop lounge music lovers rock lowercase lubbock sound lucknavi thumri luhya omutibo luk grung lullaby lundu lundum m-base madchester madrigal mafioso rap maglaal magnificat mahori mainstream jazz makossa makossa-soukous malagueñas malawian jazz malhun maloya maluf maluka mambo manaschi mandarin pop manding swing mango mangue bit mangulina manikay manila sound manouche manzuma mapouka mapouka-serré marabi maracatu marga mariachi marimba marinera marrabenta martial industrial martinetes maskanda mass matamuerte math rock mathcore matt bello maxixe mazurka mbalax mbaqanga mbube mbumba medh medieval folk rock medieval metal medieval music meditation mejorana melhoun melhûn melodic black metal melodic death metal melodic hardcore melodic metalcore melodic music melodic trance memphis blues memphis rap memphis soul mento merengue merengue típico moderno merengue-bomba meringue merseybeat metal metalcore metallic hardcore mexican pop mexican rock mexican son meykhana mezwed miami bass microhouse middle of the road midwest hip hop milonga min'yo mineras mini compas mini-jazz minimal techno minimalist music minimalist trance minneapolis sound minstrel show minuet mirolóyia modal jazz modern classical music modern laika modern rock modinha mohabelo montuno monumental dance mor lam mor lam sing morna motorpop motown mozambique mpb mugam multicultural murga musette museve mushroom jazz music drama music hall musiqi-e assil musique concrète mutuashi muwashshah muzak méringue música campesina música criolla música de la interior música llanera música nordestina música popular brasileira música tropical nagauta nakasi nangma nanguan narcocorrido nardcore narodna muzika nasheed nashville sound nashville sound/countrypolitan national socialist black metal naturalismo nederpop neo soul neo-classical metal neo-medieval neo-prog neo-psychedelia neoclassical neoclassical music neofolk neotraditional country nerdcore neue deutsche härte neue deutsche welle new age music new beat new instrumental new jack swing new orleans blues new orleans jazz new pop new prog new rave new romantic new school hip hop new taiwanese song new wave new wave of british heavy metal new wave of new wave new weird america new york blues new york house newgrass nganja nightcore nintendocore nisiótika no wave noh noise music noise pop noise rock nongak norae undong nordic folk dance music nordic folk music nortec norteño northern soul nota nu breaks nu jazz nu metal nu soul nueva canción nyatiti néo kýma obscuro oi! old school hip hop old-time oldies olonkho oltului ondo opera operatic pop oratorio orchestra organ trio organic ambient organum orgel oriental metal ottava rima outlaw country outsider music p-funk pagan metal pagan rock pagode paisley underground palm wine palm-wine pambiche panambih panchai baja panchavadyam pansori paranda parang parody parranda partido alto pasillo patriotic peace punk pelimanni music petenera peyote song philadelphia soul piano blues piano rock piedmont blues pimba pinoy pop pinoy rock pinpeat orchestra piphat piyyutim plainchant plena pleng phua cheewit pleng thai sakorn political hip hop polka polo polonaise pols polska pong lang pop pop folk pop music pop punk pop rap pop rock pop sunda pornocore porro post disco post-britpop post-disco post-grunge post-hardcore post-industrial post-metal post-minimalism post-punk post-rock post-romanticism pow-wow power electronics power metal power noise power pop powerviolence ppongtchak praise song program symphony progressive bluegrass progressive country progressive death metal progressive electronic progressive electronic music progressive folk progressive folk music progressive house progressive metal progressive rock progressive trance protopunk psych folk psychedelic music psychedelic pop psychedelic rock psychedelic trance psychobilly punk blues punk cabaret punk jazz punk rock punta punta rock qasidah qasidah modern qawwali quadrille quan ho queercore quiet storm rada raga raga rock ragga ragga jungle raggamuffin ragtime rai rake-and-scrape ramkbach ramvong ranchera rap rap metal rap rock rapcore rara rare groove rasiya rave raw rock raï rebetiko red dirt reel reggae reggae fusion reggae highlife reggaefusion reggaeton rekilaulu relax music religious rembetiko renaissance music requiem rhapsody rhyming spiritual rhythm & blues rhythm and blues ricercar riot grrrl rock rock and roll rock en español rock opera rockabilly rocksteady rococo romantic period in music rondeaux ronggeng roots reggae roots rock roots rock reggae rumba russian pop rímur sabar sacred harp sadcore saibara sakara salegy salsa salsa erotica salsa romantica saltarello samba samba-canção samba-reggae samba-rock sambai sanjo sato kagura sawt saya scat schlager schottisch schranz scottish baroque music screamo scrumpy and western sea shanty sean nós second viennese school sega music seggae seis semba sephardic music serialism set dance sevdalinka sevillana shabab shabad shalako shan'ge shango shape note shibuya-kei shidaiqu shima uta shock rock shoegaze shoegazer shoka shomyo show tune sica siguiriyas silat sinawi situational ska ska punk skacore skald skate punk skiffle slack-key guitar slide slowcore sludge metal slängpolska smooth jazz soca soft rock son son montuno son-batá sonata songo songo-salsa sophisti-pop soukous soul soul blues soul jazz soul music southern gospel southern harmony southern hip hop southern metal southern rock southern soul space age pop space music space rock spectralism speed garage speed metal speedcore spirituals spouge sprechgesang square dance squee st. louis blues steelband stoner metal stoner rock straight edge strathspeys stride string string quartet sufi music suite sunshine pop suomirock super eurobeat surf ballad surf instrumental surf music surf pop surf rock swamp blues swamp pop swamp rock swing swing music swingbeat sygyt symphonic black metal symphonic metal symphonic poem symphonic rock symphony synthpop synthpunk t'ong guitar taarab tai tu taiwanese pop tala talempong tambu tamburitza tamil christian keerthanai tango tanguk tappa tarana tarantella taranto tech tech house tech trance technical death metal technical metal techno technoid technopop techstep techtonik teen pop tejano tejano music tekno tembang sunda texas blues thai pop thillana thrash metal thrashcore thumri tibetan pop tiento timbila tin pan alley tinga tinku toeshey togaku trad jazz traditional bluegrass traditional pop music trallalero trance tribal house trikitixa trip hop trip rock trip-hop tropicalia tropicalismo tropipop truck-driving country tumba turbo-folk turkish music turkish pop turntablism tuvan throat-singing twee pop twist two tone táncház uk garage uk pub rock unblack metal underground music uplifting uplifting trance urban cowboy urban folk urban jazz vallenato vaudeville venezuela verbunkos verismo viking metal villanella virelai vispop visual kei visual music vocal vocal house vocal jazz vocal music volksmusik waila waltz wangga warabe uta wassoulou weld were music west coast hip hop west coast jazz western western blues western swing witch house wizard rock women's music wong shadow wonky pop wood work song world fusion world fusion music world music worldbeat xhosa music xoomii yo-pop yodeling yukar yé-yé zajal zapin zarzuela zeibekiko zeuhl ziglibithy zouglou zouk zouk chouv zouklove zulu music zydeco �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/lyrics.py���������������������������������������������������������������������0000644�0000765�0000024�00000041666�12405373453�017465� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2014, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Fetches, embeds, and displays lyrics. """ from __future__ import print_function import re import logging import urllib import json import unicodedata import difflib import itertools from beets.plugins import BeetsPlugin from beets import ui from beets import config # Global logger. log = logging.getLogger('beets') DIV_RE = re.compile(r'<(/?)div>?') COMMENT_RE = re.compile(r'<!--.*-->', re.S) TAG_RE = re.compile(r'<[^>]*>') BREAK_RE = re.compile(r'<br\s*/?>') URL_CHARACTERS = { u'\u2018': u"'", u'\u2019': u"'", u'\u201c': u'"', u'\u201d': u'"', u'\u2010': u'-', u'\u2011': u'-', u'\u2012': u'-', u'\u2013': u'-', u'\u2014': u'-', u'\u2015': u'-', u'\u2016': u'-', u'\u2026': u'...', } # Utilities. def fetch_url(url): """Retrieve the content at a given URL, or return None if the source is unreachable. """ try: return urllib.urlopen(url).read() except IOError as exc: log.debug(u'failed to fetch: {0} ({1})'.format(url, unicode(exc))) return None def unescape(text): """Resolves &#xxx; HTML entities (and some others).""" if isinstance(text, str): text = text.decode('utf8', 'ignore') out = text.replace(u' ', u' ') def replchar(m): num = m.group(1) return unichr(int(num)) out = re.sub(u"&#(\d+);", replchar, out) return out def extract_text(html, starttag): """Extract the text from a <DIV> tag in the HTML starting with ``starttag``. Returns None if parsing fails. """ # Strip off the leading text before opening tag. try: _, html = html.split(starttag, 1) except ValueError: return # Walk through balanced DIV tags. level = 0 parts = [] pos = 0 for match in DIV_RE.finditer(html): if match.group(1): # Closing tag. level -= 1 if level == 0: pos = match.end() else: # Opening tag. if level == 0: parts.append(html[pos:match.start()]) level += 1 if level == -1: parts.append(html[pos:match.start()]) break else: print('no closing tag found!') return lyrics = ''.join(parts) return strip_cruft(lyrics) def strip_cruft(lyrics, wscollapse=True): """Clean up HTML from an extracted lyrics string. For example, <BR> tags are replaced with newlines. """ lyrics = COMMENT_RE.sub('', lyrics) lyrics = unescape(lyrics) if wscollapse: lyrics = re.sub(r'\s+', ' ', lyrics) # Whitespace collapse. lyrics = re.sub(r'<(script).*?</\1>(?s)', '', lyrics) # Strip script tags. lyrics = BREAK_RE.sub('\n', lyrics) # <BR> newlines. lyrics = re.sub(r'\n +', '\n', lyrics) lyrics = re.sub(r' +\n', '\n', lyrics) lyrics = TAG_RE.sub('', lyrics) # Strip remaining HTML tags. lyrics = lyrics.replace('\r', '\n') lyrics = lyrics.strip() return lyrics def search_pairs(item): """Yield a pairs of artists and titles to search for. The first item in the pair is the name of the artist, the second item is a list of song names. In addition to the artist and title obtained from the `item` the method tries to strip extra information like paranthesized suffixes and featured artists from the strings and add them as caniddates. The method also tries to split multiple titles separated with `/`. """ title, artist = item.title, item.artist titles = [title] artists = [artist] # Remove any featuring artists from the artists name pattern = r"(.*?) (&|\b(and|ft|feat(uring)?\b))" match = re.search(pattern, artist, re.IGNORECASE) if match: artists.append(match.group(1)) # Remove a parenthesized suffix from a title string. Common # examples include (live), (remix), and (acoustic). pattern = r"(.+?)\s+[(].*[)]$" match = re.search(pattern, title, re.IGNORECASE) if match: titles.append(match.group(1)) # Remove any featuring artists from the title pattern = r"(.*?) \b(ft|feat(uring)?)\b" for title in titles: match = re.search(pattern, title, re.IGNORECASE) if match: titles.append(match.group(1)) # Check for a dual song (e.g. Pink Floyd - Speak to Me / Breathe) # and each of them. multi_titles = [] for title in titles: multi_titles.append([title]) if '/' in title: multi_titles.append([x.strip() for x in title.split('/')]) return itertools.product(artists, multi_titles) def _encode(s): """Encode the string for inclusion in a URL (common to both LyricsWiki and Lyrics.com). """ if isinstance(s, unicode): for char, repl in URL_CHARACTERS.items(): s = s.replace(char, repl) s = s.encode('utf8', 'ignore') return urllib.quote(s) # LyricsWiki. LYRICSWIKI_URL_PATTERN = 'http://lyrics.wikia.com/%s:%s' def _lw_encode(s): s = re.sub(r'\s+', '_', s) s = s.replace("<", "Less_Than") s = s.replace(">", "Greater_Than") s = s.replace("#", "Number_") s = re.sub(r'[\[\{]', '(', s) s = re.sub(r'[\]\}]', ')', s) return _encode(s) def fetch_lyricswiki(artist, title): """Fetch lyrics from LyricsWiki.""" url = LYRICSWIKI_URL_PATTERN % (_lw_encode(artist), _lw_encode(title)) html = fetch_url(url) if not html: return lyrics = extract_text(html, "<div class='lyricbox'>") if lyrics and 'Unfortunately, we are not licensed' not in lyrics: return lyrics # Lyrics.com. LYRICSCOM_URL_PATTERN = 'http://www.lyrics.com/%s-lyrics-%s.html' LYRICSCOM_NOT_FOUND = ( 'Sorry, we do not have the lyric', 'Submit Lyrics', ) def _lc_encode(s): s = re.sub(r'[^\w\s-]', '', s) s = re.sub(r'\s+', '-', s) return _encode(s).lower() def fetch_lyricscom(artist, title): """Fetch lyrics from Lyrics.com.""" url = LYRICSCOM_URL_PATTERN % (_lc_encode(title), _lc_encode(artist)) html = fetch_url(url) if not html: return lyrics = extract_text(html, '<div id="lyric_space">') if not lyrics: return for not_found_str in LYRICSCOM_NOT_FOUND: if not_found_str in lyrics: return parts = lyrics.split('\n---\nLyrics powered by', 1) if parts: return parts[0] # Optional Google custom search API backend. def slugify(text): """Normalize a string and remove non-alphanumeric characters. """ text = re.sub(r"[-'_\s]", '_', text) text = re.sub(r"_+", '_', text).strip('_') pat = "([^,\(]*)\((.*?)\)" # Remove content within parentheses text = re.sub(pat, '\g<1>', text).strip() try: text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore') text = unicode(re.sub('[-\s]+', ' ', text)) except UnicodeDecodeError: log.exception(u"Failing to normalize '{0}'".format(text)) return text BY_TRANS = ['by', 'par', 'de', 'von'] LYRICS_TRANS = ['lyrics', 'paroles', 'letras', 'liedtexte'] def is_page_candidate(urlLink, urlTitle, title, artist): """Return True if the URL title makes it a good candidate to be a page that contains lyrics of title by artist. """ title = slugify(title.lower()) artist = slugify(artist.lower()) sitename = re.search(u"//([^/]+)/.*", slugify(urlLink.lower())).group(1) urlTitle = slugify(urlTitle.lower()) # Check if URL title contains song title (exact match) if urlTitle.find(title) != -1: return True # or try extracting song title from URL title and check if # they are close enough tokens = [by + '_' + artist for by in BY_TRANS] + \ [artist, sitename, sitename.replace('www.', '')] + LYRICS_TRANS songTitle = re.sub(u'(%s)' % u'|'.join(tokens), u'', urlTitle) typoRatio = .8 return difflib.SequenceMatcher(None, songTitle, title).ratio() >= typoRatio def insert_line_feeds(text): """Insert newlines before upper-case characters. """ tokensStr = re.split("([a-z][A-Z])", text) for idx in range(1, len(tokensStr), 2): ltoken = list(tokensStr[idx]) tokensStr[idx] = ltoken[0] + '\n' + ltoken[1] return ''.join(tokensStr) def sanitize_lyrics(text): """Clean text, returning raw lyrics as output or None if it happens that input text is actually not lyrics content. Clean (x)html tags in text, correct layout and syntax... """ text = strip_cruft(text, False) # Restore \n in input text if '\n' not in text: text = insert_line_feeds(text) while text.count('\n\n') > text.count('\n') // 4: # Remove first occurrence of \n for each sequence of \n text = re.sub(r'\n(\n+)', '\g<1>', text) text = re.sub(r'\n\n+', '\n\n', text) # keep at most two \n in a row return text def remove_credits(text): """Remove first/last line of text if it contains the word 'lyrics' eg 'Lyrics by songsdatabase.com' """ textlines = text.split('\n') credits = None for i in (0, -1): if textlines and 'lyrics' in textlines[i].lower(): credits = textlines.pop(i) if credits: text = '\n'.join(textlines) return text def is_lyrics(text, artist=None): """Determine whether the text seems to be valid lyrics. """ if not text: return badTriggersOcc = [] nbLines = text.count('\n') if nbLines <= 1: log.debug(u"Ignoring too short lyrics '{0}'".format(text)) return 0 elif nbLines < 5: badTriggersOcc.append('too_short') else: # Lyrics look legit, remove credits to avoid being penalized further # down text = remove_credits(text) badTriggers = ['lyrics', 'copyright', 'property'] if artist: badTriggersOcc += [artist] for item in badTriggers: badTriggersOcc += [item] * len(re.findall(r'\W%s\W' % item, text, re.I)) if badTriggersOcc: log.debug(u'Bad triggers detected: {0}'.format(badTriggersOcc)) return len(badTriggersOcc) < 2 def scrape_lyrics_from_url(url): """Scrape lyrics from a URL. If no lyrics can be found, return None instead. """ from bs4 import BeautifulSoup, Comment html = fetch_url(url) if not html: return None soup = BeautifulSoup(html) for tag in soup.findAll('br'): tag.replaceWith('\n') # Remove non relevant html parts [s.extract() for s in soup(['head', 'script'])] comments = soup.findAll(text=lambda text: isinstance(text, Comment)) [s.extract() for s in comments] try: for tag in soup.findAll(True): tag.name = 'p' # keep tag contents except Exception, e: log.debug(u'Error {0} when replacing containing marker by p marker' .format(e, exc_info=True)) # Make better soup from current soup! The previous unclosed <p> sections # are now closed. Use str() rather than prettify() as it's more # conservative concerning EOL soup = BeautifulSoup(str(soup)) # In case lyrics are nested in no markup but <body> # Insert the whole body in a <p> bodyTag = soup.find('body') if bodyTag: pTag = soup.new_tag("p") bodyTag.parent.insert(0, pTag) pTag.insert(0, bodyTag) tagTokens = [] for tag in soup.findAll('p'): soup2 = BeautifulSoup(str(tag)) # Extract all text of <p> section. tagTokens += soup2.findAll(text=True) if tagTokens: # Lyrics are expected to be the longest paragraph tagTokens = sorted(tagTokens, key=len, reverse=True) soup = BeautifulSoup(tagTokens[0]) return unescape(tagTokens[0].strip("\n\r: ")) def fetch_google(artist, title): """Fetch lyrics from Google search results. """ query = u"%s %s" % (artist, title) api_key = config['lyrics']['google_API_key'].get(unicode) engine_id = config['lyrics']['google_engine_ID'].get(unicode) url = u'https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s' % \ (api_key, engine_id, urllib.quote(query.encode('utf8'))) data = urllib.urlopen(url) data = json.load(data) if 'error' in data: reason = data['error']['errors'][0]['reason'] log.debug(u'google lyrics backend error: {0}'.format(reason)) return if 'items' in data.keys(): for item in data['items']: urlLink = item['link'] urlTitle = item['title'] if not is_page_candidate(urlLink, urlTitle, title, artist): continue lyrics = scrape_lyrics_from_url(urlLink) if not lyrics: continue lyrics = sanitize_lyrics(lyrics) if is_lyrics(lyrics, artist): log.debug(u'got lyrics from {0}'.format(item['displayLink'])) return lyrics # Plugin logic. class LyricsPlugin(BeetsPlugin): def __init__(self): super(LyricsPlugin, self).__init__() self.import_stages = [self.imported] self.config.add({ 'auto': True, 'google_API_key': None, 'google_engine_ID': u'009217259823014548361:lndtuqkycfu', 'fallback': None, }) self.backends = [fetch_lyricswiki, fetch_lyricscom] if self.config['google_API_key'].get(): self.backends.insert(0, fetch_google) def commands(self): cmd = ui.Subcommand('lyrics', help='fetch song lyrics') cmd.parser.add_option('-p', '--print', dest='printlyr', action='store_true', default=False, help='print lyrics to console') cmd.parser.add_option('-f', '--force', dest='force_refetch', action='store_true', default=False, help='always re-download lyrics') def func(lib, opts, args): # The "write to files" option corresponds to the # import_write config value. write = config['import']['write'].get(bool) for item in lib.items(ui.decargs(args)): self.fetch_item_lyrics(lib, logging.INFO, item, write, opts.force_refetch) if opts.printlyr and item.lyrics: ui.print_(item.lyrics) cmd.func = func return [cmd] def imported(self, session, task): """Import hook for fetching lyrics automatically. """ if self.config['auto']: for item in task.imported_items(): self.fetch_item_lyrics(session.lib, logging.DEBUG, item, False, False) def fetch_item_lyrics(self, lib, loglevel, item, write, force): """Fetch and store lyrics for a single item. If ``write``, then the lyrics will also be written to the file itself. The ``loglevel`` parameter controls the visibility of the function's status log messages. """ # Skip if the item already has lyrics. if not force and item.lyrics: log.log(loglevel, u'lyrics already present: {0} - {1}' .format(item.artist, item.title)) return lyrics = None for artist, titles in search_pairs(item): lyrics = [self.get_lyrics(artist, title) for title in titles] if any(lyrics): break lyrics = u"\n\n---\n\n".join([l for l in lyrics if l]) if lyrics: log.log(loglevel, u'fetched lyrics: {0} - {1}' .format(item.artist, item.title)) else: log.log(loglevel, u'lyrics not found: {0} - {1}' .format(item.artist, item.title)) fallback = self.config['fallback'].get() if fallback: lyrics = fallback else: return item.lyrics = lyrics if write: item.try_write() item.store() def get_lyrics(self, artist, title): """Fetch lyrics, trying each source in turn. Return a string or None if no lyrics were found. """ for backend in self.backends: lyrics = backend(artist, title) if lyrics: if isinstance(lyrics, str): lyrics = lyrics.decode('utf8', 'ignore') log.debug(u'got lyrics from backend: {0}' .format(backend.__name__)) return lyrics.strip() ��������������������������������������������������������������������������beets-1.3.8/beetsplug/mbcollection.py���������������������������������������������������������������0000644�0000765�0000024�00000007334�12404675421�020623� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright (c) 2011, Jeffrey Aylesworth <jeffrey@aylesworth.ca> # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. from __future__ import print_function from beets.plugins import BeetsPlugin from beets.ui import Subcommand from beets import ui from beets import config import musicbrainzngs import re import logging SUBMISSION_CHUNK_SIZE = 200 UUID_REGEX = r'^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$' log = logging.getLogger('beets.bpd') def mb_call(func, *args, **kwargs): """Call a MusicBrainz API function and catch exceptions. """ try: return func(*args, **kwargs) except musicbrainzngs.AuthenticationError: raise ui.UserError('authentication with MusicBrainz failed') except musicbrainzngs.ResponseError as exc: raise ui.UserError('MusicBrainz API error: {0}'.format(exc)) except musicbrainzngs.UsageError: raise ui.UserError('MusicBrainz credentials missing') def submit_albums(collection_id, release_ids): """Add all of the release IDs to the indicated collection. Multiple requests are made if there are many release IDs to submit. """ for i in range(0, len(release_ids), SUBMISSION_CHUNK_SIZE): chunk = release_ids[i:i + SUBMISSION_CHUNK_SIZE] mb_call( musicbrainzngs.add_releases_to_collection, collection_id, chunk ) def update_album_list(album_list): """Update the MusicBrainz colleciton from a list of Beets albums """ # Get the collection to modify. collections = mb_call(musicbrainzngs.get_collections) if not collections['collection-list']: raise ui.UserError('no collections exist for user') collection_id = collections['collection-list'][0]['id'] # Get a list of all the album IDs. album_ids = [] for album in album_list: aid = album.mb_albumid if aid: if re.match(UUID_REGEX, aid): album_ids.append(aid) else: log.info(u'skipping invalid MBID: {0}'.format(aid)) # Submit to MusicBrainz. print('Updating MusicBrainz collection {0}...'.format(collection_id)) submit_albums(collection_id, album_ids) print('...MusicBrainz collection updated.') def update_collection(lib, opts, args): update_album_list(lib.albums()) update_mb_collection_cmd = Subcommand('mbupdate', help='Update MusicBrainz collection') update_mb_collection_cmd.func = update_collection class MusicBrainzCollectionPlugin(BeetsPlugin): def __init__(self): super(MusicBrainzCollectionPlugin, self).__init__() musicbrainzngs.auth( config['musicbrainz']['user'].get(unicode), config['musicbrainz']['pass'].get(unicode), ) self.config.add({'auto': False}) if self.config['auto']: self.import_stages = [self.imported] def commands(self): return [update_mb_collection_cmd] def imported(self, session, task): """Add each imported album to the collection. """ if task.is_album: update_album_list([task.album]) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/mbsync.py���������������������������������������������������������������������0000644�0000765�0000024�00000013107�12404675421�017437� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2014, Jakob Schnitzer. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Update library's tags using MusicBrainz. """ import logging from beets.plugins import BeetsPlugin from beets import autotag, library, ui, util from beets.autotag import hooks from beets import config from collections import defaultdict log = logging.getLogger('beets') def mbsync_singletons(lib, query, move, pretend, write): """Retrieve and apply info from the autotagger for items matched by query. """ for item in lib.items(query + ['singleton:true']): if not item.mb_trackid: log.info(u'Skipping singleton {0}: has no mb_trackid' .format(item.title)) continue # Get the MusicBrainz recording info. track_info = hooks.track_for_mbid(item.mb_trackid) if not track_info: log.info(u'Recording ID not found: {0}'.format(item.mb_trackid)) continue # Apply. with lib.transaction(): autotag.apply_item_metadata(item, track_info) apply_item_changes(lib, item, move, pretend, write) def mbsync_albums(lib, query, move, pretend, write): """Retrieve and apply info from the autotagger for albums matched by query and their items. """ # Process matching albums. for a in lib.albums(query): if not a.mb_albumid: log.info(u'Skipping album {0}: has no mb_albumid'.format(a.id)) continue items = list(a.items()) # Get the MusicBrainz album information. album_info = hooks.album_for_mbid(a.mb_albumid) if not album_info: log.info(u'Release ID not found: {0}'.format(a.mb_albumid)) continue # Map recording MBIDs to their information. Recordings can appear # multiple times on a release, so each MBID maps to a list of TrackInfo # objects. track_index = defaultdict(list) for track_info in album_info.tracks: track_index[track_info.track_id].append(track_info) # Construct a track mapping according to MBIDs. This should work # for albums that have missing or extra tracks. If there are multiple # copies of a recording, they are disambiguated using their disc and # track number. mapping = {} for item in items: candidates = track_index[item.mb_trackid] if len(candidates) == 1: mapping[item] = candidates[0] else: for c in candidates: if c.medium_index == item.track and c.medium == item.disc: mapping[item] = c break # Apply. with lib.transaction(): autotag.apply_metadata(album_info, mapping) changed = False for item in items: item_changed = ui.show_model_changes(item) changed |= item_changed if item_changed: apply_item_changes(lib, item, move, pretend, write) if not changed: # No change to any item. continue if not pretend: # Update album structure to reflect an item in it. for key in library.Album.item_keys: a[key] = items[0][key] a.store() # Move album art (and any inconsistent items). if move and lib.directory in util.ancestry(items[0].path): log.debug(u'moving album {0}'.format(a.id)) a.move() def apply_item_changes(lib, item, move, pretend, write): """Store, move and write the item according to the arguments. """ if not pretend: # Move the item if it's in the library. if move and lib.directory in util.ancestry(item.path): item.move(with_album=False) if write: item.try_write() item.store() def mbsync_func(lib, opts, args): """Command handler for the mbsync function. """ move = opts.move pretend = opts.pretend write = opts.write query = ui.decargs(args) mbsync_singletons(lib, query, move, pretend, write) mbsync_albums(lib, query, move, pretend, write) class MBSyncPlugin(BeetsPlugin): def __init__(self): super(MBSyncPlugin, self).__init__() def commands(self): cmd = ui.Subcommand('mbsync', help='update metadata from musicbrainz') cmd.parser.add_option('-p', '--pretend', action='store_true', help='show all changes but do nothing') cmd.parser.add_option('-M', '--nomove', action='store_false', default=True, dest='move', help="don't move files in library") cmd.parser.add_option('-W', '--nowrite', action='store_false', default=config['import']['write'], dest='write', help="don't write updated metadata to files") cmd.func = mbsync_func return [cmd] ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/missing.py��������������������������������������������������������������������0000644�0000765�0000024�00000013220�12405373453�017612� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2013, Pedro Silva. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """List missing tracks. """ import logging from beets.autotag import hooks from beets.library import Item from beets.plugins import BeetsPlugin from beets.ui import decargs, print_obj, Subcommand PLUGIN = 'missing' log = logging.getLogger('beets') def _missing_count(album): """Return number of missing items in `album`. """ return (album.tracktotal or 0) - len(album.items()) def _missing(album): """Query MusicBrainz to determine items missing from `album`. """ item_mbids = map(lambda x: x.mb_trackid, album.items()) if len([i for i in album.items()]) < album.tracktotal: # fetch missing items # TODO: Implement caching that without breaking other stuff album_info = hooks.album_for_mbid(album.mb_albumid) for track_info in getattr(album_info, 'tracks', []): if track_info.track_id not in item_mbids: item = _item(track_info, album_info, album.id) log.debug(u'{0}: track {1} in album {2}' .format(PLUGIN, track_info.track_id, album_info.album_id)) yield item def _item(track_info, album_info, album_id): """Build and return `item` from `track_info` and `album info` objects. `item` is missing what fields cannot be obtained from MusicBrainz alone (encoder, rg_track_gain, rg_track_peak, rg_album_gain, rg_album_peak, original_year, original_month, original_day, length, bitrate, format, samplerate, bitdepth, channels, mtime.) """ t = track_info a = album_info return Item(**{ 'album_id': album_id, 'album': a.album, 'albumartist': a.artist, 'albumartist_credit': a.artist_credit, 'albumartist_sort': a.artist_sort, 'albumdisambig': a.albumdisambig, 'albumstatus': a.albumstatus, 'albumtype': a.albumtype, 'artist': t.artist, 'artist_credit': t.artist_credit, 'artist_sort': t.artist_sort, 'asin': a.asin, 'catalognum': a.catalognum, 'comp': a.va, 'country': a.country, 'day': a.day, 'disc': t.medium, 'disctitle': t.disctitle, 'disctotal': a.mediums, 'label': a.label, 'language': a.language, 'length': t.length, 'mb_albumid': a.album_id, 'mb_artistid': t.artist_id, 'mb_releasegroupid': a.releasegroup_id, 'mb_trackid': t.track_id, 'media': a.media, 'month': a.month, 'script': a.script, 'title': t.title, 'track': t.index, 'tracktotal': len(a.tracks), 'year': a.year, }) class MissingPlugin(BeetsPlugin): """List missing tracks """ def __init__(self): super(MissingPlugin, self).__init__() self.config.add({ 'format': None, 'count': False, 'total': False, }) self.album_template_fields['missing'] = _missing_count self._command = Subcommand('missing', help=__doc__, aliases=['miss']) self._command.parser.add_option('-f', '--format', dest='format', action='store', type='string', help='print with custom FORMAT', metavar='FORMAT') self._command.parser.add_option('-c', '--count', dest='count', action='store_true', help='count missing tracks per album') self._command.parser.add_option('-t', '--total', dest='total', action='store_true', help='count total of missing tracks') def commands(self): def _miss(lib, opts, args): self.config.set_args(opts) fmt = self.config['format'].get() count = self.config['count'].get() total = self.config['total'].get() albums = lib.albums(decargs(args)) if total: print(sum([_missing_count(a) for a in albums])) return # Default format string for count mode. if count and not fmt: fmt = '$albumartist - $album: $missing' for album in albums: if count: missing = _missing_count(album) if missing: print_obj(album, lib, fmt=fmt) else: for item in _missing(album): print_obj(item, lib, fmt=fmt) self._command.func = _miss return [self._command] ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/mpdstats.py�������������������������������������������������������������������0000644�0000765�0000024�00000026136�12405357732�020014� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# coding=utf-8 # This file is part of beets. # Copyright 2013, Peter Schnebel and Johann Klähn. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. import logging import mpd import socket import select import time import os from beets import ui from beets import config from beets import plugins from beets import library from beets.util import displayable_path from beets.dbcore import types log = logging.getLogger('beets') # If we lose the connection, how many times do we want to retry and how # much time should we wait between retries? RETRIES = 10 RETRY_INTERVAL = 5 def is_url(path): """Try to determine if the path is an URL. """ return path.split('://', 1)[0] in ['http', 'https'] # Use the MPDClient internals to get unicode. # see http://www.tarmack.eu/code/mpdunicode.py for the general idea class MPDClient(mpd.MPDClient): def _write_command(self, command, args=[]): args = [unicode(arg).encode('utf-8') for arg in args] super(MPDClient, self)._write_command(command, args) def _read_line(self): line = super(MPDClient, self)._read_line() if line is not None: return line.decode('utf-8') return None class MPDClientWrapper(object): def __init__(self): self.music_directory = ( config['mpdstats']['music_directory'].get(unicode)) self.client = MPDClient() def connect(self): """Connect to the MPD. """ host = config['mpd']['host'].get(unicode) port = config['mpd']['port'].get(int) if host[0] in ['/', '~']: host = os.path.expanduser(host) log.info(u'mpdstats: connecting to {0}:{1}'.format(host, port)) try: self.client.connect(host, port) except socket.error as e: raise ui.UserError('could not connect to MPD: {0}'.format(e)) password = config['mpd']['password'].get(unicode) if password: try: self.client.password(password) except mpd.CommandError as e: raise ui.UserError( 'could not authenticate to MPD: {0}'.format(e) ) def disconnect(self): """Disconnect from the MPD. """ self.client.close() self.client.disconnect() def get(self, command, retries=RETRIES): """Wrapper for requests to the MPD server. Tries to re-connect if the connection was lost (f.ex. during MPD's library refresh). """ try: return getattr(self.client, command)() except (select.error, mpd.ConnectionError) as err: log.error(u'mpdstats: {0}'.format(err)) if retries <= 0: # if we exited without breaking, we couldn't reconnect in time :( raise ui.UserError(u'communication with MPD server failed') time.sleep(RETRY_INTERVAL) try: self.disconnect() except mpd.ConnectionError: pass self.connect() return self.get(command, retries=retries - 1) def playlist(self): """Return the currently active playlist. Prefixes paths with the music_directory, to get the absolute path. """ result = {} for entry in self.get('playlistinfo'): if not is_url(entry['file']): result[entry['id']] = os.path.join( self.music_directory, entry['file']) else: result[entry['id']] = entry['file'] return result def status(self): """Return the current status of the MPD. """ return self.get('status') def events(self): """Return list of events. This may block a long time while waiting for an answer from MPD. """ return self.get('idle') class MPDStats(object): def __init__(self, lib): self.lib = lib self.do_rating = config['mpdstats']['rating'].get(bool) self.rating_mix = config['mpdstats']['rating_mix'].get(float) self.time_threshold = 10.0 # TODO: maybe add config option? self.now_playing = None self.mpd = MPDClientWrapper() def rating(self, play_count, skip_count, rating, skipped): """Calculate a new rating for a song based on play count, skip count, old rating and the fact if it was skipped or not. """ if skipped: rolling = (rating - rating / 2.0) else: rolling = (rating + (1.0 - rating) / 2.0) stable = (play_count + 1.0) / (play_count + skip_count + 2.0) return (self.rating_mix * stable + (1.0 - self.rating_mix) * rolling) def get_item(self, path): """Return the beets item related to path. """ query = library.PathQuery('path', path) item = self.lib.items(query).get() if item: return item else: log.info(u'mpdstats: item not found: {0}'.format( displayable_path(path) )) @staticmethod def update_item(item, attribute, value=None, increment=None): """Update the beets item. Set attribute to value or increment the value of attribute. If the increment argument is used the value is cast to the corresponding type. """ if item is None: return if increment is not None: item.load() value = type(increment)(item.get(attribute, 0)) + increment if value is not None: item[attribute] = value item.store() log.debug(u'mpdstats: updated: {0} = {1} [{2}]'.format( attribute, item[attribute], displayable_path(item.path), )) def update_rating(self, item, skipped): """Update the rating for a beets item. """ item.load() rating = self.rating( int(item.get('play_count', 0)), int(item.get('skip_count', 0)), float(item.get('rating', 0.5)), skipped) self.update_item(item, 'rating', rating) def handle_song_change(self, song): """Determine if a song was skipped or not and update its attributes. To this end the difference between the song's supposed end time and the current time is calculated. If it's greater than a threshold, the song is considered skipped. """ diff = abs(song['remaining'] - (time.time() - song['started'])) skipped = diff >= self.time_threshold if skipped: self.handle_skipped(song) else: self.handle_played(song) if self.do_rating: self.update_rating(song['beets_item'], skipped) def handle_played(self, song): """Updates the play count of a song. """ self.update_item(song['beets_item'], 'play_count', increment=1) log.info(u'mpdstats: played {0}'.format( displayable_path(song['path']) )) def handle_skipped(self, song): """Updates the skip count of a song. """ self.update_item(song['beets_item'], 'skip_count', increment=1) log.info(u'mpdstats: skipped {0}'.format( displayable_path(song['path']) )) def on_stop(self, status): log.info(u'mpdstats: stop') if self.now_playing: self.handle_song_change(self.now_playing) self.now_playing = None def on_pause(self, status): log.info(u'mpdstats: pause') self.now_playing = None def on_play(self, status): playlist = self.mpd.playlist() path = playlist.get(status['songid']) if not path: return if is_url(path): log.info(u'mpdstats: playing stream {0}'.format( displayable_path(path) )) return played, duration = map(int, status['time'].split(':', 1)) remaining = duration - played if self.now_playing and self.now_playing['path'] != path: self.handle_song_change(self.now_playing) log.info(u'mpdstats: playing {0}'.format( displayable_path(path) )) self.now_playing = { 'started': time.time(), 'remaining': remaining, 'path': path, 'beets_item': self.get_item(path), } self.update_item(self.now_playing['beets_item'], 'last_played', value=int(time.time())) def run(self): self.mpd.connect() events = ['player'] while True: if 'player' in events: status = self.mpd.status() handler = getattr(self, 'on_' + status['state'], None) if handler: handler(status) else: log.debug(u'mpdstats: unhandled status "{0}"'. format(status)) events = self.mpd.events() class MPDStatsPlugin(plugins.BeetsPlugin): item_types = { 'play_count': types.INTEGER, 'skip_count': types.INTEGER, 'last_played': library.Date(), 'rating': types.FLOAT, } def __init__(self): super(MPDStatsPlugin, self).__init__() self.config.add({ 'music_directory': config['directory'].as_filename(), 'rating': True, 'rating_mix': 0.75, }) config['mpd'].add({ 'host': u'localhost', 'port': 6600, 'password': u'', }) def commands(self): cmd = ui.Subcommand( 'mpdstats', help='run a MPD client to gather play statistics') cmd.parser.add_option( '--host', dest='host', type='string', help='set the hostname of the server to connect to') cmd.parser.add_option( '--port', dest='port', type='int', help='set the port of the MPD server to connect to') cmd.parser.add_option( '--password', dest='password', type='string', help='set the password of the MPD server to connect to') def func(lib, opts, args): self.config.set_args(opts) # Overrides for MPD settings. if opts.host: config['mpd']['host'] = opts.host.decode('utf8') if opts.port: config['mpd']['host'] = int(opts.port) if opts.password: config['mpd']['password'] = opts.password.decode('utf8') try: MPDStats(lib).run() except KeyboardInterrupt: pass cmd.func = func return [cmd] ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/mpdupdate.py������������������������������������������������������������������0000644�0000765�0000024�00000007442�12322627125�020131� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Updates an MPD index whenever the library is changed. Put something like the following in your config.yaml to configure: mpd: host: localhost port: 6600 password: seekrit """ from __future__ import print_function from beets.plugins import BeetsPlugin import os import socket from beets import config # Global variable so that mpdupdate can detect database changes and run only # once before beets exits. database_changed = False # No need to introduce a dependency on an MPD library for such a # simple use case. Here's a simple socket abstraction to make things # easier. class BufferedSocket(object): """Socket abstraction that allows reading by line.""" def __init__(self, host, port, sep='\n'): if host[0] in ['/', '~']: self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.connect(os.path.expanduser(host)) else: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((host, port)) self.buf = '' self.sep = sep def readline(self): while self.sep not in self.buf: data = self.sock.recv(1024) if not data: break self.buf += data if '\n' in self.buf: res, self.buf = self.buf.split(self.sep, 1) return res + self.sep else: return '' def send(self, data): self.sock.send(data) def close(self): self.sock.close() def update_mpd(host='localhost', port=6600, password=None): """Sends the "update" command to the MPD server indicated, possibly authenticating with a password first. """ print('Updating MPD database...') s = BufferedSocket(host, port) resp = s.readline() if 'OK MPD' not in resp: print('MPD connection failed:', repr(resp)) return if password: s.send('password "%s"\n' % password) resp = s.readline() if 'OK' not in resp: print('Authentication failed:', repr(resp)) s.send('close\n') s.close() return s.send('update\n') resp = s.readline() if 'updating_db' not in resp: print('Update failed:', repr(resp)) s.send('close\n') s.close() print('... updated.') class MPDUpdatePlugin(BeetsPlugin): def __init__(self): super(MPDUpdatePlugin, self).__init__() config['mpd'].add({ 'host': u'localhost', 'port': 6600, 'password': u'', }) # For backwards compatibility, use any values from the # plugin-specific "mpdupdate" section. for key in config['mpd'].keys(): if self.config[key].exists(): config['mpd'][key] = self.config[key].get() @MPDUpdatePlugin.listen('database_change') def handle_change(lib=None): global database_changed database_changed = True @MPDUpdatePlugin.listen('cli_exit') def update(lib=None): if database_changed: update_mpd( config['mpd']['host'].get(unicode), config['mpd']['port'].get(int), config['mpd']['password'].get(unicode), ) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/play.py�����������������������������������������������������������������������0000644�0000765�0000024�00000010161�12405357732�017111� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2014, David Hamp-Gonsalves # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Send the results of a query to the configured music player as a playlist. """ from beets.plugins import BeetsPlugin from beets.ui import Subcommand from beets import config from beets import ui from beets import util from os.path import relpath import platform import logging import shlex from tempfile import NamedTemporaryFile log = logging.getLogger('beets') def play_music(lib, opts, args): """Execute query, create temporary playlist and execute player command passing that playlist. """ command_str = config['play']['command'].get() use_folders = config['play']['use_folders'].get(bool) relative_to = config['play']['relative_to'].get() if relative_to: relative_to = util.normpath(relative_to) if command_str: command = shlex.split(command_str) else: # If a command isn't set, then let the OS decide how to open the # playlist. sys_name = platform.system() if sys_name == 'Darwin': command = ['open'] elif sys_name == 'Windows': command = ['start'] else: # If not Mac or Windows, then assume Unixy. command = ['xdg-open'] # Preform search by album and add folders rather then tracks to playlist. if opts.album: selection = lib.albums(ui.decargs(args)) paths = [] for album in selection: if use_folders: paths.append(album.item_dir()) else: # TODO use core's sorting functionality paths.extend([item.path for item in sorted( album.items(), key=lambda item: (item.disc, item.track))]) item_type = 'album' # Preform item query and add tracks to playlist. else: selection = lib.items(ui.decargs(args)) paths = [item.path for item in selection] item_type = 'track' item_type += 's' if len(selection) > 1 else '' if not selection: ui.print_(ui.colorize('yellow', 'No {0} to play.'.format(item_type))) return # Warn user before playing any huge playlists. if len(selection) > 100: ui.print_(ui.colorize( 'yellow', 'You are about to queue {0} {1}.'.format(len(selection), item_type) )) if ui.input_options(('Continue', 'Abort')) == 'a': return # Create temporary m3u file to hold our playlist. m3u = NamedTemporaryFile('w', suffix='.m3u', delete=False) for item in paths: if relative_to: m3u.write(relpath(item, relative_to) + '\n') else: m3u.write(item + '\n') m3u.close() command.append(m3u.name) # Invoke the command and log the output. output = util.command_output(command) if output: log.debug(u'Output of {0}: {1}'.format(command[0], output)) ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type)) class PlayPlugin(BeetsPlugin): def __init__(self): super(PlayPlugin, self).__init__() config['play'].add({ 'command': None, 'use_folders': False, 'relative_to': None, }) def commands(self): play_command = Subcommand( 'play', help='send music to a player as a playlist' ) play_command.parser.add_option( '-a', '--album', action='store_true', default=False, help='query and load albums rather than tracks' ) play_command.func = play_music return [play_command] ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/random.py���������������������������������������������������������������������0000644�0000765�0000024�00000006302�12322630366�017421� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2013, Philippe Mongeau. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Get a random song or album from the library. """ from __future__ import absolute_import from beets.plugins import BeetsPlugin from beets.ui import Subcommand, decargs, print_obj from beets.util.functemplate import Template import random from operator import attrgetter from itertools import groupby def random_item(lib, opts, args): query = decargs(args) if opts.path: fmt = '$path' else: fmt = opts.format template = Template(fmt) if fmt else None if opts.album: objs = list(lib.albums(query)) else: objs = list(lib.items(query)) if opts.equal_chance: # Group the objects by artist so we can sample from them. key = attrgetter('albumartist') objs.sort(key=key) objs_by_artists = {} for artist, v in groupby(objs, key): objs_by_artists[artist] = list(v) objs = [] for _ in range(opts.number): # Terminate early if we're out of objects to select. if not objs_by_artists: break # Choose an artist and an object for that artist, removing # this choice from the pool. artist = random.choice(objs_by_artists.keys()) objs_from_artist = objs_by_artists[artist] i = random.randint(0, len(objs_from_artist) - 1) objs.append(objs_from_artist.pop(i)) # Remove the artist if we've used up all of its objects. if not objs_from_artist: del objs_by_artists[artist] else: number = min(len(objs), opts.number) objs = random.sample(objs, number) for item in objs: print_obj(item, lib, template) random_cmd = Subcommand('random', help='chose a random track or album') random_cmd.parser.add_option('-a', '--album', action='store_true', help='choose an album instead of track') random_cmd.parser.add_option('-p', '--path', action='store_true', help='print the path of the matched item') random_cmd.parser.add_option('-f', '--format', action='store', help='print with custom format', default=None) random_cmd.parser.add_option('-n', '--number', action='store', type="int", help='number of objects to choose', default=1) random_cmd.parser.add_option('-e', '--equal-chance', action='store_true', help='each artist has the same chance') random_cmd.func = random_item class Random(BeetsPlugin): def commands(self): return [random_cmd] ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/replaygain.py�����������������������������������������������������������������0000644�0000765�0000024�00000054705�12405620325�020302� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2014, Fabrice Laporte, Yevgeny Bezman, and Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. import logging import subprocess import os import collections import itertools import sys import warnings from beets import ui from beets.plugins import BeetsPlugin from beets.util import syspath, command_output, displayable_path from beets import config log = logging.getLogger('beets.replaygain') # Utilities. class ReplayGainError(Exception): """Raised when a local (to a track or an album) error occurs in one of the backends. """ class FatalReplayGainError(Exception): """Raised when a fatal error occurs in one of the backends. """ def call(args): """Execute the command and return its output or raise a ReplayGainError on failure. """ try: return command_output(args) except subprocess.CalledProcessError as e: raise ReplayGainError( "{0} exited with status {1}".format(args[0], e.returncode) ) except UnicodeEncodeError: # Due to a bug in Python 2's subprocess on Windows, Unicode # filenames can fail to encode on that platform. See: # http://code.google.com/p/beets/issues/detail?id=499 raise ReplayGainError("argument encoding failed") # Backend base and plumbing classes. Gain = collections.namedtuple("Gain", "gain peak") AlbumGain = collections.namedtuple("AlbumGain", "album_gain track_gains") class Backend(object): """An abstract class representing engine for calculating RG values. """ def __init__(self, config): """Initialize the backend with the configuration view for the plugin. """ def compute_track_gain(self, items): raise NotImplementedError() def compute_album_gain(self, album): # TODO: implement album gain in terms of track gain of the # individual tracks which can be used for any backend. raise NotImplementedError() # mpgain/aacgain CLI tool backend. class CommandBackend(Backend): def __init__(self, config): config.add({ 'command': u"", 'noclip': True, }) self.command = config["command"].get(unicode) if self.command: # Explicit executable path. if not os.path.isfile(self.command): raise FatalReplayGainError( 'replaygain command does not exist: {0}'.format( self.command ) ) else: # Check whether the program is in $PATH. for cmd in ('mp3gain', 'aacgain'): try: call([cmd, '-v']) self.command = cmd except OSError: pass if not self.command: raise FatalReplayGainError( 'no replaygain command found: install mp3gain or aacgain' ) self.noclip = config['noclip'].get(bool) target_level = config['targetlevel'].as_number() self.gain_offset = int(target_level - 89) def compute_track_gain(self, items): """Computes the track gain of the given tracks, returns a list of TrackGain objects. """ supported_items = filter(self.format_supported, items) output = self.compute_gain(supported_items, False) return output def compute_album_gain(self, album): """Computes the album gain of the given album, returns an AlbumGain object. """ # TODO: What should be done when not all tracks in the album are # supported? supported_items = filter(self.format_supported, album.items()) if len(supported_items) != len(album.items()): log.debug(u'replaygain: tracks are of unsupported format') return AlbumGain(None, []) output = self.compute_gain(supported_items, True) return AlbumGain(output[-1], output[:-1]) def format_supported(self, item): """Checks whether the given item is supported by the selected tool. """ if 'mp3gain' in self.command and item.format != 'MP3': return False elif 'aacgain' in self.command and item.format not in ('MP3', 'AAC'): return False return True def compute_gain(self, items, is_album): """Computes the track or album gain of a list of items, returns a list of TrackGain objects. When computing album gain, the last TrackGain object returned is the album gain """ if len(items) == 0: return [] """Compute ReplayGain values and return a list of results dictionaries as given by `parse_tool_output`. """ # Construct shell command. The "-o" option makes the output # easily parseable (tab-delimited). "-s s" forces gain # recalculation even if tags are already present and disables # tag-writing; this turns the mp3gain/aacgain tool into a gain # calculator rather than a tag manipulator because we take care # of changing tags ourselves. cmd = [self.command, '-o', '-s', 's'] if self.noclip: # Adjust to avoid clipping. cmd = cmd + ['-k'] else: # Disable clipping warning. cmd = cmd + ['-c'] cmd = cmd + ['-a' if is_album else '-r'] cmd = cmd + ['-d', str(self.gain_offset)] cmd = cmd + [syspath(i.path) for i in items] log.debug(u'replaygain: analyzing {0} files'.format(len(items))) log.debug(u"replaygain: executing {0}" .format(" ".join(map(displayable_path, cmd)))) output = call(cmd) log.debug(u'replaygain: analysis finished') results = self.parse_tool_output(output, len(items) + (1 if is_album else 0)) return results def parse_tool_output(self, text, num_lines): """Given the tab-delimited output from an invocation of mp3gain or aacgain, parse the text and return a list of dictionaries containing information about each analyzed file. """ out = [] for line in text.split('\n')[1:num_lines + 1]: parts = line.split('\t') if len(parts) != 6 or parts[0] == 'File': log.debug(u'replaygain: bad tool output: {0}'.format(text)) raise ReplayGainError('mp3gain failed') d = { 'file': parts[0], 'mp3gain': int(parts[1]), 'gain': float(parts[2]), 'peak': float(parts[3]) / (1 << 15), 'maxgain': int(parts[4]), 'mingain': int(parts[5]), } out.append(Gain(d['gain'], d['peak'])) return out # GStreamer-based backend. class GStreamerBackend(object): def __init__(self, config): self._import_gst() # Initialized a GStreamer pipeline of the form filesrc -> # decodebin -> audioconvert -> audioresample -> rganalysis -> # fakesink The connection between decodebin and audioconvert is # handled dynamically after decodebin figures out the type of # the input file. self._src = self.Gst.ElementFactory.make("filesrc", "src") self._decbin = self.Gst.ElementFactory.make("decodebin", "decbin") self._conv = self.Gst.ElementFactory.make("audioconvert", "conv") self._res = self.Gst.ElementFactory.make("audioresample", "res") self._rg = self.Gst.ElementFactory.make("rganalysis", "rg") # We check which files need gain ourselves, so all files given # to rganalsys should have their gain computed, even if it # already exists. self._rg.set_property("forced", True) self._rg.set_property("reference-level", config["targetlevel"].as_number()) self._sink = self.Gst.ElementFactory.make("fakesink", "sink") self._pipe = self.Gst.Pipeline() self._pipe.add(self._src) self._pipe.add(self._decbin) self._pipe.add(self._conv) self._pipe.add(self._res) self._pipe.add(self._rg) self._pipe.add(self._sink) self._src.link(self._decbin) self._conv.link(self._res) self._res.link(self._rg) self._rg.link(self._sink) self._bus = self._pipe.get_bus() self._bus.add_signal_watch() self._bus.connect("message::eos", self._on_eos) self._bus.connect("message::error", self._on_error) self._bus.connect("message::tag", self._on_tag) # Needed for handling the dynamic connection between decodebin # and audioconvert self._decbin.connect("pad-added", self._on_pad_added) self._decbin.connect("pad-removed", self._on_pad_removed) self._main_loop = self.GLib.MainLoop() self._files = [] def _import_gst(self): """Import the necessary GObject-related modules and assign `Gst` and `GObject` fields on this object. """ try: import gi gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst, GLib # Calling GObject.threads_init() is not needed for # PyGObject 3.10.2+ with warnings.catch_warnings(): warnings.simplefilter("ignore") GObject.threads_init() Gst.init([sys.argv[0]]) except: raise FatalReplayGainError( "Failed to load GStreamer; check that python-gi is installed" ) self.GObject = GObject self.GLib = GLib self.Gst = Gst def compute(self, files, album): self._error = None self._files = list(files) if len(self._files) == 0: return self._file_tags = collections.defaultdict(dict) if album: self._rg.set_property("num-tracks", len(self._files)) if self._set_first_file(): self._main_loop.run() if self._error is not None: raise self._error def compute_track_gain(self, items): self.compute(items, False) if len(self._file_tags) != len(items): raise ReplayGainError("Some tracks did not receive tags") ret = [] for item in items: ret.append(Gain(self._file_tags[item]["TRACK_GAIN"], self._file_tags[item]["TRACK_PEAK"])) return ret def compute_album_gain(self, album): items = list(album.items()) self.compute(items, True) if len(self._file_tags) != len(items): raise ReplayGainError("Some items in album did not receive tags") ret = [] for item in items: ret.append(Gain(self._file_tags[item]["TRACK_GAIN"], self._file_tags[item]["TRACK_PEAK"])) last_tags = self._file_tags[items[-1]] return AlbumGain(Gain(last_tags["ALBUM_GAIN"], last_tags["ALBUM_PEAK"]), ret) def close(self): self._bus.remove_signal_watch() def _on_eos(self, bus, message): # A file finished playing in all elements of the pipeline. The # RG tags have already been propagated. If we don't have a next # file, we stop processing. if not self._set_next_file(): self._pipe.set_state(self.Gst.State.NULL) self._main_loop.quit() def _on_error(self, bus, message): self._pipe.set_state(self.Gst.State.NULL) self._main_loop.quit() err, debug = message.parse_error() f = self._src.get_property("location") # A GStreamer error, either an unsupported format or a bug. self._error = \ ReplayGainError(u"Error {0} - {1} on file {2}".format(err, debug, f)) def _on_tag(self, bus, message): tags = message.parse_tag() def handle_tag(taglist, tag, userdata): # The rganalysis element provides both the existing tags for # files and the new computes tags. In order to ensure we # store the computed tags, we overwrite the RG values of # received a second time. if tag == self.Gst.TAG_TRACK_GAIN: self._file_tags[self._file]["TRACK_GAIN"] = \ taglist.get_double(tag)[1] elif tag == self.Gst.TAG_TRACK_PEAK: self._file_tags[self._file]["TRACK_PEAK"] = \ taglist.get_double(tag)[1] elif tag == self.Gst.TAG_ALBUM_GAIN: self._file_tags[self._file]["ALBUM_GAIN"] = \ taglist.get_double(tag)[1] elif tag == self.Gst.TAG_ALBUM_PEAK: self._file_tags[self._file]["ALBUM_PEAK"] = \ taglist.get_double(tag)[1] elif tag == self.Gst.TAG_REFERENCE_LEVEL: self._file_tags[self._file]["REFERENCE_LEVEL"] = \ taglist.get_double(tag)[1] tags.foreach(handle_tag, None) def _set_first_file(self): if len(self._files) == 0: return False self._file = self._files.pop(0) self._pipe.set_state(self.Gst.State.NULL) self._src.set_property("location", syspath(self._file.path)) self._pipe.set_state(self.Gst.State.PLAYING) return True def _set_file(self): """Initialize the filesrc element with the next file to be analyzed. """ # No more files, we're done if len(self._files) == 0: return False self._file = self._files.pop(0) # Disconnect the decodebin element from the pipeline, set its # state to READY to to clear it. self._decbin.unlink(self._conv) self._decbin.set_state(self.Gst.State.READY) # Set a new file on the filesrc element, can only be done in the # READY state self._src.set_state(self.Gst.State.READY) self._src.set_property("location", syspath(self._file.path)) # Ensure the filesrc element received the paused state of the # pipeline in a blocking manner self._src.sync_state_with_parent() self._src.get_state(self.Gst.CLOCK_TIME_NONE) # Ensure the decodebin element receives the paused state of the # pipeline in a blocking manner self._decbin.sync_state_with_parent() self._decbin.get_state(self.Gst.CLOCK_TIME_NONE) return True def _set_next_file(self): """Set the next file to be analyzed while keeping the pipeline in the PAUSED state so that the rganalysis element can correctly handle album gain. """ # A blocking pause self._pipe.set_state(self.Gst.State.PAUSED) self._pipe.get_state(self.Gst.CLOCK_TIME_NONE) # Try setting the next file ret = self._set_file() if ret: # Seek to the beginning in order to clear the EOS state of the # various elements of the pipeline self._pipe.seek_simple(self.Gst.Format.TIME, self.Gst.SeekFlags.FLUSH, 0) self._pipe.set_state(self.Gst.State.PLAYING) return ret def _on_pad_added(self, decbin, pad): sink_pad = self._conv.get_compatible_pad(pad, None) assert(sink_pad is not None) pad.link(sink_pad) def _on_pad_removed(self, decbin, pad): # Called when the decodebin element is disconnected from the # rest of the pipeline while switching input files peer = pad.get_peer() assert(peer is None) # Main plugin logic. class ReplayGainPlugin(BeetsPlugin): """Provides ReplayGain analysis. """ backends = { "command": CommandBackend, "gstreamer": GStreamerBackend, } def __init__(self): super(ReplayGainPlugin, self).__init__() self.import_stages = [self.imported] # default backend is 'command' for backward-compatibility. self.config.add({ 'overwrite': False, 'auto': True, 'backend': u'command', 'targetlevel': 89, }) self.overwrite = self.config['overwrite'].get(bool) self.automatic = self.config['auto'].get(bool) backend_name = self.config['backend'].get(unicode) if backend_name not in self.backends: raise ui.UserError( u"Selected ReplayGain backend {0} is not supported. " u"Please select one of: {1}".format( backend_name, u', '.join(self.backends.keys()) ) ) try: self.backend_instance = self.backends[backend_name]( self.config ) except (ReplayGainError, FatalReplayGainError) as e: raise ui.UserError( 'An error occurred in backend initialization: {0}'.format(e) ) def track_requires_gain(self, item): return self.overwrite or \ (not item.rg_track_gain or not item.rg_track_peak) def album_requires_gain(self, album): # Skip calculating gain only when *all* files don't need # recalculation. This way, if any file among an album's tracks # needs recalculation, we still get an accurate album gain # value. return self.overwrite or \ any([not item.rg_album_gain or not item.rg_album_peak for item in album.items()]) def store_track_gain(self, item, track_gain): item.rg_track_gain = track_gain.gain item.rg_track_peak = track_gain.peak item.store() log.debug(u'replaygain: applied track gain {0}, peak {1}'.format( item.rg_track_gain, item.rg_track_peak )) def store_album_gain(self, album, album_gain): album.rg_album_gain = album_gain.gain album.rg_album_peak = album_gain.peak album.store() log.debug(u'replaygain: applied album gain {0}, peak {1}'.format( album.rg_album_gain, album.rg_album_peak)) def handle_album(self, album, write): """Compute album and track replay gain store it in all of the album's items. If ``write`` is truthy then ``item.write()`` is called for each item. If replay gain information is already present in all items, nothing is done. """ if not self.album_requires_gain(album): log.info(u'Skipping album {0} - {1}'.format(album.albumartist, album.album)) return log.info(u'analyzing {0} - {1}'.format(album.albumartist, album.album)) try: album_gain = self.backend_instance.compute_album_gain(album) if len(album_gain.track_gains) != len(album.items()): raise ReplayGainError( u"ReplayGain backend failed " u"for some tracks in album {0} - {1}".format( album.albumartist, album.album ) ) self.store_album_gain(album, album_gain.album_gain) for item, track_gain in itertools.izip(album.items(), album_gain.track_gains): self.store_track_gain(item, track_gain) if write: item.try_write() except ReplayGainError as e: log.info(u"ReplayGain error: {0}".format(e)) except FatalReplayGainError as e: raise ui.UserError( u"Fatal replay gain error: {0}".format(e) ) def handle_track(self, item, write): """Compute track replay gain and store it in the item. If ``write`` is truthy then ``item.write()`` is called to write the data to disk. If replay gain information is already present in the item, nothing is done. """ if not self.track_requires_gain(item): log.info(u'Skipping track {0} - {1}' .format(item.artist, item.title)) return log.info(u'analyzing {0} - {1}' .format(item.artist, item.title)) try: track_gains = self.backend_instance.compute_track_gain([item]) if len(track_gains) != 1: raise ReplayGainError( u"ReplayGain backend failed for track {0} - {1}".format( item.artist, item.title ) ) self.store_track_gain(item, track_gains[0]) if write: item.try_write() except ReplayGainError as e: log.info(u"ReplayGain error: {0}".format(e)) except FatalReplayGainError as e: raise ui.UserError( u"Fatal replay gain error: {0}".format(e) ) def imported(self, session, task): """Add replay gain info to items or albums of ``task``. """ if not self.automatic: return log.setLevel(logging.WARN) if task.is_album: self.handle_album(task.album, False) else: self.handle_track(task.item, False) def commands(self): """Return the "replaygain" ui subcommand. """ def func(lib, opts, args): log.setLevel(logging.INFO) write = config['import']['write'].get(bool) if opts.album: for album in lib.albums(ui.decargs(args)): self.handle_album(album, write) else: for item in lib.items(ui.decargs(args)): self.handle_track(item, write) cmd = ui.Subcommand('replaygain', help='analyze for ReplayGain') cmd.parser.add_option('-a', '--album', action='store_true', help='analyze albums instead of tracks') cmd.func = func return [cmd] �����������������������������������������������������������beets-1.3.8/beetsplug/rewrite.py��������������������������������������������������������������������0000644�0000765�0000024�00000005174�12405373453�017633� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Uses user-specified rewriting rules to canonicalize names for path formats. """ import re import logging from collections import defaultdict from beets.plugins import BeetsPlugin from beets import ui from beets import library log = logging.getLogger('beets') def rewriter(field, rules): """Create a template field function that rewrites the given field with the given rewriting rules. ``rules`` must be a list of (pattern, replacement) pairs. """ def fieldfunc(item): value = item._values_fixed[field] for pattern, replacement in rules: if pattern.match(value.lower()): # Rewrite activated. return replacement # Not activated; return original value. return value return fieldfunc class RewritePlugin(BeetsPlugin): def __init__(self): super(RewritePlugin, self).__init__() self.config.add({}) # Gather all the rewrite rules for each field. rules = defaultdict(list) for key, view in self.config.items(): value = view.get(unicode) try: fieldname, pattern = key.split(None, 1) except ValueError: raise ui.UserError("invalid rewrite specification") if fieldname not in library.Item._fields: raise ui.UserError("invalid field name (%s) in rewriter" % fieldname) log.debug(u'adding template field {0}'.format(key)) pattern = re.compile(pattern.lower()) rules[fieldname].append((pattern, value)) if fieldname == 'artist': # Special case for the artist field: apply the same # rewrite for "albumartist" as well. rules['albumartist'].append((pattern, value)) # Replace each template field with the new rewriter function. for fieldname, fieldrules in rules.iteritems(): self.template_fields[fieldname] = rewriter(fieldname, fieldrules) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/scrub.py����������������������������������������������������������������������0000644�0000765�0000024�00000011043�12405373453�017260� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Cleans extraneous metadata from files' tags via a command or automatically whenever tags are written. """ import logging from beets.plugins import BeetsPlugin from beets import ui from beets import util from beets import config from beets import mediafile log = logging.getLogger('beets') _MUTAGEN_FORMATS = { 'asf': 'ASF', 'apev2': 'APEv2File', 'flac': 'FLAC', 'id3': 'ID3FileType', 'mp3': 'MP3', 'mp4': 'MP4', 'oggflac': 'OggFLAC', 'oggspeex': 'OggSpeex', 'oggtheora': 'OggTheora', 'oggvorbis': 'OggVorbis', 'oggopus': 'OggOpus', 'trueaudio': 'TrueAudio', 'wavpack': 'WavPack', 'monkeysaudio': 'MonkeysAudio', 'optimfrog': 'OptimFROG', } scrubbing = False class ScrubPlugin(BeetsPlugin): """Removes extraneous metadata from files' tags.""" def __init__(self): super(ScrubPlugin, self).__init__() self.config.add({ 'auto': True, }) def commands(self): def scrub_func(lib, opts, args): # This is a little bit hacky, but we set a global flag to # avoid autoscrubbing when we're also explicitly scrubbing. global scrubbing scrubbing = True # Walk through matching files and remove tags. for item in lib.items(ui.decargs(args)): log.info(u'scrubbing: {0}'.format( util.displayable_path(item.path))) # Get album art if we need to restore it. if opts.write: mf = mediafile.MediaFile(item.path, config['id3v23'].get(bool)) art = mf.art # Remove all tags. _scrub(item.path) # Restore tags, if enabled. if opts.write: log.debug(u'writing new tags after scrub') item.try_write() if art: log.info(u'restoring art') mf = mediafile.MediaFile(item.path) mf.art = art mf.save() scrubbing = False scrub_cmd = ui.Subcommand('scrub', help='clean audio tags') scrub_cmd.parser.add_option('-W', '--nowrite', dest='write', action='store_false', default=True, help='leave tags empty') scrub_cmd.func = scrub_func return [scrub_cmd] def _mutagen_classes(): """Get a list of file type classes from the Mutagen module. """ classes = [] for modname, clsname in _MUTAGEN_FORMATS.items(): mod = __import__('mutagen.{0}'.format(modname), fromlist=[clsname]) classes.append(getattr(mod, clsname)) return classes def _scrub(path): """Remove all tags from a file. """ for cls in _mutagen_classes(): # Try opening the file with this type, but just skip in the # event of any error. try: f = cls(util.syspath(path)) except Exception: continue if f.tags is None: continue # Remove the tag for this type. try: f.delete() except NotImplementedError: # Some Mutagen metadata subclasses (namely, ASFTag) do not # support .delete(), presumably because it is impossible to # remove them. In this case, we just remove all the tags. for tag in f.keys(): del f[tag] f.save() except IOError as exc: log.error(u'could not scrub {0}: {1}'.format( util.displayable_path(path), exc, )) # Automatically embed art into imported albums. @ScrubPlugin.listen('write') def write_item(path): if not scrubbing and config['scrub']['auto']: log.debug(u'auto-scrubbing {0}'.format(util.displayable_path(path))) _scrub(path) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/smartplaylist.py��������������������������������������������������������������0000644�0000765�0000024�00000010152�12405710135�021042� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2013, Dang Mai <contact@dangmai.net>. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Generates smart playlists based on beets queries. """ from __future__ import print_function from beets.plugins import BeetsPlugin from beets import config, ui, library from beets import dbcore from beets.util import normpath, syspath import os # Global variable so that smartplaylist can detect database changes and run # only once before beets exits. database_changed = False def _items_for_query(lib, playlist, album=False): """Get the matching items for a playlist's configured queries. `album` indicates whether to process the item-level query or the album-level query (if any). """ key = 'album_query' if album else 'query' if key not in playlist: return [] # Parse quer(ies). If it's a list, join the queries with OR. query_strings = playlist[key] if not isinstance(query_strings, (list, tuple)): query_strings = [query_strings] model = library.Album if album else library.Item query = dbcore.OrQuery( [library.parse_query_string(q, model)[0] for q in query_strings] ) # Execute query, depending on type. if album: result = [] for album in lib.albums(query): result.extend(album.items()) return result else: return lib.items(query) def update_playlists(lib): ui.print_("Updating smart playlists...") playlists = config['smartplaylist']['playlists'].get(list) playlist_dir = config['smartplaylist']['playlist_dir'].as_filename() relative_to = config['smartplaylist']['relative_to'].get() if relative_to: relative_to = normpath(relative_to) for playlist in playlists: items = [] items.extend(_items_for_query(lib, playlist, True)) items.extend(_items_for_query(lib, playlist, False)) m3us = {} basename = playlist['name'].encode('utf8') # As we allow tags in the m3u names, we'll need to iterate through # the items and generate the correct m3u file names. for item in items: m3u_name = item.evaluate_template(basename, True) if not (m3u_name in m3us): m3us[m3u_name] = [] item_path = item.path if relative_to: item_path = os.path.relpath(item.path, relative_to) if item_path not in m3us[m3u_name]: m3us[m3u_name].append(item_path) # Now iterate through the m3us that we need to generate for m3u in m3us: m3u_path = normpath(os.path.join(playlist_dir, m3u)) with open(syspath(m3u_path), 'w') as f: for path in m3us[m3u]: f.write(path + '\n') ui.print_("... Done") class SmartPlaylistPlugin(BeetsPlugin): def __init__(self): super(SmartPlaylistPlugin, self).__init__() self.config.add({ 'relative_to': None, 'playlist_dir': u'.', 'auto': True, 'playlists': [] }) def commands(self): def update(lib, opts, args): update_playlists(lib) spl_update = ui.Subcommand('splupdate', help='update the smart playlists') spl_update.func = update return [spl_update] @SmartPlaylistPlugin.listen('database_change') def handle_change(lib): global database_changed database_changed = True @SmartPlaylistPlugin.listen('cli_exit') def update(lib): auto = config['smartplaylist']['auto'] if database_changed and auto: update_playlists(lib) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/spotify.py��������������������������������������������������������������������0000644�0000765�0000024�00000013657�12405373532�017652� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from __future__ import print_function import re import webbrowser import requests import logging from beets.plugins import BeetsPlugin from beets.ui import decargs from beets import ui from requests.exceptions import HTTPError log = logging.getLogger('beets') class SpotifyPlugin(BeetsPlugin): # URL for the Web API of Spotify # Documentation here: https://developer.spotify.com/web-api/search-item/ base_url = "https://api.spotify.com/v1/search" open_url = "http://open.spotify.com/track/" playlist_partial = "spotify:trackset:Playlist:" def __init__(self): super(SpotifyPlugin, self).__init__() self.config.add({ 'mode': 'list', 'tiebreak': 'popularity', 'show_failures': False, 'artist_field': 'albumartist', 'album_field': 'album', 'track_field': 'title', 'region_filter': None, 'regex': [] }) def commands(self): def queries(lib, opts, args): success = self.parse_opts(opts) if success: results = self.query_spotify(lib, decargs(args)) self.output_results(results) spotify_cmd = ui.Subcommand( 'spotify', help='build a Spotify playlist' ) spotify_cmd.parser.add_option( '-m', '--mode', action='store', help='"open" to open Spotify with playlist, ' '"list" to print (default)' ) spotify_cmd.parser.add_option( '-f', '--show-failures', action='store_true', help='list tracks that did not match a Sptoify ID', dest='show_failures', ) spotify_cmd.func = queries return [spotify_cmd] def parse_opts(self, opts): if opts.mode: self.config['mode'].set(opts.mode) if opts.show_failures: self.config['show_failures'].set(True) if self.config['mode'].get() not in ['list', 'open']: log.warn(u'{0} is not a valid mode' .format(self.config['mode'].get())) return False self.opts = opts return True def query_spotify(self, lib, query): results = [] failures = [] items = lib.items(query) if not items: log.debug(u'Your beets query returned no items, skipping spotify') return log.info(u'Processing {0} tracks...'.format(len(items))) for item in items: # Apply regex transformations if provided for regex in self.config['regex'].get(): if ( not regex['field'] or not regex['search'] or not regex['replace'] ): continue value = item[regex['field']] item[regex['field']] = re.sub( regex['search'], regex['replace'], value ) # Custom values can be passed in the config (just in case) artist = item[self.config['artist_field'].get()] album = item[self.config['album_field'].get()] query = item[self.config['track_field'].get()] search_url = query + " album:" + album + " artist:" + artist # Query the Web API for each track, look for the items' JSON data r = requests.get(self.base_url, params={ "q": search_url, "type": "track" }) log.debug(r.url) try: r.raise_for_status() except HTTPError as e: log.debug(u'URL returned a {0} error' .format(e.response.status_code)) failures.append(search_url) continue r_data = r.json()['tracks']['items'] # Apply market filter if requested region_filter = self.config['region_filter'].get() if region_filter: r_data = filter( lambda x: region_filter in x['available_markets'], r_data ) # Simplest, take the first result chosen_result = None if len(r_data) == 1 or self.config['tiebreak'].get() == "first": log.debug(u'Spotify track(s) found, count: {0}' .format(len(r_data))) chosen_result = r_data[0] elif len(r_data) > 1: # Use the popularity filter log.debug(u'Most popular track chosen, count: {0}' .format(len(r_data))) chosen_result = max(r_data, key=lambda x: x['popularity']) if chosen_result: results.append(chosen_result) else: log.debug(u'No spotify track found: {0}'.format(search_url)) failures.append(search_url) failure_count = len(failures) if failure_count > 0: if self.config['show_failures'].get(): log.info(u'{0} track(s) did not match a Spotify ID:' .format(failure_count)) for track in failures: log.info(u'track: {0}'.format(track)) log.info(u'') else: log.warn(u'{0} track(s) did not match a Spotify ID;\n' u'use --show-failures to display' .format(failure_count)) return results def output_results(self, results): if results: ids = map(lambda x: x['id'], results) if self.config['mode'].get() == "open": log.info(u'Attempting to open Spotify with playlist') spotify_url = self.playlist_partial + ",".join(ids) webbrowser.open(spotify_url) else: for item in ids: print(unicode.encode(self.open_url + item)) else: log.warn(u'No Spotify tracks found from beets query') ���������������������������������������������������������������������������������beets-1.3.8/beetsplug/the.py������������������������������������������������������������������������0000644�0000765�0000024�00000006411�12322627452�016724� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2013, Blemjhoo Tezoulbr <baobab@heresiarch.info>. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """Moves patterns in path formats (suitable for moving articles).""" import re import logging from beets.plugins import BeetsPlugin __author__ = 'baobab@heresiarch.info' __version__ = '1.1' PATTERN_THE = u'^[the]{3}\s' PATTERN_A = u'^[a][n]?\s' FORMAT = u'{0}, {1}' class ThePlugin(BeetsPlugin): _instance = None _log = logging.getLogger('beets') the = True a = True format = u'' strip = False patterns = [] def __init__(self): super(ThePlugin, self).__init__() self.template_funcs['the'] = self.the_template_func self.config.add({ 'the': True, 'a': True, 'format': u'{0}, {1}', 'strip': False, 'patterns': [], }) self.patterns = self.config['patterns'].as_str_seq() for p in self.patterns: if p: try: re.compile(p) except re.error: self._log.error(u'[the] invalid pattern: {0}'.format(p)) else: if not (p.startswith('^') or p.endswith('$')): self._log.warn(u'[the] warning: \"{0}\" will not ' 'match string start/end'.format(p)) if self.config['a']: self.patterns = [PATTERN_A] + self.patterns if self.config['the']: self.patterns = [PATTERN_THE] + self.patterns if not self.patterns: self._log.warn(u'[the] no patterns defined!') def unthe(self, text, pattern): """Moves pattern in the path format string or strips it text -- text to handle pattern -- regexp pattern (case ignore is already on) strip -- if True, pattern will be removed """ if text: r = re.compile(pattern, flags=re.IGNORECASE) try: t = r.findall(text)[0] except IndexError: return text else: r = re.sub(r, '', text).strip() if self.config['strip']: return r else: fmt = self.config['format'].get(unicode) return fmt.format(r, t.strip()).strip() else: return u'' def the_template_func(self, text): if not self.patterns: return text if text: for p in self.patterns: r = self.unthe(text, p) if r != text: break self._log.debug(u'[the] \"{0}\" -> \"{1}\"'.format(text, r)) return r else: return u'' �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/types.py����������������������������������������������������������������������0000644�0000765�0000024�00000002756�12405357732�017323� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2014, Thomas Scholtes. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. from beets.plugins import BeetsPlugin from beets.dbcore import types from beets.util.confit import ConfigValueError from beets import library class TypesPlugin(BeetsPlugin): @property def item_types(self): if not self.config.exists(): return {} mytypes = {} for key, value in self.config.items(): if value.get() == 'int': mytypes[key] = types.INTEGER elif value.get() == 'float': mytypes[key] = types.FLOAT elif value.get() == 'bool': mytypes[key] = types.BOOLEAN elif value.get() == 'date': mytypes[key] = library.DateType() else: raise ConfigValueError( u"unknown type '{0}' for the '{1}' field" .format(value, key)) return mytypes ������������������beets-1.3.8/beetsplug/web/��������������������������������������������������������������������������0000755�0000765�0000024�00000000000�12406440351�016337� 5����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/web/__init__.py���������������������������������������������������������������0000644�0000765�0000024�00000016177�12322631007�020461� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This file is part of beets. # Copyright 2013, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """A Web interface to beets.""" from beets.plugins import BeetsPlugin from beets import ui from beets import util import beets.library import flask from flask import g from werkzeug.routing import BaseConverter, PathConverter import os import json # Utilities. def _rep(obj, expand=False): """Get a flat -- i.e., JSON-ish -- representation of a beets Item or Album object. For Albums, `expand` dictates whether tracks are included. """ out = dict(obj) if isinstance(obj, beets.library.Item): del out['path'] # Get the size (in bytes) of the backing file. This is useful # for the Tomahawk resolver API. try: out['size'] = os.path.getsize(util.syspath(obj.path)) except OSError: out['size'] = 0 return out elif isinstance(obj, beets.library.Album): del out['artpath'] if expand: out['items'] = [_rep(item) for item in obj.items()] return out def json_generator(items, root): """Generator that dumps list of beets Items or Albums as JSON :param root: root key for JSON :param items: list of :class:`Item` or :class:`Album` to dump :returns: generator that yields strings """ yield '{"%s":[' % root first = True for item in items: if first: first = False else: yield ',' yield json.dumps(_rep(item)) yield ']}' def resource(name): """Decorates a function to handle RESTful HTTP requests for a resource. """ def make_responder(retriever): def responder(ids): entities = [retriever(id) for id in ids] entities = [entity for entity in entities if entity] if len(entities) == 1: return flask.jsonify(_rep(entities[0])) elif entities: return app.response_class( json_generator(entities, root=name), mimetype='application/json' ) else: return flask.abort(404) responder.__name__ = 'get_%s' % name return responder return make_responder def resource_query(name): """Decorates a function to handle RESTful HTTP queries for resources. """ def make_responder(query_func): def responder(queries): return app.response_class( json_generator(query_func(queries), root='results'), mimetype='application/json' ) responder.__name__ = 'query_%s' % name return responder return make_responder def resource_list(name): """Decorates a function to handle RESTful HTTP request for a list of resources. """ def make_responder(list_all): def responder(): return app.response_class( json_generator(list_all(), root=name), mimetype='application/json' ) responder.__name__ = 'all_%s' % name return responder return make_responder class IdListConverter(BaseConverter): """Converts comma separated lists of ids in urls to integer lists. """ def to_python(self, value): ids = [] for id in value.split(','): try: ids.append(int(id)) except ValueError: pass return ids def to_url(self, value): return ','.join(value) class QueryConverter(PathConverter): """Converts slash separated lists of queries in the url to string list. """ def to_python(self, value): return value.split('/') def to_url(self, value): return ','.join(value) # Flask setup. app = flask.Flask(__name__) app.url_map.converters['idlist'] = IdListConverter app.url_map.converters['query'] = QueryConverter @app.before_request def before_request(): g.lib = app.config['lib'] # Items. @app.route('/item/<idlist:ids>') @resource('items') def get_item(id): return g.lib.get_item(id) @app.route('/item/') @app.route('/item/query/') @resource_list('items') def all_items(): return g.lib.items() @app.route('/item/<int:item_id>/file') def item_file(item_id): item = g.lib.get_item(item_id) response = flask.send_file(item.path, as_attachment=True, attachment_filename=os.path.basename(item.path)) response.headers['Content-Length'] = os.path.getsize(item.path) return response @app.route('/item/query/<query:queries>') @resource_query('items') def item_query(queries): return g.lib.items(queries) # Albums. @app.route('/album/<idlist:ids>') @resource('albums') def get_album(id): return g.lib.get_album(id) @app.route('/album/') @app.route('/album/query/') @resource_list('albums') def all_albums(): return g.lib.albums() @app.route('/album/query/<query:queries>') @resource_query('albums') def album_query(queries): return g.lib.albums(queries) @app.route('/album/<int:album_id>/art') def album_art(album_id): album = g.lib.get_album(album_id) return flask.send_file(album.artpath) # Artists. @app.route('/artist/') def all_artists(): with g.lib.transaction() as tx: rows = tx.query("SELECT DISTINCT albumartist FROM albums") all_artists = [row[0] for row in rows] return flask.jsonify(artist_names=all_artists) # Library information. @app.route('/stats') def stats(): with g.lib.transaction() as tx: item_rows = tx.query("SELECT COUNT(*) FROM items") album_rows = tx.query("SELECT COUNT(*) FROM albums") return flask.jsonify({ 'items': item_rows[0][0], 'albums': album_rows[0][0], }) # UI. @app.route('/') def home(): return flask.render_template('index.html') # Plugin hook. class WebPlugin(BeetsPlugin): def __init__(self): super(WebPlugin, self).__init__() self.config.add({ 'host': u'', 'port': 8337, }) def commands(self): cmd = ui.Subcommand('web', help='start a Web interface') cmd.parser.add_option('-d', '--debug', action='store_true', default=False, help='debug mode') def func(lib, opts, args): args = ui.decargs(args) if args: self.config['host'] = args.pop(0) if args: self.config['port'] = int(args.pop(0)) app.config['lib'] = lib app.run(host=self.config['host'].get(unicode), port=self.config['port'].get(int), debug=opts.debug, threaded=True) cmd.func = func return [cmd] �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/web/static/�������������������������������������������������������������������0000755�0000765�0000024�00000000000�12406440351�017626� 5����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/web/static/backbone.js��������������������������������������������������������0000644�0000765�0000024�00000123137�12313347401�021736� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// Backbone.js 0.5.3 // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. // Backbone may be freely distributed under the MIT license. // For all details and documentation: // http://documentcloud.github.com/backbone (function(){ // Initial Setup // ------------- // Save a reference to the global object. var root = this; // Save the previous value of the `Backbone` variable. var previousBackbone = root.Backbone; // The top-level namespace. All public Backbone classes and modules will // be attached to this. Exported for both CommonJS and the browser. var Backbone; if (typeof exports !== 'undefined') { Backbone = exports; } else { Backbone = root.Backbone = {}; } // Current version of the library. Keep in sync with `package.json`. Backbone.VERSION = '0.5.3'; // Require Underscore, if we're on the server, and it's not already present. var _ = root._; if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._; // For Backbone's purposes, jQuery or Zepto owns the `$` variable. var $ = root.jQuery || root.Zepto; // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable // to its previous owner. Returns a reference to this Backbone object. Backbone.noConflict = function() { root.Backbone = previousBackbone; return this; }; // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option will // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a // `X-Http-Method-Override` header. Backbone.emulateHTTP = false; // Turn on `emulateJSON` to support legacy servers that can't deal with direct // `application/json` requests ... will encode the body as // `application/x-www-form-urlencoded` instead and will send the model in a // form param named `model`. Backbone.emulateJSON = false; // Backbone.Events // ----------------- // A module that can be mixed in to *any object* in order to provide it with // custom events. You may `bind` or `unbind` a callback function to an event; // `trigger`-ing an event fires all callbacks in succession. // // var object = {}; // _.extend(object, Backbone.Events); // object.bind('expand', function(){ alert('expanded'); }); // object.trigger('expand'); // Backbone.Events = { // Bind an event, specified by a string name, `ev`, to a `callback` function. // Passing `"all"` will bind the callback to all events fired. bind : function(ev, callback, context) { var calls = this._callbacks || (this._callbacks = {}); var list = calls[ev] || (calls[ev] = []); list.push([callback, context]); return this; }, // Remove one or many callbacks. If `callback` is null, removes all // callbacks for the event. If `ev` is null, removes all bound callbacks // for all events. unbind : function(ev, callback) { var calls; if (!ev) { this._callbacks = {}; } else if (calls = this._callbacks) { if (!callback) { calls[ev] = []; } else { var list = calls[ev]; if (!list) return this; for (var i = 0, l = list.length; i < l; i++) { if (list[i] && callback === list[i][0]) { list[i] = null; break; } } } } return this; }, // Trigger an event, firing all bound callbacks. Callbacks are passed the // same arguments as `trigger` is, apart from the event name. // Listening for `"all"` passes the true event name as the first argument. trigger : function(eventName) { var list, calls, ev, callback, args; var both = 2; if (!(calls = this._callbacks)) return this; while (both--) { ev = both ? eventName : 'all'; if (list = calls[ev]) { for (var i = 0, l = list.length; i < l; i++) { if (!(callback = list[i])) { list.splice(i, 1); i--; l--; } else { args = both ? Array.prototype.slice.call(arguments, 1) : arguments; callback[0].apply(callback[1] || this, args); } } } } return this; } }; // Backbone.Model // -------------- // Create a new model, with defined attributes. A client id (`cid`) // is automatically generated and assigned for you. Backbone.Model = function(attributes, options) { var defaults; attributes || (attributes = {}); if (defaults = this.defaults) { if (_.isFunction(defaults)) defaults = defaults.call(this); attributes = _.extend({}, defaults, attributes); } this.attributes = {}; this._escapedAttributes = {}; this.cid = _.uniqueId('c'); this.set(attributes, {silent : true}); this._changed = false; this._previousAttributes = _.clone(this.attributes); if (options && options.collection) this.collection = options.collection; this.initialize(attributes, options); }; // Attach all inheritable methods to the Model prototype. _.extend(Backbone.Model.prototype, Backbone.Events, { // A snapshot of the model's previous attributes, taken immediately // after the last `"change"` event was fired. _previousAttributes : null, // Has the item been changed since the last `"change"` event? _changed : false, // The default name for the JSON `id` attribute is `"id"`. MongoDB and // CouchDB users may want to set this to `"_id"`. idAttribute : 'id', // Initialize is an empty function by default. Override it with your own // initialization logic. initialize : function(){}, // Return a copy of the model's `attributes` object. toJSON : function() { return _.clone(this.attributes); }, // Get the value of an attribute. get : function(attr) { return this.attributes[attr]; }, // Get the HTML-escaped value of an attribute. escape : function(attr) { var html; if (html = this._escapedAttributes[attr]) return html; var val = this.attributes[attr]; return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val); }, // Returns `true` if the attribute contains a value that is not null // or undefined. has : function(attr) { return this.attributes[attr] != null; }, // Set a hash of model attributes on the object, firing `"change"` unless you // choose to silence it. set : function(attrs, options) { // Extract attributes and options. options || (options = {}); if (!attrs) return this; if (attrs.attributes) attrs = attrs.attributes; var now = this.attributes, escaped = this._escapedAttributes; // Run validation. if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false; // Check for changes of `id`. if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; // We're about to start triggering change events. var alreadyChanging = this._changing; this._changing = true; // Update attributes. for (var attr in attrs) { var val = attrs[attr]; if (!_.isEqual(now[attr], val)) { now[attr] = val; delete escaped[attr]; this._changed = true; if (!options.silent) this.trigger('change:' + attr, this, val, options); } } // Fire the `"change"` event, if the model has been changed. if (!alreadyChanging && !options.silent && this._changed) this.change(options); this._changing = false; return this; }, // Remove an attribute from the model, firing `"change"` unless you choose // to silence it. `unset` is a noop if the attribute doesn't exist. unset : function(attr, options) { if (!(attr in this.attributes)) return this; options || (options = {}); var value = this.attributes[attr]; // Run validation. var validObj = {}; validObj[attr] = void 0; if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; // Remove the attribute. delete this.attributes[attr]; delete this._escapedAttributes[attr]; if (attr == this.idAttribute) delete this.id; this._changed = true; if (!options.silent) { this.trigger('change:' + attr, this, void 0, options); this.change(options); } return this; }, // Clear all attributes on the model, firing `"change"` unless you choose // to silence it. clear : function(options) { options || (options = {}); var attr; var old = this.attributes; // Run validation. var validObj = {}; for (attr in old) validObj[attr] = void 0; if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; this.attributes = {}; this._escapedAttributes = {}; this._changed = true; if (!options.silent) { for (attr in old) { this.trigger('change:' + attr, this, void 0, options); } this.change(options); } return this; }, // Fetch the model from the server. If the server's representation of the // model differs from its current attributes, they will be overriden, // triggering a `"change"` event. fetch : function(options) { options || (options = {}); var model = this; var success = options.success; options.success = function(resp, status, xhr) { if (!model.set(model.parse(resp, xhr), options)) return false; if (success) success(model, resp); }; options.error = wrapError(options.error, model, options); return (this.sync || Backbone.sync).call(this, 'read', this, options); }, // Set a hash of model attributes, and sync the model to the server. // If the server returns an attributes hash that differs, the model's // state will be `set` again. save : function(attrs, options) { options || (options = {}); if (attrs && !this.set(attrs, options)) return false; var model = this; var success = options.success; options.success = function(resp, status, xhr) { if (!model.set(model.parse(resp, xhr), options)) return false; if (success) success(model, resp, xhr); }; options.error = wrapError(options.error, model, options); var method = this.isNew() ? 'create' : 'update'; return (this.sync || Backbone.sync).call(this, method, this, options); }, // Destroy this model on the server if it was already persisted. Upon success, the model is removed // from its collection, if it has one. destroy : function(options) { options || (options = {}); if (this.isNew()) return this.trigger('destroy', this, this.collection, options); var model = this; var success = options.success; options.success = function(resp) { model.trigger('destroy', model, model.collection, options); if (success) success(model, resp); }; options.error = wrapError(options.error, model, options); return (this.sync || Backbone.sync).call(this, 'delete', this, options); }, // Default URL for the model's representation on the server -- if you're // using Backbone's restful methods, override this to change the endpoint // that will be called. url : function() { var base = getUrl(this.collection) || this.urlRoot || urlError(); if (this.isNew()) return base; return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); }, // **parse** converts a response into the hash of attributes to be `set` on // the model. The default implementation is just to pass the response along. parse : function(resp, xhr) { return resp; }, // Create a new model with identical attributes to this one. clone : function() { return new this.constructor(this); }, // A model is new if it has never been saved to the server, and lacks an id. isNew : function() { return this.id == null; }, // Call this method to manually fire a `change` event for this model. // Calling this will cause all objects observing the model to update. change : function(options) { this.trigger('change', this, options); this._previousAttributes = _.clone(this.attributes); this._changed = false; }, // Determine if the model has changed since the last `"change"` event. // If you specify an attribute name, determine if that attribute has changed. hasChanged : function(attr) { if (attr) return this._previousAttributes[attr] != this.attributes[attr]; return this._changed; }, // Return an object containing all the attributes that have changed, or false // if there are no changed attributes. Useful for determining what parts of a // view need to be updated and/or what attributes need to be persisted to // the server. changedAttributes : function(now) { now || (now = this.attributes); var old = this._previousAttributes; var changed = false; for (var attr in now) { if (!_.isEqual(old[attr], now[attr])) { changed = changed || {}; changed[attr] = now[attr]; } } return changed; }, // Get the previous value of an attribute, recorded at the time the last // `"change"` event was fired. previous : function(attr) { if (!attr || !this._previousAttributes) return null; return this._previousAttributes[attr]; }, // Get all of the attributes of the model at the time of the previous // `"change"` event. previousAttributes : function() { return _.clone(this._previousAttributes); }, // Run validation against a set of incoming attributes, returning `true` // if all is well. If a specific `error` callback has been passed, // call that instead of firing the general `"error"` event. _performValidation : function(attrs, options) { var error = this.validate(attrs); if (error) { if (options.error) { options.error(this, error, options); } else { this.trigger('error', this, error, options); } return false; } return true; } }); // Backbone.Collection // ------------------- // Provides a standard collection class for our sets of models, ordered // or unordered. If a `comparator` is specified, the Collection will maintain // its models in sort order, as they're added and removed. Backbone.Collection = function(models, options) { options || (options = {}); if (options.comparator) this.comparator = options.comparator; _.bindAll(this, '_onModelEvent', '_removeReference'); this._reset(); if (models) this.reset(models, {silent: true}); this.initialize.apply(this, arguments); }; // Define the Collection's inheritable methods. _.extend(Backbone.Collection.prototype, Backbone.Events, { // The default model for a collection is just a **Backbone.Model**. // This should be overridden in most cases. model : Backbone.Model, // Initialize is an empty function by default. Override it with your own // initialization logic. initialize : function(){}, // The JSON representation of a Collection is an array of the // models' attributes. toJSON : function() { return this.map(function(model){ return model.toJSON(); }); }, // Add a model, or list of models to the set. Pass **silent** to avoid // firing the `added` event for every new model. add : function(models, options) { if (_.isArray(models)) { for (var i = 0, l = models.length; i < l; i++) { this._add(models[i], options); } } else { this._add(models, options); } return this; }, // Remove a model, or a list of models from the set. Pass silent to avoid // firing the `removed` event for every model removed. remove : function(models, options) { if (_.isArray(models)) { for (var i = 0, l = models.length; i < l; i++) { this._remove(models[i], options); } } else { this._remove(models, options); } return this; }, // Get a model from the set by id. get : function(id) { if (id == null) return null; return this._byId[id.id != null ? id.id : id]; }, // Get a model from the set by client id. getByCid : function(cid) { return cid && this._byCid[cid.cid || cid]; }, // Get the model at the given index. at: function(index) { return this.models[index]; }, // Force the collection to re-sort itself. You don't need to call this under normal // circumstances, as the set will maintain sort order as each item is added. sort : function(options) { options || (options = {}); if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); this.models = this.sortBy(this.comparator); if (!options.silent) this.trigger('reset', this, options); return this; }, // Pluck an attribute from each model in the collection. pluck : function(attr) { return _.map(this.models, function(model){ return model.get(attr); }); }, // When you have more items than you want to add or remove individually, // you can reset the entire set with a new list of models, without firing // any `added` or `removed` events. Fires `reset` when finished. reset : function(models, options) { models || (models = []); options || (options = {}); this.each(this._removeReference); this._reset(); this.add(models, {silent: true}); if (!options.silent) this.trigger('reset', this, options); return this; }, // Fetch the default set of models for this collection, resetting the // collection when they arrive. If `add: true` is passed, appends the // models to the collection instead of resetting. fetch : function(options) { options || (options = {}); var collection = this; var success = options.success; options.success = function(resp, status, xhr) { collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); if (success) success(collection, resp); }; options.error = wrapError(options.error, collection, options); return (this.sync || Backbone.sync).call(this, 'read', this, options); }, // Create a new instance of a model in this collection. After the model // has been created on the server, it will be added to the collection. // Returns the model, or 'false' if validation on a new model fails. create : function(model, options) { var coll = this; options || (options = {}); model = this._prepareModel(model, options); if (!model) return false; var success = options.success; options.success = function(nextModel, resp, xhr) { coll.add(nextModel, options); if (success) success(nextModel, resp, xhr); }; model.save(null, options); return model; }, // **parse** converts a response into a list of models to be added to the // collection. The default implementation is just to pass it through. parse : function(resp, xhr) { return resp; }, // Proxy to _'s chain. Can't be proxied the same way the rest of the // underscore methods are proxied because it relies on the underscore // constructor. chain: function () { return _(this.models).chain(); }, // Reset all internal state. Called when the collection is reset. _reset : function(options) { this.length = 0; this.models = []; this._byId = {}; this._byCid = {}; }, // Prepare a model to be added to this collection _prepareModel: function(model, options) { if (!(model instanceof Backbone.Model)) { var attrs = model; model = new this.model(attrs, {collection: this}); if (model.validate && !model._performValidation(attrs, options)) model = false; } else if (!model.collection) { model.collection = this; } return model; }, // Internal implementation of adding a single model to the set, updating // hash indexes for `id` and `cid` lookups. // Returns the model, or 'false' if validation on a new model fails. _add : function(model, options) { options || (options = {}); model = this._prepareModel(model, options); if (!model) return false; var already = this.getByCid(model); if (already) throw new Error(["Can't add the same model to a set twice", already.id]); this._byId[model.id] = model; this._byCid[model.cid] = model; var index = options.at != null ? options.at : this.comparator ? this.sortedIndex(model, this.comparator) : this.length; this.models.splice(index, 0, model); model.bind('all', this._onModelEvent); this.length++; if (!options.silent) model.trigger('add', model, this, options); return model; }, // Internal implementation of removing a single model from the set, updating // hash indexes for `id` and `cid` lookups. _remove : function(model, options) { options || (options = {}); model = this.getByCid(model) || this.get(model); if (!model) return null; delete this._byId[model.id]; delete this._byCid[model.cid]; this.models.splice(this.indexOf(model), 1); this.length--; if (!options.silent) model.trigger('remove', model, this, options); this._removeReference(model); return model; }, // Internal method to remove a model's ties to a collection. _removeReference : function(model) { if (this == model.collection) { delete model.collection; } model.unbind('all', this._onModelEvent); }, // Internal method called every time a model in the set fires an event. // Sets need to update their indexes when models change ids. All other // events simply proxy through. "add" and "remove" events that originate // in other collections are ignored. _onModelEvent : function(ev, model, collection, options) { if ((ev == 'add' || ev == 'remove') && collection != this) return; if (ev == 'destroy') { this._remove(model, options); } if (model && ev === 'change:' + model.idAttribute) { delete this._byId[model.previous(model.idAttribute)]; this._byId[model.id] = model; } this.trigger.apply(this, arguments); } }); // Underscore methods that we want to implement on the Collection. var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy']; // Mix in each Underscore method as a proxy to `Collection#models`. _.each(methods, function(method) { Backbone.Collection.prototype[method] = function() { return _[method].apply(_, [this.models].concat(_.toArray(arguments))); }; }); // Backbone.Router // ------------------- // Routers map faux-URLs to actions, and fire events when routes are // matched. Creating a new one sets its `routes` hash, if not set statically. Backbone.Router = function(options) { options || (options = {}); if (options.routes) this.routes = options.routes; this._bindRoutes(); this.initialize.apply(this, arguments); }; // Cached regular expressions for matching named param parts and splatted // parts of route strings. var namedParam = /:([\w\d]+)/g; var splatParam = /\*([\w\d]+)/g; var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; // Set up all inheritable **Backbone.Router** properties and methods. _.extend(Backbone.Router.prototype, Backbone.Events, { // Initialize is an empty function by default. Override it with your own // initialization logic. initialize : function(){}, // Manually bind a single named route to a callback. For example: // // this.route('search/:query/p:num', 'search', function(query, num) { // ... // }); // route : function(route, name, callback) { Backbone.history || (Backbone.history = new Backbone.History); if (!_.isRegExp(route)) route = this._routeToRegExp(route); Backbone.history.route(route, _.bind(function(fragment) { var args = this._extractParameters(route, fragment); callback.apply(this, args); this.trigger.apply(this, ['route:' + name].concat(args)); }, this)); }, // Simple proxy to `Backbone.history` to save a fragment into the history. navigate : function(fragment, triggerRoute) { Backbone.history.navigate(fragment, triggerRoute); }, // Bind all defined routes to `Backbone.history`. We have to reverse the // order of the routes here to support behavior where the most general // routes can be defined at the bottom of the route map. _bindRoutes : function() { if (!this.routes) return; var routes = []; for (var route in this.routes) { routes.unshift([route, this.routes[route]]); } for (var i = 0, l = routes.length; i < l; i++) { this.route(routes[i][0], routes[i][1], this[routes[i][1]]); } }, // Convert a route string into a regular expression, suitable for matching // against the current location hash. _routeToRegExp : function(route) { route = route.replace(escapeRegExp, "\\$&") .replace(namedParam, "([^\/]*)") .replace(splatParam, "(.*?)"); return new RegExp('^' + route + '$'); }, // Given a route, and a URL fragment that it matches, return the array of // extracted parameters. _extractParameters : function(route, fragment) { return route.exec(fragment).slice(1); } }); // Backbone.History // ---------------- // Handles cross-browser history management, based on URL fragments. If the // browser does not support `onhashchange`, falls back to polling. Backbone.History = function() { this.handlers = []; _.bindAll(this, 'checkUrl'); }; // Cached regex for cleaning hashes. var hashStrip = /^#*/; // Cached regex for detecting MSIE. var isExplorer = /msie [\w.]+/; // Has the history handling already been started? var historyStarted = false; // Set up all inheritable **Backbone.History** properties and methods. _.extend(Backbone.History.prototype, { // The default interval to poll for hash changes, if necessary, is // twenty times a second. interval: 50, // Get the cross-browser normalized URL fragment, either from the URL, // the hash, or the override. getFragment : function(fragment, forcePushState) { if (fragment == null) { if (this._hasPushState || forcePushState) { fragment = window.location.pathname; var search = window.location.search; if (search) fragment += search; if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length); } else { fragment = window.location.hash; } } return decodeURIComponent(fragment.replace(hashStrip, '')); }, // Start the hash change handling, returning `true` if the current URL matches // an existing route, and `false` otherwise. start : function(options) { // Figure out the initial configuration. Do we need an iframe? // Is pushState desired ... is it available? if (historyStarted) throw new Error("Backbone.history has already been started"); this.options = _.extend({}, {root: '/'}, this.options, options); this._wantsPushState = !!this.options.pushState; this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); var fragment = this.getFragment(); var docMode = document.documentMode; var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); if (oldIE) { this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow; this.navigate(fragment); } // Depending on whether we're using pushState or hashes, and whether // 'onhashchange' is supported, determine how we check the URL state. if (this._hasPushState) { $(window).bind('popstate', this.checkUrl); } else if ('onhashchange' in window && !oldIE) { $(window).bind('hashchange', this.checkUrl); } else { setInterval(this.checkUrl, this.interval); } // Determine if we need to change the base url, for a pushState link // opened by a non-pushState browser. this.fragment = fragment; historyStarted = true; var loc = window.location; var atRoot = loc.pathname == this.options.root; if (this._wantsPushState && !this._hasPushState && !atRoot) { this.fragment = this.getFragment(null, true); window.location.replace(this.options.root + '#' + this.fragment); // Return immediately as browser will do redirect to new url return true; } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) { this.fragment = loc.hash.replace(hashStrip, ''); window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment); } if (!this.options.silent) { return this.loadUrl(); } }, // Add a route to be tested when the fragment changes. Routes added later may // override previous routes. route : function(route, callback) { this.handlers.unshift({route : route, callback : callback}); }, // Checks the current URL to see if it has changed, and if it has, // calls `loadUrl`, normalizing across the hidden iframe. checkUrl : function(e) { var current = this.getFragment(); if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash); if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false; if (this.iframe) this.navigate(current); this.loadUrl() || this.loadUrl(window.location.hash); }, // Attempt to load the current URL fragment. If a route succeeds with a // match, returns `true`. If no defined routes matches the fragment, // returns `false`. loadUrl : function(fragmentOverride) { var fragment = this.fragment = this.getFragment(fragmentOverride); var matched = _.any(this.handlers, function(handler) { if (handler.route.test(fragment)) { handler.callback(fragment); return true; } }); return matched; }, // Save a fragment into the hash history. You are responsible for properly // URL-encoding the fragment in advance. This does not trigger // a `hashchange` event. navigate : function(fragment, triggerRoute) { var frag = (fragment || '').replace(hashStrip, ''); if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return; if (this._hasPushState) { var loc = window.location; if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag; this.fragment = frag; window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + frag); } else { window.location.hash = this.fragment = frag; if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) { this.iframe.document.open().close(); this.iframe.location.hash = frag; } } if (triggerRoute) this.loadUrl(fragment); } }); // Backbone.View // ------------- // Creating a Backbone.View creates its initial element outside of the DOM, // if an existing element is not provided... Backbone.View = function(options) { this.cid = _.uniqueId('view'); this._configure(options || {}); this._ensureElement(); this.delegateEvents(); this.initialize.apply(this, arguments); }; // Element lookup, scoped to DOM elements within the current view. // This should be prefered to global lookups, if you're dealing with // a specific view. var selectorDelegate = function(selector) { return $(selector, this.el); }; // Cached regex to split keys for `delegate`. var eventSplitter = /^(\S+)\s*(.*)$/; // List of view options to be merged as properties. var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName']; // Set up all inheritable **Backbone.View** properties and methods. _.extend(Backbone.View.prototype, Backbone.Events, { // The default `tagName` of a View's element is `"div"`. tagName : 'div', // Attach the `selectorDelegate` function as the `$` property. $ : selectorDelegate, // Initialize is an empty function by default. Override it with your own // initialization logic. initialize : function(){}, // **render** is the core function that your view should override, in order // to populate its element (`this.el`), with the appropriate HTML. The // convention is for **render** to always return `this`. render : function() { return this; }, // Remove this view from the DOM. Note that the view isn't present in the // DOM by default, so calling this method may be a no-op. remove : function() { $(this.el).remove(); return this; }, // For small amounts of DOM Elements, where a full-blown template isn't // needed, use **make** to manufacture elements, one at a time. // // var el = this.make('li', {'class': 'row'}, this.model.escape('title')); // make : function(tagName, attributes, content) { var el = document.createElement(tagName); if (attributes) $(el).attr(attributes); if (content) $(el).html(content); return el; }, // Set callbacks, where `this.callbacks` is a hash of // // *{"event selector": "callback"}* // // { // 'mousedown .title': 'edit', // 'click .button': 'save' // } // // pairs. Callbacks will be bound to the view, with `this` set properly. // Uses event delegation for efficiency. // Omitting the selector binds the event to `this.el`. // This only works for delegate-able events: not `focus`, `blur`, and // not `change`, `submit`, and `reset` in Internet Explorer. delegateEvents : function(events) { if (!(events || (events = this.events))) return; if (_.isFunction(events)) events = events.call(this); $(this.el).unbind('.delegateEvents' + this.cid); for (var key in events) { var method = this[events[key]]; if (!method) throw new Error('Event "' + events[key] + '" does not exist'); var match = key.match(eventSplitter); var eventName = match[1], selector = match[2]; method = _.bind(method, this); eventName += '.delegateEvents' + this.cid; if (selector === '') { $(this.el).bind(eventName, method); } else { $(this.el).delegate(selector, eventName, method); } } }, // Performs the initial configuration of a View with a set of options. // Keys with special meaning *(model, collection, id, className)*, are // attached directly to the view. _configure : function(options) { if (this.options) options = _.extend({}, this.options, options); for (var i = 0, l = viewOptions.length; i < l; i++) { var attr = viewOptions[i]; if (options[attr]) this[attr] = options[attr]; } this.options = options; }, // Ensure that the View has a DOM element to render into. // If `this.el` is a string, pass it through `$()`, take the first // matching element, and re-assign it to `el`. Otherwise, create // an element from the `id`, `className` and `tagName` proeprties. _ensureElement : function() { if (!this.el) { var attrs = this.attributes || {}; if (this.id) attrs.id = this.id; if (this.className) attrs['class'] = this.className; this.el = this.make(this.tagName, attrs); } else if (_.isString(this.el)) { this.el = $(this.el).get(0); } } }); // The self-propagating extend function that Backbone classes use. var extend = function (protoProps, classProps) { var child = inherits(this, protoProps, classProps); child.extend = this.extend; return child; }; // Set up inheritance for the model, collection, and view. Backbone.Model.extend = Backbone.Collection.extend = Backbone.Router.extend = Backbone.View.extend = extend; // Map from CRUD to HTTP for our default `Backbone.sync` implementation. var methodMap = { 'create': 'POST', 'update': 'PUT', 'delete': 'DELETE', 'read' : 'GET' }; // Backbone.sync // ------------- // Override this function to change the manner in which Backbone persists // models to the server. You will be passed the type of request, and the // model in question. By default, uses makes a RESTful Ajax request // to the model's `url()`. Some possible customizations could be: // // * Use `setTimeout` to batch rapid-fire updates into a single request. // * Send up the models as XML instead of JSON. // * Persist models via WebSockets instead of Ajax. // // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests // as `POST`, with a `_method` parameter containing the true HTTP method, // as well as all requests with the body as `application/x-www-form-urlencoded` instead of // `application/json` with the model in a param named `model`. // Useful when interfacing with server-side languages like **PHP** that make // it difficult to read the body of `PUT` requests. Backbone.sync = function(method, model, options) { var type = methodMap[method]; // Default JSON-request options. var params = _.extend({ type: type, dataType: 'json' }, options); // Ensure that we have a URL. if (!params.url) { params.url = getUrl(model) || urlError(); } // Ensure that we have the appropriate request data. if (!params.data && model && (method == 'create' || method == 'update')) { params.contentType = 'application/json'; params.data = JSON.stringify(model.toJSON()); } // For older servers, emulate JSON by encoding the request into an HTML-form. if (Backbone.emulateJSON) { params.contentType = 'application/x-www-form-urlencoded'; params.data = params.data ? {model : params.data} : {}; } // For older servers, emulate HTTP by mimicking the HTTP method with `_method` // And an `X-HTTP-Method-Override` header. if (Backbone.emulateHTTP) { if (type === 'PUT' || type === 'DELETE') { if (Backbone.emulateJSON) params.data._method = type; params.type = 'POST'; params.beforeSend = function(xhr) { xhr.setRequestHeader('X-HTTP-Method-Override', type); }; } } // Don't process data on a non-GET request. if (params.type !== 'GET' && !Backbone.emulateJSON) { params.processData = false; } // Make the request. return $.ajax(params); }; // Helpers // ------- // Shared empty constructor function to aid in prototype-chain creation. var ctor = function(){}; // Helper function to correctly set up the prototype chain, for subclasses. // Similar to `goog.inherits`, but uses a hash of prototype properties and // class properties to be extended. var inherits = function(parent, protoProps, staticProps) { var child; // The constructor function for the new subclass is either defined by you // (the "constructor" property in your `extend` definition), or defaulted // by us to simply call `super()`. if (protoProps && protoProps.hasOwnProperty('constructor')) { child = protoProps.constructor; } else { child = function(){ return parent.apply(this, arguments); }; } // Inherit class (static) properties from parent. _.extend(child, parent); // Set the prototype chain to inherit from `parent`, without calling // `parent`'s constructor function. ctor.prototype = parent.prototype; child.prototype = new ctor(); // Add prototype properties (instance properties) to the subclass, // if supplied. if (protoProps) _.extend(child.prototype, protoProps); // Add static properties to the constructor function, if supplied. if (staticProps) _.extend(child, staticProps); // Correctly set child's `prototype.constructor`. child.prototype.constructor = child; // Set a convenience property in case the parent's prototype is needed later. child.__super__ = parent.prototype; return child; }; // Helper function to get a URL from a Model or Collection as a property // or as a function. var getUrl = function(object) { if (!(object && object.url)) return null; return _.isFunction(object.url) ? object.url() : object.url; }; // Throw an error when a URL is needed, and none is supplied. var urlError = function() { throw new Error('A "url" property or function must be specified'); }; // Wrap an optional error callback with a fallback error event. var wrapError = function(onError, model, options) { return function(resp) { if (onError) { onError(model, resp, options); } else { model.trigger('error', model, resp, options); } }; }; // Helper function to escape a string for HTML rendering. var escapeHTML = function(string) { return string.replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); }; }).call(this); ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/web/static/beets.css����������������������������������������������������������0000644�0000765�0000024�00000005607�12404675421�021460� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������body { font-family: Helvetica, Arial, sans-serif; } #header { position: fixed; left: 0; right: 0; top: 0; height: 36px; color: white; cursor: default; /* shadowy border */ box-shadow: 0 0 20px #999; -webkit-box-shadow: 0 0 20px #999; -moz-box-shadow: 0 0 20px #999; /* background gradient */ background: #0e0e0e; background: -moz-linear-gradient(top, #6b6b6b 0%, #0e0e0e 100%); background: -webkit-linear-gradient(top, #6b6b6b 0%,#0e0e0e 100%); } #header h1 { font-size: 1.1em; font-weight: bold; color: white; margin: 0.35em; float: left; } #entities { width: 17em; position: fixed; top: 36px; left: 0; bottom: 0; margin: 0; z-index: 1; background: #dde4eb; /* shadowy border */ box-shadow: 0 0 20px #666; -webkit-box-shadow: 0 0 20px #666; -moz-box-shadow: 0 0 20px #666; } #queryForm { display: block; text-align: center; margin: 0.25em 0; } #query { width: 95%; font-size: 1em; } #entities ul { width: 17em; position: fixed; top: 36px; left: 0; bottom: 0; margin: 2.2em 0 0 0; padding: 0; overflow-y: auto; overflow-x: hidden; } #entities ul li { list-style: none; padding: 4px 8px; margin: 0; cursor: default; } #entities ul li.selected { background: #7abcff; background: -moz-linear-gradient(top, #7abcff 0%, #60abf8 44%, #4096ee 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#7abcff), color-stop(44%,#60abf8), color-stop(100%,#4096ee)); color: white; } #entities ul li .playing { margin-left: 5px; font-size: 0.9em; } #main-detail, #extra-detail { position: fixed; left: 17em; margin: 1.0em 0 0 1.5em; } #main-detail { top: 36px; height: 98px; } #main-detail .artist, #main-detail .album, #main-detail .title { display: block; } #main-detail .title { font-size: 1.3em; font-weight: bold; } #main-detail .albumtitle { font-style: italic; } #extra-detail { overflow-x: hidden; overflow-y: auto; top: 134px; bottom: 0; right: 0; } /*Fix for correctly displaying line breaks in lyrics*/ #extra-detail .lyrics { white-space: pre-wrap; } #extra-detail dl dt, #extra-detail dl dd { list-style: none; margin: 0; padding: 0; } #extra-detail dl dt { width: 10em; float: left; text-align: right; font-weight: bold; clear: both; } #extra-detail dl dd { margin-left: 10.5em; } #player { float: left; width: 150px; height: 36px; } #player .play, #player .pause, #player .disabled { -webkit-appearance: none; font-size: 1em; font-family: Helvetica, Arial, sans-serif; background: none; border: none; color: white; padding: 5px; margin: 0; text-align: center; width: 36px; height: 36px; } #player .disabled { color: #666; } �������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/web/static/beets.js�����������������������������������������������������������0000644�0000765�0000024�00000021101�12404675421�021267� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// Format times as minutes and seconds. var timeFormat = function(secs) { if (secs == undefined || isNaN(secs)) { return '0:00'; } secs = Math.round(secs); var mins = '' + Math.round(secs / 60); secs = '' + (secs % 60); if (secs.length < 2) { secs = '0' + secs; } return mins + ':' + secs; } // jQuery extension encapsulating event hookups for audio element controls. $.fn.player = function(debug) { // Selected element should contain an HTML5 Audio element. var audio = $('audio', this).get(0); // Control elements that may be present, identified by class. var playBtn = $('.play', this); var pauseBtn = $('.pause', this); var disabledInd = $('.disabled', this); var timesEl = $('.times', this); var curTimeEl = $('.currentTime', this); var totalTimeEl = $('.totalTime', this); var sliderPlayedEl = $('.slider .played', this); var sliderLoadedEl = $('.slider .loaded', this); // Button events. playBtn.click(function() { audio.play(); }); pauseBtn.click(function(ev) { audio.pause(); }); // Utilities. var timePercent = function(cur, total) { if (cur == undefined || isNaN(cur) || total == undefined || isNaN(total) || total == 0) { return 0; } var ratio = cur / total; if (ratio > 1.0) { ratio = 1.0; } return (Math.round(ratio * 10000) / 100) + '%'; } // Event helpers. var dbg = function(msg) { if (debug) console.log(msg); } var showState = function() { if (audio.duration == undefined || isNaN(audio.duration)) { playBtn.hide(); pauseBtn.hide(); disabledInd.show(); timesEl.hide(); } else if (audio.paused) { playBtn.show(); pauseBtn.hide(); disabledInd.hide(); timesEl.show(); } else { playBtn.hide(); pauseBtn.show(); disabledInd.hide(); timesEl.show(); } } var showTimes = function() { curTimeEl.text(timeFormat(audio.currentTime)); totalTimeEl.text(timeFormat(audio.duration)); sliderPlayedEl.css('width', timePercent(audio.currentTime, audio.duration)); // last time buffered var bufferEnd = 0; for (var i = 0; i < audio.buffered.length; ++i) { if (audio.buffered.end(i) > bufferEnd) bufferEnd = audio.buffered.end(i); } sliderLoadedEl.css('width', timePercent(bufferEnd, audio.duration)); } // Initialize controls. showState(); showTimes(); // Bind events. $('audio', this).bind({ playing: function() { dbg('playing'); showState(); }, pause: function() { dbg('pause'); showState(); }, ended: function() { dbg('ended'); showState(); }, progress: function() { dbg('progress ' + audio.buffered); }, timeupdate: function() { dbg('timeupdate ' + audio.currentTime); showTimes(); }, durationchange: function() { dbg('durationchange ' + audio.duration); showState(); showTimes(); }, loadeddata: function() { dbg('loadeddata'); }, loadedmetadata: function() { dbg('loadedmetadata'); } }); } // Simple selection disable for jQuery. // Cut-and-paste from: // http://stackoverflow.com/questions/2700000 $.fn.disableSelection = function() { $(this).attr('unselectable', 'on') .css('-moz-user-select', 'none') .each(function() { this.onselectstart = function() { return false; }; }); }; $(function() { // Routes. var BeetsRouter = Backbone.Router.extend({ routes: { "item/query/:query": "itemQuery", }, itemQuery: function(query) { $.getJSON('/item/query/' + query, function(data) { var models = _.map( data['results'], function(d) { return new Item(d); } ); var results = new Items(models); app.showItems(results); }); } }); var router = new BeetsRouter(); // Model. var Item = Backbone.Model.extend({ urlRoot: '/item' }); var Items = Backbone.Collection.extend({ model: Item }); // Item views. var ItemEntryView = Backbone.View.extend({ tagName: "li", template: _.template($('#item-entry-template').html()), events: { 'click': 'select', 'dblclick': 'play' }, initialize: function() { this.playing = false; }, render: function() { $(this.el).html(this.template(this.model.toJSON())); this.setPlaying(this.playing); return this; }, select: function() { app.selectItem(this); }, play: function() { app.playItem(this.model); }, setPlaying: function(val) { this.playing = val; if (val) this.$('.playing').show(); else this.$('.playing').hide(); } }); //Holds Title, Artist, Album etc. var ItemMainDetailView = Backbone.View.extend({ tagName: "div", template: _.template($('#item-main-detail-template').html()), events: { 'click .play': 'play', }, render: function() { $(this.el).html(this.template(this.model.toJSON())); return this; }, play: function() { app.playItem(this.model); } }); // Holds Track no., Format, MusicBrainz link, Lyrics, Comments etc. var ItemExtraDetailView = Backbone.View.extend({ tagName: "div", template: _.template($('#item-extra-detail-template').html()), render: function() { $(this.el).html(this.template(this.model.toJSON())); return this; } }); // Main app view. var AppView = Backbone.View.extend({ el: $('body'), events: { 'submit #queryForm': 'querySubmit', }, querySubmit: function(ev) { ev.preventDefault(); router.navigate('item/query/' + escape($('#query').val()), true); }, initialize: function() { this.playingItem = null; this.shownItems = null; // Not sure why these events won't bind automatically. this.$('audio').bind({ 'play': _.bind(this.audioPlay, this), 'pause': _.bind(this.audioPause, this), 'ended': _.bind(this.audioEnded, this) }); }, showItems: function(items) { this.shownItems = items; $('#results').empty(); items.each(function(item) { var view = new ItemEntryView({model: item}); item.entryView = view; $('#results').append(view.render().el); }); }, selectItem: function(view) { // Mark row as selected. $('#results li').removeClass("selected"); $(view.el).addClass("selected"); // Show main and extra detail. var mainDetailView = new ItemMainDetailView({model: view.model}); $('#main-detail').empty().append(mainDetailView.render().el); var extraDetailView = new ItemExtraDetailView({model: view.model}); $('#extra-detail').empty().append(extraDetailView.render().el); }, playItem: function(item) { var url = '/item/' + item.get('id') + '/file'; $('#player audio').attr('src', url); $('#player audio').get(0).play(); if (this.playingItem != null) { this.playingItem.entryView.setPlaying(false); } item.entryView.setPlaying(true); this.playingItem = item; }, audioPause: function() { this.playingItem.entryView.setPlaying(false); }, audioPlay: function() { if (this.playingItem != null) this.playingItem.entryView.setPlaying(true); }, audioEnded: function() { this.playingItem.entryView.setPlaying(false); // Try to play the next track. var idx = this.shownItems.indexOf(this.playingItem); if (idx == -1) { // Not in current list. return; } var nextIdx = idx + 1; if (nextIdx >= this.shownItems.size()) { // End of list. return; } this.playItem(this.shownItems.at(nextIdx)); } }); var app = new AppView(); // App setup. Backbone.history.start({pushState: false}); // Disable selection on UI elements. $('#entities ul').disableSelection(); $('#header').disableSelection(); // Audio player setup. $('#player').player(); }); ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/web/static/jquery.js����������������������������������������������������������0000644�0000765�0000024�00000744653�12313347401�021525� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/*! * jQuery JavaScript Library v1.7.1 * http://jquery.com/ * * Copyright 2013, John Resig * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * Includes Sizzle.js * http://sizzlejs.com/ * Copyright 2013, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * * Date: Mon Nov 21 21:11:03 2011 -0500 */ (function( window, undefined ) { // Use the correct document accordingly with window argument (sandbox) var document = window.document, navigator = window.navigator, location = window.location; var jQuery = (function() { // Define a local copy of jQuery var jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' return new jQuery.fn.init( selector, context, rootjQuery ); }, // Map over jQuery in case of overwrite _jQuery = window.jQuery, // Map over the $ in case of overwrite _$ = window.$, // A central reference to the root jQuery(document) rootjQuery, // A simple way to check for HTML strings or ID strings // Prioritize #id over <tag> to avoid XSS via location.hash (#9521) quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, // Check if a string has a non-whitespace character in it rnotwhite = /\S/, // Used for trimming whitespace trimLeft = /^\s+/, trimRight = /\s+$/, // Match a standalone tag rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, // JSON RegExp rvalidchars = /^[\],:{}\s]*$/, rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, // Useragent RegExp rwebkit = /(webkit)[ \/]([\w.]+)/, ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, rmsie = /(msie) ([\w.]+)/, rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, // Matches dashed string for camelizing rdashAlpha = /-([a-z]|[0-9])/ig, rmsPrefix = /^-ms-/, // Used by jQuery.camelCase as callback to replace() fcamelCase = function( all, letter ) { return ( letter + "" ).toUpperCase(); }, // Keep a UserAgent string for use with jQuery.browser userAgent = navigator.userAgent, // For matching the engine and version of the browser browserMatch, // The deferred used on DOM ready readyList, // The ready event handler DOMContentLoaded, // Save a reference to some core methods toString = Object.prototype.toString, hasOwn = Object.prototype.hasOwnProperty, push = Array.prototype.push, slice = Array.prototype.slice, trim = String.prototype.trim, indexOf = Array.prototype.indexOf, // [[Class]] -> type pairs class2type = {}; jQuery.fn = jQuery.prototype = { constructor: jQuery, init: function( selector, context, rootjQuery ) { var match, elem, ret, doc; // Handle $(""), $(null), or $(undefined) if ( !selector ) { return this; } // Handle $(DOMElement) if ( selector.nodeType ) { this.context = this[0] = selector; this.length = 1; return this; } // The body element only exists once, optimize finding it if ( selector === "body" && !context && document.body ) { this.context = document; this[0] = document.body; this.selector = selector; this.length = 1; return this; } // Handle HTML strings if ( typeof selector === "string" ) { // Are we dealing with HTML string or an ID? if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { // Assume that strings that start and end with <> are HTML and skip the regex check match = [ null, selector, null ]; } else { match = quickExpr.exec( selector ); } // Verify a match, and that no context was specified for #id if ( match && (match[1] || !context) ) { // HANDLE: $(html) -> $(array) if ( match[1] ) { context = context instanceof jQuery ? context[0] : context; doc = ( context ? context.ownerDocument || context : document ); // If a single string is passed in and it's a single tag // just do a createElement and skip the rest ret = rsingleTag.exec( selector ); if ( ret ) { if ( jQuery.isPlainObject( context ) ) { selector = [ document.createElement( ret[1] ) ]; jQuery.fn.attr.call( selector, context, true ); } else { selector = [ doc.createElement( ret[1] ) ]; } } else { ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; } return jQuery.merge( this, selector ); // HANDLE: $("#id") } else { elem = document.getElementById( match[2] ); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 if ( elem && elem.parentNode ) { // Handle the case where IE and Opera return items // by name instead of ID if ( elem.id !== match[2] ) { return rootjQuery.find( selector ); } // Otherwise, we inject the element directly into the jQuery object this.length = 1; this[0] = elem; } this.context = document; this.selector = selector; return this; } // HANDLE: $(expr, $(...)) } else if ( !context || context.jquery ) { return ( context || rootjQuery ).find( selector ); // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) } else { return this.constructor( context ).find( selector ); } // HANDLE: $(function) // Shortcut for document ready } else if ( jQuery.isFunction( selector ) ) { return rootjQuery.ready( selector ); } if ( selector.selector !== undefined ) { this.selector = selector.selector; this.context = selector.context; } return jQuery.makeArray( selector, this ); }, // Start with an empty selector selector: "", // The current version of jQuery being used jquery: "1.7.1", // The default length of a jQuery object is 0 length: 0, // The number of elements contained in the matched element set size: function() { return this.length; }, toArray: function() { return slice.call( this, 0 ); }, // Get the Nth element in the matched element set OR // Get the whole matched element set as a clean array get: function( num ) { return num == null ? // Return a 'clean' array this.toArray() : // Return just the object ( num < 0 ? this[ this.length + num ] : this[ num ] ); }, // Take an array of elements and push it onto the stack // (returning the new matched element set) pushStack: function( elems, name, selector ) { // Build a new jQuery matched element set var ret = this.constructor(); if ( jQuery.isArray( elems ) ) { push.apply( ret, elems ); } else { jQuery.merge( ret, elems ); } // Add the old object onto the stack (as a reference) ret.prevObject = this; ret.context = this.context; if ( name === "find" ) { ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; } else if ( name ) { ret.selector = this.selector + "." + name + "(" + selector + ")"; } // Return the newly-formed element set return ret; }, // Execute a callback for every element in the matched set. // (You can seed the arguments with an array of args, but this is // only used internally.) each: function( callback, args ) { return jQuery.each( this, callback, args ); }, ready: function( fn ) { // Attach the listeners jQuery.bindReady(); // Add the callback readyList.add( fn ); return this; }, eq: function( i ) { i = +i; return i === -1 ? this.slice( i ) : this.slice( i, i + 1 ); }, first: function() { return this.eq( 0 ); }, last: function() { return this.eq( -1 ); }, slice: function() { return this.pushStack( slice.apply( this, arguments ), "slice", slice.call(arguments).join(",") ); }, map: function( callback ) { return this.pushStack( jQuery.map(this, function( elem, i ) { return callback.call( elem, i, elem ); })); }, end: function() { return this.prevObject || this.constructor(null); }, // For internal use only. // Behaves like an Array's method, not like a jQuery method. push: push, sort: [].sort, splice: [].splice }; // Give the init function the jQuery prototype for later instantiation jQuery.fn.init.prototype = jQuery.fn; jQuery.extend = jQuery.fn.extend = function() { var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, i = 1, length = arguments.length, deep = false; // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; target = arguments[1] || {}; // skip the boolean and the target i = 2; } // Handle case when target is a string or something (possible in deep copy) if ( typeof target !== "object" && !jQuery.isFunction(target) ) { target = {}; } // extend jQuery itself if only one argument is passed if ( length === i ) { target = this; --i; } for ( ; i < length; i++ ) { // Only deal with non-null/undefined values if ( (options = arguments[ i ]) != null ) { // Extend the base object for ( name in options ) { src = target[ name ]; copy = options[ name ]; // Prevent never-ending loop if ( target === copy ) { continue; } // Recurse if we're merging plain objects or arrays if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { if ( copyIsArray ) { copyIsArray = false; clone = src && jQuery.isArray(src) ? src : []; } else { clone = src && jQuery.isPlainObject(src) ? src : {}; } // Never move original objects, clone them target[ name ] = jQuery.extend( deep, clone, copy ); // Don't bring in undefined values } else if ( copy !== undefined ) { target[ name ] = copy; } } } } // Return the modified object return target; }; jQuery.extend({ noConflict: function( deep ) { if ( window.$ === jQuery ) { window.$ = _$; } if ( deep && window.jQuery === jQuery ) { window.jQuery = _jQuery; } return jQuery; }, // Is the DOM ready to be used? Set to true once it occurs. isReady: false, // A counter to track how many items to wait for before // the ready event fires. See #6781 readyWait: 1, // Hold (or release) the ready event holdReady: function( hold ) { if ( hold ) { jQuery.readyWait++; } else { jQuery.ready( true ); } }, // Handle when the DOM is ready ready: function( wait ) { // Either a released hold or an DOMready/load event and not yet ready if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). if ( !document.body ) { return setTimeout( jQuery.ready, 1 ); } // Remember that the DOM is ready jQuery.isReady = true; // If a normal DOM Ready event fired, decrement, and wait if need be if ( wait !== true && --jQuery.readyWait > 0 ) { return; } // If there are functions bound, to execute readyList.fireWith( document, [ jQuery ] ); // Trigger any bound ready events if ( jQuery.fn.trigger ) { jQuery( document ).trigger( "ready" ).off( "ready" ); } } }, bindReady: function() { if ( readyList ) { return; } readyList = jQuery.Callbacks( "once memory" ); // Catch cases where $(document).ready() is called after the // browser event has already occurred. if ( document.readyState === "complete" ) { // Handle it asynchronously to allow scripts the opportunity to delay ready return setTimeout( jQuery.ready, 1 ); } // Mozilla, Opera and webkit nightlies currently support this event if ( document.addEventListener ) { // Use the handy event callback document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); // A fallback to window.onload, that will always work window.addEventListener( "load", jQuery.ready, false ); // If IE event model is used } else if ( document.attachEvent ) { // ensure firing before onload, // maybe late but safe also for iframes document.attachEvent( "onreadystatechange", DOMContentLoaded ); // A fallback to window.onload, that will always work window.attachEvent( "onload", jQuery.ready ); // If IE and not a frame // continually check to see if the document is ready var toplevel = false; try { toplevel = window.frameElement == null; } catch(e) {} if ( document.documentElement.doScroll && toplevel ) { doScrollCheck(); } } }, // See test/unit/core.js for details concerning isFunction. // Since version 1.3, DOM methods and functions like alert // aren't supported. They return false on IE (#2968). isFunction: function( obj ) { return jQuery.type(obj) === "function"; }, isArray: Array.isArray || function( obj ) { return jQuery.type(obj) === "array"; }, // A crude way of determining if an object is a window isWindow: function( obj ) { return obj && typeof obj === "object" && "setInterval" in obj; }, isNumeric: function( obj ) { return !isNaN( parseFloat(obj) ) && isFinite( obj ); }, type: function( obj ) { return obj == null ? String( obj ) : class2type[ toString.call(obj) ] || "object"; }, isPlainObject: function( obj ) { // Must be an Object. // Because of IE, we also have to check the presence of the constructor property. // Make sure that DOM nodes and window objects don't pass through, as well if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { return false; } try { // Not own constructor property must be Object if ( obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { return false; } } catch ( e ) { // IE8,9 Will throw exceptions on certain host objects #9897 return false; } // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. var key; for ( key in obj ) {} return key === undefined || hasOwn.call( obj, key ); }, isEmptyObject: function( obj ) { for ( var name in obj ) { return false; } return true; }, error: function( msg ) { throw new Error( msg ); }, parseJSON: function( data ) { if ( typeof data !== "string" || !data ) { return null; } // Make sure leading/trailing whitespace is removed (IE can't handle it) data = jQuery.trim( data ); // Attempt to parse using the native JSON parser first if ( window.JSON && window.JSON.parse ) { return window.JSON.parse( data ); } // Make sure the incoming data is actual JSON // Logic borrowed from http://json.org/json2.js if ( rvalidchars.test( data.replace( rvalidescape, "@" ) .replace( rvalidtokens, "]" ) .replace( rvalidbraces, "")) ) { return ( new Function( "return " + data ) )(); } jQuery.error( "Invalid JSON: " + data ); }, // Cross-browser xml parsing parseXML: function( data ) { var xml, tmp; try { if ( window.DOMParser ) { // Standard tmp = new DOMParser(); xml = tmp.parseFromString( data , "text/xml" ); } else { // IE xml = new ActiveXObject( "Microsoft.XMLDOM" ); xml.async = "false"; xml.loadXML( data ); } } catch( e ) { xml = undefined; } if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { jQuery.error( "Invalid XML: " + data ); } return xml; }, noop: function() {}, // Evaluates a script in a global context // Workarounds based on findings by Jim Driscoll // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context globalEval: function( data ) { if ( data && rnotwhite.test( data ) ) { // We use execScript on Internet Explorer // We use an anonymous function so that context is window // rather than jQuery in Firefox ( window.execScript || function( data ) { window[ "eval" ].call( window, data ); } )( data ); } }, // Convert dashed to camelCase; used by the css and data modules // Microsoft forgot to hump their vendor prefix (#9572) camelCase: function( string ) { return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); }, nodeName: function( elem, name ) { return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); }, // args is for internal usage only each: function( object, callback, args ) { var name, i = 0, length = object.length, isObj = length === undefined || jQuery.isFunction( object ); if ( args ) { if ( isObj ) { for ( name in object ) { if ( callback.apply( object[ name ], args ) === false ) { break; } } } else { for ( ; i < length; ) { if ( callback.apply( object[ i++ ], args ) === false ) { break; } } } // A special, fast, case for the most common use of each } else { if ( isObj ) { for ( name in object ) { if ( callback.call( object[ name ], name, object[ name ] ) === false ) { break; } } } else { for ( ; i < length; ) { if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { break; } } } } return object; }, // Use native String.trim function wherever possible trim: trim ? function( text ) { return text == null ? "" : trim.call( text ); } : // Otherwise use our own trimming functionality function( text ) { return text == null ? "" : text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); }, // results is for internal usage only makeArray: function( array, results ) { var ret = results || []; if ( array != null ) { // The window, strings (and functions) also have 'length' // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 var type = jQuery.type( array ); if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { push.call( ret, array ); } else { jQuery.merge( ret, array ); } } return ret; }, inArray: function( elem, array, i ) { var len; if ( array ) { if ( indexOf ) { return indexOf.call( array, elem, i ); } len = array.length; i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; for ( ; i < len; i++ ) { // Skip accessing in sparse arrays if ( i in array && array[ i ] === elem ) { return i; } } } return -1; }, merge: function( first, second ) { var i = first.length, j = 0; if ( typeof second.length === "number" ) { for ( var l = second.length; j < l; j++ ) { first[ i++ ] = second[ j ]; } } else { while ( second[j] !== undefined ) { first[ i++ ] = second[ j++ ]; } } first.length = i; return first; }, grep: function( elems, callback, inv ) { var ret = [], retVal; inv = !!inv; // Go through the array, only saving the items // that pass the validator function for ( var i = 0, length = elems.length; i < length; i++ ) { retVal = !!callback( elems[ i ], i ); if ( inv !== retVal ) { ret.push( elems[ i ] ); } } return ret; }, // arg is for internal usage only map: function( elems, callback, arg ) { var value, key, ret = [], i = 0, length = elems.length, // jquery objects are treated as arrays isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; // Go through the array, translating each of the items to their if ( isArray ) { for ( ; i < length; i++ ) { value = callback( elems[ i ], i, arg ); if ( value != null ) { ret[ ret.length ] = value; } } // Go through every key on the object, } else { for ( key in elems ) { value = callback( elems[ key ], key, arg ); if ( value != null ) { ret[ ret.length ] = value; } } } // Flatten any nested arrays return ret.concat.apply( [], ret ); }, // A global GUID counter for objects guid: 1, // Bind a function to a context, optionally partially applying any // arguments. proxy: function( fn, context ) { if ( typeof context === "string" ) { var tmp = fn[ context ]; context = fn; fn = tmp; } // Quick check to determine if target is callable, in the spec // this throws a TypeError, but we will just return undefined. if ( !jQuery.isFunction( fn ) ) { return undefined; } // Simulated bind var args = slice.call( arguments, 2 ), proxy = function() { return fn.apply( context, args.concat( slice.call( arguments ) ) ); }; // Set the guid of unique handler to the same of original handler, so it can be removed proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; return proxy; }, // Mutifunctional method to get and set values to a collection // The value/s can optionally be executed if it's a function access: function( elems, key, value, exec, fn, pass ) { var length = elems.length; // Setting many attributes if ( typeof key === "object" ) { for ( var k in key ) { jQuery.access( elems, k, key[k], exec, fn, value ); } return elems; } // Setting one attribute if ( value !== undefined ) { // Optionally, function values get executed if exec is true exec = !pass && exec && jQuery.isFunction(value); for ( var i = 0; i < length; i++ ) { fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); } return elems; } // Getting an attribute return length ? fn( elems[0], key ) : undefined; }, now: function() { return ( new Date() ).getTime(); }, // Use of jQuery.browser is frowned upon. // More details: http://docs.jquery.com/Utilities/jQuery.browser uaMatch: function( ua ) { ua = ua.toLowerCase(); var match = rwebkit.exec( ua ) || ropera.exec( ua ) || rmsie.exec( ua ) || ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || []; return { browser: match[1] || "", version: match[2] || "0" }; }, sub: function() { function jQuerySub( selector, context ) { return new jQuerySub.fn.init( selector, context ); } jQuery.extend( true, jQuerySub, this ); jQuerySub.superclass = this; jQuerySub.fn = jQuerySub.prototype = this(); jQuerySub.fn.constructor = jQuerySub; jQuerySub.sub = this.sub; jQuerySub.fn.init = function init( selector, context ) { if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { context = jQuerySub( context ); } return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); }; jQuerySub.fn.init.prototype = jQuerySub.fn; var rootjQuerySub = jQuerySub(document); return jQuerySub; }, browser: {} }); // Populate the class2type map jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); }); browserMatch = jQuery.uaMatch( userAgent ); if ( browserMatch.browser ) { jQuery.browser[ browserMatch.browser ] = true; jQuery.browser.version = browserMatch.version; } // Deprecated, use jQuery.browser.webkit instead if ( jQuery.browser.webkit ) { jQuery.browser.safari = true; } // IE doesn't match non-breaking spaces with \s if ( rnotwhite.test( "\xA0" ) ) { trimLeft = /^[\s\xA0]+/; trimRight = /[\s\xA0]+$/; } // All jQuery objects should point back to these rootjQuery = jQuery(document); // Cleanup functions for the document ready method if ( document.addEventListener ) { DOMContentLoaded = function() { document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); jQuery.ready(); }; } else if ( document.attachEvent ) { DOMContentLoaded = function() { // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). if ( document.readyState === "complete" ) { document.detachEvent( "onreadystatechange", DOMContentLoaded ); jQuery.ready(); } }; } // The DOM ready check for Internet Explorer function doScrollCheck() { if ( jQuery.isReady ) { return; } try { // If IE is used, use the trick by Diego Perini // http://javascript.nwbox.com/IEContentLoaded/ document.documentElement.doScroll("left"); } catch(e) { setTimeout( doScrollCheck, 1 ); return; } // and execute any waiting functions jQuery.ready(); } return jQuery; })(); // String to Object flags format cache var flagsCache = {}; // Convert String-formatted flags into Object-formatted ones and store in cache function createFlags( flags ) { var object = flagsCache[ flags ] = {}, i, length; flags = flags.split( /\s+/ ); for ( i = 0, length = flags.length; i < length; i++ ) { object[ flags[i] ] = true; } return object; } /* * Create a callback list using the following parameters: * * flags: an optional list of space-separated flags that will change how * the callback list behaves * * By default a callback list will act like an event callback list and can be * "fired" multiple times. * * Possible flags: * * once: will ensure the callback list can only be fired once (like a Deferred) * * memory: will keep track of previous values and will call any callback added * after the list has been fired right away with the latest "memorized" * values (like a Deferred) * * unique: will ensure a callback can only be added once (no duplicate in the list) * * stopOnFalse: interrupt callings when a callback returns false * */ jQuery.Callbacks = function( flags ) { // Convert flags from String-formatted to Object-formatted // (we check in cache first) flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; var // Actual callback list list = [], // Stack of fire calls for repeatable lists stack = [], // Last fire value (for non-forgettable lists) memory, // Flag to know if list is currently firing firing, // First callback to fire (used internally by add and fireWith) firingStart, // End of the loop when firing firingLength, // Index of currently firing callback (modified by remove if needed) firingIndex, // Add one or several callbacks to the list add = function( args ) { var i, length, elem, type, actual; for ( i = 0, length = args.length; i < length; i++ ) { elem = args[ i ]; type = jQuery.type( elem ); if ( type === "array" ) { // Inspect recursively add( elem ); } else if ( type === "function" ) { // Add if not in unique mode and callback is not in if ( !flags.unique || !self.has( elem ) ) { list.push( elem ); } } } }, // Fire callbacks fire = function( context, args ) { args = args || []; memory = !flags.memory || [ context, args ]; firing = true; firingIndex = firingStart || 0; firingStart = 0; firingLength = list.length; for ( ; list && firingIndex < firingLength; firingIndex++ ) { if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { memory = true; // Mark as halted break; } } firing = false; if ( list ) { if ( !flags.once ) { if ( stack && stack.length ) { memory = stack.shift(); self.fireWith( memory[ 0 ], memory[ 1 ] ); } } else if ( memory === true ) { self.disable(); } else { list = []; } } }, // Actual Callbacks object self = { // Add a callback or a collection of callbacks to the list add: function() { if ( list ) { var length = list.length; add( arguments ); // Do we need to add the callbacks to the // current firing batch? if ( firing ) { firingLength = list.length; // With memory, if we're not firing then // we should call right away, unless previous // firing was halted (stopOnFalse) } else if ( memory && memory !== true ) { firingStart = length; fire( memory[ 0 ], memory[ 1 ] ); } } return this; }, // Remove a callback from the list remove: function() { if ( list ) { var args = arguments, argIndex = 0, argLength = args.length; for ( ; argIndex < argLength ; argIndex++ ) { for ( var i = 0; i < list.length; i++ ) { if ( args[ argIndex ] === list[ i ] ) { // Handle firingIndex and firingLength if ( firing ) { if ( i <= firingLength ) { firingLength--; if ( i <= firingIndex ) { firingIndex--; } } } // Remove the element list.splice( i--, 1 ); // If we have some unicity property then // we only need to do this once if ( flags.unique ) { break; } } } } } return this; }, // Control if a given callback is in the list has: function( fn ) { if ( list ) { var i = 0, length = list.length; for ( ; i < length; i++ ) { if ( fn === list[ i ] ) { return true; } } } return false; }, // Remove all callbacks from the list empty: function() { list = []; return this; }, // Have the list do nothing anymore disable: function() { list = stack = memory = undefined; return this; }, // Is it disabled? disabled: function() { return !list; }, // Lock the list in its current state lock: function() { stack = undefined; if ( !memory || memory === true ) { self.disable(); } return this; }, // Is it locked? locked: function() { return !stack; }, // Call all callbacks with the given context and arguments fireWith: function( context, args ) { if ( stack ) { if ( firing ) { if ( !flags.once ) { stack.push( [ context, args ] ); } } else if ( !( flags.once && memory ) ) { fire( context, args ); } } return this; }, // Call all the callbacks with the given arguments fire: function() { self.fireWith( this, arguments ); return this; }, // To know if the callbacks have already been called at least once fired: function() { return !!memory; } }; return self; }; var // Static reference to slice sliceDeferred = [].slice; jQuery.extend({ Deferred: function( func ) { var doneList = jQuery.Callbacks( "once memory" ), failList = jQuery.Callbacks( "once memory" ), progressList = jQuery.Callbacks( "memory" ), state = "pending", lists = { resolve: doneList, reject: failList, notify: progressList }, promise = { done: doneList.add, fail: failList.add, progress: progressList.add, state: function() { return state; }, // Deprecated isResolved: doneList.fired, isRejected: failList.fired, then: function( doneCallbacks, failCallbacks, progressCallbacks ) { deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); return this; }, always: function() { deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); return this; }, pipe: function( fnDone, fnFail, fnProgress ) { return jQuery.Deferred(function( newDefer ) { jQuery.each( { done: [ fnDone, "resolve" ], fail: [ fnFail, "reject" ], progress: [ fnProgress, "notify" ] }, function( handler, data ) { var fn = data[ 0 ], action = data[ 1 ], returned; if ( jQuery.isFunction( fn ) ) { deferred[ handler ](function() { returned = fn.apply( this, arguments ); if ( returned && jQuery.isFunction( returned.promise ) ) { returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); } else { newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); } }); } else { deferred[ handler ]( newDefer[ action ] ); } }); }).promise(); }, // Get a promise for this deferred // If obj is provided, the promise aspect is added to the object promise: function( obj ) { if ( obj == null ) { obj = promise; } else { for ( var key in promise ) { obj[ key ] = promise[ key ]; } } return obj; } }, deferred = promise.promise({}), key; for ( key in lists ) { deferred[ key ] = lists[ key ].fire; deferred[ key + "With" ] = lists[ key ].fireWith; } // Handle state deferred.done( function() { state = "resolved"; }, failList.disable, progressList.lock ).fail( function() { state = "rejected"; }, doneList.disable, progressList.lock ); // Call given func if any if ( func ) { func.call( deferred, deferred ); } // All done! return deferred; }, // Deferred helper when: function( firstParam ) { var args = sliceDeferred.call( arguments, 0 ), i = 0, length = args.length, pValues = new Array( length ), count = length, pCount = length, deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? firstParam : jQuery.Deferred(), promise = deferred.promise(); function resolveFunc( i ) { return function( value ) { args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; if ( !( --count ) ) { deferred.resolveWith( deferred, args ); } }; } function progressFunc( i ) { return function( value ) { pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; deferred.notifyWith( promise, pValues ); }; } if ( length > 1 ) { for ( ; i < length; i++ ) { if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); } else { --count; } } if ( !count ) { deferred.resolveWith( deferred, args ); } } else if ( deferred !== firstParam ) { deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); } return promise; } }); jQuery.support = (function() { var support, all, a, select, opt, input, marginDiv, fragment, tds, events, eventName, i, isSupported, div = document.createElement( "div" ), documentElement = document.documentElement; // Preliminary tests div.setAttribute("className", "t"); div.innerHTML = " <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>"; all = div.getElementsByTagName( "*" ); a = div.getElementsByTagName( "a" )[ 0 ]; // Can't get basic test support if ( !all || !all.length || !a ) { return {}; } // First batch of supports tests select = document.createElement( "select" ); opt = select.appendChild( document.createElement("option") ); input = div.getElementsByTagName( "input" )[ 0 ]; support = { // IE strips leading whitespace when .innerHTML is used leadingWhitespace: ( div.firstChild.nodeType === 3 ), // Make sure that tbody elements aren't automatically inserted // IE will insert them into empty tables tbody: !div.getElementsByTagName("tbody").length, // Make sure that link elements get serialized correctly by innerHTML // This requires a wrapper element in IE htmlSerialize: !!div.getElementsByTagName("link").length, // Get the style information from getAttribute // (IE uses .cssText instead) style: /top/.test( a.getAttribute("style") ), // Make sure that URLs aren't manipulated // (IE normalizes it by default) hrefNormalized: ( a.getAttribute("href") === "/a" ), // Make sure that element opacity exists // (IE uses filter instead) // Use a regex to work around a WebKit issue. See #5145 opacity: /^0.55/.test( a.style.opacity ), // Verify style float existence // (IE uses styleFloat instead of cssFloat) cssFloat: !!a.style.cssFloat, // Make sure that if no value is specified for a checkbox // that it defaults to "on". // (WebKit defaults to "" instead) checkOn: ( input.value === "on" ), // Make sure that a selected-by-default option has a working selected property. // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) optSelected: opt.selected, // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) getSetAttribute: div.className !== "t", // Tests for enctype support on a form(#6743) enctype: !!document.createElement("form").enctype, // Makes sure cloning an html5 element does not cause problems // Where outerHTML is undefined, this still works html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>", // Will be defined later submitBubbles: true, changeBubbles: true, focusinBubbles: false, deleteExpando: true, noCloneEvent: true, inlineBlockNeedsLayout: false, shrinkWrapBlocks: false, reliableMarginRight: true }; // Make sure checked status is properly cloned input.checked = true; support.noCloneChecked = input.cloneNode( true ).checked; // Make sure that the options inside disabled selects aren't marked as disabled // (WebKit marks them as disabled) select.disabled = true; support.optDisabled = !opt.disabled; // Test to see if it's possible to delete an expando from an element // Fails in Internet Explorer try { delete div.test; } catch( e ) { support.deleteExpando = false; } if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { div.attachEvent( "onclick", function() { // Cloning a node shouldn't copy over any // bound event handlers (IE does this) support.noCloneEvent = false; }); div.cloneNode( true ).fireEvent( "onclick" ); } // Check if a radio maintains its value // after being appended to the DOM input = document.createElement("input"); input.value = "t"; input.setAttribute("type", "radio"); support.radioValue = input.value === "t"; input.setAttribute("checked", "checked"); div.appendChild( input ); fragment = document.createDocumentFragment(); fragment.appendChild( div.lastChild ); // WebKit doesn't clone checked state correctly in fragments support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; // Check if a disconnected checkbox will retain its checked // value of true after appended to the DOM (IE6/7) support.appendChecked = input.checked; fragment.removeChild( input ); fragment.appendChild( div ); div.innerHTML = ""; // Check if div with explicit width and no margin-right incorrectly // gets computed margin-right based on width of container. For more // info see bug #3333 // Fails in WebKit before Feb 2011 nightlies // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right if ( window.getComputedStyle ) { marginDiv = document.createElement( "div" ); marginDiv.style.width = "0"; marginDiv.style.marginRight = "0"; div.style.width = "2px"; div.appendChild( marginDiv ); support.reliableMarginRight = ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; } // Technique from Juriy Zaytsev // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ // We only care about the case where non-standard event systems // are used, namely in IE. Short-circuiting here helps us to // avoid an eval call (in setAttribute) which can cause CSP // to go haywire. See: https://developer.mozilla.org/en/Security/CSP if ( div.attachEvent ) { for( i in { submit: 1, change: 1, focusin: 1 }) { eventName = "on" + i; isSupported = ( eventName in div ); if ( !isSupported ) { div.setAttribute( eventName, "return;" ); isSupported = ( typeof div[ eventName ] === "function" ); } support[ i + "Bubbles" ] = isSupported; } } fragment.removeChild( div ); // Null elements to avoid leaks in IE fragment = select = opt = marginDiv = div = input = null; // Run tests that need a body at doc ready jQuery(function() { var container, outer, inner, table, td, offsetSupport, conMarginTop, ptlm, vb, style, html, body = document.getElementsByTagName("body")[0]; if ( !body ) { // Return for frameset docs that don't have a body return; } conMarginTop = 1; ptlm = "position:absolute;top:0;left:0;width:1px;height:1px;margin:0;"; vb = "visibility:hidden;border:0;"; style = "style='" + ptlm + "border:5px solid #000;padding:0;'"; html = "<div " + style + "><div></div></div>" + "<table " + style + " cellpadding='0' cellspacing='0'>" + "<tr><td></td></tr></table>"; container = document.createElement("div"); container.style.cssText = vb + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; body.insertBefore( container, body.firstChild ); // Construct the test element div = document.createElement("div"); container.appendChild( div ); // Check if table cells still have offsetWidth/Height when they are set // to display:none and there are still other visible table cells in a // table row; if so, offsetWidth/Height are not reliable for use when // determining if an element has been hidden directly using // display:none (it is still safe to use offsets if a parent element is // hidden; don safety goggles and see bug #4512 for more information). // (only IE 8 fails this test) div.innerHTML = "<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>"; tds = div.getElementsByTagName( "td" ); isSupported = ( tds[ 0 ].offsetHeight === 0 ); tds[ 0 ].style.display = ""; tds[ 1 ].style.display = "none"; // Check if empty table cells still have offsetWidth/Height // (IE <= 8 fail this test) support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); // Figure out if the W3C box model works as expected div.innerHTML = ""; div.style.width = div.style.paddingLeft = "1px"; jQuery.boxModel = support.boxModel = div.offsetWidth === 2; if ( typeof div.style.zoom !== "undefined" ) { // Check if natively block-level elements act like inline-block // elements when setting their display to 'inline' and giving // them layout // (IE < 8 does this) div.style.display = "inline"; div.style.zoom = 1; support.inlineBlockNeedsLayout = ( div.offsetWidth === 2 ); // Check if elements with layout shrink-wrap their children // (IE 6 does this) div.style.display = ""; div.innerHTML = "<div style='width:4px;'></div>"; support.shrinkWrapBlocks = ( div.offsetWidth !== 2 ); } div.style.cssText = ptlm + vb; div.innerHTML = html; outer = div.firstChild; inner = outer.firstChild; td = outer.nextSibling.firstChild.firstChild; offsetSupport = { doesNotAddBorder: ( inner.offsetTop !== 5 ), doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) }; inner.style.position = "fixed"; inner.style.top = "20px"; // safari subtracts parent border width here which is 5px offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); inner.style.position = inner.style.top = ""; outer.style.overflow = "hidden"; outer.style.position = "relative"; offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); body.removeChild( container ); div = container = null; jQuery.extend( support, offsetSupport ); }); return support; })(); var rbrace = /^(?:\{.*\}|\[.*\])$/, rmultiDash = /([A-Z])/g; jQuery.extend({ cache: {}, // Please use with caution uuid: 0, // Unique for each copy of jQuery on the page // Non-digits removed to match rinlinejQuery expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), // The following elements throw uncatchable exceptions if you // attempt to add expando properties to them. noData: { "embed": true, // Ban all objects except for Flash (which handle expandos) "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", "applet": true }, hasData: function( elem ) { elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; return !!elem && !isEmptyDataObject( elem ); }, data: function( elem, name, data, pvt /* Internal Use Only */ ) { if ( !jQuery.acceptData( elem ) ) { return; } var privateCache, thisCache, ret, internalKey = jQuery.expando, getByName = typeof name === "string", // We have to handle DOM nodes and JS objects differently because IE6-7 // can't GC object references properly across the DOM-JS boundary isNode = elem.nodeType, // Only DOM nodes need the global jQuery cache; JS object data is // attached directly to the object so GC can occur automatically cache = isNode ? jQuery.cache : elem, // Only defining an ID for JS objects if its cache already exists allows // the code to shortcut on the same path as a DOM node with no cache id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, isEvents = name === "events"; // Avoid doing any more work than we need to when trying to get data on an // object that has no data at all if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { return; } if ( !id ) { // Only DOM nodes need a new unique ID for each element since their data // ends up in the global cache if ( isNode ) { elem[ internalKey ] = id = ++jQuery.uuid; } else { id = internalKey; } } if ( !cache[ id ] ) { cache[ id ] = {}; // Avoids exposing jQuery metadata on plain JS objects when the object // is serialized using JSON.stringify if ( !isNode ) { cache[ id ].toJSON = jQuery.noop; } } // An object can be passed to jQuery.data instead of a key/value pair; this gets // shallow copied over onto the existing cache if ( typeof name === "object" || typeof name === "function" ) { if ( pvt ) { cache[ id ] = jQuery.extend( cache[ id ], name ); } else { cache[ id ].data = jQuery.extend( cache[ id ].data, name ); } } privateCache = thisCache = cache[ id ]; // jQuery data() is stored in a separate object inside the object's internal data // cache in order to avoid key collisions between internal data and user-defined // data. if ( !pvt ) { if ( !thisCache.data ) { thisCache.data = {}; } thisCache = thisCache.data; } if ( data !== undefined ) { thisCache[ jQuery.camelCase( name ) ] = data; } // Users should not attempt to inspect the internal events object using jQuery.data, // it is undocumented and subject to change. But does anyone listen? No. if ( isEvents && !thisCache[ name ] ) { return privateCache.events; } // Check for both converted-to-camel and non-converted data property names // If a data property was specified if ( getByName ) { // First Try to find as-is property data ret = thisCache[ name ]; // Test for null|undefined property data if ( ret == null ) { // Try to find the camelCased property ret = thisCache[ jQuery.camelCase( name ) ]; } } else { ret = thisCache; } return ret; }, removeData: function( elem, name, pvt /* Internal Use Only */ ) { if ( !jQuery.acceptData( elem ) ) { return; } var thisCache, i, l, // Reference to internal data cache key internalKey = jQuery.expando, isNode = elem.nodeType, // See jQuery.data for more information cache = isNode ? jQuery.cache : elem, // See jQuery.data for more information id = isNode ? elem[ internalKey ] : internalKey; // If there is already no cache entry for this object, there is no // purpose in continuing if ( !cache[ id ] ) { return; } if ( name ) { thisCache = pvt ? cache[ id ] : cache[ id ].data; if ( thisCache ) { // Support array or space separated string names for data keys if ( !jQuery.isArray( name ) ) { // try the string as a key before any manipulation if ( name in thisCache ) { name = [ name ]; } else { // split the camel cased version by spaces unless a key with the spaces exists name = jQuery.camelCase( name ); if ( name in thisCache ) { name = [ name ]; } else { name = name.split( " " ); } } } for ( i = 0, l = name.length; i < l; i++ ) { delete thisCache[ name[i] ]; } // If there is no data left in the cache, we want to continue // and let the cache object itself get destroyed if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { return; } } } // See jQuery.data for more information if ( !pvt ) { delete cache[ id ].data; // Don't destroy the parent cache unless the internal data object // had been the only thing left in it if ( !isEmptyDataObject(cache[ id ]) ) { return; } } // Browsers that fail expando deletion also refuse to delete expandos on // the window, but it will allow it on all other JS objects; other browsers // don't care // Ensure that `cache` is not a window object #10080 if ( jQuery.support.deleteExpando || !cache.setInterval ) { delete cache[ id ]; } else { cache[ id ] = null; } // We destroyed the cache and need to eliminate the expando on the node to avoid // false lookups in the cache for entries that no longer exist if ( isNode ) { // IE does not allow us to delete expando properties from nodes, // nor does it have a removeAttribute function on Document nodes; // we must handle all of these cases if ( jQuery.support.deleteExpando ) { delete elem[ internalKey ]; } else if ( elem.removeAttribute ) { elem.removeAttribute( internalKey ); } else { elem[ internalKey ] = null; } } }, // For internal use only. _data: function( elem, name, data ) { return jQuery.data( elem, name, data, true ); }, // A method for determining if a DOM node can handle the data expando acceptData: function( elem ) { if ( elem.nodeName ) { var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; if ( match ) { return !(match === true || elem.getAttribute("classid") !== match); } } return true; } }); jQuery.fn.extend({ data: function( key, value ) { var parts, attr, name, data = null; if ( typeof key === "undefined" ) { if ( this.length ) { data = jQuery.data( this[0] ); if ( this[0].nodeType === 1 && !jQuery._data( this[0], "parsedAttrs" ) ) { attr = this[0].attributes; for ( var i = 0, l = attr.length; i < l; i++ ) { name = attr[i].name; if ( name.indexOf( "data-" ) === 0 ) { name = jQuery.camelCase( name.substring(5) ); dataAttr( this[0], name, data[ name ] ); } } jQuery._data( this[0], "parsedAttrs", true ); } } return data; } else if ( typeof key === "object" ) { return this.each(function() { jQuery.data( this, key ); }); } parts = key.split("."); parts[1] = parts[1] ? "." + parts[1] : ""; if ( value === undefined ) { data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); // Try to fetch any internally stored data first if ( data === undefined && this.length ) { data = jQuery.data( this[0], key ); data = dataAttr( this[0], key, data ); } return data === undefined && parts[1] ? this.data( parts[0] ) : data; } else { return this.each(function() { var self = jQuery( this ), args = [ parts[0], value ]; self.triggerHandler( "setData" + parts[1] + "!", args ); jQuery.data( this, key, value ); self.triggerHandler( "changeData" + parts[1] + "!", args ); }); } }, removeData: function( key ) { return this.each(function() { jQuery.removeData( this, key ); }); } }); function dataAttr( elem, key, data ) { // If nothing was found internally, try to fetch any // data from the HTML5 data-* attribute if ( data === undefined && elem.nodeType === 1 ) { var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); data = elem.getAttribute( name ); if ( typeof data === "string" ) { try { data = data === "true" ? true : data === "false" ? false : data === "null" ? null : jQuery.isNumeric( data ) ? parseFloat( data ) : rbrace.test( data ) ? jQuery.parseJSON( data ) : data; } catch( e ) {} // Make sure we set the data so it isn't changed later jQuery.data( elem, key, data ); } else { data = undefined; } } return data; } // checks a cache object for emptiness function isEmptyDataObject( obj ) { for ( var name in obj ) { // if the public data object is empty, the private is still empty if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { continue; } if ( name !== "toJSON" ) { return false; } } return true; } function handleQueueMarkDefer( elem, type, src ) { var deferDataKey = type + "defer", queueDataKey = type + "queue", markDataKey = type + "mark", defer = jQuery._data( elem, deferDataKey ); if ( defer && ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { // Give room for hard-coded callbacks to fire first // and eventually mark/queue something else on the element setTimeout( function() { if ( !jQuery._data( elem, queueDataKey ) && !jQuery._data( elem, markDataKey ) ) { jQuery.removeData( elem, deferDataKey, true ); defer.fire(); } }, 0 ); } } jQuery.extend({ _mark: function( elem, type ) { if ( elem ) { type = ( type || "fx" ) + "mark"; jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); } }, _unmark: function( force, elem, type ) { if ( force !== true ) { type = elem; elem = force; force = false; } if ( elem ) { type = type || "fx"; var key = type + "mark", count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); if ( count ) { jQuery._data( elem, key, count ); } else { jQuery.removeData( elem, key, true ); handleQueueMarkDefer( elem, type, "mark" ); } } }, queue: function( elem, type, data ) { var q; if ( elem ) { type = ( type || "fx" ) + "queue"; q = jQuery._data( elem, type ); // Speed up dequeue by getting out quickly if this is just a lookup if ( data ) { if ( !q || jQuery.isArray(data) ) { q = jQuery._data( elem, type, jQuery.makeArray(data) ); } else { q.push( data ); } } return q || []; } }, dequeue: function( elem, type ) { type = type || "fx"; var queue = jQuery.queue( elem, type ), fn = queue.shift(), hooks = {}; // If the fx queue is dequeued, always remove the progress sentinel if ( fn === "inprogress" ) { fn = queue.shift(); } if ( fn ) { // Add a progress sentinel to prevent the fx queue from being // automatically dequeued if ( type === "fx" ) { queue.unshift( "inprogress" ); } jQuery._data( elem, type + ".run", hooks ); fn.call( elem, function() { jQuery.dequeue( elem, type ); }, hooks ); } if ( !queue.length ) { jQuery.removeData( elem, type + "queue " + type + ".run", true ); handleQueueMarkDefer( elem, type, "queue" ); } } }); jQuery.fn.extend({ queue: function( type, data ) { if ( typeof type !== "string" ) { data = type; type = "fx"; } if ( data === undefined ) { return jQuery.queue( this[0], type ); } return this.each(function() { var queue = jQuery.queue( this, type, data ); if ( type === "fx" && queue[0] !== "inprogress" ) { jQuery.dequeue( this, type ); } }); }, dequeue: function( type ) { return this.each(function() { jQuery.dequeue( this, type ); }); }, // Based off of the plugin by Clint Helfers, with permission. // http://blindsignals.com/index.php/2009/07/jquery-delay/ delay: function( time, type ) { time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; type = type || "fx"; return this.queue( type, function( next, hooks ) { var timeout = setTimeout( next, time ); hooks.stop = function() { clearTimeout( timeout ); }; }); }, clearQueue: function( type ) { return this.queue( type || "fx", [] ); }, // Get a promise resolved when queues of a certain type // are emptied (fx is the type by default) promise: function( type, object ) { if ( typeof type !== "string" ) { object = type; type = undefined; } type = type || "fx"; var defer = jQuery.Deferred(), elements = this, i = elements.length, count = 1, deferDataKey = type + "defer", queueDataKey = type + "queue", markDataKey = type + "mark", tmp; function resolve() { if ( !( --count ) ) { defer.resolveWith( elements, [ elements ] ); } } while( i-- ) { if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { count++; tmp.add( resolve ); } } resolve(); return defer.promise(); } }); var rclass = /[\n\t\r]/g, rspace = /\s+/, rreturn = /\r/g, rtype = /^(?:button|input)$/i, rfocusable = /^(?:button|input|object|select|textarea)$/i, rclickable = /^a(?:rea)?$/i, rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, getSetAttribute = jQuery.support.getSetAttribute, nodeHook, boolHook, fixSpecified; jQuery.fn.extend({ attr: function( name, value ) { return jQuery.access( this, name, value, true, jQuery.attr ); }, removeAttr: function( name ) { return this.each(function() { jQuery.removeAttr( this, name ); }); }, prop: function( name, value ) { return jQuery.access( this, name, value, true, jQuery.prop ); }, removeProp: function( name ) { name = jQuery.propFix[ name ] || name; return this.each(function() { // try/catch handles cases where IE balks (such as removing a property on window) try { this[ name ] = undefined; delete this[ name ]; } catch( e ) {} }); }, addClass: function( value ) { var classNames, i, l, elem, setClass, c, cl; if ( jQuery.isFunction( value ) ) { return this.each(function( j ) { jQuery( this ).addClass( value.call(this, j, this.className) ); }); } if ( value && typeof value === "string" ) { classNames = value.split( rspace ); for ( i = 0, l = this.length; i < l; i++ ) { elem = this[ i ]; if ( elem.nodeType === 1 ) { if ( !elem.className && classNames.length === 1 ) { elem.className = value; } else { setClass = " " + elem.className + " "; for ( c = 0, cl = classNames.length; c < cl; c++ ) { if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) { setClass += classNames[ c ] + " "; } } elem.className = jQuery.trim( setClass ); } } } } return this; }, removeClass: function( value ) { var classNames, i, l, elem, className, c, cl; if ( jQuery.isFunction( value ) ) { return this.each(function( j ) { jQuery( this ).removeClass( value.call(this, j, this.className) ); }); } if ( (value && typeof value === "string") || value === undefined ) { classNames = ( value || "" ).split( rspace ); for ( i = 0, l = this.length; i < l; i++ ) { elem = this[ i ]; if ( elem.nodeType === 1 && elem.className ) { if ( value ) { className = (" " + elem.className + " ").replace( rclass, " " ); for ( c = 0, cl = classNames.length; c < cl; c++ ) { className = className.replace(" " + classNames[ c ] + " ", " "); } elem.className = jQuery.trim( className ); } else { elem.className = ""; } } } } return this; }, toggleClass: function( value, stateVal ) { var type = typeof value, isBool = typeof stateVal === "boolean"; if ( jQuery.isFunction( value ) ) { return this.each(function( i ) { jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); }); } return this.each(function() { if ( type === "string" ) { // toggle individual class names var className, i = 0, self = jQuery( this ), state = stateVal, classNames = value.split( rspace ); while ( (className = classNames[ i++ ]) ) { // check each className given, space seperated list state = isBool ? state : !self.hasClass( className ); self[ state ? "addClass" : "removeClass" ]( className ); } } else if ( type === "undefined" || type === "boolean" ) { if ( this.className ) { // store className if set jQuery._data( this, "__className__", this.className ); } // toggle whole className this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; } }); }, hasClass: function( selector ) { var className = " " + selector + " ", i = 0, l = this.length; for ( ; i < l; i++ ) { if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { return true; } } return false; }, val: function( value ) { var hooks, ret, isFunction, elem = this[0]; if ( !arguments.length ) { if ( elem ) { hooks = jQuery.valHooks[ elem.nodeName.toLowerCase() ] || jQuery.valHooks[ elem.type ]; if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { return ret; } ret = elem.value; return typeof ret === "string" ? // handle most common string cases ret.replace(rreturn, "") : // handle cases where value is null/undef or number ret == null ? "" : ret; } return; } isFunction = jQuery.isFunction( value ); return this.each(function( i ) { var self = jQuery(this), val; if ( this.nodeType !== 1 ) { return; } if ( isFunction ) { val = value.call( this, i, self.val() ); } else { val = value; } // Treat null/undefined as ""; convert numbers to string if ( val == null ) { val = ""; } else if ( typeof val === "number" ) { val += ""; } else if ( jQuery.isArray( val ) ) { val = jQuery.map(val, function ( value ) { return value == null ? "" : value + ""; }); } hooks = jQuery.valHooks[ this.nodeName.toLowerCase() ] || jQuery.valHooks[ this.type ]; // If set returns undefined, fall back to normal setting if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { this.value = val; } }); } }); jQuery.extend({ valHooks: { option: { get: function( elem ) { // attributes.value is undefined in Blackberry 4.7 but // uses .value. See #6932 var val = elem.attributes.value; return !val || val.specified ? elem.value : elem.text; } }, select: { get: function( elem ) { var value, i, max, option, index = elem.selectedIndex, values = [], options = elem.options, one = elem.type === "select-one"; // Nothing was selected if ( index < 0 ) { return null; } // Loop through all the selected options i = one ? index : 0; max = one ? index + 1 : options.length; for ( ; i < max; i++ ) { option = options[ i ]; // Don't return options that are disabled or in a disabled optgroup if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { // Get the specific value for the option value = jQuery( option ).val(); // We don't need an array for one selects if ( one ) { return value; } // Multi-Selects return an array values.push( value ); } } // Fixes Bug #2551 -- select.val() broken in IE after form.reset() if ( one && !values.length && options.length ) { return jQuery( options[ index ] ).val(); } return values; }, set: function( elem, value ) { var values = jQuery.makeArray( value ); jQuery(elem).find("option").each(function() { this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; }); if ( !values.length ) { elem.selectedIndex = -1; } return values; } } }, attrFn: { val: true, css: true, html: true, text: true, data: true, width: true, height: true, offset: true }, attr: function( elem, name, value, pass ) { var ret, hooks, notxml, nType = elem.nodeType; // don't get/set attributes on text, comment and attribute nodes if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { return; } if ( pass && name in jQuery.attrFn ) { return jQuery( elem )[ name ]( value ); } // Fallback to prop when attributes are not supported if ( typeof elem.getAttribute === "undefined" ) { return jQuery.prop( elem, name, value ); } notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); // All attributes are lowercase // Grab necessary hook if one is defined if ( notxml ) { name = name.toLowerCase(); hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); } if ( value !== undefined ) { if ( value === null ) { jQuery.removeAttr( elem, name ); return; } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { return ret; } else { elem.setAttribute( name, "" + value ); return value; } } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { return ret; } else { ret = elem.getAttribute( name ); // Non-existent attributes return null, we normalize to undefined return ret === null ? undefined : ret; } }, removeAttr: function( elem, value ) { var propName, attrNames, name, l, i = 0; if ( value && elem.nodeType === 1 ) { attrNames = value.toLowerCase().split( rspace ); l = attrNames.length; for ( ; i < l; i++ ) { name = attrNames[ i ]; if ( name ) { propName = jQuery.propFix[ name ] || name; // See #9699 for explanation of this approach (setting first, then removal) jQuery.attr( elem, name, "" ); elem.removeAttribute( getSetAttribute ? name : propName ); // Set corresponding property to false for boolean attributes if ( rboolean.test( name ) && propName in elem ) { elem[ propName ] = false; } } } } }, attrHooks: { type: { set: function( elem, value ) { // We can't allow the type property to be changed (since it causes problems in IE) if ( rtype.test( elem.nodeName ) && elem.parentNode ) { jQuery.error( "type property can't be changed" ); } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { // Setting the type on a radio button after the value resets the value in IE6-9 // Reset value to it's default in case type is set after value // This is for element creation var val = elem.value; elem.setAttribute( "type", value ); if ( val ) { elem.value = val; } return value; } } }, // Use the value property for back compat // Use the nodeHook for button elements in IE6/7 (#1954) value: { get: function( elem, name ) { if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { return nodeHook.get( elem, name ); } return name in elem ? elem.value : null; }, set: function( elem, value, name ) { if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { return nodeHook.set( elem, value, name ); } // Does not return so that setAttribute is also used elem.value = value; } } }, propFix: { tabindex: "tabIndex", readonly: "readOnly", "for": "htmlFor", "class": "className", maxlength: "maxLength", cellspacing: "cellSpacing", cellpadding: "cellPadding", rowspan: "rowSpan", colspan: "colSpan", usemap: "useMap", frameborder: "frameBorder", contenteditable: "contentEditable" }, prop: function( elem, name, value ) { var ret, hooks, notxml, nType = elem.nodeType; // don't get/set properties on text, comment and attribute nodes if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { return; } notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); if ( notxml ) { // Fix name and attach hooks name = jQuery.propFix[ name ] || name; hooks = jQuery.propHooks[ name ]; } if ( value !== undefined ) { if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { return ret; } else { return ( elem[ name ] = value ); } } else { if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { return ret; } else { return elem[ name ]; } } }, propHooks: { tabIndex: { get: function( elem ) { // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ var attributeNode = elem.getAttributeNode("tabindex"); return attributeNode && attributeNode.specified ? parseInt( attributeNode.value, 10 ) : rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? 0 : undefined; } } } }); // Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; // Hook for boolean attributes boolHook = { get: function( elem, name ) { // Align boolean attributes with corresponding properties // Fall back to attribute presence where some booleans are not supported var attrNode, property = jQuery.prop( elem, name ); return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? name.toLowerCase() : undefined; }, set: function( elem, value, name ) { var propName; if ( value === false ) { // Remove boolean attributes when set to false jQuery.removeAttr( elem, name ); } else { // value is true since we know at this point it's type boolean and not false // Set boolean attributes to the same name and set the DOM property propName = jQuery.propFix[ name ] || name; if ( propName in elem ) { // Only set the IDL specifically if it already exists on the element elem[ propName ] = true; } elem.setAttribute( name, name.toLowerCase() ); } return name; } }; // IE6/7 do not support getting/setting some attributes with get/setAttribute if ( !getSetAttribute ) { fixSpecified = { name: true, id: true }; // Use this for any attribute in IE6/7 // This fixes almost every IE6/7 issue nodeHook = jQuery.valHooks.button = { get: function( elem, name ) { var ret; ret = elem.getAttributeNode( name ); return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? ret.nodeValue : undefined; }, set: function( elem, value, name ) { // Set the existing or create a new attribute node var ret = elem.getAttributeNode( name ); if ( !ret ) { ret = document.createAttribute( name ); elem.setAttributeNode( ret ); } return ( ret.nodeValue = value + "" ); } }; // Apply the nodeHook to tabindex jQuery.attrHooks.tabindex.set = nodeHook.set; // Set width and height to auto instead of 0 on empty string( Bug #8150 ) // This is for removals jQuery.each([ "width", "height" ], function( i, name ) { jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { set: function( elem, value ) { if ( value === "" ) { elem.setAttribute( name, "auto" ); return value; } } }); }); // Set contenteditable to false on removals(#10429) // Setting to empty string throws an error as an invalid value jQuery.attrHooks.contenteditable = { get: nodeHook.get, set: function( elem, value, name ) { if ( value === "" ) { value = "false"; } nodeHook.set( elem, value, name ); } }; } // Some attributes require a special call on IE if ( !jQuery.support.hrefNormalized ) { jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { get: function( elem ) { var ret = elem.getAttribute( name, 2 ); return ret === null ? undefined : ret; } }); }); } if ( !jQuery.support.style ) { jQuery.attrHooks.style = { get: function( elem ) { // Return undefined in the case of empty string // Normalize to lowercase since IE uppercases css property names return elem.style.cssText.toLowerCase() || undefined; }, set: function( elem, value ) { return ( elem.style.cssText = "" + value ); } }; } // Safari mis-reports the default selected property of an option // Accessing the parent's selectedIndex property fixes it if ( !jQuery.support.optSelected ) { jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { get: function( elem ) { var parent = elem.parentNode; if ( parent ) { parent.selectedIndex; // Make sure that it also works with optgroups, see #5701 if ( parent.parentNode ) { parent.parentNode.selectedIndex; } } return null; } }); } // IE6/7 call enctype encoding if ( !jQuery.support.enctype ) { jQuery.propFix.enctype = "encoding"; } // Radios and checkboxes getter/setter if ( !jQuery.support.checkOn ) { jQuery.each([ "radio", "checkbox" ], function() { jQuery.valHooks[ this ] = { get: function( elem ) { // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified return elem.getAttribute("value") === null ? "on" : elem.value; } }; }); } jQuery.each([ "radio", "checkbox" ], function() { jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { set: function( elem, value ) { if ( jQuery.isArray( value ) ) { return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); } } }); }); var rformElems = /^(?:textarea|input|select)$/i, rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, rhoverHack = /\bhover(\.\S+)?\b/, rkeyEvent = /^key/, rmouseEvent = /^(?:mouse|contextmenu)|click/, rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, quickParse = function( selector ) { var quick = rquickIs.exec( selector ); if ( quick ) { // 0 1 2 3 // [ _, tag, id, class ] quick[1] = ( quick[1] || "" ).toLowerCase(); quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); } return quick; }, quickIs = function( elem, m ) { var attrs = elem.attributes || {}; return ( (!m[1] || elem.nodeName.toLowerCase() === m[1]) && (!m[2] || (attrs.id || {}).value === m[2]) && (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) ); }, hoverHack = function( events ) { return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); }; /* * Helper functions for managing events -- not part of the public interface. * Props to Dean Edwards' addEvent library for many of the ideas. */ jQuery.event = { add: function( elem, types, handler, data, selector ) { var elemData, eventHandle, events, t, tns, type, namespaces, handleObj, handleObjIn, quick, handlers, special; // Don't attach events to noData or text/comment nodes (allow plain objects tho) if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { return; } // Caller can pass in an object of custom data in lieu of the handler if ( handler.handler ) { handleObjIn = handler; handler = handleObjIn.handler; } // Make sure that the handler has a unique ID, used to find/remove it later if ( !handler.guid ) { handler.guid = jQuery.guid++; } // Init the element's event structure and main handler, if this is the first events = elemData.events; if ( !events ) { elemData.events = events = {}; } eventHandle = elemData.handle; if ( !eventHandle ) { elemData.handle = eventHandle = function( e ) { // Discard the second event of a jQuery.event.trigger() and // when an event is called after a page has unloaded return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : undefined; }; // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events eventHandle.elem = elem; } // Handle multiple events separated by a space // jQuery(...).bind("mouseover mouseout", fn); types = jQuery.trim( hoverHack(types) ).split( " " ); for ( t = 0; t < types.length; t++ ) { tns = rtypenamespace.exec( types[t] ) || []; type = tns[1]; namespaces = ( tns[2] || "" ).split( "." ).sort(); // If event changes its type, use the special event handlers for the changed type special = jQuery.event.special[ type ] || {}; // If selector defined, determine special event api type, otherwise given type type = ( selector ? special.delegateType : special.bindType ) || type; // Update special based on newly reset type special = jQuery.event.special[ type ] || {}; // handleObj is passed to all event handlers handleObj = jQuery.extend({ type: type, origType: tns[1], data: data, handler: handler, guid: handler.guid, selector: selector, quick: quickParse( selector ), namespace: namespaces.join(".") }, handleObjIn ); // Init the event handler queue if we're the first handlers = events[ type ]; if ( !handlers ) { handlers = events[ type ] = []; handlers.delegateCount = 0; // Only use addEventListener/attachEvent if the special events handler returns false if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { // Bind the global event handler to the element if ( elem.addEventListener ) { elem.addEventListener( type, eventHandle, false ); } else if ( elem.attachEvent ) { elem.attachEvent( "on" + type, eventHandle ); } } } if ( special.add ) { special.add.call( elem, handleObj ); if ( !handleObj.handler.guid ) { handleObj.handler.guid = handler.guid; } } // Add to the element's handler list, delegates in front if ( selector ) { handlers.splice( handlers.delegateCount++, 0, handleObj ); } else { handlers.push( handleObj ); } // Keep track of which events have ever been used, for event optimization jQuery.event.global[ type ] = true; } // Nullify elem to prevent memory leaks in IE elem = null; }, global: {}, // Detach an event or set of events from an element remove: function( elem, types, handler, selector, mappedTypes ) { var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), t, tns, type, origType, namespaces, origCount, j, events, special, handle, eventType, handleObj; if ( !elemData || !(events = elemData.events) ) { return; } // Once for each type.namespace in types; type may be omitted types = jQuery.trim( hoverHack( types || "" ) ).split(" "); for ( t = 0; t < types.length; t++ ) { tns = rtypenamespace.exec( types[t] ) || []; type = origType = tns[1]; namespaces = tns[2]; // Unbind all events (on this namespace, if provided) for the element if ( !type ) { for ( type in events ) { jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); } continue; } special = jQuery.event.special[ type ] || {}; type = ( selector? special.delegateType : special.bindType ) || type; eventType = events[ type ] || []; origCount = eventType.length; namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; // Remove matching events for ( j = 0; j < eventType.length; j++ ) { handleObj = eventType[ j ]; if ( ( mappedTypes || origType === handleObj.origType ) && ( !handler || handler.guid === handleObj.guid ) && ( !namespaces || namespaces.test( handleObj.namespace ) ) && ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { eventType.splice( j--, 1 ); if ( handleObj.selector ) { eventType.delegateCount--; } if ( special.remove ) { special.remove.call( elem, handleObj ); } } } // Remove generic event handler if we removed something and no more handlers exist // (avoids potential for endless recursion during removal of special event handlers) if ( eventType.length === 0 && origCount !== eventType.length ) { if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { jQuery.removeEvent( elem, type, elemData.handle ); } delete events[ type ]; } } // Remove the expando if it's no longer used if ( jQuery.isEmptyObject( events ) ) { handle = elemData.handle; if ( handle ) { handle.elem = null; } // removeData also checks for emptiness and clears the expando if empty // so use it instead of delete jQuery.removeData( elem, [ "events", "handle" ], true ); } }, // Events that are safe to short-circuit if no handlers are attached. // Native DOM events should not be added, they may have inline handlers. customEvent: { "getData": true, "setData": true, "changeData": true }, trigger: function( event, data, elem, onlyHandlers ) { // Don't do events on text and comment nodes if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { return; } // Event object or event type var type = event.type || event, namespaces = [], cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; // focus/blur morphs to focusin/out; ensure we're not firing them right now if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { return; } if ( type.indexOf( "!" ) >= 0 ) { // Exclusive events trigger only for the exact event (no namespaces) type = type.slice(0, -1); exclusive = true; } if ( type.indexOf( "." ) >= 0 ) { // Namespaced trigger; create a regexp to match event type in handle() namespaces = type.split("."); type = namespaces.shift(); namespaces.sort(); } if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { // No jQuery handlers for this event type, and it can't have inline handlers return; } // Caller can pass in an Event, Object, or just an event type string event = typeof event === "object" ? // jQuery.Event object event[ jQuery.expando ] ? event : // Object literal new jQuery.Event( type, event ) : // Just the event type (string) new jQuery.Event( type ); event.type = type; event.isTrigger = true; event.exclusive = exclusive; event.namespace = namespaces.join( "." ); event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; // Handle a global trigger if ( !elem ) { // TODO: Stop taunting the data cache; remove global events and always attach to document cache = jQuery.cache; for ( i in cache ) { if ( cache[ i ].events && cache[ i ].events[ type ] ) { jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); } } return; } // Clean up the event in case it is being reused event.result = undefined; if ( !event.target ) { event.target = elem; } // Clone any incoming data and prepend the event, creating the handler arg list data = data != null ? jQuery.makeArray( data ) : []; data.unshift( event ); // Allow special events to draw outside the lines special = jQuery.event.special[ type ] || {}; if ( special.trigger && special.trigger.apply( elem, data ) === false ) { return; } // Determine event propagation path in advance, per W3C events spec (#9951) // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) eventPath = [[ elem, special.bindType || type ]]; if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { bubbleType = special.delegateType || type; cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; old = null; for ( ; cur; cur = cur.parentNode ) { eventPath.push([ cur, bubbleType ]); old = cur; } // Only add window if we got to document (e.g., not plain obj or detached DOM) if ( old && old === elem.ownerDocument ) { eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); } } // Fire handlers on the event path for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { cur = eventPath[i][0]; event.type = eventPath[i][1]; handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); if ( handle ) { handle.apply( cur, data ); } // Note that this is a bare JS function and not a jQuery handler handle = ontype && cur[ ontype ]; if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { event.preventDefault(); } } event.type = type; // If nobody prevented the default action, do it now if ( !onlyHandlers && !event.isDefaultPrevented() ) { if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { // Call a native DOM method on the target with the same name name as the event. // Can't use an .isFunction() check here because IE6/7 fails that test. // Don't do default actions on window, that's where global variables be (#6170) // IE<9 dies on focus/blur to hidden element (#1486) if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { // Don't re-trigger an onFOO event when we call its FOO() method old = elem[ ontype ]; if ( old ) { elem[ ontype ] = null; } // Prevent re-triggering of the same event, since we already bubbled it above jQuery.event.triggered = type; elem[ type ](); jQuery.event.triggered = undefined; if ( old ) { elem[ ontype ] = old; } } } } return event.result; }, dispatch: function( event ) { // Make a writable jQuery.Event from the native event object event = jQuery.event.fix( event || window.event ); var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), delegateCount = handlers.delegateCount, args = [].slice.call( arguments, 0 ), run_all = !event.exclusive && !event.namespace, handlerQueue = [], i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; // Use the fix-ed jQuery.Event rather than the (read-only) native event args[0] = event; event.delegateTarget = this; // Determine handlers that should run if there are delegated events // Avoid disabled elements in IE (#6911) and non-left-click bubbling in Firefox (#3861) if ( delegateCount && !event.target.disabled && !(event.button && event.type === "click") ) { // Pregenerate a single jQuery object for reuse with .is() jqcur = jQuery(this); jqcur.context = this.ownerDocument || this; for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { selMatch = {}; matches = []; jqcur[0] = cur; for ( i = 0; i < delegateCount; i++ ) { handleObj = handlers[ i ]; sel = handleObj.selector; if ( selMatch[ sel ] === undefined ) { selMatch[ sel ] = ( handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) ); } if ( selMatch[ sel ] ) { matches.push( handleObj ); } } if ( matches.length ) { handlerQueue.push({ elem: cur, matches: matches }); } } } // Add the remaining (directly-bound) handlers if ( handlers.length > delegateCount ) { handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); } // Run delegates first; they may want to stop propagation beneath us for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { matched = handlerQueue[ i ]; event.currentTarget = matched.elem; for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { handleObj = matched.matches[ j ]; // Triggered event must either 1) be non-exclusive and have no namespace, or // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { event.data = handleObj.data; event.handleObj = handleObj; ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) .apply( matched.elem, args ); if ( ret !== undefined ) { event.result = ret; if ( ret === false ) { event.preventDefault(); event.stopPropagation(); } } } } } return event.result; }, // Includes some event props shared by KeyEvent and MouseEvent // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), fixHooks: {}, keyHooks: { props: "char charCode key keyCode".split(" "), filter: function( event, original ) { // Add which for key events if ( event.which == null ) { event.which = original.charCode != null ? original.charCode : original.keyCode; } return event; } }, mouseHooks: { props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), filter: function( event, original ) { var eventDoc, doc, body, button = original.button, fromElement = original.fromElement; // Calculate pageX/Y if missing and clientX/Y available if ( event.pageX == null && original.clientX != null ) { eventDoc = event.target.ownerDocument || document; doc = eventDoc.documentElement; body = eventDoc.body; event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); } // Add relatedTarget, if necessary if ( !event.relatedTarget && fromElement ) { event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; } // Add which for click: 1 === left; 2 === middle; 3 === right // Note: button is not normalized, so don't use it if ( !event.which && button !== undefined ) { event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); } return event; } }, fix: function( event ) { if ( event[ jQuery.expando ] ) { return event; } // Create a writable copy of the event object and normalize some properties var i, prop, originalEvent = event, fixHook = jQuery.event.fixHooks[ event.type ] || {}, copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; event = jQuery.Event( originalEvent ); for ( i = copy.length; i; ) { prop = copy[ --i ]; event[ prop ] = originalEvent[ prop ]; } // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) if ( !event.target ) { event.target = originalEvent.srcElement || document; } // Target should not be a text node (#504, Safari) if ( event.target.nodeType === 3 ) { event.target = event.target.parentNode; } // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) if ( event.metaKey === undefined ) { event.metaKey = event.ctrlKey; } return fixHook.filter? fixHook.filter( event, originalEvent ) : event; }, special: { ready: { // Make sure the ready event is setup setup: jQuery.bindReady }, load: { // Prevent triggered image.load events from bubbling to window.load noBubble: true }, focus: { delegateType: "focusin" }, blur: { delegateType: "focusout" }, beforeunload: { setup: function( data, namespaces, eventHandle ) { // We only want to do this special case on windows if ( jQuery.isWindow( this ) ) { this.onbeforeunload = eventHandle; } }, teardown: function( namespaces, eventHandle ) { if ( this.onbeforeunload === eventHandle ) { this.onbeforeunload = null; } } } }, simulate: function( type, elem, event, bubble ) { // Piggyback on a donor event to simulate a different one. // Fake originalEvent to avoid donor's stopPropagation, but if the // simulated event prevents default then we do the same on the donor. var e = jQuery.extend( new jQuery.Event(), event, { type: type, isSimulated: true, originalEvent: {} } ); if ( bubble ) { jQuery.event.trigger( e, null, elem ); } else { jQuery.event.dispatch.call( elem, e ); } if ( e.isDefaultPrevented() ) { event.preventDefault(); } } }; // Some plugins are using, but it's undocumented/deprecated and will be removed. // The 1.7 special event interface should provide all the hooks needed now. jQuery.event.handle = jQuery.event.dispatch; jQuery.removeEvent = document.removeEventListener ? function( elem, type, handle ) { if ( elem.removeEventListener ) { elem.removeEventListener( type, handle, false ); } } : function( elem, type, handle ) { if ( elem.detachEvent ) { elem.detachEvent( "on" + type, handle ); } }; jQuery.Event = function( src, props ) { // Allow instantiation without the 'new' keyword if ( !(this instanceof jQuery.Event) ) { return new jQuery.Event( src, props ); } // Event object if ( src && src.type ) { this.originalEvent = src; this.type = src.type; // Events bubbling up the document may have been marked as prevented // by a handler lower down the tree; reflect the correct value. this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; // Event type } else { this.type = src; } // Put explicitly provided properties onto the event object if ( props ) { jQuery.extend( this, props ); } // Create a timestamp if incoming event doesn't have one this.timeStamp = src && src.timeStamp || jQuery.now(); // Mark it as fixed this[ jQuery.expando ] = true; }; function returnFalse() { return false; } function returnTrue() { return true; } // jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding // http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html jQuery.Event.prototype = { preventDefault: function() { this.isDefaultPrevented = returnTrue; var e = this.originalEvent; if ( !e ) { return; } // if preventDefault exists run it on the original event if ( e.preventDefault ) { e.preventDefault(); // otherwise set the returnValue property of the original event to false (IE) } else { e.returnValue = false; } }, stopPropagation: function() { this.isPropagationStopped = returnTrue; var e = this.originalEvent; if ( !e ) { return; } // if stopPropagation exists run it on the original event if ( e.stopPropagation ) { e.stopPropagation(); } // otherwise set the cancelBubble property of the original event to true (IE) e.cancelBubble = true; }, stopImmediatePropagation: function() { this.isImmediatePropagationStopped = returnTrue; this.stopPropagation(); }, isDefaultPrevented: returnFalse, isPropagationStopped: returnFalse, isImmediatePropagationStopped: returnFalse }; // Create mouseenter/leave events using mouseover/out and event-time checks jQuery.each({ mouseenter: "mouseover", mouseleave: "mouseout" }, function( orig, fix ) { jQuery.event.special[ orig ] = { delegateType: fix, bindType: fix, handle: function( event ) { var target = this, related = event.relatedTarget, handleObj = event.handleObj, selector = handleObj.selector, ret; // For mousenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window if ( !related || (related !== target && !jQuery.contains( target, related )) ) { event.type = handleObj.origType; ret = handleObj.handler.apply( this, arguments ); event.type = fix; } return ret; } }; }); // IE submit delegation if ( !jQuery.support.submitBubbles ) { jQuery.event.special.submit = { setup: function() { // Only need this for delegated form submit events if ( jQuery.nodeName( this, "form" ) ) { return false; } // Lazy-add a submit handler when a descendant form may potentially be submitted jQuery.event.add( this, "click._submit keypress._submit", function( e ) { // Node name check avoids a VML-related crash in IE (#9807) var elem = e.target, form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; if ( form && !form._submit_attached ) { jQuery.event.add( form, "submit._submit", function( event ) { // If form was submitted by the user, bubble the event up the tree if ( this.parentNode && !event.isTrigger ) { jQuery.event.simulate( "submit", this.parentNode, event, true ); } }); form._submit_attached = true; } }); // return undefined since we don't need an event listener }, teardown: function() { // Only need this for delegated form submit events if ( jQuery.nodeName( this, "form" ) ) { return false; } // Remove delegated handlers; cleanData eventually reaps submit handlers attached above jQuery.event.remove( this, "._submit" ); } }; } // IE change delegation and checkbox/radio fix if ( !jQuery.support.changeBubbles ) { jQuery.event.special.change = { setup: function() { if ( rformElems.test( this.nodeName ) ) { // IE doesn't fire change on a check/radio until blur; trigger it on click // after a propertychange. Eat the blur-change in special.change.handle. // This still fires onchange a second time for check/radio after blur. if ( this.type === "checkbox" || this.type === "radio" ) { jQuery.event.add( this, "propertychange._change", function( event ) { if ( event.originalEvent.propertyName === "checked" ) { this._just_changed = true; } }); jQuery.event.add( this, "click._change", function( event ) { if ( this._just_changed && !event.isTrigger ) { this._just_changed = false; jQuery.event.simulate( "change", this, event, true ); } }); } return false; } // Delegated event; lazy-add a change handler on descendant inputs jQuery.event.add( this, "beforeactivate._change", function( e ) { var elem = e.target; if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { jQuery.event.add( elem, "change._change", function( event ) { if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { jQuery.event.simulate( "change", this.parentNode, event, true ); } }); elem._change_attached = true; } }); }, handle: function( event ) { var elem = event.target; // Swallow native change events from checkbox/radio, we already triggered them above if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { return event.handleObj.handler.apply( this, arguments ); } }, teardown: function() { jQuery.event.remove( this, "._change" ); return rformElems.test( this.nodeName ); } }; } // Create "bubbling" focus and blur events if ( !jQuery.support.focusinBubbles ) { jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { // Attach a single capturing handler while someone wants focusin/focusout var attaches = 0, handler = function( event ) { jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); }; jQuery.event.special[ fix ] = { setup: function() { if ( attaches++ === 0 ) { document.addEventListener( orig, handler, true ); } }, teardown: function() { if ( --attaches === 0 ) { document.removeEventListener( orig, handler, true ); } } }; }); } jQuery.fn.extend({ on: function( types, selector, data, fn, /*INTERNAL*/ one ) { var origFn, type; // Types can be a map of types/handlers if ( typeof types === "object" ) { // ( types-Object, selector, data ) if ( typeof selector !== "string" ) { // ( types-Object, data ) data = selector; selector = undefined; } for ( type in types ) { this.on( type, selector, data, types[ type ], one ); } return this; } if ( data == null && fn == null ) { // ( types, fn ) fn = selector; data = selector = undefined; } else if ( fn == null ) { if ( typeof selector === "string" ) { // ( types, selector, fn ) fn = data; data = undefined; } else { // ( types, data, fn ) fn = data; data = selector; selector = undefined; } } if ( fn === false ) { fn = returnFalse; } else if ( !fn ) { return this; } if ( one === 1 ) { origFn = fn; fn = function( event ) { // Can use an empty set, since event contains the info jQuery().off( event ); return origFn.apply( this, arguments ); }; // Use same guid so caller can remove using origFn fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); } return this.each( function() { jQuery.event.add( this, types, fn, data, selector ); }); }, one: function( types, selector, data, fn ) { return this.on.call( this, types, selector, data, fn, 1 ); }, off: function( types, selector, fn ) { if ( types && types.preventDefault && types.handleObj ) { // ( event ) dispatched jQuery.Event var handleObj = types.handleObj; jQuery( types.delegateTarget ).off( handleObj.namespace? handleObj.type + "." + handleObj.namespace : handleObj.type, handleObj.selector, handleObj.handler ); return this; } if ( typeof types === "object" ) { // ( types-object [, selector] ) for ( var type in types ) { this.off( type, selector, types[ type ] ); } return this; } if ( selector === false || typeof selector === "function" ) { // ( types [, fn] ) fn = selector; selector = undefined; } if ( fn === false ) { fn = returnFalse; } return this.each(function() { jQuery.event.remove( this, types, fn, selector ); }); }, bind: function( types, data, fn ) { return this.on( types, null, data, fn ); }, unbind: function( types, fn ) { return this.off( types, null, fn ); }, live: function( types, data, fn ) { jQuery( this.context ).on( types, this.selector, data, fn ); return this; }, die: function( types, fn ) { jQuery( this.context ).off( types, this.selector || "**", fn ); return this; }, delegate: function( selector, types, data, fn ) { return this.on( types, selector, data, fn ); }, undelegate: function( selector, types, fn ) { // ( namespace ) or ( selector, types [, fn] ) return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); }, trigger: function( type, data ) { return this.each(function() { jQuery.event.trigger( type, data, this ); }); }, triggerHandler: function( type, data ) { if ( this[0] ) { return jQuery.event.trigger( type, data, this[0], true ); } }, toggle: function( fn ) { // Save reference to arguments for access in closure var args = arguments, guid = fn.guid || jQuery.guid++, i = 0, toggler = function( event ) { // Figure out which function to execute var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); // Make sure that clicks stop event.preventDefault(); // and execute the function return args[ lastToggle ].apply( this, arguments ) || false; }; // link all the functions, so any of them can unbind this click handler toggler.guid = guid; while ( i < args.length ) { args[ i++ ].guid = guid; } return this.click( toggler ); }, hover: function( fnOver, fnOut ) { return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); } }); jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { // Handle event binding jQuery.fn[ name ] = function( data, fn ) { if ( fn == null ) { fn = data; data = null; } return arguments.length > 0 ? this.on( name, null, data, fn ) : this.trigger( name ); }; if ( jQuery.attrFn ) { jQuery.attrFn[ name ] = true; } if ( rkeyEvent.test( name ) ) { jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; } if ( rmouseEvent.test( name ) ) { jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; } }); /*! * Sizzle CSS Selector Engine * Copyright 2013, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * More information: http://sizzlejs.com/ */ (function(){ var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, expando = "sizcache" + (Math.random() + '').replace('.', ''), done = 0, toString = Object.prototype.toString, hasDuplicate = false, baseHasDuplicate = true, rBackslash = /\\/g, rReturn = /\r\n/g, rNonWord = /\W/; // Here we check if the JavaScript engine is using some sort of // optimization where it does not always call our comparision // function. If that is the case, discard the hasDuplicate value. // Thus far that includes Google Chrome. [0, 0].sort(function() { baseHasDuplicate = false; return 0; }); var Sizzle = function( selector, context, results, seed ) { results = results || []; context = context || document; var origContext = context; if ( context.nodeType !== 1 && context.nodeType !== 9 ) { return []; } if ( !selector || typeof selector !== "string" ) { return results; } var m, set, checkSet, extra, ret, cur, pop, i, prune = true, contextXML = Sizzle.isXML( context ), parts = [], soFar = selector; // Reset the position of the chunker regexp (start from head) do { chunker.exec( "" ); m = chunker.exec( soFar ); if ( m ) { soFar = m[3]; parts.push( m[1] ); if ( m[2] ) { extra = m[3]; break; } } } while ( m ); if ( parts.length > 1 && origPOS.exec( selector ) ) { if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { set = posProcess( parts[0] + parts[1], context, seed ); } else { set = Expr.relative[ parts[0] ] ? [ context ] : Sizzle( parts.shift(), context ); while ( parts.length ) { selector = parts.shift(); if ( Expr.relative[ selector ] ) { selector += parts.shift(); } set = posProcess( selector, set, seed ); } } } else { // Take a shortcut and set the context if the root selector is an ID // (but not if it'll be faster if the inner selector is an ID) if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { ret = Sizzle.find( parts.shift(), context, contextXML ); context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; } if ( context ) { ret = seed ? { expr: parts.pop(), set: makeArray(seed) } : Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; if ( parts.length > 0 ) { checkSet = makeArray( set ); } else { prune = false; } while ( parts.length ) { cur = parts.pop(); pop = cur; if ( !Expr.relative[ cur ] ) { cur = ""; } else { pop = parts.pop(); } if ( pop == null ) { pop = context; } Expr.relative[ cur ]( checkSet, pop, contextXML ); } } else { checkSet = parts = []; } } if ( !checkSet ) { checkSet = set; } if ( !checkSet ) { Sizzle.error( cur || selector ); } if ( toString.call(checkSet) === "[object Array]" ) { if ( !prune ) { results.push.apply( results, checkSet ); } else if ( context && context.nodeType === 1 ) { for ( i = 0; checkSet[i] != null; i++ ) { if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { results.push( set[i] ); } } } else { for ( i = 0; checkSet[i] != null; i++ ) { if ( checkSet[i] && checkSet[i].nodeType === 1 ) { results.push( set[i] ); } } } } else { makeArray( checkSet, results ); } if ( extra ) { Sizzle( extra, origContext, results, seed ); Sizzle.uniqueSort( results ); } return results; }; Sizzle.uniqueSort = function( results ) { if ( sortOrder ) { hasDuplicate = baseHasDuplicate; results.sort( sortOrder ); if ( hasDuplicate ) { for ( var i = 1; i < results.length; i++ ) { if ( results[i] === results[ i - 1 ] ) { results.splice( i--, 1 ); } } } } return results; }; Sizzle.matches = function( expr, set ) { return Sizzle( expr, null, null, set ); }; Sizzle.matchesSelector = function( node, expr ) { return Sizzle( expr, null, null, [node] ).length > 0; }; Sizzle.find = function( expr, context, isXML ) { var set, i, len, match, type, left; if ( !expr ) { return []; } for ( i = 0, len = Expr.order.length; i < len; i++ ) { type = Expr.order[i]; if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { left = match[1]; match.splice( 1, 1 ); if ( left.substr( left.length - 1 ) !== "\\" ) { match[1] = (match[1] || "").replace( rBackslash, "" ); set = Expr.find[ type ]( match, context, isXML ); if ( set != null ) { expr = expr.replace( Expr.match[ type ], "" ); break; } } } } if ( !set ) { set = typeof context.getElementsByTagName !== "undefined" ? context.getElementsByTagName( "*" ) : []; } return { set: set, expr: expr }; }; Sizzle.filter = function( expr, set, inplace, not ) { var match, anyFound, type, found, item, filter, left, i, pass, old = expr, result = [], curLoop = set, isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); while ( expr && set.length ) { for ( type in Expr.filter ) { if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { filter = Expr.filter[ type ]; left = match[1]; anyFound = false; match.splice(1,1); if ( left.substr( left.length - 1 ) === "\\" ) { continue; } if ( curLoop === result ) { result = []; } if ( Expr.preFilter[ type ] ) { match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); if ( !match ) { anyFound = found = true; } else if ( match === true ) { continue; } } if ( match ) { for ( i = 0; (item = curLoop[i]) != null; i++ ) { if ( item ) { found = filter( item, match, i, curLoop ); pass = not ^ found; if ( inplace && found != null ) { if ( pass ) { anyFound = true; } else { curLoop[i] = false; } } else if ( pass ) { result.push( item ); anyFound = true; } } } } if ( found !== undefined ) { if ( !inplace ) { curLoop = result; } expr = expr.replace( Expr.match[ type ], "" ); if ( !anyFound ) { return []; } break; } } } // Improper expression if ( expr === old ) { if ( anyFound == null ) { Sizzle.error( expr ); } else { break; } } old = expr; } return curLoop; }; Sizzle.error = function( msg ) { throw new Error( "Syntax error, unrecognized expression: " + msg ); }; /** * Utility function for retreiving the text value of an array of DOM nodes * @param {Array|Element} elem */ var getText = Sizzle.getText = function( elem ) { var i, node, nodeType = elem.nodeType, ret = ""; if ( nodeType ) { if ( nodeType === 1 || nodeType === 9 ) { // Use textContent || innerText for elements if ( typeof elem.textContent === 'string' ) { return elem.textContent; } else if ( typeof elem.innerText === 'string' ) { // Replace IE's carriage returns return elem.innerText.replace( rReturn, '' ); } else { // Traverse it's children for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { ret += getText( elem ); } } } else if ( nodeType === 3 || nodeType === 4 ) { return elem.nodeValue; } } else { // If no nodeType, this is expected to be an array for ( i = 0; (node = elem[i]); i++ ) { // Do not traverse comment nodes if ( node.nodeType !== 8 ) { ret += getText( node ); } } } return ret; }; var Expr = Sizzle.selectors = { order: [ "ID", "NAME", "TAG" ], match: { ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ }, leftMatch: {}, attrMap: { "class": "className", "for": "htmlFor" }, attrHandle: { href: function( elem ) { return elem.getAttribute( "href" ); }, type: function( elem ) { return elem.getAttribute( "type" ); } }, relative: { "+": function(checkSet, part){ var isPartStr = typeof part === "string", isTag = isPartStr && !rNonWord.test( part ), isPartStrNotTag = isPartStr && !isTag; if ( isTag ) { part = part.toLowerCase(); } for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { if ( (elem = checkSet[i]) ) { while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? elem || false : elem === part; } } if ( isPartStrNotTag ) { Sizzle.filter( part, checkSet, true ); } }, ">": function( checkSet, part ) { var elem, isPartStr = typeof part === "string", i = 0, l = checkSet.length; if ( isPartStr && !rNonWord.test( part ) ) { part = part.toLowerCase(); for ( ; i < l; i++ ) { elem = checkSet[i]; if ( elem ) { var parent = elem.parentNode; checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; } } } else { for ( ; i < l; i++ ) { elem = checkSet[i]; if ( elem ) { checkSet[i] = isPartStr ? elem.parentNode : elem.parentNode === part; } } if ( isPartStr ) { Sizzle.filter( part, checkSet, true ); } } }, "": function(checkSet, part, isXML){ var nodeCheck, doneName = done++, checkFn = dirCheck; if ( typeof part === "string" && !rNonWord.test( part ) ) { part = part.toLowerCase(); nodeCheck = part; checkFn = dirNodeCheck; } checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); }, "~": function( checkSet, part, isXML ) { var nodeCheck, doneName = done++, checkFn = dirCheck; if ( typeof part === "string" && !rNonWord.test( part ) ) { part = part.toLowerCase(); nodeCheck = part; checkFn = dirNodeCheck; } checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); } }, find: { ID: function( match, context, isXML ) { if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 return m && m.parentNode ? [m] : []; } }, NAME: function( match, context ) { if ( typeof context.getElementsByName !== "undefined" ) { var ret = [], results = context.getElementsByName( match[1] ); for ( var i = 0, l = results.length; i < l; i++ ) { if ( results[i].getAttribute("name") === match[1] ) { ret.push( results[i] ); } } return ret.length === 0 ? null : ret; } }, TAG: function( match, context ) { if ( typeof context.getElementsByTagName !== "undefined" ) { return context.getElementsByTagName( match[1] ); } } }, preFilter: { CLASS: function( match, curLoop, inplace, result, not, isXML ) { match = " " + match[1].replace( rBackslash, "" ) + " "; if ( isXML ) { return match; } for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { if ( elem ) { if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { if ( !inplace ) { result.push( elem ); } } else if ( inplace ) { curLoop[i] = false; } } } return false; }, ID: function( match ) { return match[1].replace( rBackslash, "" ); }, TAG: function( match, curLoop ) { return match[1].replace( rBackslash, "" ).toLowerCase(); }, CHILD: function( match ) { if ( match[1] === "nth" ) { if ( !match[2] ) { Sizzle.error( match[0] ); } match[2] = match[2].replace(/^\+|\s*/g, ''); // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); // calculate the numbers (first)n+(last) including if they are negative match[2] = (test[1] + (test[2] || 1)) - 0; match[3] = test[3] - 0; } else if ( match[2] ) { Sizzle.error( match[0] ); } // TODO: Move to normal caching system match[0] = done++; return match; }, ATTR: function( match, curLoop, inplace, result, not, isXML ) { var name = match[1] = match[1].replace( rBackslash, "" ); if ( !isXML && Expr.attrMap[name] ) { match[1] = Expr.attrMap[name]; } // Handle if an un-quoted value was used match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); if ( match[2] === "~=" ) { match[4] = " " + match[4] + " "; } return match; }, PSEUDO: function( match, curLoop, inplace, result, not ) { if ( match[1] === "not" ) { // If we're dealing with a complex expression, or a simple one if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { match[3] = Sizzle(match[3], null, null, curLoop); } else { var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); if ( !inplace ) { result.push.apply( result, ret ); } return false; } } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { return true; } return match; }, POS: function( match ) { match.unshift( true ); return match; } }, filters: { enabled: function( elem ) { return elem.disabled === false && elem.type !== "hidden"; }, disabled: function( elem ) { return elem.disabled === true; }, checked: function( elem ) { return elem.checked === true; }, selected: function( elem ) { // Accessing this property makes selected-by-default // options in Safari work properly if ( elem.parentNode ) { elem.parentNode.selectedIndex; } return elem.selected === true; }, parent: function( elem ) { return !!elem.firstChild; }, empty: function( elem ) { return !elem.firstChild; }, has: function( elem, i, match ) { return !!Sizzle( match[3], elem ).length; }, header: function( elem ) { return (/h\d/i).test( elem.nodeName ); }, text: function( elem ) { var attr = elem.getAttribute( "type" ), type = elem.type; // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) // use getAttribute instead to test this case return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); }, radio: function( elem ) { return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; }, checkbox: function( elem ) { return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; }, file: function( elem ) { return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; }, password: function( elem ) { return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; }, submit: function( elem ) { var name = elem.nodeName.toLowerCase(); return (name === "input" || name === "button") && "submit" === elem.type; }, image: function( elem ) { return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; }, reset: function( elem ) { var name = elem.nodeName.toLowerCase(); return (name === "input" || name === "button") && "reset" === elem.type; }, button: function( elem ) { var name = elem.nodeName.toLowerCase(); return name === "input" && "button" === elem.type || name === "button"; }, input: function( elem ) { return (/input|select|textarea|button/i).test( elem.nodeName ); }, focus: function( elem ) { return elem === elem.ownerDocument.activeElement; } }, setFilters: { first: function( elem, i ) { return i === 0; }, last: function( elem, i, match, array ) { return i === array.length - 1; }, even: function( elem, i ) { return i % 2 === 0; }, odd: function( elem, i ) { return i % 2 === 1; }, lt: function( elem, i, match ) { return i < match[3] - 0; }, gt: function( elem, i, match ) { return i > match[3] - 0; }, nth: function( elem, i, match ) { return match[3] - 0 === i; }, eq: function( elem, i, match ) { return match[3] - 0 === i; } }, filter: { PSEUDO: function( elem, match, i, array ) { var name = match[1], filter = Expr.filters[ name ]; if ( filter ) { return filter( elem, i, match, array ); } else if ( name === "contains" ) { return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; } else if ( name === "not" ) { var not = match[3]; for ( var j = 0, l = not.length; j < l; j++ ) { if ( not[j] === elem ) { return false; } } return true; } else { Sizzle.error( name ); } }, CHILD: function( elem, match ) { var first, last, doneName, parent, cache, count, diff, type = match[1], node = elem; switch ( type ) { case "only": case "first": while ( (node = node.previousSibling) ) { if ( node.nodeType === 1 ) { return false; } } if ( type === "first" ) { return true; } node = elem; case "last": while ( (node = node.nextSibling) ) { if ( node.nodeType === 1 ) { return false; } } return true; case "nth": first = match[2]; last = match[3]; if ( first === 1 && last === 0 ) { return true; } doneName = match[0]; parent = elem.parentNode; if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { count = 0; for ( node = parent.firstChild; node; node = node.nextSibling ) { if ( node.nodeType === 1 ) { node.nodeIndex = ++count; } } parent[ expando ] = doneName; } diff = elem.nodeIndex - last; if ( first === 0 ) { return diff === 0; } else { return ( diff % first === 0 && diff / first >= 0 ); } } }, ID: function( elem, match ) { return elem.nodeType === 1 && elem.getAttribute("id") === match; }, TAG: function( elem, match ) { return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; }, CLASS: function( elem, match ) { return (" " + (elem.className || elem.getAttribute("class")) + " ") .indexOf( match ) > -1; }, ATTR: function( elem, match ) { var name = match[1], result = Sizzle.attr ? Sizzle.attr( elem, name ) : Expr.attrHandle[ name ] ? Expr.attrHandle[ name ]( elem ) : elem[ name ] != null ? elem[ name ] : elem.getAttribute( name ), value = result + "", type = match[2], check = match[4]; return result == null ? type === "!=" : !type && Sizzle.attr ? result != null : type === "=" ? value === check : type === "*=" ? value.indexOf(check) >= 0 : type === "~=" ? (" " + value + " ").indexOf(check) >= 0 : !check ? value && result !== false : type === "!=" ? value !== check : type === "^=" ? value.indexOf(check) === 0 : type === "$=" ? value.substr(value.length - check.length) === check : type === "|=" ? value === check || value.substr(0, check.length + 1) === check + "-" : false; }, POS: function( elem, match, i, array ) { var name = match[2], filter = Expr.setFilters[ name ]; if ( filter ) { return filter( elem, i, match, array ); } } } }; var origPOS = Expr.match.POS, fescape = function(all, num){ return "\\" + (num - 0 + 1); }; for ( var type in Expr.match ) { Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); } var makeArray = function( array, results ) { array = Array.prototype.slice.call( array, 0 ); if ( results ) { results.push.apply( results, array ); return results; } return array; }; // Perform a simple check to determine if the browser is capable of // converting a NodeList to an array using builtin methods. // Also verifies that the returned array holds DOM nodes // (which is not the case in the Blackberry browser) try { Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; // Provide a fallback method if it does not work } catch( e ) { makeArray = function( array, results ) { var i = 0, ret = results || []; if ( toString.call(array) === "[object Array]" ) { Array.prototype.push.apply( ret, array ); } else { if ( typeof array.length === "number" ) { for ( var l = array.length; i < l; i++ ) { ret.push( array[i] ); } } else { for ( ; array[i]; i++ ) { ret.push( array[i] ); } } } return ret; }; } var sortOrder, siblingCheck; if ( document.documentElement.compareDocumentPosition ) { sortOrder = function( a, b ) { if ( a === b ) { hasDuplicate = true; return 0; } if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { return a.compareDocumentPosition ? -1 : 1; } return a.compareDocumentPosition(b) & 4 ? -1 : 1; }; } else { sortOrder = function( a, b ) { // The nodes are identical, we can exit early if ( a === b ) { hasDuplicate = true; return 0; // Fallback to using sourceIndex (in IE) if it's available on both nodes } else if ( a.sourceIndex && b.sourceIndex ) { return a.sourceIndex - b.sourceIndex; } var al, bl, ap = [], bp = [], aup = a.parentNode, bup = b.parentNode, cur = aup; // If the nodes are siblings (or identical) we can do a quick check if ( aup === bup ) { return siblingCheck( a, b ); // If no parents were found then the nodes are disconnected } else if ( !aup ) { return -1; } else if ( !bup ) { return 1; } // Otherwise they're somewhere else in the tree so we need // to build up a full list of the parentNodes for comparison while ( cur ) { ap.unshift( cur ); cur = cur.parentNode; } cur = bup; while ( cur ) { bp.unshift( cur ); cur = cur.parentNode; } al = ap.length; bl = bp.length; // Start walking down the tree looking for a discrepancy for ( var i = 0; i < al && i < bl; i++ ) { if ( ap[i] !== bp[i] ) { return siblingCheck( ap[i], bp[i] ); } } // We ended someplace up the tree so do a sibling check return i === al ? siblingCheck( a, bp[i], -1 ) : siblingCheck( ap[i], b, 1 ); }; siblingCheck = function( a, b, ret ) { if ( a === b ) { return ret; } var cur = a.nextSibling; while ( cur ) { if ( cur === b ) { return -1; } cur = cur.nextSibling; } return 1; }; } // Check to see if the browser returns elements by name when // querying by getElementById (and provide a workaround) (function(){ // We're going to inject a fake input element with a specified name var form = document.createElement("div"), id = "script" + (new Date()).getTime(), root = document.documentElement; form.innerHTML = "<a name='" + id + "'/>"; // Inject it into the root element, check its status, and remove it quickly root.insertBefore( form, root.firstChild ); // The workaround has to do additional checks after a getElementById // Which slows things down for other browsers (hence the branching) if ( document.getElementById( id ) ) { Expr.find.ID = function( match, context, isXML ) { if ( typeof context.getElementById !== "undefined" && !isXML ) { var m = context.getElementById(match[1]); return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; } }; Expr.filter.ID = function( elem, match ) { var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); return elem.nodeType === 1 && node && node.nodeValue === match; }; } root.removeChild( form ); // release memory in IE root = form = null; })(); (function(){ // Check to see if the browser returns only elements // when doing getElementsByTagName("*") // Create a fake element var div = document.createElement("div"); div.appendChild( document.createComment("") ); // Make sure no comments are found if ( div.getElementsByTagName("*").length > 0 ) { Expr.find.TAG = function( match, context ) { var results = context.getElementsByTagName( match[1] ); // Filter out possible comments if ( match[1] === "*" ) { var tmp = []; for ( var i = 0; results[i]; i++ ) { if ( results[i].nodeType === 1 ) { tmp.push( results[i] ); } } results = tmp; } return results; }; } // Check to see if an attribute returns normalized href attributes div.innerHTML = "<a href='#'></a>"; if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && div.firstChild.getAttribute("href") !== "#" ) { Expr.attrHandle.href = function( elem ) { return elem.getAttribute( "href", 2 ); }; } // release memory in IE div = null; })(); if ( document.querySelectorAll ) { (function(){ var oldSizzle = Sizzle, div = document.createElement("div"), id = "__sizzle__"; div.innerHTML = "<p class='TEST'></p>"; // Safari can't handle uppercase or unicode characters when // in quirks mode. if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { return; } Sizzle = function( query, context, extra, seed ) { context = context || document; // Only use querySelectorAll on non-XML documents // (ID selectors don't work in non-HTML documents) if ( !seed && !Sizzle.isXML(context) ) { // See if we find a selector to speed up var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { // Speed-up: Sizzle("TAG") if ( match[1] ) { return makeArray( context.getElementsByTagName( query ), extra ); // Speed-up: Sizzle(".CLASS") } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { return makeArray( context.getElementsByClassName( match[2] ), extra ); } } if ( context.nodeType === 9 ) { // Speed-up: Sizzle("body") // The body element only exists once, optimize finding it if ( query === "body" && context.body ) { return makeArray( [ context.body ], extra ); // Speed-up: Sizzle("#ID") } else if ( match && match[3] ) { var elem = context.getElementById( match[3] ); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 if ( elem && elem.parentNode ) { // Handle the case where IE and Opera return items // by name instead of ID if ( elem.id === match[3] ) { return makeArray( [ elem ], extra ); } } else { return makeArray( [], extra ); } } try { return makeArray( context.querySelectorAll(query), extra ); } catch(qsaError) {} // qSA works strangely on Element-rooted queries // We can work around this by specifying an extra ID on the root // and working up from there (Thanks to Andrew Dupont for the technique) // IE 8 doesn't work on object elements } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { var oldContext = context, old = context.getAttribute( "id" ), nid = old || id, hasParent = context.parentNode, relativeHierarchySelector = /^\s*[+~]/.test( query ); if ( !old ) { context.setAttribute( "id", nid ); } else { nid = nid.replace( /'/g, "\\$&" ); } if ( relativeHierarchySelector && hasParent ) { context = context.parentNode; } try { if ( !relativeHierarchySelector || hasParent ) { return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); } } catch(pseudoError) { } finally { if ( !old ) { oldContext.removeAttribute( "id" ); } } } } return oldSizzle(query, context, extra, seed); }; for ( var prop in oldSizzle ) { Sizzle[ prop ] = oldSizzle[ prop ]; } // release memory in IE div = null; })(); } (function(){ var html = document.documentElement, matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; if ( matches ) { // Check to see if it's possible to do matchesSelector // on a disconnected node (IE 9 fails this) var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), pseudoWorks = false; try { // This should fail with an exception // Gecko does not error, returns false instead matches.call( document.documentElement, "[test!='']:sizzle" ); } catch( pseudoError ) { pseudoWorks = true; } Sizzle.matchesSelector = function( node, expr ) { // Make sure that attribute selectors are quoted expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); if ( !Sizzle.isXML( node ) ) { try { if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { var ret = matches.call( node, expr ); // IE 9's matchesSelector returns false on disconnected nodes if ( ret || !disconnectedMatch || // As well, disconnected nodes are said to be in a document // fragment in IE 9, so check for that node.document && node.document.nodeType !== 11 ) { return ret; } } } catch(e) {} } return Sizzle(expr, null, null, [node]).length > 0; }; } })(); (function(){ var div = document.createElement("div"); div.innerHTML = "<div class='test e'></div><div class='test'></div>"; // Opera can't find a second classname (in 9.6) // Also, make sure that getElementsByClassName actually exists if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { return; } // Safari caches class attributes, doesn't catch changes (in 3.2) div.lastChild.className = "e"; if ( div.getElementsByClassName("e").length === 1 ) { return; } Expr.order.splice(1, 0, "CLASS"); Expr.find.CLASS = function( match, context, isXML ) { if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { return context.getElementsByClassName(match[1]); } }; // release memory in IE div = null; })(); function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { var match = false; elem = elem[dir]; while ( elem ) { if ( elem[ expando ] === doneName ) { match = checkSet[elem.sizset]; break; } if ( elem.nodeType === 1 && !isXML ){ elem[ expando ] = doneName; elem.sizset = i; } if ( elem.nodeName.toLowerCase() === cur ) { match = elem; break; } elem = elem[dir]; } checkSet[i] = match; } } } function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { for ( var i = 0, l = checkSet.length; i < l; i++ ) { var elem = checkSet[i]; if ( elem ) { var match = false; elem = elem[dir]; while ( elem ) { if ( elem[ expando ] === doneName ) { match = checkSet[elem.sizset]; break; } if ( elem.nodeType === 1 ) { if ( !isXML ) { elem[ expando ] = doneName; elem.sizset = i; } if ( typeof cur !== "string" ) { if ( elem === cur ) { match = true; break; } } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { match = elem; break; } } elem = elem[dir]; } checkSet[i] = match; } } } if ( document.documentElement.contains ) { Sizzle.contains = function( a, b ) { return a !== b && (a.contains ? a.contains(b) : true); }; } else if ( document.documentElement.compareDocumentPosition ) { Sizzle.contains = function( a, b ) { return !!(a.compareDocumentPosition(b) & 16); }; } else { Sizzle.contains = function() { return false; }; } Sizzle.isXML = function( elem ) { // documentElement is verified for cases where it doesn't yet exist // (such as loading iframes in IE - #4833) var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; return documentElement ? documentElement.nodeName !== "HTML" : false; }; var posProcess = function( selector, context, seed ) { var match, tmpSet = [], later = "", root = context.nodeType ? [context] : context; // Position selectors must be done after the filter // And so must :not(positional) so we move all PSEUDOs to the end while ( (match = Expr.match.PSEUDO.exec( selector )) ) { later += match[0]; selector = selector.replace( Expr.match.PSEUDO, "" ); } selector = Expr.relative[selector] ? selector + "*" : selector; for ( var i = 0, l = root.length; i < l; i++ ) { Sizzle( selector, root[i], tmpSet, seed ); } return Sizzle.filter( later, tmpSet ); }; // EXPOSE // Override sizzle attribute retrieval Sizzle.attr = jQuery.attr; Sizzle.selectors.attrMap = {}; jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; jQuery.expr[":"] = jQuery.expr.filters; jQuery.unique = Sizzle.uniqueSort; jQuery.text = Sizzle.getText; jQuery.isXMLDoc = Sizzle.isXML; jQuery.contains = Sizzle.contains; })(); var runtil = /Until$/, rparentsprev = /^(?:parents|prevUntil|prevAll)/, // Note: This RegExp should be improved, or likely pulled from Sizzle rmultiselector = /,/, isSimple = /^.[^:#\[\.,]*$/, slice = Array.prototype.slice, POS = jQuery.expr.match.POS, // methods guaranteed to produce a unique set when starting from a unique set guaranteedUnique = { children: true, contents: true, next: true, prev: true }; jQuery.fn.extend({ find: function( selector ) { var self = this, i, l; if ( typeof selector !== "string" ) { return jQuery( selector ).filter(function() { for ( i = 0, l = self.length; i < l; i++ ) { if ( jQuery.contains( self[ i ], this ) ) { return true; } } }); } var ret = this.pushStack( "", "find", selector ), length, n, r; for ( i = 0, l = this.length; i < l; i++ ) { length = ret.length; jQuery.find( selector, this[i], ret ); if ( i > 0 ) { // Make sure that the results are unique for ( n = length; n < ret.length; n++ ) { for ( r = 0; r < length; r++ ) { if ( ret[r] === ret[n] ) { ret.splice(n--, 1); break; } } } } } return ret; }, has: function( target ) { var targets = jQuery( target ); return this.filter(function() { for ( var i = 0, l = targets.length; i < l; i++ ) { if ( jQuery.contains( this, targets[i] ) ) { return true; } } }); }, not: function( selector ) { return this.pushStack( winnow(this, selector, false), "not", selector); }, filter: function( selector ) { return this.pushStack( winnow(this, selector, true), "filter", selector ); }, is: function( selector ) { return !!selector && ( typeof selector === "string" ? // If this is a positional selector, check membership in the returned set // so $("p:first").is("p:last") won't return true for a doc with two "p". POS.test( selector ) ? jQuery( selector, this.context ).index( this[0] ) >= 0 : jQuery.filter( selector, this ).length > 0 : this.filter( selector ).length > 0 ); }, closest: function( selectors, context ) { var ret = [], i, l, cur = this[0]; // Array (deprecated as of jQuery 1.7) if ( jQuery.isArray( selectors ) ) { var level = 1; while ( cur && cur.ownerDocument && cur !== context ) { for ( i = 0; i < selectors.length; i++ ) { if ( jQuery( cur ).is( selectors[ i ] ) ) { ret.push({ selector: selectors[ i ], elem: cur, level: level }); } } cur = cur.parentNode; level++; } return ret; } // String var pos = POS.test( selectors ) || typeof selectors !== "string" ? jQuery( selectors, context || this.context ) : 0; for ( i = 0, l = this.length; i < l; i++ ) { cur = this[i]; while ( cur ) { if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { ret.push( cur ); break; } else { cur = cur.parentNode; if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { break; } } } } ret = ret.length > 1 ? jQuery.unique( ret ) : ret; return this.pushStack( ret, "closest", selectors ); }, // Determine the position of an element within // the matched set of elements index: function( elem ) { // No argument, return index in parent if ( !elem ) { return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; } // index in selector if ( typeof elem === "string" ) { return jQuery.inArray( this[0], jQuery( elem ) ); } // Locate the position of the desired element return jQuery.inArray( // If it receives a jQuery object, the first element is used elem.jquery ? elem[0] : elem, this ); }, add: function( selector, context ) { var set = typeof selector === "string" ? jQuery( selector, context ) : jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), all = jQuery.merge( this.get(), set ); return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? all : jQuery.unique( all ) ); }, andSelf: function() { return this.add( this.prevObject ); } }); // A painfully simple check to see if an element is disconnected // from a document (should be improved, where feasible). function isDisconnected( node ) { return !node || !node.parentNode || node.parentNode.nodeType === 11; } jQuery.each({ parent: function( elem ) { var parent = elem.parentNode; return parent && parent.nodeType !== 11 ? parent : null; }, parents: function( elem ) { return jQuery.dir( elem, "parentNode" ); }, parentsUntil: function( elem, i, until ) { return jQuery.dir( elem, "parentNode", until ); }, next: function( elem ) { return jQuery.nth( elem, 2, "nextSibling" ); }, prev: function( elem ) { return jQuery.nth( elem, 2, "previousSibling" ); }, nextAll: function( elem ) { return jQuery.dir( elem, "nextSibling" ); }, prevAll: function( elem ) { return jQuery.dir( elem, "previousSibling" ); }, nextUntil: function( elem, i, until ) { return jQuery.dir( elem, "nextSibling", until ); }, prevUntil: function( elem, i, until ) { return jQuery.dir( elem, "previousSibling", until ); }, siblings: function( elem ) { return jQuery.sibling( elem.parentNode.firstChild, elem ); }, children: function( elem ) { return jQuery.sibling( elem.firstChild ); }, contents: function( elem ) { return jQuery.nodeName( elem, "iframe" ) ? elem.contentDocument || elem.contentWindow.document : jQuery.makeArray( elem.childNodes ); } }, function( name, fn ) { jQuery.fn[ name ] = function( until, selector ) { var ret = jQuery.map( this, fn, until ); if ( !runtil.test( name ) ) { selector = until; } if ( selector && typeof selector === "string" ) { ret = jQuery.filter( selector, ret ); } ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { ret = ret.reverse(); } return this.pushStack( ret, name, slice.call( arguments ).join(",") ); }; }); jQuery.extend({ filter: function( expr, elems, not ) { if ( not ) { expr = ":not(" + expr + ")"; } return elems.length === 1 ? jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : jQuery.find.matches(expr, elems); }, dir: function( elem, dir, until ) { var matched = [], cur = elem[ dir ]; while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { if ( cur.nodeType === 1 ) { matched.push( cur ); } cur = cur[dir]; } return matched; }, nth: function( cur, result, dir, elem ) { result = result || 1; var num = 0; for ( ; cur; cur = cur[dir] ) { if ( cur.nodeType === 1 && ++num === result ) { break; } } return cur; }, sibling: function( n, elem ) { var r = []; for ( ; n; n = n.nextSibling ) { if ( n.nodeType === 1 && n !== elem ) { r.push( n ); } } return r; } }); // Implement the identical functionality for filter and not function winnow( elements, qualifier, keep ) { // Can't pass null or undefined to indexOf in Firefox 4 // Set to 0 to skip string check qualifier = qualifier || 0; if ( jQuery.isFunction( qualifier ) ) { return jQuery.grep(elements, function( elem, i ) { var retVal = !!qualifier.call( elem, i, elem ); return retVal === keep; }); } else if ( qualifier.nodeType ) { return jQuery.grep(elements, function( elem, i ) { return ( elem === qualifier ) === keep; }); } else if ( typeof qualifier === "string" ) { var filtered = jQuery.grep(elements, function( elem ) { return elem.nodeType === 1; }); if ( isSimple.test( qualifier ) ) { return jQuery.filter(qualifier, filtered, !keep); } else { qualifier = jQuery.filter( qualifier, filtered ); } } return jQuery.grep(elements, function( elem, i ) { return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; }); } function createSafeFragment( document ) { var list = nodeNames.split( "|" ), safeFrag = document.createDocumentFragment(); if ( safeFrag.createElement ) { while ( list.length ) { safeFrag.createElement( list.pop() ); } } return safeFrag; } var nodeNames = "abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|" + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, rleadingWhitespace = /^\s+/, rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, rtagName = /<([\w:]+)/, rtbody = /<tbody/i, rhtml = /<|&#?\w+;/, rnoInnerhtml = /<(?:script|style)/i, rnocache = /<(?:script|object|embed|option|style)/i, rnoshimcache = new RegExp("<(?:" + nodeNames + ")", "i"), // checked="checked" or checked rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, rscriptType = /\/(java|ecma)script/i, rcleanScript = /^\s*<!(?:\[CDATA\[|\-\-)/, wrapMap = { option: [ 1, "<select multiple='multiple'>", "</select>" ], legend: [ 1, "<fieldset>", "</fieldset>" ], thead: [ 1, "<table>", "</table>" ], tr: [ 2, "<table><tbody>", "</tbody></table>" ], td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ], area: [ 1, "<map>", "</map>" ], _default: [ 0, "", "" ] }, safeFragment = createSafeFragment( document ); wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; // IE can't serialize <link> and <script> tags normally if ( !jQuery.support.htmlSerialize ) { wrapMap._default = [ 1, "div<div>", "</div>" ]; } jQuery.fn.extend({ text: function( text ) { if ( jQuery.isFunction(text) ) { return this.each(function(i) { var self = jQuery( this ); self.text( text.call(this, i, self.text()) ); }); } if ( typeof text !== "object" && text !== undefined ) { return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); } return jQuery.text( this ); }, wrapAll: function( html ) { if ( jQuery.isFunction( html ) ) { return this.each(function(i) { jQuery(this).wrapAll( html.call(this, i) ); }); } if ( this[0] ) { // The elements to wrap the target around var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); if ( this[0].parentNode ) { wrap.insertBefore( this[0] ); } wrap.map(function() { var elem = this; while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { elem = elem.firstChild; } return elem; }).append( this ); } return this; }, wrapInner: function( html ) { if ( jQuery.isFunction( html ) ) { return this.each(function(i) { jQuery(this).wrapInner( html.call(this, i) ); }); } return this.each(function() { var self = jQuery( this ), contents = self.contents(); if ( contents.length ) { contents.wrapAll( html ); } else { self.append( html ); } }); }, wrap: function( html ) { var isFunction = jQuery.isFunction( html ); return this.each(function(i) { jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); }); }, unwrap: function() { return this.parent().each(function() { if ( !jQuery.nodeName( this, "body" ) ) { jQuery( this ).replaceWith( this.childNodes ); } }).end(); }, append: function() { return this.domManip(arguments, true, function( elem ) { if ( this.nodeType === 1 ) { this.appendChild( elem ); } }); }, prepend: function() { return this.domManip(arguments, true, function( elem ) { if ( this.nodeType === 1 ) { this.insertBefore( elem, this.firstChild ); } }); }, before: function() { if ( this[0] && this[0].parentNode ) { return this.domManip(arguments, false, function( elem ) { this.parentNode.insertBefore( elem, this ); }); } else if ( arguments.length ) { var set = jQuery.clean( arguments ); set.push.apply( set, this.toArray() ); return this.pushStack( set, "before", arguments ); } }, after: function() { if ( this[0] && this[0].parentNode ) { return this.domManip(arguments, false, function( elem ) { this.parentNode.insertBefore( elem, this.nextSibling ); }); } else if ( arguments.length ) { var set = this.pushStack( this, "after", arguments ); set.push.apply( set, jQuery.clean(arguments) ); return set; } }, // keepData is for internal use only--do not document remove: function( selector, keepData ) { for ( var i = 0, elem; (elem = this[i]) != null; i++ ) { if ( !selector || jQuery.filter( selector, [ elem ] ).length ) { if ( !keepData && elem.nodeType === 1 ) { jQuery.cleanData( elem.getElementsByTagName("*") ); jQuery.cleanData( [ elem ] ); } if ( elem.parentNode ) { elem.parentNode.removeChild( elem ); } } } return this; }, empty: function() { for ( var i = 0, elem; (elem = this[i]) != null; i++ ) { // Remove element nodes and prevent memory leaks if ( elem.nodeType === 1 ) { jQuery.cleanData( elem.getElementsByTagName("*") ); } // Remove any remaining nodes while ( elem.firstChild ) { elem.removeChild( elem.firstChild ); } } return this; }, clone: function( dataAndEvents, deepDataAndEvents ) { dataAndEvents = dataAndEvents == null ? false : dataAndEvents; deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; return this.map( function () { return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); }); }, html: function( value ) { if ( value === undefined ) { return this[0] && this[0].nodeType === 1 ? this[0].innerHTML.replace(rinlinejQuery, "") : null; // See if we can take a shortcut and just use innerHTML } else if ( typeof value === "string" && !rnoInnerhtml.test( value ) && (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) && !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) { value = value.replace(rxhtmlTag, "<$1></$2>"); try { for ( var i = 0, l = this.length; i < l; i++ ) { // Remove element nodes and prevent memory leaks if ( this[i].nodeType === 1 ) { jQuery.cleanData( this[i].getElementsByTagName("*") ); this[i].innerHTML = value; } } // If using innerHTML throws an exception, use the fallback method } catch(e) { this.empty().append( value ); } } else if ( jQuery.isFunction( value ) ) { this.each(function(i){ var self = jQuery( this ); self.html( value.call(this, i, self.html()) ); }); } else { this.empty().append( value ); } return this; }, replaceWith: function( value ) { if ( this[0] && this[0].parentNode ) { // Make sure that the elements are removed from the DOM before they are inserted // this can help fix replacing a parent with child elements if ( jQuery.isFunction( value ) ) { return this.each(function(i) { var self = jQuery(this), old = self.html(); self.replaceWith( value.call( this, i, old ) ); }); } if ( typeof value !== "string" ) { value = jQuery( value ).detach(); } return this.each(function() { var next = this.nextSibling, parent = this.parentNode; jQuery( this ).remove(); if ( next ) { jQuery(next).before( value ); } else { jQuery(parent).append( value ); } }); } else { return this.length ? this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) : this; } }, detach: function( selector ) { return this.remove( selector, true ); }, domManip: function( args, table, callback ) { var results, first, fragment, parent, value = args[0], scripts = []; // We can't cloneNode fragments that contain checked, in WebKit if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) { return this.each(function() { jQuery(this).domManip( args, table, callback, true ); }); } if ( jQuery.isFunction(value) ) { return this.each(function(i) { var self = jQuery(this); args[0] = value.call(this, i, table ? self.html() : undefined); self.domManip( args, table, callback ); }); } if ( this[0] ) { parent = value && value.parentNode; // If we're in a fragment, just use that instead of building a new one if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) { results = { fragment: parent }; } else { results = jQuery.buildFragment( args, this, scripts ); } fragment = results.fragment; if ( fragment.childNodes.length === 1 ) { first = fragment = fragment.firstChild; } else { first = fragment.firstChild; } if ( first ) { table = table && jQuery.nodeName( first, "tr" ); for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) { callback.call( table ? root(this[i], first) : this[i], // Make sure that we do not leak memory by inadvertently discarding // the original fragment (which might have attached data) instead of // using it; in addition, use the original fragment object for the last // item instead of first because it can end up being emptied incorrectly // in certain situations (Bug #8070). // Fragments from the fragment cache must always be cloned and never used // in place. results.cacheable || ( l > 1 && i < lastIndex ) ? jQuery.clone( fragment, true, true ) : fragment ); } } if ( scripts.length ) { jQuery.each( scripts, evalScript ); } } return this; } }); function root( elem, cur ) { return jQuery.nodeName(elem, "table") ? (elem.getElementsByTagName("tbody")[0] || elem.appendChild(elem.ownerDocument.createElement("tbody"))) : elem; } function cloneCopyEvent( src, dest ) { if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { return; } var type, i, l, oldData = jQuery._data( src ), curData = jQuery._data( dest, oldData ), events = oldData.events; if ( events ) { delete curData.handle; curData.events = {}; for ( type in events ) { for ( i = 0, l = events[ type ].length; i < l; i++ ) { jQuery.event.add( dest, type + ( events[ type ][ i ].namespace ? "." : "" ) + events[ type ][ i ].namespace, events[ type ][ i ], events[ type ][ i ].data ); } } } // make the cloned public data object a copy from the original if ( curData.data ) { curData.data = jQuery.extend( {}, curData.data ); } } function cloneFixAttributes( src, dest ) { var nodeName; // We do not need to do anything for non-Elements if ( dest.nodeType !== 1 ) { return; } // clearAttributes removes the attributes, which we don't want, // but also removes the attachEvent events, which we *do* want if ( dest.clearAttributes ) { dest.clearAttributes(); } // mergeAttributes, in contrast, only merges back on the // original attributes, not the events if ( dest.mergeAttributes ) { dest.mergeAttributes( src ); } nodeName = dest.nodeName.toLowerCase(); // IE6-8 fail to clone children inside object elements that use // the proprietary classid attribute value (rather than the type // attribute) to identify the type of content to display if ( nodeName === "object" ) { dest.outerHTML = src.outerHTML; } else if ( nodeName === "input" && (src.type === "checkbox" || src.type === "radio") ) { // IE6-8 fails to persist the checked state of a cloned checkbox // or radio button. Worse, IE6-7 fail to give the cloned element // a checked appearance if the defaultChecked value isn't also set if ( src.checked ) { dest.defaultChecked = dest.checked = src.checked; } // IE6-7 get confused and end up setting the value of a cloned // checkbox/radio button to an empty string instead of "on" if ( dest.value !== src.value ) { dest.value = src.value; } // IE6-8 fails to return the selected option to the default selected // state when cloning options } else if ( nodeName === "option" ) { dest.selected = src.defaultSelected; // IE6-8 fails to set the defaultValue to the correct value when // cloning other types of input fields } else if ( nodeName === "input" || nodeName === "textarea" ) { dest.defaultValue = src.defaultValue; } // Event data gets referenced instead of copied if the expando // gets copied too dest.removeAttribute( jQuery.expando ); } jQuery.buildFragment = function( args, nodes, scripts ) { var fragment, cacheable, cacheresults, doc, first = args[ 0 ]; // nodes may contain either an explicit document object, // a jQuery collection or context object. // If nodes[0] contains a valid object to assign to doc if ( nodes && nodes[0] ) { doc = nodes[0].ownerDocument || nodes[0]; } // Ensure that an attr object doesn't incorrectly stand in as a document object // Chrome and Firefox seem to allow this to occur and will throw exception // Fixes #8950 if ( !doc.createDocumentFragment ) { doc = document; } // Only cache "small" (1/2 KB) HTML strings that are associated with the main document // Cloning options loses the selected state, so don't cache them // IE 6 doesn't like it when you put <object> or <embed> elements in a fragment // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache // Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501 if ( args.length === 1 && typeof first === "string" && first.length < 512 && doc === document && first.charAt(0) === "<" && !rnocache.test( first ) && (jQuery.support.checkClone || !rchecked.test( first )) && (jQuery.support.html5Clone || !rnoshimcache.test( first )) ) { cacheable = true; cacheresults = jQuery.fragments[ first ]; if ( cacheresults && cacheresults !== 1 ) { fragment = cacheresults; } } if ( !fragment ) { fragment = doc.createDocumentFragment(); jQuery.clean( args, doc, fragment, scripts ); } if ( cacheable ) { jQuery.fragments[ first ] = cacheresults ? fragment : 1; } return { fragment: fragment, cacheable: cacheable }; }; jQuery.fragments = {}; jQuery.each({ appendTo: "append", prependTo: "prepend", insertBefore: "before", insertAfter: "after", replaceAll: "replaceWith" }, function( name, original ) { jQuery.fn[ name ] = function( selector ) { var ret = [], insert = jQuery( selector ), parent = this.length === 1 && this[0].parentNode; if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) { insert[ original ]( this[0] ); return this; } else { for ( var i = 0, l = insert.length; i < l; i++ ) { var elems = ( i > 0 ? this.clone(true) : this ).get(); jQuery( insert[i] )[ original ]( elems ); ret = ret.concat( elems ); } return this.pushStack( ret, name, insert.selector ); } }; }); function getAll( elem ) { if ( typeof elem.getElementsByTagName !== "undefined" ) { return elem.getElementsByTagName( "*" ); } else if ( typeof elem.querySelectorAll !== "undefined" ) { return elem.querySelectorAll( "*" ); } else { return []; } } // Used in clean, fixes the defaultChecked property function fixDefaultChecked( elem ) { if ( elem.type === "checkbox" || elem.type === "radio" ) { elem.defaultChecked = elem.checked; } } // Finds all inputs and passes them to fixDefaultChecked function findInputs( elem ) { var nodeName = ( elem.nodeName || "" ).toLowerCase(); if ( nodeName === "input" ) { fixDefaultChecked( elem ); // Skip scripts, get other children } else if ( nodeName !== "script" && typeof elem.getElementsByTagName !== "undefined" ) { jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked ); } } // Derived From: http://www.iecss.com/shimprove/javascript/shimprove.1-0-1.js function shimCloneNode( elem ) { var div = document.createElement( "div" ); safeFragment.appendChild( div ); div.innerHTML = elem.outerHTML; return div.firstChild; } jQuery.extend({ clone: function( elem, dataAndEvents, deepDataAndEvents ) { var srcElements, destElements, i, // IE<=8 does not properly clone detached, unknown element nodes clone = jQuery.support.html5Clone || !rnoshimcache.test( "<" + elem.nodeName ) ? elem.cloneNode( true ) : shimCloneNode( elem ); if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { // IE copies events bound via attachEvent when using cloneNode. // Calling detachEvent on the clone will also remove the events // from the original. In order to get around this, we use some // proprietary methods to clear the events. Thanks to MooTools // guys for this hotness. cloneFixAttributes( elem, clone ); // Using Sizzle here is crazy slow, so we use getElementsByTagName instead srcElements = getAll( elem ); destElements = getAll( clone ); // Weird iteration because IE will replace the length property // with an element if you are cloning the body and one of the // elements on the page has a name or id of "length" for ( i = 0; srcElements[i]; ++i ) { // Ensure that the destination node is not null; Fixes #9587 if ( destElements[i] ) { cloneFixAttributes( srcElements[i], destElements[i] ); } } } // Copy the events from the original to the clone if ( dataAndEvents ) { cloneCopyEvent( elem, clone ); if ( deepDataAndEvents ) { srcElements = getAll( elem ); destElements = getAll( clone ); for ( i = 0; srcElements[i]; ++i ) { cloneCopyEvent( srcElements[i], destElements[i] ); } } } srcElements = destElements = null; // Return the cloned set return clone; }, clean: function( elems, context, fragment, scripts ) { var checkScriptType; context = context || document; // !context.createElement fails in IE with an error but returns typeof 'object' if ( typeof context.createElement === "undefined" ) { context = context.ownerDocument || context[0] && context[0].ownerDocument || document; } var ret = [], j; for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { if ( typeof elem === "number" ) { elem += ""; } if ( !elem ) { continue; } // Convert html string into DOM nodes if ( typeof elem === "string" ) { if ( !rhtml.test( elem ) ) { elem = context.createTextNode( elem ); } else { // Fix "XHTML"-style tags in all browsers elem = elem.replace(rxhtmlTag, "<$1></$2>"); // Trim whitespace, otherwise indexOf won't work as expected var tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(), wrap = wrapMap[ tag ] || wrapMap._default, depth = wrap[0], div = context.createElement("div"); // Append wrapper element to unknown element safe doc fragment if ( context === document ) { // Use the fragment we've already created for this document safeFragment.appendChild( div ); } else { // Use a fragment created with the owner document createSafeFragment( context ).appendChild( div ); } // Go to html and back, then peel off extra wrappers div.innerHTML = wrap[1] + elem + wrap[2]; // Move to the right depth while ( depth-- ) { div = div.lastChild; } // Remove IE's autoinserted <tbody> from table fragments if ( !jQuery.support.tbody ) { // String was a <table>, *may* have spurious <tbody> var hasBody = rtbody.test(elem), tbody = tag === "table" && !hasBody ? div.firstChild && div.firstChild.childNodes : // String was a bare <thead> or <tfoot> wrap[1] === "<table>" && !hasBody ? div.childNodes : []; for ( j = tbody.length - 1; j >= 0 ; --j ) { if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) { tbody[ j ].parentNode.removeChild( tbody[ j ] ); } } } // IE completely kills leading whitespace when innerHTML is used if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild ); } elem = div.childNodes; } } // Resets defaultChecked for any radios and checkboxes // about to be appended to the DOM in IE 6/7 (#8060) var len; if ( !jQuery.support.appendChecked ) { if ( elem[0] && typeof (len = elem.length) === "number" ) { for ( j = 0; j < len; j++ ) { findInputs( elem[j] ); } } else { findInputs( elem ); } } if ( elem.nodeType ) { ret.push( elem ); } else { ret = jQuery.merge( ret, elem ); } } if ( fragment ) { checkScriptType = function( elem ) { return !elem.type || rscriptType.test( elem.type ); }; for ( i = 0; ret[i]; i++ ) { if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) { scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] ); } else { if ( ret[i].nodeType === 1 ) { var jsTags = jQuery.grep( ret[i].getElementsByTagName( "script" ), checkScriptType ); ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) ); } fragment.appendChild( ret[i] ); } } } return ret; }, cleanData: function( elems ) { var data, id, cache = jQuery.cache, special = jQuery.event.special, deleteExpando = jQuery.support.deleteExpando; for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) { continue; } id = elem[ jQuery.expando ]; if ( id ) { data = cache[ id ]; if ( data && data.events ) { for ( var type in data.events ) { if ( special[ type ] ) { jQuery.event.remove( elem, type ); // This is a shortcut to avoid jQuery.event.remove's overhead } else { jQuery.removeEvent( elem, type, data.handle ); } } // Null the DOM reference to avoid IE6/7/8 leak (#7054) if ( data.handle ) { data.handle.elem = null; } } if ( deleteExpando ) { delete elem[ jQuery.expando ]; } else if ( elem.removeAttribute ) { elem.removeAttribute( jQuery.expando ); } delete cache[ id ]; } } } }); function evalScript( i, elem ) { if ( elem.src ) { jQuery.ajax({ url: elem.src, async: false, dataType: "script" }); } else { jQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || "" ).replace( rcleanScript, "/*$0*/" ) ); } if ( elem.parentNode ) { elem.parentNode.removeChild( elem ); } } var ralpha = /alpha\([^)]*\)/i, ropacity = /opacity=([^)]*)/, // fixed for IE9, see #8346 rupper = /([A-Z]|^ms)/g, rnumpx = /^-?\d+(?:px)?$/i, rnum = /^-?\d/, rrelNum = /^([\-+])=([\-+.\de]+)/, cssShow = { position: "absolute", visibility: "hidden", display: "block" }, cssWidth = [ "Left", "Right" ], cssHeight = [ "Top", "Bottom" ], curCSS, getComputedStyle, currentStyle; jQuery.fn.css = function( name, value ) { // Setting 'undefined' is a no-op if ( arguments.length === 2 && value === undefined ) { return this; } return jQuery.access( this, name, value, true, function( elem, name, value ) { return value !== undefined ? jQuery.style( elem, name, value ) : jQuery.css( elem, name ); }); }; jQuery.extend({ // Add in style property hooks for overriding the default // behavior of getting and setting a style property cssHooks: { opacity: { get: function( elem, computed ) { if ( computed ) { // We should always get a number back from opacity var ret = curCSS( elem, "opacity", "opacity" ); return ret === "" ? "1" : ret; } else { return elem.style.opacity; } } } }, // Exclude the following css properties to add px cssNumber: { "fillOpacity": true, "fontWeight": true, "lineHeight": true, "opacity": true, "orphans": true, "widows": true, "zIndex": true, "zoom": true }, // Add in properties whose names you wish to fix before // setting or getting the value cssProps: { // normalize float css property "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" }, // Get and set the style property on a DOM Node style: function( elem, name, value, extra ) { // Don't set styles on text and comment nodes if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { return; } // Make sure that we're working with the right name var ret, type, origName = jQuery.camelCase( name ), style = elem.style, hooks = jQuery.cssHooks[ origName ]; name = jQuery.cssProps[ origName ] || origName; // Check if we're setting a value if ( value !== undefined ) { type = typeof value; // convert relative number strings (+= or -=) to relative numbers. #7345 if ( type === "string" && (ret = rrelNum.exec( value )) ) { value = ( +( ret[1] + 1) * +ret[2] ) + parseFloat( jQuery.css( elem, name ) ); // Fixes bug #9237 type = "number"; } // Make sure that NaN and null values aren't set. See: #7116 if ( value == null || type === "number" && isNaN( value ) ) { return; } // If a number was passed in, add 'px' to the (except for certain CSS properties) if ( type === "number" && !jQuery.cssNumber[ origName ] ) { value += "px"; } // If a hook was provided, use that value, otherwise just set the specified value if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value )) !== undefined ) { // Wrapped to prevent IE from throwing errors when 'invalid' values are provided // Fixes bug #5509 try { style[ name ] = value; } catch(e) {} } } else { // If a hook was provided get the non-computed value from there if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { return ret; } // Otherwise just get the value from the style object return style[ name ]; } }, css: function( elem, name, extra ) { var ret, hooks; // Make sure that we're working with the right name name = jQuery.camelCase( name ); hooks = jQuery.cssHooks[ name ]; name = jQuery.cssProps[ name ] || name; // cssFloat needs a special treatment if ( name === "cssFloat" ) { name = "float"; } // If a hook was provided get the computed value from there if ( hooks && "get" in hooks && (ret = hooks.get( elem, true, extra )) !== undefined ) { return ret; // Otherwise, if a way to get the computed value exists, use that } else if ( curCSS ) { return curCSS( elem, name ); } }, // A method for quickly swapping in/out CSS properties to get correct calculations swap: function( elem, options, callback ) { var old = {}; // Remember the old values, and insert the new ones for ( var name in options ) { old[ name ] = elem.style[ name ]; elem.style[ name ] = options[ name ]; } callback.call( elem ); // Revert the old values for ( name in options ) { elem.style[ name ] = old[ name ]; } } }); // DEPRECATED, Use jQuery.css() instead jQuery.curCSS = jQuery.css; jQuery.each(["height", "width"], function( i, name ) { jQuery.cssHooks[ name ] = { get: function( elem, computed, extra ) { var val; if ( computed ) { if ( elem.offsetWidth !== 0 ) { return getWH( elem, name, extra ); } else { jQuery.swap( elem, cssShow, function() { val = getWH( elem, name, extra ); }); } return val; } }, set: function( elem, value ) { if ( rnumpx.test( value ) ) { // ignore negative width and height values #1599 value = parseFloat( value ); if ( value >= 0 ) { return value + "px"; } } else { return value; } } }; }); if ( !jQuery.support.opacity ) { jQuery.cssHooks.opacity = { get: function( elem, computed ) { // IE uses filters for opacity return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ? ( parseFloat( RegExp.$1 ) / 100 ) + "" : computed ? "1" : ""; }, set: function( elem, value ) { var style = elem.style, currentStyle = elem.currentStyle, opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", filter = currentStyle && currentStyle.filter || style.filter || ""; // IE has trouble with opacity if it does not have layout // Force it by setting the zoom level style.zoom = 1; // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652 if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" ) { // Setting style.filter to null, "" & " " still leave "filter:" in the cssText // if "filter:" is present at all, clearType is disabled, we want to avoid this // style.removeAttribute is IE Only, but so apparently is this code path... style.removeAttribute( "filter" ); // if there there is no filter style applied in a css rule, we are done if ( currentStyle && !currentStyle.filter ) { return; } } // otherwise, set new filter values style.filter = ralpha.test( filter ) ? filter.replace( ralpha, opacity ) : filter + " " + opacity; } }; } jQuery(function() { // This hook cannot be added until DOM ready because the support test // for it is not run until after DOM ready if ( !jQuery.support.reliableMarginRight ) { jQuery.cssHooks.marginRight = { get: function( elem, computed ) { // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right // Work around by temporarily setting element display to inline-block var ret; jQuery.swap( elem, { "display": "inline-block" }, function() { if ( computed ) { ret = curCSS( elem, "margin-right", "marginRight" ); } else { ret = elem.style.marginRight; } }); return ret; } }; } }); if ( document.defaultView && document.defaultView.getComputedStyle ) { getComputedStyle = function( elem, name ) { var ret, defaultView, computedStyle; name = name.replace( rupper, "-$1" ).toLowerCase(); if ( (defaultView = elem.ownerDocument.defaultView) && (computedStyle = defaultView.getComputedStyle( elem, null )) ) { ret = computedStyle.getPropertyValue( name ); if ( ret === "" && !jQuery.contains( elem.ownerDocument.documentElement, elem ) ) { ret = jQuery.style( elem, name ); } } return ret; }; } if ( document.documentElement.currentStyle ) { currentStyle = function( elem, name ) { var left, rsLeft, uncomputed, ret = elem.currentStyle && elem.currentStyle[ name ], style = elem.style; // Avoid setting ret to empty string here // so we don't default to auto if ( ret === null && style && (uncomputed = style[ name ]) ) { ret = uncomputed; } // From the awesome hack by Dean Edwards // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 // If we're not dealing with a regular pixel number // but a number that has a weird ending, we need to convert it to pixels if ( !rnumpx.test( ret ) && rnum.test( ret ) ) { // Remember the original values left = style.left; rsLeft = elem.runtimeStyle && elem.runtimeStyle.left; // Put in the new values to get a computed value out if ( rsLeft ) { elem.runtimeStyle.left = elem.currentStyle.left; } style.left = name === "fontSize" ? "1em" : ( ret || 0 ); ret = style.pixelLeft + "px"; // Revert the changed values style.left = left; if ( rsLeft ) { elem.runtimeStyle.left = rsLeft; } } return ret === "" ? "auto" : ret; }; } curCSS = getComputedStyle || currentStyle; function getWH( elem, name, extra ) { // Start with offset property var val = name === "width" ? elem.offsetWidth : elem.offsetHeight, which = name === "width" ? cssWidth : cssHeight, i = 0, len = which.length; if ( val > 0 ) { if ( extra !== "border" ) { for ( ; i < len; i++ ) { if ( !extra ) { val -= parseFloat( jQuery.css( elem, "padding" + which[ i ] ) ) || 0; } if ( extra === "margin" ) { val += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0; } else { val -= parseFloat( jQuery.css( elem, "border" + which[ i ] + "Width" ) ) || 0; } } } return val + "px"; } // Fall back to computed then uncomputed css if necessary val = curCSS( elem, name, name ); if ( val < 0 || val == null ) { val = elem.style[ name ] || 0; } // Normalize "", auto, and prepare for extra val = parseFloat( val ) || 0; // Add padding, border, margin if ( extra ) { for ( ; i < len; i++ ) { val += parseFloat( jQuery.css( elem, "padding" + which[ i ] ) ) || 0; if ( extra !== "padding" ) { val += parseFloat( jQuery.css( elem, "border" + which[ i ] + "Width" ) ) || 0; } if ( extra === "margin" ) { val += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0; } } } return val + "px"; } if ( jQuery.expr && jQuery.expr.filters ) { jQuery.expr.filters.hidden = function( elem ) { var width = elem.offsetWidth, height = elem.offsetHeight; return ( width === 0 && height === 0 ) || (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none"); }; jQuery.expr.filters.visible = function( elem ) { return !jQuery.expr.filters.hidden( elem ); }; } var r20 = /%20/g, rbracket = /\[\]$/, rCRLF = /\r?\n/g, rhash = /#.*$/, rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL rinput = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i, // #7653, #8125, #8152: local protocol detection rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/, rnoContent = /^(?:GET|HEAD)$/, rprotocol = /^\/\//, rquery = /\?/, rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, rselectTextarea = /^(?:select|textarea)/i, rspacesAjax = /\s+/, rts = /([?&])_=[^&]*/, rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/, // Keep a copy of the old load method _load = jQuery.fn.load, /* Prefilters * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) * 2) These are called: * - BEFORE asking for a transport * - AFTER param serialization (s.data is a string if s.processData is true) * 3) key is the dataType * 4) the catchall symbol "*" can be used * 5) execution will start with transport dataType and THEN continue down to "*" if needed */ prefilters = {}, /* Transports bindings * 1) key is the dataType * 2) the catchall symbol "*" can be used * 3) selection will start with transport dataType and THEN go to "*" if needed */ transports = {}, // Document location ajaxLocation, // Document location segments ajaxLocParts, // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression allTypes = ["*/"] + ["*"]; // #8138, IE may throw an exception when accessing // a field from window.location if document.domain has been set try { ajaxLocation = location.href; } catch( e ) { // Use the href attribute of an A element // since IE will modify it given document.location ajaxLocation = document.createElement( "a" ); ajaxLocation.href = ""; ajaxLocation = ajaxLocation.href; } // Segment location into parts ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || []; // Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport function addToPrefiltersOrTransports( structure ) { // dataTypeExpression is optional and defaults to "*" return function( dataTypeExpression, func ) { if ( typeof dataTypeExpression !== "string" ) { func = dataTypeExpression; dataTypeExpression = "*"; } if ( jQuery.isFunction( func ) ) { var dataTypes = dataTypeExpression.toLowerCase().split( rspacesAjax ), i = 0, length = dataTypes.length, dataType, list, placeBefore; // For each dataType in the dataTypeExpression for ( ; i < length; i++ ) { dataType = dataTypes[ i ]; // We control if we're asked to add before // any existing element placeBefore = /^\+/.test( dataType ); if ( placeBefore ) { dataType = dataType.substr( 1 ) || "*"; } list = structure[ dataType ] = structure[ dataType ] || []; // then we add to the structure accordingly list[ placeBefore ? "unshift" : "push" ]( func ); } } }; } // Base inspection function for prefilters and transports function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR, dataType /* internal */, inspected /* internal */ ) { dataType = dataType || options.dataTypes[ 0 ]; inspected = inspected || {}; inspected[ dataType ] = true; var list = structure[ dataType ], i = 0, length = list ? list.length : 0, executeOnly = ( structure === prefilters ), selection; for ( ; i < length && ( executeOnly || !selection ); i++ ) { selection = list[ i ]( options, originalOptions, jqXHR ); // If we got redirected to another dataType // we try there if executing only and not done already if ( typeof selection === "string" ) { if ( !executeOnly || inspected[ selection ] ) { selection = undefined; } else { options.dataTypes.unshift( selection ); selection = inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR, selection, inspected ); } } } // If we're only executing or nothing was selected // we try the catchall dataType if not done already if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) { selection = inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR, "*", inspected ); } // unnecessary when only executing (prefilters) // but it'll be ignored by the caller in that case return selection; } // A special extend for ajax options // that takes "flat" options (not to be deep extended) // Fixes #9887 function ajaxExtend( target, src ) { var key, deep, flatOptions = jQuery.ajaxSettings.flatOptions || {}; for ( key in src ) { if ( src[ key ] !== undefined ) { ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; } } if ( deep ) { jQuery.extend( true, target, deep ); } } jQuery.fn.extend({ load: function( url, params, callback ) { if ( typeof url !== "string" && _load ) { return _load.apply( this, arguments ); // Don't do a request if no elements are being requested } else if ( !this.length ) { return this; } var off = url.indexOf( " " ); if ( off >= 0 ) { var selector = url.slice( off, url.length ); url = url.slice( 0, off ); } // Default to a GET request var type = "GET"; // If the second parameter was provided if ( params ) { // If it's a function if ( jQuery.isFunction( params ) ) { // We assume that it's the callback callback = params; params = undefined; // Otherwise, build a param string } else if ( typeof params === "object" ) { params = jQuery.param( params, jQuery.ajaxSettings.traditional ); type = "POST"; } } var self = this; // Request the remote document jQuery.ajax({ url: url, type: type, dataType: "html", data: params, // Complete callback (responseText is used internally) complete: function( jqXHR, status, responseText ) { // Store the response as specified by the jqXHR object responseText = jqXHR.responseText; // If successful, inject the HTML into all the matched elements if ( jqXHR.isResolved() ) { // #4825: Get the actual response in case // a dataFilter is present in ajaxSettings jqXHR.done(function( r ) { responseText = r; }); // See if a selector was specified self.html( selector ? // Create a dummy div to hold the results jQuery("<div>") // inject the contents of the document in, removing the scripts // to avoid any 'Permission Denied' errors in IE .append(responseText.replace(rscript, "")) // Locate the specified elements .find(selector) : // If not, just inject the full result responseText ); } if ( callback ) { self.each( callback, [ responseText, status, jqXHR ] ); } } }); return this; }, serialize: function() { return jQuery.param( this.serializeArray() ); }, serializeArray: function() { return this.map(function(){ return this.elements ? jQuery.makeArray( this.elements ) : this; }) .filter(function(){ return this.name && !this.disabled && ( this.checked || rselectTextarea.test( this.nodeName ) || rinput.test( this.type ) ); }) .map(function( i, elem ){ var val = jQuery( this ).val(); return val == null ? null : jQuery.isArray( val ) ? jQuery.map( val, function( val, i ){ return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; }) : { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; }).get(); } }); // Attach a bunch of functions for handling common AJAX events jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){ jQuery.fn[ o ] = function( f ){ return this.on( o, f ); }; }); jQuery.each( [ "get", "post" ], function( i, method ) { jQuery[ method ] = function( url, data, callback, type ) { // shift arguments if data argument was omitted if ( jQuery.isFunction( data ) ) { type = type || callback; callback = data; data = undefined; } return jQuery.ajax({ type: method, url: url, data: data, success: callback, dataType: type }); }; }); jQuery.extend({ getScript: function( url, callback ) { return jQuery.get( url, undefined, callback, "script" ); }, getJSON: function( url, data, callback ) { return jQuery.get( url, data, callback, "json" ); }, // Creates a full fledged settings object into target // with both ajaxSettings and settings fields. // If target is omitted, writes into ajaxSettings. ajaxSetup: function( target, settings ) { if ( settings ) { // Building a settings object ajaxExtend( target, jQuery.ajaxSettings ); } else { // Extending ajaxSettings settings = target; target = jQuery.ajaxSettings; } ajaxExtend( target, settings ); return target; }, ajaxSettings: { url: ajaxLocation, isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ), global: true, type: "GET", contentType: "application/x-www-form-urlencoded", processData: true, async: true, /* timeout: 0, data: null, dataType: null, username: null, password: null, cache: null, traditional: false, headers: {}, */ accepts: { xml: "application/xml, text/xml", html: "text/html", text: "text/plain", json: "application/json, text/javascript", "*": allTypes }, contents: { xml: /xml/, html: /html/, json: /json/ }, responseFields: { xml: "responseXML", text: "responseText" }, // List of data converters // 1) key format is "source_type destination_type" (a single space in-between) // 2) the catchall symbol "*" can be used for source_type converters: { // Convert anything to text "* text": window.String, // Text to html (true = no transformation) "text html": true, // Evaluate text as a json expression "text json": jQuery.parseJSON, // Parse text as xml "text xml": jQuery.parseXML }, // For options that shouldn't be deep extended: // you can add your own custom options here if // and when you create one that shouldn't be // deep extended (see ajaxExtend) flatOptions: { context: true, url: true } }, ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), ajaxTransport: addToPrefiltersOrTransports( transports ), // Main method ajax: function( url, options ) { // If url is an object, simulate pre-1.5 signature if ( typeof url === "object" ) { options = url; url = undefined; } // Force options to be an object options = options || {}; var // Create the final options object s = jQuery.ajaxSetup( {}, options ), // Callbacks context callbackContext = s.context || s, // Context for global events // It's the callbackContext if one was provided in the options // and if it's a DOM node or a jQuery collection globalEventContext = callbackContext !== s && ( callbackContext.nodeType || callbackContext instanceof jQuery ) ? jQuery( callbackContext ) : jQuery.event, // Deferreds deferred = jQuery.Deferred(), completeDeferred = jQuery.Callbacks( "once memory" ), // Status-dependent callbacks statusCode = s.statusCode || {}, // ifModified key ifModifiedKey, // Headers (they are sent all at once) requestHeaders = {}, requestHeadersNames = {}, // Response headers responseHeadersString, responseHeaders, // transport transport, // timeout handle timeoutTimer, // Cross-domain detection vars parts, // The jqXHR state state = 0, // To know if global events are to be dispatched fireGlobals, // Loop variable i, // Fake xhr jqXHR = { readyState: 0, // Caches the header setRequestHeader: function( name, value ) { if ( !state ) { var lname = name.toLowerCase(); name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; requestHeaders[ name ] = value; } return this; }, // Raw string getAllResponseHeaders: function() { return state === 2 ? responseHeadersString : null; }, // Builds headers hashtable if needed getResponseHeader: function( key ) { var match; if ( state === 2 ) { if ( !responseHeaders ) { responseHeaders = {}; while( ( match = rheaders.exec( responseHeadersString ) ) ) { responseHeaders[ match[1].toLowerCase() ] = match[ 2 ]; } } match = responseHeaders[ key.toLowerCase() ]; } return match === undefined ? null : match; }, // Overrides response content-type header overrideMimeType: function( type ) { if ( !state ) { s.mimeType = type; } return this; }, // Cancel the request abort: function( statusText ) { statusText = statusText || "abort"; if ( transport ) { transport.abort( statusText ); } done( 0, statusText ); return this; } }; // Callback for when everything is done // It is defined here because jslint complains if it is declared // at the end of the function (which would be more logical and readable) function done( status, nativeStatusText, responses, headers ) { // Called once if ( state === 2 ) { return; } // State is "done" now state = 2; // Clear timeout if it exists if ( timeoutTimer ) { clearTimeout( timeoutTimer ); } // Dereference transport for early garbage collection // (no matter how long the jqXHR object will be used) transport = undefined; // Cache response headers responseHeadersString = headers || ""; // Set readyState jqXHR.readyState = status > 0 ? 4 : 0; var isSuccess, success, error, statusText = nativeStatusText, response = responses ? ajaxHandleResponses( s, jqXHR, responses ) : undefined, lastModified, etag; // If successful, handle type chaining if ( status >= 200 && status < 300 || status === 304 ) { // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. if ( s.ifModified ) { if ( ( lastModified = jqXHR.getResponseHeader( "Last-Modified" ) ) ) { jQuery.lastModified[ ifModifiedKey ] = lastModified; } if ( ( etag = jqXHR.getResponseHeader( "Etag" ) ) ) { jQuery.etag[ ifModifiedKey ] = etag; } } // If not modified if ( status === 304 ) { statusText = "notmodified"; isSuccess = true; // If we have data } else { try { success = ajaxConvert( s, response ); statusText = "success"; isSuccess = true; } catch(e) { // We have a parsererror statusText = "parsererror"; error = e; } } } else { // We extract error from statusText // then normalize statusText and status for non-aborts error = statusText; if ( !statusText || status ) { statusText = "error"; if ( status < 0 ) { status = 0; } } } // Set data for the fake xhr object jqXHR.status = status; jqXHR.statusText = "" + ( nativeStatusText || statusText ); // Success/Error if ( isSuccess ) { deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); } else { deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); } // Status-dependent callbacks jqXHR.statusCode( statusCode ); statusCode = undefined; if ( fireGlobals ) { globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ), [ jqXHR, s, isSuccess ? success : error ] ); } // Complete completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); if ( fireGlobals ) { globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); // Handle the global AJAX counter if ( !( --jQuery.active ) ) { jQuery.event.trigger( "ajaxStop" ); } } } // Attach deferreds deferred.promise( jqXHR ); jqXHR.success = jqXHR.done; jqXHR.error = jqXHR.fail; jqXHR.complete = completeDeferred.add; // Status-dependent callbacks jqXHR.statusCode = function( map ) { if ( map ) { var tmp; if ( state < 2 ) { for ( tmp in map ) { statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ]; } } else { tmp = map[ jqXHR.status ]; jqXHR.then( tmp, tmp ); } } return this; }; // Remove hash character (#7531: and string promotion) // Add protocol if not provided (#5866: IE7 issue with protocol-less urls) // We also use the url parameter if available s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); // Extract dataTypes list s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( rspacesAjax ); // Determine if a cross-domain request is in order if ( s.crossDomain == null ) { parts = rurl.exec( s.url.toLowerCase() ); s.crossDomain = !!( parts && ( parts[ 1 ] != ajaxLocParts[ 1 ] || parts[ 2 ] != ajaxLocParts[ 2 ] || ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) != ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) ) ); } // Convert data if not already a string if ( s.data && s.processData && typeof s.data !== "string" ) { s.data = jQuery.param( s.data, s.traditional ); } // Apply prefilters inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); // If request was aborted inside a prefiler, stop there if ( state === 2 ) { return false; } // We can fire global events as of now if asked to fireGlobals = s.global; // Uppercase the type s.type = s.type.toUpperCase(); // Determine if request has content s.hasContent = !rnoContent.test( s.type ); // Watch for a new set of requests if ( fireGlobals && jQuery.active++ === 0 ) { jQuery.event.trigger( "ajaxStart" ); } // More options handling for requests with no content if ( !s.hasContent ) { // If data is available, append data to url if ( s.data ) { s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data; // #9682: remove data so that it's not used in an eventual retry delete s.data; } // Get ifModifiedKey before adding the anti-cache parameter ifModifiedKey = s.url; // Add anti-cache in url if needed if ( s.cache === false ) { var ts = jQuery.now(), // try replacing _= if it is there ret = s.url.replace( rts, "$1_=" + ts ); // if nothing was replaced, add timestamp to the end s.url = ret + ( ( ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" ); } } // Set the correct header, if data is being sent if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { jqXHR.setRequestHeader( "Content-Type", s.contentType ); } // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. if ( s.ifModified ) { ifModifiedKey = ifModifiedKey || s.url; if ( jQuery.lastModified[ ifModifiedKey ] ) { jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ ifModifiedKey ] ); } if ( jQuery.etag[ ifModifiedKey ] ) { jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ ifModifiedKey ] ); } } // Set the Accepts header for the server, depending on the dataType jqXHR.setRequestHeader( "Accept", s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : s.accepts[ "*" ] ); // Check for headers option for ( i in s.headers ) { jqXHR.setRequestHeader( i, s.headers[ i ] ); } // Allow custom headers/mimetypes and early abort if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) { // Abort if not done already jqXHR.abort(); return false; } // Install callbacks on deferreds for ( i in { success: 1, error: 1, complete: 1 } ) { jqXHR[ i ]( s[ i ] ); } // Get transport transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); // If no transport, we auto-abort if ( !transport ) { done( -1, "No Transport" ); } else { jqXHR.readyState = 1; // Send global event if ( fireGlobals ) { globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); } // Timeout if ( s.async && s.timeout > 0 ) { timeoutTimer = setTimeout( function(){ jqXHR.abort( "timeout" ); }, s.timeout ); } try { state = 1; transport.send( requestHeaders, done ); } catch (e) { // Propagate exception as error if not done if ( state < 2 ) { done( -1, e ); // Simply rethrow otherwise } else { throw e; } } } return jqXHR; }, // Serialize an array of form elements or a set of // key/values into a query string param: function( a, traditional ) { var s = [], add = function( key, value ) { // If value is a function, invoke it and return its value value = jQuery.isFunction( value ) ? value() : value; s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value ); }; // Set traditional to true for jQuery <= 1.3.2 behavior. if ( traditional === undefined ) { traditional = jQuery.ajaxSettings.traditional; } // If an array was passed in, assume that it is an array of form elements. if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { // Serialize the form elements jQuery.each( a, function() { add( this.name, this.value ); }); } else { // If traditional, encode the "old" way (the way 1.3.2 or older // did it), otherwise encode params recursively. for ( var prefix in a ) { buildParams( prefix, a[ prefix ], traditional, add ); } } // Return the resulting serialization return s.join( "&" ).replace( r20, "+" ); } }); function buildParams( prefix, obj, traditional, add ) { if ( jQuery.isArray( obj ) ) { // Serialize array item. jQuery.each( obj, function( i, v ) { if ( traditional || rbracket.test( prefix ) ) { // Treat each array item as a scalar. add( prefix, v ); } else { // If array item is non-scalar (array or object), encode its // numeric index to resolve deserialization ambiguity issues. // Note that rack (as of 1.0.0) can't currently deserialize // nested arrays properly, and attempting to do so may cause // a server error. Possible fixes are to modify rack's // deserialization algorithm or to provide an option or flag // to force array serialization to be shallow. buildParams( prefix + "[" + ( typeof v === "object" || jQuery.isArray(v) ? i : "" ) + "]", v, traditional, add ); } }); } else if ( !traditional && obj != null && typeof obj === "object" ) { // Serialize object item. for ( var name in obj ) { buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); } } else { // Serialize scalar item. add( prefix, obj ); } } // This is still on the jQuery object... for now // Want to move this to jQuery.ajax some day jQuery.extend({ // Counter for holding the number of active queries active: 0, // Last-Modified header cache for next request lastModified: {}, etag: {} }); /* Handles responses to an ajax request: * - sets all responseXXX fields accordingly * - finds the right dataType (mediates between content-type and expected dataType) * - returns the corresponding response */ function ajaxHandleResponses( s, jqXHR, responses ) { var contents = s.contents, dataTypes = s.dataTypes, responseFields = s.responseFields, ct, type, finalDataType, firstDataType; // Fill responseXXX fields for ( type in responseFields ) { if ( type in responses ) { jqXHR[ responseFields[type] ] = responses[ type ]; } } // Remove auto dataType and get content-type in the process while( dataTypes[ 0 ] === "*" ) { dataTypes.shift(); if ( ct === undefined ) { ct = s.mimeType || jqXHR.getResponseHeader( "content-type" ); } } // Check if we're dealing with a known content-type if ( ct ) { for ( type in contents ) { if ( contents[ type ] && contents[ type ].test( ct ) ) { dataTypes.unshift( type ); break; } } } // Check to see if we have a response for the expected dataType if ( dataTypes[ 0 ] in responses ) { finalDataType = dataTypes[ 0 ]; } else { // Try convertible dataTypes for ( type in responses ) { if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) { finalDataType = type; break; } if ( !firstDataType ) { firstDataType = type; } } // Or just use first one finalDataType = finalDataType || firstDataType; } // If we found a dataType // We add the dataType to the list if needed // and return the corresponding response if ( finalDataType ) { if ( finalDataType !== dataTypes[ 0 ] ) { dataTypes.unshift( finalDataType ); } return responses[ finalDataType ]; } } // Chain conversions given the request and the original response function ajaxConvert( s, response ) { // Apply the dataFilter if provided if ( s.dataFilter ) { response = s.dataFilter( response, s.dataType ); } var dataTypes = s.dataTypes, converters = {}, i, key, length = dataTypes.length, tmp, // Current and previous dataTypes current = dataTypes[ 0 ], prev, // Conversion expression conversion, // Conversion function conv, // Conversion functions (transitive conversion) conv1, conv2; // For each dataType in the chain for ( i = 1; i < length; i++ ) { // Create converters map // with lowercased keys if ( i === 1 ) { for ( key in s.converters ) { if ( typeof key === "string" ) { converters[ key.toLowerCase() ] = s.converters[ key ]; } } } // Get the dataTypes prev = current; current = dataTypes[ i ]; // If current is auto dataType, update it to prev if ( current === "*" ) { current = prev; // If no auto and dataTypes are actually different } else if ( prev !== "*" && prev !== current ) { // Get the converter conversion = prev + " " + current; conv = converters[ conversion ] || converters[ "* " + current ]; // If there is no direct converter, search transitively if ( !conv ) { conv2 = undefined; for ( conv1 in converters ) { tmp = conv1.split( " " ); if ( tmp[ 0 ] === prev || tmp[ 0 ] === "*" ) { conv2 = converters[ tmp[1] + " " + current ]; if ( conv2 ) { conv1 = converters[ conv1 ]; if ( conv1 === true ) { conv = conv2; } else if ( conv2 === true ) { conv = conv1; } break; } } } } // If we found no converter, dispatch an error if ( !( conv || conv2 ) ) { jQuery.error( "No conversion from " + conversion.replace(" "," to ") ); } // If found converter is not an equivalence if ( conv !== true ) { // Convert with 1 or 2 converters accordingly response = conv ? conv( response ) : conv2( conv1(response) ); } } } return response; } var jsc = jQuery.now(), jsre = /(\=)\?(&|$)|\?\?/i; // Default jsonp settings jQuery.ajaxSetup({ jsonp: "callback", jsonpCallback: function() { return jQuery.expando + "_" + ( jsc++ ); } }); // Detect, normalize options and install callbacks for jsonp requests jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { var inspectData = s.contentType === "application/x-www-form-urlencoded" && ( typeof s.data === "string" ); if ( s.dataTypes[ 0 ] === "jsonp" || s.jsonp !== false && ( jsre.test( s.url ) || inspectData && jsre.test( s.data ) ) ) { var responseContainer, jsonpCallback = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback, previous = window[ jsonpCallback ], url = s.url, data = s.data, replace = "$1" + jsonpCallback + "$2"; if ( s.jsonp !== false ) { url = url.replace( jsre, replace ); if ( s.url === url ) { if ( inspectData ) { data = data.replace( jsre, replace ); } if ( s.data === data ) { // Add callback manually url += (/\?/.test( url ) ? "&" : "?") + s.jsonp + "=" + jsonpCallback; } } } s.url = url; s.data = data; // Install callback window[ jsonpCallback ] = function( response ) { responseContainer = [ response ]; }; // Clean-up function jqXHR.always(function() { // Set callback back to previous value window[ jsonpCallback ] = previous; // Call if it was a function and we have a response if ( responseContainer && jQuery.isFunction( previous ) ) { window[ jsonpCallback ]( responseContainer[ 0 ] ); } }); // Use data converter to retrieve json after script execution s.converters["script json"] = function() { if ( !responseContainer ) { jQuery.error( jsonpCallback + " was not called" ); } return responseContainer[ 0 ]; }; // force json dataType s.dataTypes[ 0 ] = "json"; // Delegate to script return "script"; } }); // Install script dataType jQuery.ajaxSetup({ accepts: { script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" }, contents: { script: /javascript|ecmascript/ }, converters: { "text script": function( text ) { jQuery.globalEval( text ); return text; } } }); // Handle cache's special case and global jQuery.ajaxPrefilter( "script", function( s ) { if ( s.cache === undefined ) { s.cache = false; } if ( s.crossDomain ) { s.type = "GET"; s.global = false; } }); // Bind script tag hack transport jQuery.ajaxTransport( "script", function(s) { // This transport only deals with cross domain requests if ( s.crossDomain ) { var script, head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement; return { send: function( _, callback ) { script = document.createElement( "script" ); script.async = "async"; if ( s.scriptCharset ) { script.charset = s.scriptCharset; } script.src = s.url; // Attach handlers for all browsers script.onload = script.onreadystatechange = function( _, isAbort ) { if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { // Handle memory leak in IE script.onload = script.onreadystatechange = null; // Remove the script if ( head && script.parentNode ) { head.removeChild( script ); } // Dereference the script script = undefined; // Callback if not abort if ( !isAbort ) { callback( 200, "success" ); } } }; // Use insertBefore instead of appendChild to circumvent an IE6 bug. // This arises when a base node is used (#2709 and #4378). head.insertBefore( script, head.firstChild ); }, abort: function() { if ( script ) { script.onload( 0, 1 ); } } }; } }); var // #5280: Internet Explorer will keep connections alive if we don't abort on unload xhrOnUnloadAbort = window.ActiveXObject ? function() { // Abort all pending requests for ( var key in xhrCallbacks ) { xhrCallbacks[ key ]( 0, 1 ); } } : false, xhrId = 0, xhrCallbacks; // Functions to create xhrs function createStandardXHR() { try { return new window.XMLHttpRequest(); } catch( e ) {} } function createActiveXHR() { try { return new window.ActiveXObject( "Microsoft.XMLHTTP" ); } catch( e ) {} } // Create the request object // (This is still attached to ajaxSettings for backward compatibility) jQuery.ajaxSettings.xhr = window.ActiveXObject ? /* Microsoft failed to properly * implement the XMLHttpRequest in IE7 (can't request local files), * so we use the ActiveXObject when it is available * Additionally XMLHttpRequest can be disabled in IE7/IE8 so * we need a fallback. */ function() { return !this.isLocal && createStandardXHR() || createActiveXHR(); } : // For all other browsers, use the standard XMLHttpRequest object createStandardXHR; // Determine support properties (function( xhr ) { jQuery.extend( jQuery.support, { ajax: !!xhr, cors: !!xhr && ( "withCredentials" in xhr ) }); })( jQuery.ajaxSettings.xhr() ); // Create transport if the browser can provide an xhr if ( jQuery.support.ajax ) { jQuery.ajaxTransport(function( s ) { // Cross domain only allowed if supported through XMLHttpRequest if ( !s.crossDomain || jQuery.support.cors ) { var callback; return { send: function( headers, complete ) { // Get a new xhr var xhr = s.xhr(), handle, i; // Open the socket // Passing null username, generates a login popup on Opera (#2865) if ( s.username ) { xhr.open( s.type, s.url, s.async, s.username, s.password ); } else { xhr.open( s.type, s.url, s.async ); } // Apply custom fields if provided if ( s.xhrFields ) { for ( i in s.xhrFields ) { xhr[ i ] = s.xhrFields[ i ]; } } // Override mime type if needed if ( s.mimeType && xhr.overrideMimeType ) { xhr.overrideMimeType( s.mimeType ); } // X-Requested-With header // For cross-domain requests, seeing as conditions for a preflight are // akin to a jigsaw puzzle, we simply never set it to be sure. // (it can always be set on a per-request basis or even using ajaxSetup) // For same-domain requests, won't change header if already provided. if ( !s.crossDomain && !headers["X-Requested-With"] ) { headers[ "X-Requested-With" ] = "XMLHttpRequest"; } // Need an extra try/catch for cross domain requests in Firefox 3 try { for ( i in headers ) { xhr.setRequestHeader( i, headers[ i ] ); } } catch( _ ) {} // Do send the request // This may raise an exception which is actually // handled in jQuery.ajax (so no try/catch here) xhr.send( ( s.hasContent && s.data ) || null ); // Listener callback = function( _, isAbort ) { var status, statusText, responseHeaders, responses, xml; // Firefox throws exceptions when accessing properties // of an xhr when a network error occured // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE) try { // Was never called and is aborted or complete if ( callback && ( isAbort || xhr.readyState === 4 ) ) { // Only called once callback = undefined; // Do not keep as active anymore if ( handle ) { xhr.onreadystatechange = jQuery.noop; if ( xhrOnUnloadAbort ) { delete xhrCallbacks[ handle ]; } } // If it's an abort if ( isAbort ) { // Abort it manually if needed if ( xhr.readyState !== 4 ) { xhr.abort(); } } else { status = xhr.status; responseHeaders = xhr.getAllResponseHeaders(); responses = {}; xml = xhr.responseXML; // Construct response list if ( xml && xml.documentElement /* #4958 */ ) { responses.xml = xml; } responses.text = xhr.responseText; // Firefox throws an exception when accessing // statusText for faulty cross-domain requests try { statusText = xhr.statusText; } catch( e ) { // We normalize with Webkit giving an empty statusText statusText = ""; } // Filter status for non standard behaviors // If the request is local and we have data: assume a success // (success with no data won't get notified, that's the best we // can do given current implementations) if ( !status && s.isLocal && !s.crossDomain ) { status = responses.text ? 200 : 404; // IE - #1450: sometimes returns 1223 when it should be 204 } else if ( status === 1223 ) { status = 204; } } } } catch( firefoxAccessException ) { if ( !isAbort ) { complete( -1, firefoxAccessException ); } } // Call complete if needed if ( responses ) { complete( status, statusText, responses, responseHeaders ); } }; // if we're in sync mode or it's in cache // and has been retrieved directly (IE6 & IE7) // we need to manually fire the callback if ( !s.async || xhr.readyState === 4 ) { callback(); } else { handle = ++xhrId; if ( xhrOnUnloadAbort ) { // Create the active xhrs callbacks list if needed // and attach the unload handler if ( !xhrCallbacks ) { xhrCallbacks = {}; jQuery( window ).unload( xhrOnUnloadAbort ); } // Add to list of active xhrs callbacks xhrCallbacks[ handle ] = callback; } xhr.onreadystatechange = callback; } }, abort: function() { if ( callback ) { callback(0,1); } } }; } }); } var elemdisplay = {}, iframe, iframeDoc, rfxtypes = /^(?:toggle|show|hide)$/, rfxnum = /^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i, timerId, fxAttrs = [ // height animations [ "height", "marginTop", "marginBottom", "paddingTop", "paddingBottom" ], // width animations [ "width", "marginLeft", "marginRight", "paddingLeft", "paddingRight" ], // opacity animations [ "opacity" ] ], fxNow; jQuery.fn.extend({ show: function( speed, easing, callback ) { var elem, display; if ( speed || speed === 0 ) { return this.animate( genFx("show", 3), speed, easing, callback ); } else { for ( var i = 0, j = this.length; i < j; i++ ) { elem = this[ i ]; if ( elem.style ) { display = elem.style.display; // Reset the inline display of this element to learn if it is // being hidden by cascaded rules or not if ( !jQuery._data(elem, "olddisplay") && display === "none" ) { display = elem.style.display = ""; } // Set elements which have been overridden with display: none // in a stylesheet to whatever the default browser style is // for such an element if ( display === "" && jQuery.css(elem, "display") === "none" ) { jQuery._data( elem, "olddisplay", defaultDisplay(elem.nodeName) ); } } } // Set the display of most of the elements in a second loop // to avoid the constant reflow for ( i = 0; i < j; i++ ) { elem = this[ i ]; if ( elem.style ) { display = elem.style.display; if ( display === "" || display === "none" ) { elem.style.display = jQuery._data( elem, "olddisplay" ) || ""; } } } return this; } }, hide: function( speed, easing, callback ) { if ( speed || speed === 0 ) { return this.animate( genFx("hide", 3), speed, easing, callback); } else { var elem, display, i = 0, j = this.length; for ( ; i < j; i++ ) { elem = this[i]; if ( elem.style ) { display = jQuery.css( elem, "display" ); if ( display !== "none" && !jQuery._data( elem, "olddisplay" ) ) { jQuery._data( elem, "olddisplay", display ); } } } // Set the display of the elements in a second loop // to avoid the constant reflow for ( i = 0; i < j; i++ ) { if ( this[i].style ) { this[i].style.display = "none"; } } return this; } }, // Save the old toggle function _toggle: jQuery.fn.toggle, toggle: function( fn, fn2, callback ) { var bool = typeof fn === "boolean"; if ( jQuery.isFunction(fn) && jQuery.isFunction(fn2) ) { this._toggle.apply( this, arguments ); } else if ( fn == null || bool ) { this.each(function() { var state = bool ? fn : jQuery(this).is(":hidden"); jQuery(this)[ state ? "show" : "hide" ](); }); } else { this.animate(genFx("toggle", 3), fn, fn2, callback); } return this; }, fadeTo: function( speed, to, easing, callback ) { return this.filter(":hidden").css("opacity", 0).show().end() .animate({opacity: to}, speed, easing, callback); }, animate: function( prop, speed, easing, callback ) { var optall = jQuery.speed( speed, easing, callback ); if ( jQuery.isEmptyObject( prop ) ) { return this.each( optall.complete, [ false ] ); } // Do not change referenced properties as per-property easing will be lost prop = jQuery.extend( {}, prop ); function doAnimation() { // XXX 'this' does not always have a nodeName when running the // test suite if ( optall.queue === false ) { jQuery._mark( this ); } var opt = jQuery.extend( {}, optall ), isElement = this.nodeType === 1, hidden = isElement && jQuery(this).is(":hidden"), name, val, p, e, parts, start, end, unit, method; // will store per property easing and be used to determine when an animation is complete opt.animatedProperties = {}; for ( p in prop ) { // property name normalization name = jQuery.camelCase( p ); if ( p !== name ) { prop[ name ] = prop[ p ]; delete prop[ p ]; } val = prop[ name ]; // easing resolution: per property > opt.specialEasing > opt.easing > 'swing' (default) if ( jQuery.isArray( val ) ) { opt.animatedProperties[ name ] = val[ 1 ]; val = prop[ name ] = val[ 0 ]; } else { opt.animatedProperties[ name ] = opt.specialEasing && opt.specialEasing[ name ] || opt.easing || 'swing'; } if ( val === "hide" && hidden || val === "show" && !hidden ) { return opt.complete.call( this ); } if ( isElement && ( name === "height" || name === "width" ) ) { // Make sure that nothing sneaks out // Record all 3 overflow attributes because IE does not // change the overflow attribute when overflowX and // overflowY are set to the same value opt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ]; // Set display property to inline-block for height/width // animations on inline elements that are having width/height animated if ( jQuery.css( this, "display" ) === "inline" && jQuery.css( this, "float" ) === "none" ) { // inline-level elements accept inline-block; // block-level elements need to be inline with layout if ( !jQuery.support.inlineBlockNeedsLayout || defaultDisplay( this.nodeName ) === "inline" ) { this.style.display = "inline-block"; } else { this.style.zoom = 1; } } } } if ( opt.overflow != null ) { this.style.overflow = "hidden"; } for ( p in prop ) { e = new jQuery.fx( this, opt, p ); val = prop[ p ]; if ( rfxtypes.test( val ) ) { // Tracks whether to show or hide based on private // data attached to the element method = jQuery._data( this, "toggle" + p ) || ( val === "toggle" ? hidden ? "show" : "hide" : 0 ); if ( method ) { jQuery._data( this, "toggle" + p, method === "show" ? "hide" : "show" ); e[ method ](); } else { e[ val ](); } } else { parts = rfxnum.exec( val ); start = e.cur(); if ( parts ) { end = parseFloat( parts[2] ); unit = parts[3] || ( jQuery.cssNumber[ p ] ? "" : "px" ); // We need to compute starting value if ( unit !== "px" ) { jQuery.style( this, p, (end || 1) + unit); start = ( (end || 1) / e.cur() ) * start; jQuery.style( this, p, start + unit); } // If a +=/-= token was provided, we're doing a relative animation if ( parts[1] ) { end = ( (parts[ 1 ] === "-=" ? -1 : 1) * end ) + start; } e.custom( start, end, unit ); } else { e.custom( start, val, "" ); } } } // For JS strict compliance return true; } return optall.queue === false ? this.each( doAnimation ) : this.queue( optall.queue, doAnimation ); }, stop: function( type, clearQueue, gotoEnd ) { if ( typeof type !== "string" ) { gotoEnd = clearQueue; clearQueue = type; type = undefined; } if ( clearQueue && type !== false ) { this.queue( type || "fx", [] ); } return this.each(function() { var index, hadTimers = false, timers = jQuery.timers, data = jQuery._data( this ); // clear marker counters if we know they won't be if ( !gotoEnd ) { jQuery._unmark( true, this ); } function stopQueue( elem, data, index ) { var hooks = data[ index ]; jQuery.removeData( elem, index, true ); hooks.stop( gotoEnd ); } if ( type == null ) { for ( index in data ) { if ( data[ index ] && data[ index ].stop && index.indexOf(".run") === index.length - 4 ) { stopQueue( this, data, index ); } } } else if ( data[ index = type + ".run" ] && data[ index ].stop ){ stopQueue( this, data, index ); } for ( index = timers.length; index--; ) { if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { if ( gotoEnd ) { // force the next step to be the last timers[ index ]( true ); } else { timers[ index ].saveState(); } hadTimers = true; timers.splice( index, 1 ); } } // start the next in the queue if the last step wasn't forced // timers currently will call their complete callbacks, which will dequeue // but only if they were gotoEnd if ( !( gotoEnd && hadTimers ) ) { jQuery.dequeue( this, type ); } }); } }); // Animations created synchronously will run synchronously function createFxNow() { setTimeout( clearFxNow, 0 ); return ( fxNow = jQuery.now() ); } function clearFxNow() { fxNow = undefined; } // Generate parameters to create a standard animation function genFx( type, num ) { var obj = {}; jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice( 0, num )), function() { obj[ this ] = type; }); return obj; } // Generate shortcuts for custom animations jQuery.each({ slideDown: genFx( "show", 1 ), slideUp: genFx( "hide", 1 ), slideToggle: genFx( "toggle", 1 ), fadeIn: { opacity: "show" }, fadeOut: { opacity: "hide" }, fadeToggle: { opacity: "toggle" } }, function( name, props ) { jQuery.fn[ name ] = function( speed, easing, callback ) { return this.animate( props, speed, easing, callback ); }; }); jQuery.extend({ speed: function( speed, easing, fn ) { var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { complete: fn || !fn && easing || jQuery.isFunction( speed ) && speed, duration: speed, easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing }; opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; // normalize opt.queue - true/undefined/null -> "fx" if ( opt.queue == null || opt.queue === true ) { opt.queue = "fx"; } // Queueing opt.old = opt.complete; opt.complete = function( noUnmark ) { if ( jQuery.isFunction( opt.old ) ) { opt.old.call( this ); } if ( opt.queue ) { jQuery.dequeue( this, opt.queue ); } else if ( noUnmark !== false ) { jQuery._unmark( this ); } }; return opt; }, easing: { linear: function( p, n, firstNum, diff ) { return firstNum + diff * p; }, swing: function( p, n, firstNum, diff ) { return ( ( -Math.cos( p*Math.PI ) / 2 ) + 0.5 ) * diff + firstNum; } }, timers: [], fx: function( elem, options, prop ) { this.options = options; this.elem = elem; this.prop = prop; options.orig = options.orig || {}; } }); jQuery.fx.prototype = { // Simple function for setting a style value update: function() { if ( this.options.step ) { this.options.step.call( this.elem, this.now, this ); } ( jQuery.fx.step[ this.prop ] || jQuery.fx.step._default )( this ); }, // Get the current size cur: function() { if ( this.elem[ this.prop ] != null && (!this.elem.style || this.elem.style[ this.prop ] == null) ) { return this.elem[ this.prop ]; } var parsed, r = jQuery.css( this.elem, this.prop ); // Empty strings, null, undefined and "auto" are converted to 0, // complex values such as "rotate(1rad)" are returned as is, // simple values such as "10px" are parsed to Float. return isNaN( parsed = parseFloat( r ) ) ? !r || r === "auto" ? 0 : r : parsed; }, // Start an animation from one number to another custom: function( from, to, unit ) { var self = this, fx = jQuery.fx; this.startTime = fxNow || createFxNow(); this.end = to; this.now = this.start = from; this.pos = this.state = 0; this.unit = unit || this.unit || ( jQuery.cssNumber[ this.prop ] ? "" : "px" ); function t( gotoEnd ) { return self.step( gotoEnd ); } t.queue = this.options.queue; t.elem = this.elem; t.saveState = function() { if ( self.options.hide && jQuery._data( self.elem, "fxshow" + self.prop ) === undefined ) { jQuery._data( self.elem, "fxshow" + self.prop, self.start ); } }; if ( t() && jQuery.timers.push(t) && !timerId ) { timerId = setInterval( fx.tick, fx.interval ); } }, // Simple 'show' function show: function() { var dataShow = jQuery._data( this.elem, "fxshow" + this.prop ); // Remember where we started, so that we can go back to it later this.options.orig[ this.prop ] = dataShow || jQuery.style( this.elem, this.prop ); this.options.show = true; // Begin the animation // Make sure that we start at a small width/height to avoid any flash of content if ( dataShow !== undefined ) { // This show is picking up where a previous hide or show left off this.custom( this.cur(), dataShow ); } else { this.custom( this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur() ); } // Start by showing the element jQuery( this.elem ).show(); }, // Simple 'hide' function hide: function() { // Remember where we started, so that we can go back to it later this.options.orig[ this.prop ] = jQuery._data( this.elem, "fxshow" + this.prop ) || jQuery.style( this.elem, this.prop ); this.options.hide = true; // Begin the animation this.custom( this.cur(), 0 ); }, // Each step of an animation step: function( gotoEnd ) { var p, n, complete, t = fxNow || createFxNow(), done = true, elem = this.elem, options = this.options; if ( gotoEnd || t >= options.duration + this.startTime ) { this.now = this.end; this.pos = this.state = 1; this.update(); options.animatedProperties[ this.prop ] = true; for ( p in options.animatedProperties ) { if ( options.animatedProperties[ p ] !== true ) { done = false; } } if ( done ) { // Reset the overflow if ( options.overflow != null && !jQuery.support.shrinkWrapBlocks ) { jQuery.each( [ "", "X", "Y" ], function( index, value ) { elem.style[ "overflow" + value ] = options.overflow[ index ]; }); } // Hide the element if the "hide" operation was done if ( options.hide ) { jQuery( elem ).hide(); } // Reset the properties, if the item has been hidden or shown if ( options.hide || options.show ) { for ( p in options.animatedProperties ) { jQuery.style( elem, p, options.orig[ p ] ); jQuery.removeData( elem, "fxshow" + p, true ); // Toggle data is no longer needed jQuery.removeData( elem, "toggle" + p, true ); } } // Execute the complete function // in the event that the complete function throws an exception // we must ensure it won't be called twice. #5684 complete = options.complete; if ( complete ) { options.complete = false; complete.call( elem ); } } return false; } else { // classical easing cannot be used with an Infinity duration if ( options.duration == Infinity ) { this.now = t; } else { n = t - this.startTime; this.state = n / options.duration; // Perform the easing function, defaults to swing this.pos = jQuery.easing[ options.animatedProperties[this.prop] ]( this.state, n, 0, 1, options.duration ); this.now = this.start + ( (this.end - this.start) * this.pos ); } // Perform the next step of the animation this.update(); } return true; } }; jQuery.extend( jQuery.fx, { tick: function() { var timer, timers = jQuery.timers, i = 0; for ( ; i < timers.length; i++ ) { timer = timers[ i ]; // Checks the timer has not already been removed if ( !timer() && timers[ i ] === timer ) { timers.splice( i--, 1 ); } } if ( !timers.length ) { jQuery.fx.stop(); } }, interval: 13, stop: function() { clearInterval( timerId ); timerId = null; }, speeds: { slow: 600, fast: 200, // Default speed _default: 400 }, step: { opacity: function( fx ) { jQuery.style( fx.elem, "opacity", fx.now ); }, _default: function( fx ) { if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) { fx.elem.style[ fx.prop ] = fx.now + fx.unit; } else { fx.elem[ fx.prop ] = fx.now; } } } }); // Adds width/height step functions // Do not set anything below 0 jQuery.each([ "width", "height" ], function( i, prop ) { jQuery.fx.step[ prop ] = function( fx ) { jQuery.style( fx.elem, prop, Math.max(0, fx.now) + fx.unit ); }; }); if ( jQuery.expr && jQuery.expr.filters ) { jQuery.expr.filters.animated = function( elem ) { return jQuery.grep(jQuery.timers, function( fn ) { return elem === fn.elem; }).length; }; } // Try to restore the default display value of an element function defaultDisplay( nodeName ) { if ( !elemdisplay[ nodeName ] ) { var body = document.body, elem = jQuery( "<" + nodeName + ">" ).appendTo( body ), display = elem.css( "display" ); elem.remove(); // If the simple way fails, // get element's real default display by attaching it to a temp iframe if ( display === "none" || display === "" ) { // No iframe to use yet, so create it if ( !iframe ) { iframe = document.createElement( "iframe" ); iframe.frameBorder = iframe.width = iframe.height = 0; } body.appendChild( iframe ); // Create a cacheable copy of the iframe document on first call. // IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML // document to it; WebKit & Firefox won't allow reusing the iframe document. if ( !iframeDoc || !iframe.createElement ) { iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document; iframeDoc.write( ( document.compatMode === "CSS1Compat" ? "<!doctype html>" : "" ) + "<html><body>" ); iframeDoc.close(); } elem = iframeDoc.createElement( nodeName ); iframeDoc.body.appendChild( elem ); display = jQuery.css( elem, "display" ); body.removeChild( iframe ); } // Store the correct default display elemdisplay[ nodeName ] = display; } return elemdisplay[ nodeName ]; } var rtable = /^t(?:able|d|h)$/i, rroot = /^(?:body|html)$/i; if ( "getBoundingClientRect" in document.documentElement ) { jQuery.fn.offset = function( options ) { var elem = this[0], box; if ( options ) { return this.each(function( i ) { jQuery.offset.setOffset( this, options, i ); }); } if ( !elem || !elem.ownerDocument ) { return null; } if ( elem === elem.ownerDocument.body ) { return jQuery.offset.bodyOffset( elem ); } try { box = elem.getBoundingClientRect(); } catch(e) {} var doc = elem.ownerDocument, docElem = doc.documentElement; // Make sure we're not dealing with a disconnected DOM node if ( !box || !jQuery.contains( docElem, elem ) ) { return box ? { top: box.top, left: box.left } : { top: 0, left: 0 }; } var body = doc.body, win = getWindow(doc), clientTop = docElem.clientTop || body.clientTop || 0, clientLeft = docElem.clientLeft || body.clientLeft || 0, scrollTop = win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop, scrollLeft = win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft, top = box.top + scrollTop - clientTop, left = box.left + scrollLeft - clientLeft; return { top: top, left: left }; }; } else { jQuery.fn.offset = function( options ) { var elem = this[0]; if ( options ) { return this.each(function( i ) { jQuery.offset.setOffset( this, options, i ); }); } if ( !elem || !elem.ownerDocument ) { return null; } if ( elem === elem.ownerDocument.body ) { return jQuery.offset.bodyOffset( elem ); } var computedStyle, offsetParent = elem.offsetParent, prevOffsetParent = elem, doc = elem.ownerDocument, docElem = doc.documentElement, body = doc.body, defaultView = doc.defaultView, prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle, top = elem.offsetTop, left = elem.offsetLeft; while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) { if ( jQuery.support.fixedPosition && prevComputedStyle.position === "fixed" ) { break; } computedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle; top -= elem.scrollTop; left -= elem.scrollLeft; if ( elem === offsetParent ) { top += elem.offsetTop; left += elem.offsetLeft; if ( jQuery.support.doesNotAddBorder && !(jQuery.support.doesAddBorderForTableAndCells && rtable.test(elem.nodeName)) ) { top += parseFloat( computedStyle.borderTopWidth ) || 0; left += parseFloat( computedStyle.borderLeftWidth ) || 0; } prevOffsetParent = offsetParent; offsetParent = elem.offsetParent; } if ( jQuery.support.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) { top += parseFloat( computedStyle.borderTopWidth ) || 0; left += parseFloat( computedStyle.borderLeftWidth ) || 0; } prevComputedStyle = computedStyle; } if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ) { top += body.offsetTop; left += body.offsetLeft; } if ( jQuery.support.fixedPosition && prevComputedStyle.position === "fixed" ) { top += Math.max( docElem.scrollTop, body.scrollTop ); left += Math.max( docElem.scrollLeft, body.scrollLeft ); } return { top: top, left: left }; }; } jQuery.offset = { bodyOffset: function( body ) { var top = body.offsetTop, left = body.offsetLeft; if ( jQuery.support.doesNotIncludeMarginInBodyOffset ) { top += parseFloat( jQuery.css(body, "marginTop") ) || 0; left += parseFloat( jQuery.css(body, "marginLeft") ) || 0; } return { top: top, left: left }; }, setOffset: function( elem, options, i ) { var position = jQuery.css( elem, "position" ); // set position first, in-case top/left are set even on static elem if ( position === "static" ) { elem.style.position = "relative"; } var curElem = jQuery( elem ), curOffset = curElem.offset(), curCSSTop = jQuery.css( elem, "top" ), curCSSLeft = jQuery.css( elem, "left" ), calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1, props = {}, curPosition = {}, curTop, curLeft; // need to be able to calculate position if either top or left is auto and position is either absolute or fixed if ( calculatePosition ) { curPosition = curElem.position(); curTop = curPosition.top; curLeft = curPosition.left; } else { curTop = parseFloat( curCSSTop ) || 0; curLeft = parseFloat( curCSSLeft ) || 0; } if ( jQuery.isFunction( options ) ) { options = options.call( elem, i, curOffset ); } if ( options.top != null ) { props.top = ( options.top - curOffset.top ) + curTop; } if ( options.left != null ) { props.left = ( options.left - curOffset.left ) + curLeft; } if ( "using" in options ) { options.using.call( elem, props ); } else { curElem.css( props ); } } }; jQuery.fn.extend({ position: function() { if ( !this[0] ) { return null; } var elem = this[0], // Get *real* offsetParent offsetParent = this.offsetParent(), // Get correct offsets offset = this.offset(), parentOffset = rroot.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset(); // Subtract element margins // note: when an element has margin: auto the offsetLeft and marginLeft // are the same in Safari causing offset.left to incorrectly be 0 offset.top -= parseFloat( jQuery.css(elem, "marginTop") ) || 0; offset.left -= parseFloat( jQuery.css(elem, "marginLeft") ) || 0; // Add offsetParent borders parentOffset.top += parseFloat( jQuery.css(offsetParent[0], "borderTopWidth") ) || 0; parentOffset.left += parseFloat( jQuery.css(offsetParent[0], "borderLeftWidth") ) || 0; // Subtract the two offsets return { top: offset.top - parentOffset.top, left: offset.left - parentOffset.left }; }, offsetParent: function() { return this.map(function() { var offsetParent = this.offsetParent || document.body; while ( offsetParent && (!rroot.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) { offsetParent = offsetParent.offsetParent; } return offsetParent; }); } }); // Create scrollLeft and scrollTop methods jQuery.each( ["Left", "Top"], function( i, name ) { var method = "scroll" + name; jQuery.fn[ method ] = function( val ) { var elem, win; if ( val === undefined ) { elem = this[ 0 ]; if ( !elem ) { return null; } win = getWindow( elem ); // Return the scroll offset return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] : jQuery.support.boxModel && win.document.documentElement[ method ] || win.document.body[ method ] : elem[ method ]; } // Set the scroll offset return this.each(function() { win = getWindow( this ); if ( win ) { win.scrollTo( !i ? val : jQuery( win ).scrollLeft(), i ? val : jQuery( win ).scrollTop() ); } else { this[ method ] = val; } }); }; }); function getWindow( elem ) { return jQuery.isWindow( elem ) ? elem : elem.nodeType === 9 ? elem.defaultView || elem.parentWindow : false; } // Create width, height, innerHeight, innerWidth, outerHeight and outerWidth methods jQuery.each([ "Height", "Width" ], function( i, name ) { var type = name.toLowerCase(); // innerHeight and innerWidth jQuery.fn[ "inner" + name ] = function() { var elem = this[0]; return elem ? elem.style ? parseFloat( jQuery.css( elem, type, "padding" ) ) : this[ type ]() : null; }; // outerHeight and outerWidth jQuery.fn[ "outer" + name ] = function( margin ) { var elem = this[0]; return elem ? elem.style ? parseFloat( jQuery.css( elem, type, margin ? "margin" : "border" ) ) : this[ type ]() : null; }; jQuery.fn[ type ] = function( size ) { // Get window width or height var elem = this[0]; if ( !elem ) { return size == null ? null : this; } if ( jQuery.isFunction( size ) ) { return this.each(function( i ) { var self = jQuery( this ); self[ type ]( size.call( this, i, self[ type ]() ) ); }); } if ( jQuery.isWindow( elem ) ) { // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode // 3rd condition allows Nokia support, as it supports the docElem prop but not CSS1Compat var docElemProp = elem.document.documentElement[ "client" + name ], body = elem.document.body; return elem.document.compatMode === "CSS1Compat" && docElemProp || body && body[ "client" + name ] || docElemProp; // Get document width or height } else if ( elem.nodeType === 9 ) { // Either scroll[Width/Height] or offset[Width/Height], whichever is greater return Math.max( elem.documentElement["client" + name], elem.body["scroll" + name], elem.documentElement["scroll" + name], elem.body["offset" + name], elem.documentElement["offset" + name] ); // Get or set width or height on the element } else if ( size === undefined ) { var orig = jQuery.css( elem, type ), ret = parseFloat( orig ); return jQuery.isNumeric( ret ) ? ret : orig; // Set the width or height on the element (default to pixels if value is unitless) } else { return this.css( type, typeof size === "string" ? size : size + "px" ); } }; }); // Expose jQuery to the global object window.jQuery = window.$ = jQuery; // Expose jQuery as an AMD module, but only for AMD loaders that // understand the issues with loading multiple versions of jQuery // in a page that all might call define(). The loader will indicate // they have special allowances for multiple jQuery versions by // specifying define.amd.jQuery = true. Register as a named module, // since jQuery can be concatenated with other files that may use define, // but not use a proper concatenation script that understands anonymous // AMD modules. A named AMD is safest and most robust way to register. // Lowercase jquery is used because AMD module names are derived from // file names, and jQuery is normally delivered in a lowercase file name. // Do this after creating the global so that if an AMD module wants to call // noConflict to hide this version of jQuery, it will work. if ( typeof define === "function" && define.amd && define.amd.jQuery ) { define( "jquery", [], function () { return jQuery; } ); } })( window ); �������������������������������������������������������������������������������������beets-1.3.8/beetsplug/web/static/underscore.js������������������������������������������������������0000644�0000765�0000024�00000103302�12313347401�022333� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������// Underscore.js 1.2.2 // (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. // Underscore is freely distributable under the MIT license. // Portions of Underscore are inspired or borrowed from Prototype, // Oliver Steele's Functional, and John Resig's Micro-Templating. // For all details and documentation: // http://documentcloud.github.com/underscore (function() { // Baseline setup // -------------- // Establish the root object, `window` in the browser, or `global` on the server. var root = this; // Save the previous value of the `_` variable. var previousUnderscore = root._; // Establish the object that gets returned to break out of a loop iteration. var breaker = {}; // Save bytes in the minified (but not gzipped) version: var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; // Create quick reference variables for speed access to core prototypes. var slice = ArrayProto.slice, unshift = ArrayProto.unshift, toString = ObjProto.toString, hasOwnProperty = ObjProto.hasOwnProperty; // All **ECMAScript 5** native function implementations that we hope to use // are declared here. var nativeForEach = ArrayProto.forEach, nativeMap = ArrayProto.map, nativeReduce = ArrayProto.reduce, nativeReduceRight = ArrayProto.reduceRight, nativeFilter = ArrayProto.filter, nativeEvery = ArrayProto.every, nativeSome = ArrayProto.some, nativeIndexOf = ArrayProto.indexOf, nativeLastIndexOf = ArrayProto.lastIndexOf, nativeIsArray = Array.isArray, nativeKeys = Object.keys, nativeBind = FuncProto.bind; // Create a safe reference to the Underscore object for use below. var _ = function(obj) { return new wrapper(obj); }; // Export the Underscore object for **Node.js** and **"CommonJS"**, with // backwards-compatibility for the old `require()` API. If we're not in // CommonJS, add `_` to the global object. if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { exports = module.exports = _; } exports._ = _; } else if (typeof define === 'function' && define.amd) { // Register as a named module with AMD. define('underscore', function() { return _; }); } else { // Exported as a string, for Closure Compiler "advanced" mode. root['_'] = _; } // Current version. _.VERSION = '1.2.2'; // Collection Functions // -------------------- // The cornerstone, an `each` implementation, aka `forEach`. // Handles objects with the built-in `forEach`, arrays, and raw objects. // Delegates to **ECMAScript 5**'s native `forEach` if available. var each = _.each = _.forEach = function(obj, iterator, context) { if (obj == null) return; if (nativeForEach && obj.forEach === nativeForEach) { obj.forEach(iterator, context); } else if (obj.length === +obj.length) { for (var i = 0, l = obj.length; i < l; i++) { if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; } } else { for (var key in obj) { if (hasOwnProperty.call(obj, key)) { if (iterator.call(context, obj[key], key, obj) === breaker) return; } } } }; // Return the results of applying the iterator to each element. // Delegates to **ECMAScript 5**'s native `map` if available. _.map = function(obj, iterator, context) { var results = []; if (obj == null) return results; if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); each(obj, function(value, index, list) { results[results.length] = iterator.call(context, value, index, list); }); return results; }; // **Reduce** builds up a single result from a list of values, aka `inject`, // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { var initial = memo !== void 0; if (obj == null) obj = []; if (nativeReduce && obj.reduce === nativeReduce) { if (context) iterator = _.bind(iterator, context); return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); } each(obj, function(value, index, list) { if (!initial) { memo = value; initial = true; } else { memo = iterator.call(context, memo, value, index, list); } }); if (!initial) throw new TypeError("Reduce of empty array with no initial value"); return memo; }; // The right-associative version of reduce, also known as `foldr`. // Delegates to **ECMAScript 5**'s native `reduceRight` if available. _.reduceRight = _.foldr = function(obj, iterator, memo, context) { if (obj == null) obj = []; if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { if (context) iterator = _.bind(iterator, context); return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); } var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse(); return _.reduce(reversed, iterator, memo, context); }; // Return the first value which passes a truth test. Aliased as `detect`. _.find = _.detect = function(obj, iterator, context) { var result; any(obj, function(value, index, list) { if (iterator.call(context, value, index, list)) { result = value; return true; } }); return result; }; // Return all the elements that pass a truth test. // Delegates to **ECMAScript 5**'s native `filter` if available. // Aliased as `select`. _.filter = _.select = function(obj, iterator, context) { var results = []; if (obj == null) return results; if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); each(obj, function(value, index, list) { if (iterator.call(context, value, index, list)) results[results.length] = value; }); return results; }; // Return all the elements for which a truth test fails. _.reject = function(obj, iterator, context) { var results = []; if (obj == null) return results; each(obj, function(value, index, list) { if (!iterator.call(context, value, index, list)) results[results.length] = value; }); return results; }; // Determine whether all of the elements match a truth test. // Delegates to **ECMAScript 5**'s native `every` if available. // Aliased as `all`. _.every = _.all = function(obj, iterator, context) { var result = true; if (obj == null) return result; if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); each(obj, function(value, index, list) { if (!(result = result && iterator.call(context, value, index, list))) return breaker; }); return result; }; // Determine if at least one element in the object matches a truth test. // Delegates to **ECMAScript 5**'s native `some` if available. // Aliased as `any`. var any = _.some = _.any = function(obj, iterator, context) { iterator = iterator || _.identity; var result = false; if (obj == null) return result; if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); each(obj, function(value, index, list) { if (result || (result = iterator.call(context, value, index, list))) return breaker; }); return !!result; }; // Determine if a given value is included in the array or object using `===`. // Aliased as `contains`. _.include = _.contains = function(obj, target) { var found = false; if (obj == null) return found; if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; found = any(obj, function(value) { return value === target; }); return found; }; // Invoke a method (with arguments) on every item in a collection. _.invoke = function(obj, method) { var args = slice.call(arguments, 2); return _.map(obj, function(value) { return (method.call ? method || value : value[method]).apply(value, args); }); }; // Convenience version of a common use case of `map`: fetching a property. _.pluck = function(obj, key) { return _.map(obj, function(value){ return value[key]; }); }; // Return the maximum element or (element-based computation). _.max = function(obj, iterator, context) { if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); if (!iterator && _.isEmpty(obj)) return -Infinity; var result = {computed : -Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; computed >= result.computed && (result = {value : value, computed : computed}); }); return result.value; }; // Return the minimum element (or element-based computation). _.min = function(obj, iterator, context) { if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj); if (!iterator && _.isEmpty(obj)) return Infinity; var result = {computed : Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; computed < result.computed && (result = {value : value, computed : computed}); }); return result.value; }; // Shuffle an array. _.shuffle = function(obj) { var shuffled = [], rand; each(obj, function(value, index, list) { if (index == 0) { shuffled[0] = value; } else { rand = Math.floor(Math.random() * (index + 1)); shuffled[index] = shuffled[rand]; shuffled[rand] = value; } }); return shuffled; }; // Sort the object's values by a criterion produced by an iterator. _.sortBy = function(obj, iterator, context) { return _.pluck(_.map(obj, function(value, index, list) { return { value : value, criteria : iterator.call(context, value, index, list) }; }).sort(function(left, right) { var a = left.criteria, b = right.criteria; return a < b ? -1 : a > b ? 1 : 0; }), 'value'); }; // Groups the object's values by a criterion. Pass either a string attribute // to group by, or a function that returns the criterion. _.groupBy = function(obj, val) { var result = {}; var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; each(obj, function(value, index) { var key = iterator(value, index); (result[key] || (result[key] = [])).push(value); }); return result; }; // Use a comparator function to figure out at what index an object should // be inserted so as to maintain order. Uses binary search. _.sortedIndex = function(array, obj, iterator) { iterator || (iterator = _.identity); var low = 0, high = array.length; while (low < high) { var mid = (low + high) >> 1; iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; } return low; }; // Safely convert anything iterable into a real, live array. _.toArray = function(iterable) { if (!iterable) return []; if (iterable.toArray) return iterable.toArray(); if (_.isArray(iterable)) return slice.call(iterable); if (_.isArguments(iterable)) return slice.call(iterable); return _.values(iterable); }; // Return the number of elements in an object. _.size = function(obj) { return _.toArray(obj).length; }; // Array Functions // --------------- // Get the first element of an array. Passing **n** will return the first N // values in the array. Aliased as `head`. The **guard** check allows it to work // with `_.map`. _.first = _.head = function(array, n, guard) { return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; }; // Returns everything but the last entry of the array. Especcialy useful on // the arguments object. Passing **n** will return all the values in // the array, excluding the last N. The **guard** check allows it to work with // `_.map`. _.initial = function(array, n, guard) { return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); }; // Get the last element of an array. Passing **n** will return the last N // values in the array. The **guard** check allows it to work with `_.map`. _.last = function(array, n, guard) { if ((n != null) && !guard) { return slice.call(array, Math.max(array.length - n, 0)); } else { return array[array.length - 1]; } }; // Returns everything but the first entry of the array. Aliased as `tail`. // Especially useful on the arguments object. Passing an **index** will return // the rest of the values in the array from that index onward. The **guard** // check allows it to work with `_.map`. _.rest = _.tail = function(array, index, guard) { return slice.call(array, (index == null) || guard ? 1 : index); }; // Trim out all falsy values from an array. _.compact = function(array) { return _.filter(array, function(value){ return !!value; }); }; // Return a completely flattened version of an array. _.flatten = function(array, shallow) { return _.reduce(array, function(memo, value) { if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value)); memo[memo.length] = value; return memo; }, []); }; // Return a version of the array that does not contain the specified value(s). _.without = function(array) { return _.difference(array, slice.call(arguments, 1)); }; // Produce a duplicate-free version of the array. If the array has already // been sorted, you have the option of using a faster algorithm. // Aliased as `unique`. _.uniq = _.unique = function(array, isSorted, iterator) { var initial = iterator ? _.map(array, iterator) : array; var result = []; _.reduce(initial, function(memo, el, i) { if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) { memo[memo.length] = el; result[result.length] = array[i]; } return memo; }, []); return result; }; // Produce an array that contains the union: each distinct element from all of // the passed-in arrays. _.union = function() { return _.uniq(_.flatten(arguments, true)); }; // Produce an array that contains every item shared between all the // passed-in arrays. (Aliased as "intersect" for back-compat.) _.intersection = _.intersect = function(array) { var rest = slice.call(arguments, 1); return _.filter(_.uniq(array), function(item) { return _.every(rest, function(other) { return _.indexOf(other, item) >= 0; }); }); }; // Take the difference between one array and another. // Only the elements present in just the first array will remain. _.difference = function(array, other) { return _.filter(array, function(value){ return !_.include(other, value); }); }; // Zip together multiple lists into a single array -- elements that share // an index go together. _.zip = function() { var args = slice.call(arguments); var length = _.max(_.pluck(args, 'length')); var results = new Array(length); for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); return results; }; // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), // we need this function. Return the position of the first occurrence of an // item in an array, or -1 if the item is not included in the array. // Delegates to **ECMAScript 5**'s native `indexOf` if available. // If the array is large and already in sort order, pass `true` // for **isSorted** to use binary search. _.indexOf = function(array, item, isSorted) { if (array == null) return -1; var i, l; if (isSorted) { i = _.sortedIndex(array, item); return array[i] === item ? i : -1; } if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i; return -1; }; // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. _.lastIndexOf = function(array, item) { if (array == null) return -1; if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); var i = array.length; while (i--) if (array[i] === item) return i; return -1; }; // Generate an integer Array containing an arithmetic progression. A port of // the native Python `range()` function. See // [the Python documentation](http://docs.python.org/library/functions.html#range). _.range = function(start, stop, step) { if (arguments.length <= 1) { stop = start || 0; start = 0; } step = arguments[2] || 1; var len = Math.max(Math.ceil((stop - start) / step), 0); var idx = 0; var range = new Array(len); while(idx < len) { range[idx++] = start; start += step; } return range; }; // Function (ahem) Functions // ------------------ // Reusable constructor function for prototype setting. var ctor = function(){}; // Create a function bound to a given object (assigning `this`, and arguments, // optionally). Binding with arguments is also known as `curry`. // Delegates to **ECMAScript 5**'s native `Function.bind` if available. // We check for `func.bind` first, to fail fast when `func` is undefined. _.bind = function bind(func, context) { var bound, args; if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); if (!_.isFunction(func)) throw new TypeError; args = slice.call(arguments, 2); return bound = function() { if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); ctor.prototype = func.prototype; var self = new ctor; var result = func.apply(self, args.concat(slice.call(arguments))); if (Object(result) === result) return result; return self; }; }; // Bind all of an object's methods to that object. Useful for ensuring that // all callbacks defined on an object belong to it. _.bindAll = function(obj) { var funcs = slice.call(arguments, 1); if (funcs.length == 0) funcs = _.functions(obj); each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); return obj; }; // Memoize an expensive function by storing its results. _.memoize = function(func, hasher) { var memo = {}; hasher || (hasher = _.identity); return function() { var key = hasher.apply(this, arguments); return hasOwnProperty.call(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); }; }; // Delays a function for the given number of milliseconds, and then calls // it with the arguments supplied. _.delay = function(func, wait) { var args = slice.call(arguments, 2); return setTimeout(function(){ return func.apply(func, args); }, wait); }; // Defers a function, scheduling it to run after the current call stack has // cleared. _.defer = function(func) { return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); }; // Returns a function, that, when invoked, will only be triggered at most once // during a given window of time. _.throttle = function(func, wait) { var context, args, timeout, throttling, more; var whenDone = _.debounce(function(){ more = throttling = false; }, wait); return function() { context = this; args = arguments; var later = function() { timeout = null; if (more) func.apply(context, args); whenDone(); }; if (!timeout) timeout = setTimeout(later, wait); if (throttling) { more = true; } else { func.apply(context, args); } whenDone(); throttling = true; }; }; // Returns a function, that, as long as it continues to be invoked, will not // be triggered. The function will be called after it stops being called for // N milliseconds. _.debounce = function(func, wait) { var timeout; return function() { var context = this, args = arguments; var later = function() { timeout = null; func.apply(context, args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }; // Returns a function that will be executed at most one time, no matter how // often you call it. Useful for lazy initialization. _.once = function(func) { var ran = false, memo; return function() { if (ran) return memo; ran = true; return memo = func.apply(this, arguments); }; }; // Returns the first function passed as an argument to the second, // allowing you to adjust arguments, run code before and after, and // conditionally execute the original function. _.wrap = function(func, wrapper) { return function() { var args = [func].concat(slice.call(arguments)); return wrapper.apply(this, args); }; }; // Returns a function that is the composition of a list of functions, each // consuming the return value of the function that follows. _.compose = function() { var funcs = slice.call(arguments); return function() { var args = slice.call(arguments); for (var i = funcs.length - 1; i >= 0; i--) { args = [funcs[i].apply(this, args)]; } return args[0]; }; }; // Returns a function that will only be executed after being called N times. _.after = function(times, func) { if (times <= 0) return func(); return function() { if (--times < 1) { return func.apply(this, arguments); } }; }; // Object Functions // ---------------- // Retrieve the names of an object's properties. // Delegates to **ECMAScript 5**'s native `Object.keys` _.keys = nativeKeys || function(obj) { if (obj !== Object(obj)) throw new TypeError('Invalid object'); var keys = []; for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key; return keys; }; // Retrieve the values of an object's properties. _.values = function(obj) { return _.map(obj, _.identity); }; // Return a sorted list of the function names available on the object. // Aliased as `methods` _.functions = _.methods = function(obj) { var names = []; for (var key in obj) { if (_.isFunction(obj[key])) names.push(key); } return names.sort(); }; // Extend a given object with all the properties in passed-in object(s). _.extend = function(obj) { each(slice.call(arguments, 1), function(source) { for (var prop in source) { if (source[prop] !== void 0) obj[prop] = source[prop]; } }); return obj; }; // Fill in a given object with default properties. _.defaults = function(obj) { each(slice.call(arguments, 1), function(source) { for (var prop in source) { if (obj[prop] == null) obj[prop] = source[prop]; } }); return obj; }; // Create a (shallow-cloned) duplicate of an object. _.clone = function(obj) { if (!_.isObject(obj)) return obj; return _.isArray(obj) ? obj.slice() : _.extend({}, obj); }; // Invokes interceptor with the obj, and then returns obj. // The primary purpose of this method is to "tap into" a method chain, in // order to perform operations on intermediate results within the chain. _.tap = function(obj, interceptor) { interceptor(obj); return obj; }; // Internal recursive comparison function. function eq(a, b, stack) { // Identical objects are equal. `0 === -0`, but they aren't identical. // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. if (a === b) return a !== 0 || 1 / a == 1 / b; // A strict comparison is necessary because `null == undefined`. if (a == null || b == null) return a === b; // Unwrap any wrapped objects. if (a._chain) a = a._wrapped; if (b._chain) b = b._wrapped; // Invoke a custom `isEqual` method if one is provided. if (_.isFunction(a.isEqual)) return a.isEqual(b); if (_.isFunction(b.isEqual)) return b.isEqual(a); // Compare `[[Class]]` names. var className = toString.call(a); if (className != toString.call(b)) return false; switch (className) { // Strings, numbers, dates, and booleans are compared by value. case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. return String(a) == String(b); case '[object Number]': a = +a; b = +b; // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for // other numeric values. return a != a ? b != b : (a == 0 ? 1 / a == 1 / b : a == b); case '[object Date]': case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. return +a == +b; // RegExps are compared by their source patterns and flags. case '[object RegExp]': return a.source == b.source && a.global == b.global && a.multiline == b.multiline && a.ignoreCase == b.ignoreCase; } if (typeof a != 'object' || typeof b != 'object') return false; // Assume equality for cyclic structures. The algorithm for detecting cyclic // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. var length = stack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. if (stack[length] == a) return true; } // Add the first object to the stack of traversed objects. stack.push(a); var size = 0, result = true; // Recursively compare objects and arrays. if (className == '[object Array]') { // Compare array lengths to determine if a deep comparison is necessary. size = a.length; result = size == b.length; if (result) { // Deep compare the contents, ignoring non-numeric properties. while (size--) { // Ensure commutative equality for sparse arrays. if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; } } } else { // Objects with different constructors are not equivalent. if ("constructor" in a != "constructor" in b || a.constructor != b.constructor) return false; // Deep compare objects. for (var key in a) { if (hasOwnProperty.call(a, key)) { // Count the expected number of properties. size++; // Deep compare each member. if (!(result = hasOwnProperty.call(b, key) && eq(a[key], b[key], stack))) break; } } // Ensure that both objects contain the same number of properties. if (result) { for (key in b) { if (hasOwnProperty.call(b, key) && !(size--)) break; } result = !size; } } // Remove the first object from the stack of traversed objects. stack.pop(); return result; } // Perform a deep comparison to check if two objects are equal. _.isEqual = function(a, b) { return eq(a, b, []); }; // Is a given array, string, or object empty? // An "empty" object has no enumerable own-properties. _.isEmpty = function(obj) { if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; for (var key in obj) if (hasOwnProperty.call(obj, key)) return false; return true; }; // Is a given value a DOM element? _.isElement = function(obj) { return !!(obj && obj.nodeType == 1); }; // Is a given value an array? // Delegates to ECMA5's native Array.isArray _.isArray = nativeIsArray || function(obj) { return toString.call(obj) == '[object Array]'; }; // Is a given variable an object? _.isObject = function(obj) { return obj === Object(obj); }; // Is a given variable an arguments object? if (toString.call(arguments) == '[object Arguments]') { _.isArguments = function(obj) { return toString.call(obj) == '[object Arguments]'; }; } else { _.isArguments = function(obj) { return !!(obj && hasOwnProperty.call(obj, 'callee')); }; } // Is a given value a function? _.isFunction = function(obj) { return toString.call(obj) == '[object Function]'; }; // Is a given value a string? _.isString = function(obj) { return toString.call(obj) == '[object String]'; }; // Is a given value a number? _.isNumber = function(obj) { return toString.call(obj) == '[object Number]'; }; // Is the given value `NaN`? _.isNaN = function(obj) { // `NaN` is the only value for which `===` is not reflexive. return obj !== obj; }; // Is a given value a boolean? _.isBoolean = function(obj) { return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; }; // Is a given value a date? _.isDate = function(obj) { return toString.call(obj) == '[object Date]'; }; // Is the given value a regular expression? _.isRegExp = function(obj) { return toString.call(obj) == '[object RegExp]'; }; // Is a given value equal to null? _.isNull = function(obj) { return obj === null; }; // Is a given variable undefined? _.isUndefined = function(obj) { return obj === void 0; }; // Utility Functions // ----------------- // Run Underscore.js in *noConflict* mode, returning the `_` variable to its // previous owner. Returns a reference to the Underscore object. _.noConflict = function() { root._ = previousUnderscore; return this; }; // Keep the identity function around for default iterators. _.identity = function(value) { return value; }; // Run a function **n** times. _.times = function (n, iterator, context) { for (var i = 0; i < n; i++) iterator.call(context, i); }; // Escape a string for HTML interpolation. _.escape = function(string) { return (''+string).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); }; // Add your own custom functions to the Underscore object, ensuring that // they're correctly added to the OOP wrapper as well. _.mixin = function(obj) { each(_.functions(obj), function(name){ addToWrapper(name, _[name] = obj[name]); }); }; // Generate a unique integer id (unique within the entire client session). // Useful for temporary DOM ids. var idCounter = 0; _.uniqueId = function(prefix) { var id = idCounter++; return prefix ? prefix + id : id; }; // By default, Underscore uses ERB-style template delimiters, change the // following template settings to use alternative delimiters. _.templateSettings = { evaluate : /<%([\s\S]+?)%>/g, interpolate : /<%=([\s\S]+?)%>/g, escape : /<%-([\s\S]+?)%>/g }; // JavaScript micro-templating, similar to John Resig's implementation. // Underscore templating handles arbitrary delimiters, preserves whitespace, // and correctly escapes quotes within interpolated code. _.template = function(str, data) { var c = _.templateSettings; var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' + 'with(obj||{}){__p.push(\'' + str.replace(/\\/g, '\\\\') .replace(/'/g, "\\'") .replace(c.escape, function(match, code) { return "',_.escape(" + code.replace(/\\'/g, "'") + "),'"; }) .replace(c.interpolate, function(match, code) { return "'," + code.replace(/\\'/g, "'") + ",'"; }) .replace(c.evaluate || null, function(match, code) { return "');" + code.replace(/\\'/g, "'") .replace(/[\r\n\t]/g, ' ') + ";__p.push('"; }) .replace(/\r/g, '\\r') .replace(/\n/g, '\\n') .replace(/\t/g, '\\t') + "');}return __p.join('');"; var func = new Function('obj', '_', tmpl); return data ? func(data, _) : function(data) { return func(data, _) }; }; // The OOP Wrapper // --------------- // If Underscore is called as a function, it returns a wrapped object that // can be used OO-style. This wrapper holds altered versions of all the // underscore functions. Wrapped objects may be chained. var wrapper = function(obj) { this._wrapped = obj; }; // Expose `wrapper.prototype` as `_.prototype` _.prototype = wrapper.prototype; // Helper function to continue chaining intermediate results. var result = function(obj, chain) { return chain ? _(obj).chain() : obj; }; // A method to easily add functions to the OOP wrapper. var addToWrapper = function(name, func) { wrapper.prototype[name] = function() { var args = slice.call(arguments); unshift.call(args, this._wrapped); return result(func.apply(_, args), this._chain); }; }; // Add all of the Underscore functions to the wrapper object. _.mixin(_); // Add all mutator Array functions to the wrapper. each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { var method = ArrayProto[name]; wrapper.prototype[name] = function() { method.apply(this._wrapped, arguments); return result(this._wrapped, this._chain); }; }); // Add all accessor Array functions to the wrapper. each(['concat', 'join', 'slice'], function(name) { var method = ArrayProto[name]; wrapper.prototype[name] = function() { return result(method.apply(this._wrapped, arguments), this._chain); }; }); // Start chaining a wrapped Underscore object. wrapper.prototype.chain = function() { this._chain = true; return this; }; // Extracts the result from a wrapped and chained object. wrapper.prototype.value = function() { return this._wrapped; }; }).call(this); ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/web/templates/����������������������������������������������������������������0000755�0000765�0000024�00000000000�12406440351�020335� 5����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������beets-1.3.8/beetsplug/web/templates/index.html������������������������������������������������������0000644�0000765�0000024�00000006353�12404675421�022347� 0����������������������������������������������������������������������������������������������������ustar �asampson������������������������staff���������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!DOCTYPE html> <html> <head> <title>beets
beets-1.3.8/beetsplug/zero.py0000644000076500000240000000622012406436341017117 0ustar asampsonstaff00000000000000# This file is part of beets. # Copyright 2013, Blemjhoo Tezoulbr . # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # 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. """ Clears tag fields in media files.""" import re import logging from beets.plugins import BeetsPlugin from beets.library import Item from beets.importer import action from beets.util import confit __author__ = 'baobab@heresiarch.info' __version__ = '0.10' log = logging.getLogger('beets') class ZeroPlugin(BeetsPlugin): _instance = None def __init__(self): super(ZeroPlugin, self).__init__() # Listeners. self.register_listener('write', self.write_event) self.register_listener('import_task_choice', self.import_task_choice_event) self.config.add({ 'fields': [], }) self.patterns = {} self.warned = False for field in self.config['fields'].as_str_seq(): if field in ('id', 'path', 'album_id'): log.warn(u'[zero] field \'{0}\' ignored, zeroing ' u'it would be dangerous'.format(field)) continue if field not in Item._fields.keys(): log.error(u'[zero] invalid field: {0}'.format(field)) continue try: self.patterns[field] = self.config[field].as_str_seq() except confit.NotFoundError: # Matches everything self.patterns[field] = [u''] def import_task_choice_event(self, session, task): """Listen for import_task_choice event.""" if task.choice_flag == action.ASIS and not self.warned: log.warn(u'[zero] cannot zero in \"as-is\" mode') self.warned = True # TODO request write in as-is mode @classmethod def match_patterns(cls, field, patterns): """Check if field (as string) is matching any of the patterns in the list. """ for p in patterns: if re.search(p, unicode(field), flags=re.IGNORECASE): return True return False def write_event(self, item, path, tags): """Listen for write event.""" if not self.patterns: log.warn(u'[zero] no fields, nothing to do') return for field, patterns in self.patterns.items(): if field not in tags: log.error(u'[zero] no such field: {0}'.format(field)) continue value = tags[field] if self.match_patterns(value, patterns): log.debug(u'[zero] {0}: {1} -> None'.format(field, value)) tags[field] = None beets-1.3.8/docs/0000755000076500000240000000000012406440351014520 5ustar asampsonstaff00000000000000beets-1.3.8/docs/changelog.rst0000644000076500000240000034420212406440212017202 0ustar asampsonstaff00000000000000Changelog ========= 1.3.8 (September 17, 2014) -------------------------- This release has two big new chunks of functionality. Queries now support **sorting** and user-defined fields can now have **types**. If you want to see all your songs in reverse chronological order, just type ``beet list year-``. It couldn't be easier. For details, see :ref:`query-sort`. Flexible field types mean that some functionality that has previously only worked for built-in fields, like range queries, can now work with plugin- and user-defined fields too. For starters, the :doc:`/plugins/echonest/` and :doc:`/plugins/mpdstats` now mark the types of the fields they provide---so you can now say, for example, ``beet ls liveness:0.5..1.5`` for the Echo Nest "liveness" attribute. The :doc:`/plugins/types` makes it easy to specify field types in your config file. One upgrade note: if you use the :doc:`/plugins/discogs`, you will need to upgrade the Discogs client library to use this version. Just type ``pip install -U discogs-client``. Other new features: * :doc:`/plugins/info`: Target files can now be specified through library queries (in addition to filenames). The ``--library`` option prints library fields instead of tags. Multiple files can be summarized together with the new ``--summarize`` option. * :doc:`/plugins/mbcollection`: A new option lets you automatically update your collection on import. Thanks to Olin Gay. * :doc:`/plugins/convert`: A new ``never_convert_lossy_files`` option can prevent lossy transcoding. Thanks to Simon Kohlmeyer. * :doc:`/plugins/convert`: A new ``--yes`` command-line flag skips the confirmation. Still more fixes and little improvements: * Invalid state files don't crash the importer. * :doc:`/plugins/lyrics`: Only strip featured artists and parenthesized title suffixes if no lyrics for the original artist and title were found. * Fix a crash when reading some files with missing tags. * :doc:`/plugins/discogs`: Compatibility with the new 2.0 version of the `discogs_client`_ Python library. If you were using the old version, you wil need to upgrade to the latest version of the library to use the correspondingly new version of the plugin (e.g., with ``pip install -U discogs-client``). Thanks to Andriy Kohut. * Fix a crash when writing files that can't be read. Thanks to Jocelyn De La Rosa. * The :ref:`stats-cmd` command now counts album artists. The album count also more accurately reflects the number of albums in the database. * :doc:`/plugins/convert`: Avoid crashes when tags cannot be written to newly converted files. * Formatting templates with item data no longer confusingly shows album-level data when the two are inconsistent. * Resuming imports and beginning incremental imports should now be much faster when there is a lot of previously-imported music to skip. * :doc:`/plugins/lyrics`: Remove ``