", "\n", html)
def scrape_lyrics_from_html(html):
"""Scrape lyrics from a URL. If no lyrics can be found, return None
instead.
"""
def is_text_notcode(text):
if not text:
return False
length = len(text)
return (
length > 20
and text.count(" ") > length / 25
and (text.find("{") == -1 or text.find(";") == -1)
)
html = _scrape_strip_cruft(html)
html = _scrape_merge_paragraphs(html)
# extract all long text blocks that are not code
soup = try_parse_html(html, parse_only=SoupStrainer(string=is_text_notcode))
if not soup:
return None
# Get the longest text element (if any).
strings = sorted(soup.stripped_strings, key=len, reverse=True)
if strings:
return strings[0]
else:
return None
class Google(Backend):
"""Fetch lyrics from Google search results."""
REQUIRES_BS = True
def __init__(self, config, log):
super().__init__(config, log)
self.api_key = config["google_API_key"].as_str()
self.engine_id = config["google_engine_ID"].as_str()
def is_lyrics(self, text, artist=None):
"""Determine whether the text seems to be valid lyrics."""
if not text:
return False
bad_triggers_occ = []
nb_lines = text.count("\n")
if nb_lines <= 1:
self._log.debug("Ignoring too short lyrics '{0}'", text)
return False
elif nb_lines < 5:
bad_triggers_occ.append("too_short")
else:
# Lyrics look legit, remove credits to avoid being penalized
# further down
text = remove_credits(text)
bad_triggers = ["lyrics", "copyright", "property", "links"]
if artist:
bad_triggers += [artist]
for item in bad_triggers:
bad_triggers_occ += [item] * len(
re.findall(r"\W%s\W" % item, text, re.I)
)
if bad_triggers_occ:
self._log.debug("Bad triggers detected: {0}", bad_triggers_occ)
return len(bad_triggers_occ) < 2
def slugify(self, text):
"""Normalize a string and remove non-alphanumeric characters."""
text = re.sub(r"[-'_\s]", "_", text)
text = re.sub(r"_+", "_", text).strip("_")
pat = r"([^,\(]*)\((.*?)\)" # Remove content within parentheses
text = re.sub(pat, r"\g<1>", text).strip()
try:
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore")
text = str(re.sub(r"[-\s]+", " ", text.decode("utf-8")))
except UnicodeDecodeError:
self._log.exception("Failing to normalize '{0}'", text)
return text
BY_TRANS = ["by", "par", "de", "von"]
LYRICS_TRANS = ["lyrics", "paroles", "letras", "liedtexte"]
def is_page_candidate(self, url_link, url_title, 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 = self.slugify(title.lower())
artist = self.slugify(artist.lower())
sitename = re.search(
"//([^/]+)/.*", self.slugify(url_link.lower())
).group(1)
url_title = self.slugify(url_title.lower())
# Check if URL title contains song title (exact match)
if url_title.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 self.BY_TRANS]
+ [artist, sitename, sitename.replace("www.", "")]
+ self.LYRICS_TRANS
)
tokens = [re.escape(t) for t in tokens]
song_title = re.sub("(%s)" % "|".join(tokens), "", url_title)
song_title = song_title.strip("_|")
typo_ratio = 0.9
ratio = difflib.SequenceMatcher(None, song_title, title).ratio()
return ratio >= typo_ratio
def fetch(self, artist, title, album=None, length=None):
query = f"{artist} {title}"
url = "https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s" % (
self.api_key,
self.engine_id,
quote(query.encode("utf-8")),
)
data = self.fetch_url(url)
if not data:
self._log.debug("google backend returned no data")
return None
try:
data = json.loads(data)
except ValueError as exc:
self._log.debug("google backend returned malformed JSON: {}", exc)
if "error" in data:
reason = data["error"]["errors"][0]["reason"]
self._log.debug("google backend error: {0}", reason)
return None
if "items" in data.keys():
for item in data["items"]:
url_link = item["link"]
url_title = item.get("title", "")
if not self.is_page_candidate(
url_link, url_title, title, artist
):
continue
html = self.fetch_url(url_link)
if not html:
continue
lyrics = scrape_lyrics_from_html(html)
if not lyrics:
continue
if self.is_lyrics(lyrics, artist):
self._log.debug("got lyrics from {0}", item["displayLink"])
return lyrics
return None
class LyricsPlugin(plugins.BeetsPlugin):
SOURCES = ["google", "musixmatch", "genius", "tekstowo", "lrclib"]
SOURCE_BACKENDS = {
"google": Google,
"musixmatch": MusiXmatch,
"genius": Genius,
"tekstowo": Tekstowo,
"lrclib": LRCLib,
}
def __init__(self):
super().__init__()
self.import_stages = [self.imported]
self.config.add(
{
"auto": True,
"bing_client_secret": None,
"bing_lang_from": [],
"bing_lang_to": None,
"google_API_key": None,
"google_engine_ID": "009217259823014548361:lndtuqkycfu",
"genius_api_key": "Ryq93pUGm8bM6eUWwD_M3NOFFDAtp2yEE7W"
"76V-uFL5jks5dNvcGCdarqFjDhP9c",
"fallback": None,
"force": False,
"local": False,
"synced": False,
# Musixmatch is disabled by default as they are currently blocking
# requests with the beets user agent.
"sources": [s for s in self.SOURCES if s != "musixmatch"],
"dist_thresh": 0.1,
}
)
self.config["bing_client_secret"].redact = True
self.config["google_API_key"].redact = True
self.config["google_engine_ID"].redact = True
self.config["genius_api_key"].redact = True
# State information for the ReST writer.
# First, the current artist we're writing.
self.artist = "Unknown artist"
# The current album: False means no album yet.
self.album = False
# The current rest file content. None means the file is not
# open yet.
self.rest = None
available_sources = list(self.SOURCES)
sources = plugins.sanitize_choices(
self.config["sources"].as_str_seq(), available_sources
)
if not HAS_BEAUTIFUL_SOUP:
sources = self.sanitize_bs_sources(sources)
if "google" in sources:
if not self.config["google_API_key"].get():
# We log a *debug* message here because the default
# configuration includes `google`. This way, the source
# is silent by default but can be enabled just by
# setting an API key.
self._log.debug(
"Disabling google source: " "no API key configured."
)
sources.remove("google")
self.config["bing_lang_from"] = [
x.lower() for x in self.config["bing_lang_from"].as_str_seq()
]
self.bing_auth_token = None
if not HAS_LANGDETECT and self.config["bing_client_secret"].get():
self._log.warning(
"To use bing translations, you need to "
"install the langdetect module. See the "
"documentation for further details."
)
self.backends = [
self.SOURCE_BACKENDS[source](self.config, self._log)
for source in sources
]
def sanitize_bs_sources(self, sources):
enabled_sources = []
for source in sources:
if self.SOURCE_BACKENDS[source].REQUIRES_BS:
self._log.debug(
"To use the %s lyrics source, you must "
"install the beautifulsoup4 module. See "
"the documentation for further details." % source
)
else:
enabled_sources.append(source)
return enabled_sources
def get_bing_access_token(self):
params = {
"client_id": "beets",
"client_secret": self.config["bing_client_secret"],
"scope": "https://api.microsofttranslator.com",
"grant_type": "client_credentials",
}
oauth_url = "https://datamarket.accesscontrol.windows.net/v2/OAuth2-13"
oauth_token = json.loads(
requests.post(
oauth_url,
data=urlencode(params),
timeout=10,
).content
)
if "access_token" in oauth_token:
return "Bearer " + oauth_token["access_token"]
else:
self._log.warning(
"Could not get Bing Translate API access token."
' Check your "bing_client_secret" password'
)
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(
"-r",
"--write-rest",
dest="writerest",
action="store",
default=None,
metavar="dir",
help="write lyrics to given directory as ReST files",
)
cmd.parser.add_option(
"-f",
"--force",
dest="force_refetch",
action="store_true",
default=False,
help="always re-download lyrics",
)
cmd.parser.add_option(
"-l",
"--local",
dest="local_only",
action="store_true",
default=False,
help="do not fetch missing lyrics",
)
def func(lib, opts, args):
# The "write to files" option corresponds to the
# import_write config value.
write = ui.should_write()
if opts.writerest:
self.writerest_indexes(opts.writerest)
items = lib.items(ui.decargs(args))
for item in items:
if not opts.local_only and not self.config["local"]:
self.fetch_item_lyrics(
lib,
item,
write,
opts.force_refetch or self.config["force"],
)
if item.lyrics:
if opts.printlyr:
ui.print_(item.lyrics)
if opts.writerest:
self.appendrest(opts.writerest, item)
if opts.writerest and items:
# flush last artist & write to ReST
self.writerest(opts.writerest)
ui.print_("ReST files generated. to build, use one of:")
ui.print_(
" sphinx-build -b html %s _build/html" % opts.writerest
)
ui.print_(
" sphinx-build -b epub %s _build/epub" % opts.writerest
)
ui.print_(
(
" sphinx-build -b latex %s _build/latex "
"&& make -C _build/latex all-pdf"
)
% opts.writerest
)
cmd.func = func
return [cmd]
def appendrest(self, directory, item):
"""Append the item to an ReST file
This will keep state (in the `rest` variable) in order to avoid
writing continuously to the same files.
"""
if slug(self.artist) != slug(item.albumartist):
# Write current file and start a new one ~ item.albumartist
self.writerest(directory)
self.artist = item.albumartist.strip()
self.rest = "%s\n%s\n\n.. contents::\n :local:\n\n" % (
self.artist,
"=" * len(self.artist),
)
if self.album != item.album:
tmpalbum = self.album = item.album.strip()
if self.album == "":
tmpalbum = "Unknown album"
self.rest += "{}\n{}\n\n".format(tmpalbum, "-" * len(tmpalbum))
title_str = ":index:`%s`" % item.title.strip()
block = "| " + item.lyrics.replace("\n", "\n| ")
self.rest += "{}\n{}\n\n{}\n\n".format(
title_str, "~" * len(title_str), block
)
def writerest(self, directory):
"""Write self.rest to a ReST file"""
if self.rest is not None and self.artist is not None:
path = os.path.join(
directory, "artists", slug(self.artist) + ".rst"
)
with open(path, "wb") as output:
output.write(self.rest.encode("utf-8"))
def writerest_indexes(self, directory):
"""Write conf.py and index.rst files necessary for Sphinx
We write minimal configurations that are necessary for Sphinx
to operate. We do not overwrite existing files so that
customizations are respected."""
try:
os.makedirs(os.path.join(directory, "artists"))
except OSError as e:
if e.errno == errno.EEXIST:
pass
else:
raise
indexfile = os.path.join(directory, "index.rst")
if not os.path.exists(indexfile):
with open(indexfile, "w") as output:
output.write(REST_INDEX_TEMPLATE)
conffile = os.path.join(directory, "conf.py")
if not os.path.exists(conffile):
with open(conffile, "w") as output:
output.write(REST_CONF_TEMPLATE)
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, item, False, self.config["force"]
)
def fetch_item_lyrics(self, lib, item, write, force):
"""Fetch and store lyrics for a single item. If ``write``, then the
lyrics will also be written to the file itself.
"""
# Skip if the item already has lyrics.
if not force and item.lyrics:
self._log.info("lyrics already present: {0}", item)
return
lyrics = None
album = item.album
length = round(item.length)
for artist, titles in search_pairs(item):
lyrics = [
self.get_lyrics(artist, title, album=album, length=length)
for title in titles
]
if any(lyrics):
break
lyrics = "\n\n---\n\n".join(filter(None, lyrics))
if lyrics:
self._log.info("fetched lyrics: {0}", item)
if HAS_LANGDETECT and self.config["bing_client_secret"].get():
lang_from = langdetect.detect(lyrics)
if self.config["bing_lang_to"].get() != lang_from and (
not self.config["bing_lang_from"]
or (lang_from in self.config["bing_lang_from"].as_str_seq())
):
lyrics = self.append_translation(
lyrics, self.config["bing_lang_to"]
)
else:
self._log.info("lyrics not found: {0}", item)
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, album=None, length=None):
"""Fetch lyrics, trying each source in turn. Return a string or
None if no lyrics were found.
"""
for backend in self.backends:
lyrics = backend.fetch(artist, title, album=album, length=length)
if lyrics:
self._log.debug(
"got lyrics from backend: {0}", backend.__class__.__name__
)
return _scrape_strip_cruft(lyrics, True)
def append_translation(self, text, to_lang):
from xml.etree import ElementTree
if not self.bing_auth_token:
self.bing_auth_token = self.get_bing_access_token()
if self.bing_auth_token:
# Extract unique lines to limit API request size per song
text_lines = set(text.split("\n"))
url = (
"https://api.microsofttranslator.com/v2/Http.svc/"
"Translate?text=%s&to=%s" % ("|".join(text_lines), to_lang)
)
r = requests.get(
url,
headers={"Authorization ": self.bing_auth_token},
timeout=10,
)
if r.status_code != 200:
self._log.debug(
"translation API error {}: {}", r.status_code, r.text
)
if "token has expired" in r.text:
self.bing_auth_token = None
return self.append_translation(text, to_lang)
return text
lines_translated = ElementTree.fromstring(
r.text.encode("utf-8")
).text
# Use a translation mapping dict to build resulting lyrics
translations = dict(zip(text_lines, lines_translated.split("|")))
result = ""
for line in text.split("\n"):
result += "{} / {}\n".format(line, translations[line])
return result
beetbox-beets-01f1faf/beetsplug/mbcollection.py 0000664 0000000 0000000 00000013624 14723254774 0021773 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright (c) 2011, Jeffrey Aylesworth
#
# 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 re
import musicbrainzngs
from beets import config, ui
from beets.plugins import BeetsPlugin
from beets.ui import Subcommand
SUBMISSION_CHUNK_SIZE = 200
FETCH_CHUNK_SIZE = 100
UUID_REGEX = r"^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$"
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, musicbrainzngs.NetworkError) as exc:
raise ui.UserError(f"MusicBrainz API error: {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)
class MusicBrainzCollectionPlugin(BeetsPlugin):
def __init__(self):
super().__init__()
config["musicbrainz"]["pass"].redact = True
musicbrainzngs.auth(
config["musicbrainz"]["user"].as_str(),
config["musicbrainz"]["pass"].as_str(),
)
self.config.add(
{
"auto": False,
"collection": "",
"remove": False,
}
)
if self.config["auto"]:
self.import_stages = [self.imported]
def _get_collection(self):
collections = mb_call(musicbrainzngs.get_collections)
if not collections["collection-list"]:
raise ui.UserError("no collections exist for user")
# Get all collection IDs, avoiding event collections
collection_ids = [x["id"] for x in collections["collection-list"]]
if not collection_ids:
raise ui.UserError("No collection found.")
# Check that the collection exists so we can present a nice error
collection = self.config["collection"].as_str()
if collection:
if collection not in collection_ids:
raise ui.UserError(
"invalid collection ID: {}".format(collection)
)
return collection
# No specified collection. Just return the first collection ID
return collection_ids[0]
def _get_albums_in_collection(self, id):
def _fetch(offset):
res = mb_call(
musicbrainzngs.get_releases_in_collection,
id,
limit=FETCH_CHUNK_SIZE,
offset=offset,
)["collection"]
return [x["id"] for x in res["release-list"]], res["release-count"]
offset = 0
albums_in_collection, release_count = _fetch(offset)
for i in range(0, release_count, FETCH_CHUNK_SIZE):
albums_in_collection += _fetch(offset)[0]
offset += FETCH_CHUNK_SIZE
return albums_in_collection
def commands(self):
mbupdate = Subcommand("mbupdate", help="Update MusicBrainz collection")
mbupdate.parser.add_option(
"-r",
"--remove",
action="store_true",
default=None,
dest="remove",
help="Remove albums not in beets library",
)
mbupdate.func = self.update_collection
return [mbupdate]
def remove_missing(self, collection_id, lib_albums):
lib_ids = {x.mb_albumid for x in lib_albums}
albums_in_collection = self._get_albums_in_collection(collection_id)
remove_me = list(set(albums_in_collection) - lib_ids)
for i in range(0, len(remove_me), FETCH_CHUNK_SIZE):
chunk = remove_me[i : i + FETCH_CHUNK_SIZE]
mb_call(
musicbrainzngs.remove_releases_from_collection,
collection_id,
chunk,
)
def update_collection(self, lib, opts, args):
self.config.set_args(opts)
remove_missing = self.config["remove"].get(bool)
self.update_album_list(lib, lib.albums(), remove_missing)
def imported(self, session, task):
"""Add each imported album to the collection."""
if task.is_album:
self.update_album_list(session.lib, [task.album])
def update_album_list(self, lib, album_list, remove_missing=False):
"""Update the MusicBrainz collection from a list of Beets albums"""
collection_id = self._get_collection()
# 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:
self._log.info("skipping invalid MBID: {0}", aid)
# Submit to MusicBrainz.
self._log.info("Updating MusicBrainz collection {0}...", collection_id)
submit_albums(collection_id, album_ids)
if remove_missing:
self.remove_missing(collection_id, lib.albums())
self._log.info("...MusicBrainz collection updated.")
beetbox-beets-01f1faf/beetsplug/mbsubmit.py 0000664 0000000 0000000 00000006532 14723254774 0021143 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, Adrian Sampson and Diego Moreda.
#
# 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.
"""Aid in submitting information to MusicBrainz.
This plugin allows the user to print track information in a format that is
parseable by the MusicBrainz track parser [1]. Programmatic submitting is not
implemented by MusicBrainz yet.
[1] https://wiki.musicbrainz.org/History:How_To_Parse_Track_Listings
"""
import subprocess
from beets import ui
from beets.autotag import Recommendation
from beets.plugins import BeetsPlugin
from beets.ui.commands import PromptChoice
from beets.util import displayable_path
from beetsplug.info import print_data
class MBSubmitPlugin(BeetsPlugin):
def __init__(self):
super().__init__()
self.config.add(
{
"format": "$track. $title - $artist ($length)",
"threshold": "medium",
"picard_path": "picard",
}
)
# Validate and store threshold.
self.threshold = self.config["threshold"].as_choice(
{
"none": Recommendation.none,
"low": Recommendation.low,
"medium": Recommendation.medium,
"strong": Recommendation.strong,
}
)
self.register_listener(
"before_choose_candidate", self.before_choose_candidate_event
)
def before_choose_candidate_event(self, session, task):
if task.rec <= self.threshold:
return [
PromptChoice("p", "Print tracks", self.print_tracks),
PromptChoice("o", "Open files with Picard", self.picard),
]
def picard(self, session, task):
paths = []
for p in task.paths:
paths.append(displayable_path(p))
try:
picard_path = self.config["picard_path"].as_str()
subprocess.Popen([picard_path] + paths)
self._log.info("launched picard from\n{}", picard_path)
except OSError as exc:
self._log.error(f"Could not open picard, got error:\n{exc}")
def print_tracks(self, session, task):
for i in sorted(task.items, key=lambda i: i.track):
print_data(None, i, self.config["format"].as_str())
def commands(self):
"""Add beet UI commands for mbsubmit."""
mbsubmit_cmd = ui.Subcommand(
"mbsubmit", help="Submit Tracks to MusicBrainz"
)
def func(lib, opts, args):
items = lib.items(ui.decargs(args))
self._mbsubmit(items)
mbsubmit_cmd.func = func
return [mbsubmit_cmd]
def _mbsubmit(self, items):
"""Print track information to be submitted to MusicBrainz."""
for i in sorted(items, key=lambda i: i.track):
print_data(None, i, self.config["format"].as_str())
beetbox-beets-01f1faf/beetsplug/mbsync.py 0000664 0000000 0000000 00000017107 14723254774 0020614 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, 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 re
from collections import defaultdict
from beets import autotag, library, ui, util
from beets.autotag import hooks
from beets.plugins import BeetsPlugin, apply_item_changes
MBID_REGEX = r"(\d|\w){8}-(\d|\w){4}-(\d|\w){4}-(\d|\w){4}-(\d|\w){12}"
class MBSyncPlugin(BeetsPlugin):
def __init__(self):
super().__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",
"--move",
action="store_true",
dest="move",
help="move files in the library directory",
)
cmd.parser.add_option(
"-M",
"--nomove",
action="store_false",
dest="move",
help="don't move files in library",
)
cmd.parser.add_option(
"-W",
"--nowrite",
action="store_false",
default=None,
dest="write",
help="don't write updated metadata to files",
)
cmd.parser.add_format_option()
cmd.func = self.func
return [cmd]
def func(self, lib, opts, args):
"""Command handler for the mbsync function."""
move = ui.should_move(opts.move)
pretend = opts.pretend
write = ui.should_write(opts.write)
query = ui.decargs(args)
self.singletons(lib, query, move, pretend, write)
self.albums(lib, query, move, pretend, write)
def singletons(self, 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"]):
item_formatted = format(item)
if not item.mb_trackid:
self._log.info(
"Skipping singleton with no mb_trackid: {0}", item_formatted
)
continue
# Do we have a valid MusicBrainz track ID?
if not re.match(MBID_REGEX, item.mb_trackid):
self._log.info(
"Skipping singleton with invalid mb_trackid:" + " {0}",
item_formatted,
)
continue
# Get the MusicBrainz recording info.
track_info = hooks.track_for_mbid(item.mb_trackid)
if not track_info:
self._log.info(
"Recording ID not found: {0} for track {0}",
item.mb_trackid,
item_formatted,
)
continue
# Apply.
with lib.transaction():
autotag.apply_item_metadata(item, track_info)
apply_item_changes(lib, item, move, pretend, write)
def albums(self, 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):
album_formatted = format(a)
if not a.mb_albumid:
self._log.info(
"Skipping album with no mb_albumid: {0}", album_formatted
)
continue
items = list(a.items())
# Do we have a valid MusicBrainz album ID?
if not re.match(MBID_REGEX, a.mb_albumid):
self._log.info(
"Skipping album with invalid mb_albumid: {0}",
album_formatted,
)
continue
# Get the MusicBrainz album information.
album_info = hooks.album_for_mbid(a.mb_albumid)
if not album_info:
self._log.info(
"Release ID {0} not found for album {1}",
a.mb_albumid,
album_formatted,
)
continue
# Map release track and recording MBIDs to their information.
# Recordings can appear multiple times on a release, so each MBID
# maps to a list of TrackInfo objects.
releasetrack_index = {}
track_index = defaultdict(list)
for track_info in album_info.tracks:
releasetrack_index[track_info.release_track_id] = track_info
track_index[track_info.track_id].append(track_info)
# Construct a track mapping according to MBIDs (release track MBIDs
# first, if available, and recording MBIDs otherwise). This should
# work for albums that have missing or extra tracks.
mapping = {}
for item in items:
if (
item.mb_releasetrackid
and item.mb_releasetrackid in releasetrack_index
):
mapping[item] = releasetrack_index[item.mb_releasetrackid]
else:
candidates = track_index[item.mb_trackid]
if len(candidates) == 1:
mapping[item] = candidates[0]
else:
# If there are multiple copies of a recording, they are
# disambiguated using their disc and track number.
for c in candidates:
if (
c.medium_index == item.track
and c.medium == item.disc
):
mapping[item] = c
break
# Apply.
self._log.debug("applying changes to {}", album_formatted)
with lib.transaction():
autotag.apply_metadata(album_info, mapping)
changed = False
# Find any changed item to apply MusicBrainz changes to album.
any_changed_item = items[0]
for item in items:
item_changed = ui.show_model_changes(item)
changed |= item_changed
if item_changed:
any_changed_item = item
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] = any_changed_item[key]
a.store()
# Move album art (and any inconsistent items).
if move and lib.directory in util.ancestry(items[0].path):
self._log.debug("moving album {0}", album_formatted)
a.move()
beetbox-beets-01f1faf/beetsplug/metasync/ 0000775 0000000 0000000 00000000000 14723254774 0020564 5 ustar 00root root 0000000 0000000 beetbox-beets-01f1faf/beetsplug/metasync/__init__.py 0000664 0000000 0000000 00000010264 14723254774 0022700 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, Heinz Wiesinger.
#
# 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.
"""Synchronize information from music player libraries"""
from abc import ABCMeta, abstractmethod
from importlib import import_module
from confuse import ConfigValueError
from beets import ui
from beets.plugins import BeetsPlugin
METASYNC_MODULE = "beetsplug.metasync"
# Dictionary to map the MODULE and the CLASS NAME of meta sources
SOURCES = {
"amarok": "Amarok",
"itunes": "Itunes",
}
class MetaSource(metaclass=ABCMeta):
def __init__(self, config, log):
self.item_types = {}
self.config = config
self._log = log
@abstractmethod
def sync_from_source(self, item):
pass
def load_meta_sources():
"""Returns a dictionary of all the MetaSources
E.g., {'itunes': Itunes} with isinstance(Itunes, MetaSource) true
"""
meta_sources = {}
for module_path, class_name in SOURCES.items():
module = import_module(METASYNC_MODULE + "." + module_path)
meta_sources[class_name.lower()] = getattr(module, class_name)
return meta_sources
META_SOURCES = load_meta_sources()
def load_item_types():
"""Returns a dictionary containing the item_types of all the MetaSources"""
item_types = {}
for meta_source in META_SOURCES.values():
item_types.update(meta_source.item_types)
return item_types
class MetaSyncPlugin(BeetsPlugin):
item_types = load_item_types()
def __init__(self):
super().__init__()
def commands(self):
cmd = ui.Subcommand(
"metasync", help="update metadata from music player libraries"
)
cmd.parser.add_option(
"-p",
"--pretend",
action="store_true",
help="show all changes but do nothing",
)
cmd.parser.add_option(
"-s",
"--source",
default=[],
action="append",
dest="sources",
help="comma-separated list of sources to sync",
)
cmd.parser.add_format_option()
cmd.func = self.func
return [cmd]
def func(self, lib, opts, args):
"""Command handler for the metasync function."""
pretend = opts.pretend
query = ui.decargs(args)
sources = []
for source in opts.sources:
sources.extend(source.split(","))
sources = sources or self.config["source"].as_str_seq()
meta_source_instances = {}
items = lib.items(query)
# Avoid needlessly instantiating meta sources (can be expensive)
if not items:
self._log.info("No items found matching query")
return
# Instantiate the meta sources
for player in sources:
try:
cls = META_SOURCES[player]
except KeyError:
self._log.error("Unknown metadata source '{}'".format(player))
try:
meta_source_instances[player] = cls(self.config, self._log)
except (ImportError, ConfigValueError) as e:
self._log.error(
f"Failed to instantiate metadata source {player!r}: {e}"
)
# Avoid needlessly iterating over items
if not meta_source_instances:
self._log.error("No valid metadata sources found")
return
# Sync the items with all of the meta sources
for item in items:
for meta_source in meta_source_instances.values():
meta_source.sync_from_source(item)
changed = ui.show_model_changes(item)
if changed and not pretend:
item.store()
beetbox-beets-01f1faf/beetsplug/metasync/amarok.py 0000664 0000000 0000000 00000007657 14723254774 0022427 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, Heinz Wiesinger.
#
# 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.
"""Synchronize information from amarok's library via dbus"""
from datetime import datetime
from os.path import basename
from time import mktime
from xml.sax.saxutils import quoteattr
from beets.dbcore import types
from beets.library import DateType
from beets.util import displayable_path
from beetsplug.metasync import MetaSource
def import_dbus():
try:
return __import__("dbus")
except ImportError:
return None
dbus = import_dbus()
class Amarok(MetaSource):
item_types = {
"amarok_rating": types.INTEGER,
"amarok_score": types.FLOAT,
"amarok_uid": types.STRING,
"amarok_playcount": types.INTEGER,
"amarok_firstplayed": DateType(),
"amarok_lastplayed": DateType(),
}
query_xml = ' \
\
\
\
'
def __init__(self, config, log):
super().__init__(config, log)
if not dbus:
raise ImportError("failed to import dbus")
self.collection = dbus.SessionBus().get_object(
"org.kde.amarok", "/Collection"
)
def sync_from_source(self, item):
path = displayable_path(item.path)
# amarok unfortunately doesn't allow searching for the full path, only
# for the patch relative to the mount point. But the full path is part
# of the result set. So query for the filename and then try to match
# the correct item from the results we get back
results = self.collection.Query(
self.query_xml % quoteattr(basename(path))
)
for result in results:
if result["xesam:url"] != path:
continue
item.amarok_rating = result["xesam:userRating"]
item.amarok_score = result["xesam:autoRating"]
item.amarok_playcount = result["xesam:useCount"]
item.amarok_uid = result["xesam:id"].replace(
"amarok-sqltrackuid://", ""
)
if result["xesam:firstUsed"][0][0] != 0:
# These dates are stored as timestamps in amarok's db, but
# exposed over dbus as fixed integers in the current timezone.
first_played = datetime(
result["xesam:firstUsed"][0][0],
result["xesam:firstUsed"][0][1],
result["xesam:firstUsed"][0][2],
result["xesam:firstUsed"][1][0],
result["xesam:firstUsed"][1][1],
result["xesam:firstUsed"][1][2],
)
if result["xesam:lastUsed"][0][0] != 0:
last_played = datetime(
result["xesam:lastUsed"][0][0],
result["xesam:lastUsed"][0][1],
result["xesam:lastUsed"][0][2],
result["xesam:lastUsed"][1][0],
result["xesam:lastUsed"][1][1],
result["xesam:lastUsed"][1][2],
)
else:
last_played = first_played
item.amarok_firstplayed = mktime(first_played.timetuple())
item.amarok_lastplayed = mktime(last_played.timetuple())
beetbox-beets-01f1faf/beetsplug/metasync/itunes.py 0000664 0000000 0000000 00000010733 14723254774 0022451 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, Tom Jaspers.
#
# 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.
"""Synchronize information from iTunes's library"""
import os
import plistlib
import shutil
import tempfile
from contextlib import contextmanager
from time import mktime
from urllib.parse import unquote, urlparse
from confuse import ConfigValueError
from beets import util
from beets.dbcore import types
from beets.library import DateType
from beets.util import bytestring_path, syspath
from beetsplug.metasync import MetaSource
@contextmanager
def create_temporary_copy(path):
temp_dir = bytestring_path(tempfile.mkdtemp())
temp_path = os.path.join(temp_dir, b"temp_itunes_lib")
shutil.copyfile(syspath(path), syspath(temp_path))
try:
yield temp_path
finally:
shutil.rmtree(syspath(temp_dir))
def _norm_itunes_path(path):
# Itunes prepends the location with 'file://' on posix systems,
# and with 'file://localhost/' on Windows systems.
# The actual path to the file is always saved as posix form
# E.g., 'file://Users/Music/bar' or 'file://localhost/G:/Music/bar'
# The entire path will also be capitalized (e.g., '/Music/Alt-J')
# Note that this means the path will always have a leading separator,
# which is unwanted in the case of Windows systems.
# E.g., '\\G:\\Music\\bar' needs to be stripped to 'G:\\Music\\bar'
return util.bytestring_path(
os.path.normpath(unquote(urlparse(path).path)).lstrip("\\")
).lower()
class Itunes(MetaSource):
item_types = {
"itunes_rating": types.INTEGER, # 0..100 scale
"itunes_playcount": types.INTEGER,
"itunes_skipcount": types.INTEGER,
"itunes_lastplayed": DateType(),
"itunes_lastskipped": DateType(),
"itunes_dateadded": DateType(),
}
def __init__(self, config, log):
super().__init__(config, log)
config.add({"itunes": {"library": "~/Music/iTunes/iTunes Library.xml"}})
# Load the iTunes library, which has to be the .xml one (not the .itl)
library_path = config["itunes"]["library"].as_filename()
try:
self._log.debug(f"loading iTunes library from {library_path}")
with create_temporary_copy(library_path) as library_copy:
with open(library_copy, "rb") as library_copy_f:
raw_library = plistlib.load(library_copy_f)
except OSError as e:
raise ConfigValueError("invalid iTunes library: " + e.strerror)
except Exception:
# It's likely the user configured their '.itl' library (<> xml)
if os.path.splitext(library_path)[1].lower() != ".xml":
hint = (
": please ensure that the configured path"
" points to the .XML library"
)
else:
hint = ""
raise ConfigValueError("invalid iTunes library" + hint)
# Make the iTunes library queryable using the path
self.collection = {
_norm_itunes_path(track["Location"]): track
for track in raw_library["Tracks"].values()
if "Location" in track
}
def sync_from_source(self, item):
result = self.collection.get(util.bytestring_path(item.path).lower())
if not result:
self._log.warning(f"no iTunes match found for {item}")
return
item.itunes_rating = result.get("Rating")
item.itunes_playcount = result.get("Play Count")
item.itunes_skipcount = result.get("Skip Count")
if result.get("Play Date UTC"):
item.itunes_lastplayed = mktime(
result.get("Play Date UTC").timetuple()
)
if result.get("Skip Date"):
item.itunes_lastskipped = mktime(
result.get("Skip Date").timetuple()
)
if result.get("Date Added"):
item.itunes_dateadded = mktime(result.get("Date Added").timetuple())
beetbox-beets-01f1faf/beetsplug/missing.py 0000664 0000000 0000000 00000017564 14723254774 0021001 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, Pedro Silva.
# Copyright 2017, Quentin Young.
#
# 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."""
from collections import defaultdict
import musicbrainzngs
from musicbrainzngs.musicbrainz import MusicBrainzError
from beets import config
from beets.autotag import hooks
from beets.dbcore import types
from beets.library import Item
from beets.plugins import BeetsPlugin
from beets.ui import Subcommand, decargs, print_
def _missing_count(album):
"""Return number of missing items in `album`."""
return (album.albumtotal or 0) - len(album.items())
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": t.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"""
album_types = {
"missing": types.INTEGER,
}
def __init__(self):
super().__init__()
self.config.add(
{
"count": False,
"total": False,
"album": False,
}
)
self.album_template_fields["missing"] = _missing_count
self._command = Subcommand("missing", help=__doc__, aliases=["miss"])
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",
)
self._command.parser.add_option(
"-a",
"--album",
dest="album",
action="store_true",
help="show missing albums for artist instead of tracks",
)
self._command.parser.add_format_option()
def commands(self):
def _miss(lib, opts, args):
self.config.set_args(opts)
albms = self.config["album"].get()
helper = self._missing_albums if albms else self._missing_tracks
helper(lib, decargs(args))
self._command.func = _miss
return [self._command]
def _missing_tracks(self, lib, query):
"""Print a listing of tracks missing from each album in the library
matching query.
"""
albums = lib.albums(query)
count = self.config["count"].get()
total = self.config["total"].get()
fmt = config["format_album" if count else "format_item"].get()
if total:
print(sum([_missing_count(a) for a in albums]))
return
# Default format string for count mode.
if count:
fmt += ": $missing"
for album in albums:
if count:
if _missing_count(album):
print_(format(album, fmt))
else:
for item in self._missing(album):
print_(format(item, fmt))
def _missing_albums(self, lib, query):
"""Print a listing of albums missing from each artist in the library
matching query.
"""
total = self.config["total"].get()
albums = lib.albums(query)
# build dict mapping artist to list of their albums in library
albums_by_artist = defaultdict(list)
for alb in albums:
artist = (alb["albumartist"], alb["mb_albumartistid"])
albums_by_artist[artist].append(alb)
total_missing = 0
# build dict mapping artist to list of all albums
for artist, albums in albums_by_artist.items():
if artist[1] is None or artist[1] == "":
albs_no_mbid = ["'" + a["album"] + "'" for a in albums]
self._log.info(
"No musicbrainz ID for artist '{}' found in album(s) {}; "
"skipping",
artist[0],
", ".join(albs_no_mbid),
)
continue
try:
resp = musicbrainzngs.browse_release_groups(artist=artist[1])
release_groups = resp["release-group-list"]
except MusicBrainzError as err:
self._log.info(
"Couldn't fetch info for artist '{}' ({}) - '{}'",
artist[0],
artist[1],
err,
)
continue
missing = []
present = []
for rg in release_groups:
missing.append(rg)
for alb in albums:
if alb["mb_releasegroupid"] == rg["id"]:
missing.remove(rg)
present.append(rg)
break
total_missing += len(missing)
if total:
continue
missing_titles = {rg["title"] for rg in missing}
for release_title in missing_titles:
print_("{} - {}".format(artist[0], release_title))
if total:
print(total_missing)
def _missing(self, album):
"""Query MusicBrainz to determine items missing from `album`."""
item_mbids = [x.mb_trackid for x in album.items()]
if len(list(album.items())) < album.albumtotal:
# 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)
self._log.debug(
"track {0} in album {1}",
track_info.track_id,
album_info.album_id,
)
yield item
beetbox-beets-01f1faf/beetsplug/mpdstats.py 0000664 0000000 0000000 00000030000 14723254774 0021143 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, 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 os
import time
import mpd
from beets import config, library, plugins, ui
from beets.dbcore import types
from beets.util import displayable_path
# 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
mpd_config = config["mpd"]
def is_url(path):
"""Try to determine if the path is an URL."""
if isinstance(path, bytes): # if it's bytes, then it's a path
return False
return path.split("://", 1)[0] in ["http", "https"]
class MPDClientWrapper:
def __init__(self, log):
self._log = log
self.music_directory = mpd_config["music_directory"].as_str()
self.strip_path = mpd_config["strip_path"].as_str()
# Ensure strip_path end with '/'
if not self.strip_path.endswith("/"):
self.strip_path += "/"
self._log.debug("music_directory: {0}", self.music_directory)
self._log.debug("strip_path: {0}", self.strip_path)
self.client = mpd.MPDClient()
def connect(self):
"""Connect to the MPD."""
host = mpd_config["host"].as_str()
port = mpd_config["port"].get(int)
if host[0] in ["/", "~"]:
host = os.path.expanduser(host)
self._log.info("connecting to {0}:{1}", host, port)
try:
self.client.connect(host, port)
except OSError as e:
raise ui.UserError(f"could not connect to MPD: {e}")
password = mpd_config["password"].as_str()
if password:
try:
self.client.password(password)
except mpd.CommandError as e:
raise ui.UserError(f"could not authenticate to MPD: {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 (OSError, mpd.ConnectionError) as err:
self._log.error("{0}", err)
if retries <= 0:
# if we exited without breaking, we couldn't reconnect in time :(
raise ui.UserError("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 currentsong(self):
"""Return the path to the currently playing song, along with its
songid. Prefixes paths with the music_directory, to get the absolute
path.
In some cases, we need to remove the local path from MPD server,
we replace 'strip_path' with ''.
`strip_path` defaults to ''.
"""
result = None
entry = self.get("currentsong")
if "file" in entry:
if not is_url(entry["file"]):
file = entry["file"]
if file.startswith(self.strip_path):
file = file[len(self.strip_path) :]
result = os.path.join(self.music_directory, file)
else:
result = entry["file"]
self._log.debug("returning: {0}", result)
return result, entry.get("id")
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:
def __init__(self, lib, log):
self.lib = lib
self._log = log
self.do_rating = mpd_config["rating"].get(bool)
self.rating_mix = mpd_config["rating_mix"].get(float)
self.time_threshold = 10.0 # TODO: maybe add config option?
self.now_playing = None
self.mpd = MPDClientWrapper(log)
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:
self._log.info("item not found: {0}", displayable_path(path))
def update_item(self, 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()
self._log.debug(
"updated: {0} = {1} [{2}]",
attribute,
item[attribute],
displayable_path(item.path),
)
def update_rating(self, item, skipped):
"""Update the rating for a beets item. The `item` can either be a
beets `Item` or None. If the item is None, nothing changes.
"""
if item is None:
return
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.
Returns whether the change was manual (skipped previous song or not)
"""
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)
return skipped
def handle_played(self, song):
"""Updates the play count of a song."""
self.update_item(song["beets_item"], "play_count", increment=1)
self._log.info("played {0}", 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)
self._log.info("skipped {0}", displayable_path(song["path"]))
def on_stop(self, status):
self._log.info("stop")
# if the current song stays the same it means that we stopped on the
# current track and should not record a skip.
if self.now_playing and self.now_playing["id"] != status.get("songid"):
self.handle_song_change(self.now_playing)
self.now_playing = None
def on_pause(self, status):
self._log.info("pause")
self.now_playing = None
def on_play(self, status):
path, songid = self.mpd.currentsong()
if not path:
return
played, duration = map(int, status["time"].split(":", 1))
remaining = duration - played
if self.now_playing:
if self.now_playing["path"] != path:
self.handle_song_change(self.now_playing)
else:
# In case we got mpd play event with same song playing
# multiple times,
# assume low diff means redundant second play event
# after natural song start.
diff = abs(time.time() - self.now_playing["started"])
if diff <= self.time_threshold:
return
if self.now_playing["path"] == path and played == 0:
self.handle_song_change(self.now_playing)
if is_url(path):
self._log.info("playing stream {0}", displayable_path(path))
self.now_playing = None
return
self._log.info("playing {0}", displayable_path(path))
self.now_playing = {
"started": time.time(),
"remaining": remaining,
"path": path,
"id": songid,
"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:
self._log.debug('unhandled status "{0}"', status)
events = self.mpd.events()
class MPDStatsPlugin(plugins.BeetsPlugin):
item_types = {
"play_count": types.INTEGER,
"skip_count": types.INTEGER,
"last_played": library.DateType(),
"rating": types.FLOAT,
}
def __init__(self):
super().__init__()
mpd_config.add(
{
"music_directory": config["directory"].as_filename(),
"strip_path": "",
"rating": True,
"rating_mix": 0.75,
"host": os.environ.get("MPD_HOST", "localhost"),
"port": int(os.environ.get("MPD_PORT", 6600)),
"password": "",
}
)
mpd_config["password"].redact = True
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):
mpd_config.set_args(opts)
# Overrides for MPD settings.
if opts.host:
mpd_config["host"] = opts.host.decode("utf-8")
if opts.port:
mpd_config["host"] = int(opts.port)
if opts.password:
mpd_config["password"] = opts.password.decode("utf-8")
try:
MPDStats(lib, self._log).run()
except KeyboardInterrupt:
pass
cmd.func = func
return [cmd]
beetbox-beets-01f1faf/beetsplug/mpdupdate.py 0000664 0000000 0000000 00000010033 14723254774 0021273 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, 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
"""
import os
import socket
from beets import config
from beets.plugins import BeetsPlugin
# 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:
"""Socket abstraction that allows reading by line."""
def __init__(self, host, port, sep=b"\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 = b""
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 self.sep in self.buf:
res, self.buf = self.buf.split(self.sep, 1)
return res + self.sep
else:
return b""
def send(self, data):
self.sock.send(data)
def close(self):
self.sock.close()
class MPDUpdatePlugin(BeetsPlugin):
def __init__(self):
super().__init__()
config["mpd"].add(
{
"host": os.environ.get("MPD_HOST", "localhost"),
"port": int(os.environ.get("MPD_PORT", 6600)),
"password": "",
}
)
config["mpd"]["password"].redact = True
# 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()
self.register_listener("database_change", self.db_change)
def db_change(self, lib, model):
self.register_listener("cli_exit", self.update)
def update(self, lib):
self.update_mpd(
config["mpd"]["host"].as_str(),
config["mpd"]["port"].get(int),
config["mpd"]["password"].as_str(),
)
def update_mpd(self, host="localhost", port=6600, password=None):
"""Sends the "update" command to the MPD server indicated,
possibly authenticating with a password first.
"""
self._log.info("Updating MPD database...")
try:
s = BufferedSocket(host, port)
except OSError as e:
self._log.warning("MPD connection failed: {0}", str(e.strerror))
return
resp = s.readline()
if b"OK MPD" not in resp:
self._log.warning("MPD connection failed: {0!r}", resp)
return
if password:
s.send(b'password "%s"\n' % password.encode("utf8"))
resp = s.readline()
if b"OK" not in resp:
self._log.warning("Authentication failed: {0!r}", resp)
s.send(b"close\n")
s.close()
return
s.send(b"update\n")
resp = s.readline()
if b"updating_db" not in resp:
self._log.warning("Update failed: {0!r}", resp)
s.send(b"close\n")
s.close()
self._log.info("Database updated.")
beetbox-beets-01f1faf/beetsplug/parentwork.py 0000664 0000000 0000000 00000017716 14723254774 0021523 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2017, Dorian Soergel.
#
# 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 parent work, its disambiguation and id, composer, composer sort name
and work composition date
"""
import musicbrainzngs
from beets import ui
from beets.plugins import BeetsPlugin
def direct_parent_id(mb_workid, work_date=None):
"""Given a Musicbrainz work id, find the id one of the works the work is
part of and the first composition date it encounters.
"""
work_info = musicbrainzngs.get_work_by_id(
mb_workid, includes=["work-rels", "artist-rels"]
)
if "artist-relation-list" in work_info["work"] and work_date is None:
for artist in work_info["work"]["artist-relation-list"]:
if artist["type"] == "composer":
if "end" in artist.keys():
work_date = artist["end"]
if "work-relation-list" in work_info["work"]:
for direct_parent in work_info["work"]["work-relation-list"]:
if (
direct_parent["type"] == "parts"
and direct_parent.get("direction") == "backward"
):
direct_id = direct_parent["work"]["id"]
return direct_id, work_date
return None, work_date
def work_parent_id(mb_workid):
"""Find the parent work id and composition date of a work given its id."""
work_date = None
while True:
new_mb_workid, work_date = direct_parent_id(mb_workid, work_date)
if not new_mb_workid:
return mb_workid, work_date
mb_workid = new_mb_workid
return mb_workid, work_date
def find_parentwork_info(mb_workid):
"""Get the MusicBrainz information dict about a parent work, including
the artist relations, and the composition date for a work's parent work.
"""
parent_id, work_date = work_parent_id(mb_workid)
work_info = musicbrainzngs.get_work_by_id(
parent_id, includes=["artist-rels"]
)
return work_info, work_date
class ParentWorkPlugin(BeetsPlugin):
def __init__(self):
super().__init__()
self.config.add(
{
"auto": False,
"force": False,
}
)
if self.config["auto"]:
self.import_stages = [self.imported]
def commands(self):
def func(lib, opts, args):
self.config.set_args(opts)
force_parent = self.config["force"].get(bool)
write = ui.should_write()
for item in lib.items(ui.decargs(args)):
changed = self.find_work(item, force_parent)
if changed:
item.store()
if write:
item.try_write()
command = ui.Subcommand(
"parentwork", help="fetch parent works, composers and dates"
)
command.parser.add_option(
"-f",
"--force",
dest="force",
action="store_true",
default=None,
help="re-fetch when parent work is already present",
)
command.func = func
return [command]
def imported(self, session, task):
"""Import hook for fetching parent works automatically."""
force_parent = self.config["force"].get(bool)
for item in task.imported_items():
self.find_work(item, force_parent)
item.store()
def get_info(self, item, work_info):
"""Given the parent work info dict, fetch parent_composer,
parent_composer_sort, parentwork, parentwork_disambig, mb_workid and
composer_ids.
"""
parent_composer = []
parent_composer_sort = []
parentwork_info = {}
composer_exists = False
if "artist-relation-list" in work_info["work"]:
for artist in work_info["work"]["artist-relation-list"]:
if artist["type"] == "composer":
composer_exists = True
parent_composer.append(artist["artist"]["name"])
parent_composer_sort.append(artist["artist"]["sort-name"])
if "end" in artist.keys():
parentwork_info["parentwork_date"] = artist["end"]
parentwork_info["parent_composer"] = ", ".join(parent_composer)
parentwork_info["parent_composer_sort"] = ", ".join(
parent_composer_sort
)
if not composer_exists:
self._log.debug(
"no composer for {}; add one at "
"https://musicbrainz.org/work/{}",
item,
work_info["work"]["id"],
)
parentwork_info["parentwork"] = work_info["work"]["title"]
parentwork_info["mb_parentworkid"] = work_info["work"]["id"]
if "disambiguation" in work_info["work"]:
parentwork_info["parentwork_disambig"] = work_info["work"][
"disambiguation"
]
else:
parentwork_info["parentwork_disambig"] = None
return parentwork_info
def find_work(self, item, force):
"""Finds the parent work of a recording and populates the tags
accordingly.
The parent work is found recursively, by finding the direct parent
repeatedly until there are no more links in the chain. We return the
final, topmost work in the chain.
Namely, the tags parentwork, parentwork_disambig, mb_parentworkid,
parent_composer, parent_composer_sort and work_date are populated.
"""
if not item.mb_workid:
self._log.info(
"No work for {}, \
add one at https://musicbrainz.org/recording/{}",
item,
item.mb_trackid,
)
return
hasparent = hasattr(item, "parentwork")
work_changed = True
if hasattr(item, "parentwork_workid_current"):
work_changed = item.parentwork_workid_current != item.mb_workid
if force or not hasparent or work_changed:
try:
work_info, work_date = find_parentwork_info(item.mb_workid)
except musicbrainzngs.musicbrainz.WebServiceError as e:
self._log.debug("error fetching work: {}", e)
return
parent_info = self.get_info(item, work_info)
parent_info["parentwork_workid_current"] = item.mb_workid
if "parent_composer" in parent_info:
self._log.debug(
"Work fetched: {} - {}",
parent_info["parentwork"],
parent_info["parent_composer"],
)
else:
self._log.debug(
"Work fetched: {} - no parent composer",
parent_info["parentwork"],
)
elif hasparent:
self._log.debug("{}: Work present, skipping", item)
return
# apply all non-null values to the item
for key, value in parent_info.items():
if value:
item[key] = value
if work_date:
item["work_date"] = work_date
return ui.show_model_changes(
item,
fields=[
"parentwork",
"parentwork_disambig",
"mb_parentworkid",
"parent_composer",
"parent_composer_sort",
"work_date",
"parentwork_workid_current",
"parentwork_date",
],
)
beetbox-beets-01f1faf/beetsplug/permissions.py 0000664 0000000 0000000 00000010145 14723254774 0021667 0 ustar 00root root 0000000 0000000 """Fixes file permissions after the file gets written on import. Put something
like the following in your config.yaml to configure:
permissions:
file: 644
dir: 755
"""
import os
import stat
from beets import config
from beets.plugins import BeetsPlugin
from beets.util import ancestry, displayable_path, syspath
def convert_perm(perm):
"""Convert a string to an integer, interpreting the text as octal.
Or, if `perm` is an integer, reinterpret it as an octal number that
has been "misinterpreted" as decimal.
"""
if isinstance(perm, int):
perm = str(perm)
return int(perm, 8)
def check_permissions(path, permission):
"""Check whether the file's permissions equal the given vector.
Return a boolean.
"""
return oct(stat.S_IMODE(os.stat(syspath(path)).st_mode)) == oct(permission)
def assert_permissions(path, permission, log):
"""Check whether the file's permissions are as expected, otherwise,
log a warning message. Return a boolean indicating the match, like
`check_permissions`.
"""
if not check_permissions(path, permission):
log.warning("could not set permissions on {}", displayable_path(path))
log.debug(
"set permissions to {}, but permissions are now {}",
permission,
os.stat(syspath(path)).st_mode & 0o777,
)
def dirs_in_library(library, item):
"""Creates a list of ancestor directories in the beets library path."""
return [
ancestor for ancestor in ancestry(item) if ancestor.startswith(library)
][1:]
class Permissions(BeetsPlugin):
def __init__(self):
super().__init__()
# Adding defaults.
self.config.add(
{
"file": "644",
"dir": "755",
}
)
self.register_listener("item_imported", self.fix)
self.register_listener("album_imported", self.fix)
self.register_listener("art_set", self.fix_art)
def fix(self, lib, item=None, album=None):
"""Fix the permissions for an imported Item or Album."""
files = []
dirs = set()
if item:
files.append(item.path)
dirs.update(dirs_in_library(lib.directory, item.path))
elif album:
for album_item in album.items():
files.append(album_item.path)
dirs.update(dirs_in_library(lib.directory, album_item.path))
self.set_permissions(files=files, dirs=dirs)
def fix_art(self, album):
"""Fix the permission for Album art file."""
if album.artpath:
self.set_permissions(files=[album.artpath])
def set_permissions(self, files=[], dirs=[]):
# Get the configured permissions. The user can specify this either a
# string (in YAML quotes) or, for convenience, as an integer so the
# quotes can be omitted. In the latter case, we need to reinterpret the
# integer as octal, not decimal.
file_perm = config["permissions"]["file"].get()
dir_perm = config["permissions"]["dir"].get()
file_perm = convert_perm(file_perm)
dir_perm = convert_perm(dir_perm)
for path in files:
# Changing permissions on the destination file.
self._log.debug(
"setting file permissions on {}",
displayable_path(path),
)
if not check_permissions(path, file_perm):
os.chmod(syspath(path), file_perm)
# Checks if the destination path has the permissions configured.
assert_permissions(path, file_perm, self._log)
# Change permissions for the directories.
for path in dirs:
# Changing permissions on the destination directory.
self._log.debug(
"setting directory permissions on {}",
displayable_path(path),
)
if not check_permissions(path, dir_perm):
os.chmod(syspath(path), dir_perm)
# Checks if the destination path has the permissions configured.
assert_permissions(path, dir_perm, self._log)
beetbox-beets-01f1faf/beetsplug/play.py 0000664 0000000 0000000 00000017137 14723254774 0020271 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, 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."""
import shlex
import subprocess
from os.path import relpath
from beets import config, ui, util
from beets.plugins import BeetsPlugin
from beets.ui import Subcommand
from beets.ui.commands import PromptChoice
from beets.util import get_temp_filename
# Indicate where arguments should be inserted into the command string.
# If this is missing, they're placed at the end.
ARGS_MARKER = "$args"
def play(
command_str,
selection,
paths,
open_args,
log,
item_type="track",
keep_open=False,
):
"""Play items in paths with command_str and optional arguments. If
keep_open, return to beets, otherwise exit once command runs.
"""
# Print number of tracks or albums to be played, log command to be run.
item_type += "s" if len(selection) > 1 else ""
ui.print_("Playing {} {}.".format(len(selection), item_type))
log.debug("executing command: {} {!r}", command_str, open_args)
try:
if keep_open:
command = shlex.split(command_str)
command = command + open_args
subprocess.call(command)
else:
util.interactive_open(open_args, command_str)
except OSError as exc:
raise ui.UserError(f"Could not play the query: {exc}")
class PlayPlugin(BeetsPlugin):
def __init__(self):
super().__init__()
config["play"].add(
{
"command": None,
"use_folders": False,
"relative_to": None,
"raw": False,
"warning_threshold": 100,
"bom": False,
}
)
self.register_listener(
"before_choose_candidate", self.before_choose_candidate_listener
)
def commands(self):
play_command = Subcommand(
"play", help="send music to a player as a playlist"
)
play_command.parser.add_album_option()
play_command.parser.add_option(
"-A",
"--args",
action="store",
help="add additional arguments to the command",
)
play_command.parser.add_option(
"-y",
"--yes",
action="store_true",
help="skip the warning threshold",
)
play_command.func = self._play_command
return [play_command]
def _play_command(self, lib, opts, args):
"""The CLI command function for `beet play`. Create a list of paths
from query, determine if tracks or albums are to be played.
"""
use_folders = config["play"]["use_folders"].get(bool)
relative_to = config["play"]["relative_to"].get()
if relative_to:
relative_to = util.normpath(relative_to)
# Perform search by album and add folders rather than tracks to
# playlist.
if opts.album:
selection = lib.albums(ui.decargs(args))
paths = []
sort = lib.get_default_album_sort()
for album in selection:
if use_folders:
paths.append(album.item_dir())
else:
paths.extend(item.path for item in sort.sort(album.items()))
item_type = "album"
# Perform item query and add tracks to playlist.
else:
selection = lib.items(ui.decargs(args))
paths = [item.path for item in selection]
item_type = "track"
if relative_to:
paths = [relpath(path, relative_to) for path in paths]
if not selection:
ui.print_(ui.colorize("text_warning", f"No {item_type} to play."))
return
open_args = self._playlist_or_paths(paths)
command_str = self._command_str(opts.args)
# Check if the selection exceeds configured threshold. If True,
# cancel, otherwise proceed with play command.
if opts.yes or not self._exceeds_threshold(
selection, command_str, open_args, item_type
):
play(command_str, selection, paths, open_args, self._log, item_type)
def _command_str(self, args=None):
"""Create a command string from the config command and optional args."""
command_str = config["play"]["command"].get()
if not command_str:
return util.open_anything()
# Add optional arguments to the player command.
if args:
if ARGS_MARKER in command_str:
return command_str.replace(ARGS_MARKER, args)
else:
return f"{command_str} {args}"
else:
# Don't include the marker in the command.
return command_str.replace(" " + ARGS_MARKER, "")
def _playlist_or_paths(self, paths):
"""Return either the raw paths of items or a playlist of the items."""
if config["play"]["raw"]:
return paths
else:
return [self._create_tmp_playlist(paths)]
def _exceeds_threshold(
self, selection, command_str, open_args, item_type="track"
):
"""Prompt user whether to abort if playlist exceeds threshold. If
True, cancel playback. If False, execute play command.
"""
warning_threshold = config["play"]["warning_threshold"].get(int)
# Warn user before playing any huge playlists.
if warning_threshold and len(selection) > warning_threshold:
if len(selection) > 1:
item_type += "s"
ui.print_(
ui.colorize(
"text_warning",
"You are about to queue {} {}.".format(
len(selection), item_type
),
)
)
if ui.input_options(("Continue", "Abort")) == "a":
return True
return False
def _create_tmp_playlist(self, paths_list):
"""Create a temporary .m3u file. Return the filename."""
utf8_bom = config["play"]["bom"].get(bool)
filename = get_temp_filename(__name__, suffix=".m3u")
with open(filename, "wb") as m3u:
if utf8_bom:
m3u.write(b"\xef\xbb\xbf")
for item in paths_list:
m3u.write(item + b"\n")
return filename
def before_choose_candidate_listener(self, session, task):
"""Append a "Play" choice to the interactive importer prompt."""
return [PromptChoice("y", "plaY", self.importer_play)]
def importer_play(self, session, task):
"""Get items from current import task and send to play function."""
selection = task.items
paths = [item.path for item in selection]
open_args = self._playlist_or_paths(paths)
command_str = self._command_str()
if not self._exceeds_threshold(selection, command_str, open_args):
play(
command_str,
selection,
paths,
open_args,
self._log,
keep_open=True,
)
beetbox-beets-01f1faf/beetsplug/playlist.py 0000664 0000000 0000000 00000015607 14723254774 0021165 0 ustar 00root root 0000000 0000000 # This file is part of beets.
#
# 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 fnmatch
import os
import tempfile
from typing import Sequence
import beets
from beets.dbcore.query import InQuery
from beets.library import BLOB_TYPE
from beets.util import path_as_posix
class PlaylistQuery(InQuery[bytes]):
"""Matches files listed by a playlist file."""
@property
def subvals(self) -> Sequence[BLOB_TYPE]:
return [BLOB_TYPE(p) for p in self.pattern]
def __init__(self, _, pattern: str, __):
config = beets.config["playlist"]
# Get the full path to the playlist
playlist_paths = (
pattern,
os.path.abspath(
os.path.join(
config["playlist_dir"].as_filename(),
f"{pattern}.m3u",
)
),
)
paths = []
for playlist_path in playlist_paths:
if not fnmatch.fnmatch(playlist_path, "*.[mM]3[uU]"):
# This is not am M3U playlist, skip this candidate
continue
try:
f = open(beets.util.syspath(playlist_path), mode="rb")
except OSError:
continue
if config["relative_to"].get() == "library":
relative_to = beets.config["directory"].as_filename()
elif config["relative_to"].get() == "playlist":
relative_to = os.path.dirname(playlist_path)
else:
relative_to = config["relative_to"].as_filename()
relative_to = beets.util.bytestring_path(relative_to)
for line in f:
if line[0] == "#":
# ignore comments, and extm3u extension
continue
paths.append(
beets.util.normpath(
os.path.join(relative_to, line.rstrip())
)
)
f.close()
break
super().__init__("path", paths)
class PlaylistPlugin(beets.plugins.BeetsPlugin):
item_queries = {"playlist": PlaylistQuery}
def __init__(self):
super().__init__()
self.config.add(
{
"auto": False,
"playlist_dir": ".",
"relative_to": "library",
"forward_slash": False,
}
)
self.playlist_dir = self.config["playlist_dir"].as_filename()
self.changes = {}
if self.config["relative_to"].get() == "library":
self.relative_to = beets.util.bytestring_path(
beets.config["directory"].as_filename()
)
elif self.config["relative_to"].get() != "playlist":
self.relative_to = beets.util.bytestring_path(
self.config["relative_to"].as_filename()
)
else:
self.relative_to = None
if self.config["auto"]:
self.register_listener("item_moved", self.item_moved)
self.register_listener("item_removed", self.item_removed)
self.register_listener("cli_exit", self.cli_exit)
def item_moved(self, item, source, destination):
self.changes[source] = destination
def item_removed(self, item):
if not os.path.exists(beets.util.syspath(item.path)):
self.changes[item.path] = None
def cli_exit(self, lib):
for playlist in self.find_playlists():
self._log.info(f"Updating playlist: {playlist}")
base_dir = beets.util.bytestring_path(
self.relative_to
if self.relative_to
else os.path.dirname(playlist)
)
try:
self.update_playlist(playlist, base_dir)
except beets.util.FilesystemError:
self._log.error(
"Failed to update playlist: {}".format(
beets.util.displayable_path(playlist)
)
)
def find_playlists(self):
"""Find M3U playlists in the playlist directory."""
try:
dir_contents = os.listdir(beets.util.syspath(self.playlist_dir))
except OSError:
self._log.warning(
"Unable to open playlist directory {}".format(
beets.util.displayable_path(self.playlist_dir)
)
)
return
for filename in dir_contents:
if fnmatch.fnmatch(filename, "*.[mM]3[uU]"):
yield os.path.join(self.playlist_dir, filename)
def update_playlist(self, filename, base_dir):
"""Find M3U playlists in the specified directory."""
changes = 0
deletions = 0
with tempfile.NamedTemporaryFile(mode="w+b", delete=False) as tempfp:
new_playlist = tempfp.name
with open(filename, mode="rb") as fp:
for line in fp:
original_path = line.rstrip(b"\r\n")
# Ensure that path from playlist is absolute
is_relative = not os.path.isabs(line)
if is_relative:
lookup = os.path.join(base_dir, original_path)
else:
lookup = original_path
try:
new_path = self.changes[beets.util.normpath(lookup)]
except KeyError:
if self.config["forward_slash"]:
line = path_as_posix(line)
tempfp.write(line)
else:
if new_path is None:
# Item has been deleted
deletions += 1
continue
changes += 1
if is_relative:
new_path = os.path.relpath(new_path, base_dir)
line = line.replace(original_path, new_path)
if self.config["forward_slash"]:
line = path_as_posix(line)
tempfp.write(line)
if changes or deletions:
self._log.info(
"Updated playlist {} ({} changes, {} deletions)".format(
filename, changes, deletions
)
)
beets.util.copy(new_playlist, filename, replace=True)
beets.util.remove(new_playlist)
beetbox-beets-01f1faf/beetsplug/plexupdate.py 0000664 0000000 0000000 00000007076 14723254774 0021500 0 ustar 00root root 0000000 0000000 """Updates an Plex library whenever the beets library is changed.
Plex Home users enter the Plex Token to enable updating.
Put something like the following in your config.yaml to configure:
plex:
host: localhost
port: 32400
token: token
"""
from urllib.parse import urlencode, urljoin
from xml.etree import ElementTree
import requests
from beets import config
from beets.plugins import BeetsPlugin
def get_music_section(
host, port, token, library_name, secure, ignore_cert_errors
):
"""Getting the section key for the music library in Plex."""
api_endpoint = append_token("library/sections", token)
url = urljoin(
"{}://{}:{}".format(get_protocol(secure), host, port), api_endpoint
)
# Sends request.
r = requests.get(
url,
verify=not ignore_cert_errors,
timeout=10,
)
# Parse xml tree and extract music section key.
tree = ElementTree.fromstring(r.content)
for child in tree.findall("Directory"):
if child.get("title") == library_name:
return child.get("key")
def update_plex(host, port, token, library_name, secure, ignore_cert_errors):
"""Ignore certificate errors if configured to."""
if ignore_cert_errors:
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
"""Sends request to the Plex api to start a library refresh.
"""
# Getting section key and build url.
section_key = get_music_section(
host, port, token, library_name, secure, ignore_cert_errors
)
api_endpoint = f"library/sections/{section_key}/refresh"
api_endpoint = append_token(api_endpoint, token)
url = urljoin(
"{}://{}:{}".format(get_protocol(secure), host, port), api_endpoint
)
# Sends request and returns requests object.
r = requests.get(
url,
verify=not ignore_cert_errors,
timeout=10,
)
return r
def append_token(url, token):
"""Appends the Plex Home token to the api call if required."""
if token:
url += "?" + urlencode({"X-Plex-Token": token})
return url
def get_protocol(secure):
if secure:
return "https"
else:
return "http"
class PlexUpdate(BeetsPlugin):
def __init__(self):
super().__init__()
# Adding defaults.
config["plex"].add(
{
"host": "localhost",
"port": 32400,
"token": "",
"library_name": "Music",
"secure": False,
"ignore_cert_errors": False,
}
)
config["plex"]["token"].redact = True
self.register_listener("database_change", self.listen_for_db_change)
def listen_for_db_change(self, lib, model):
"""Listens for beets db change and register the update for the end"""
self.register_listener("cli_exit", self.update)
def update(self, lib):
"""When the client exists try to send refresh request to Plex server."""
self._log.info("Updating Plex library...")
# Try to send update request.
try:
update_plex(
config["plex"]["host"].get(),
config["plex"]["port"].get(),
config["plex"]["token"].get(),
config["plex"]["library_name"].get(),
config["plex"]["secure"].get(bool),
config["plex"]["ignore_cert_errors"].get(bool),
)
self._log.info("... started.")
except requests.exceptions.RequestException:
self._log.warning("Update failed.")
beetbox-beets-01f1faf/beetsplug/random.py 0000664 0000000 0000000 00000003650 14723254774 0020577 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, 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 beets.plugins import BeetsPlugin
from beets.random import random_objs
from beets.ui import Subcommand, decargs, print_
def random_func(lib, opts, args):
"""Select some random items or albums and print the results."""
# Fetch all the objects matching the query into a list.
query = decargs(args)
if opts.album:
objs = list(lib.albums(query))
else:
objs = list(lib.items(query))
# Print a random subset.
objs = random_objs(
objs, opts.album, opts.number, opts.time, opts.equal_chance
)
for obj in objs:
print_(format(obj))
random_cmd = Subcommand("random", help="choose a random track or album")
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.parser.add_option(
"-t",
"--time",
action="store",
type="float",
help="total length in minutes of objects to choose",
)
random_cmd.parser.add_all_common_options()
random_cmd.func = random_func
class Random(BeetsPlugin):
def commands(self):
return [random_cmd]
beetbox-beets-01f1faf/beetsplug/replaygain.py 0000664 0000000 0000000 00000153253 14723254774 0021457 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, 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 collections
import enum
import math
import optparse
import os
import queue
import signal
import subprocess
import sys
import warnings
from abc import ABC, abstractmethod
from dataclasses import dataclass
from logging import Logger
from multiprocessing.pool import ThreadPool
from threading import Event, Thread
from typing import (
Any,
Callable,
DefaultDict,
Dict,
List,
Optional,
Sequence,
Tuple,
Type,
TypeVar,
Union,
cast,
)
from confuse import ConfigView
from beets import ui
from beets.importer import ImportSession, ImportTask
from beets.library import Album, Item, Library
from beets.plugins import BeetsPlugin
from beets.util import command_output, displayable_path, syspath
# 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."""
class FatalGstreamerPluginReplayGainError(FatalReplayGainError):
"""Raised when a fatal error occurs in the GStreamerBackend when
loading the required plugins."""
def call(args: List[Any], log: Logger, **kwargs: Any):
"""Execute the command and return its output or raise a
ReplayGainError on failure.
"""
try:
return command_output(args, **kwargs)
except subprocess.CalledProcessError as e:
log.debug(e.output.decode("utf8", "ignore"))
raise ReplayGainError(
"{} exited with status {}".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:
# https://github.com/google-code-export/beets/issues/499
raise ReplayGainError("argument encoding failed")
def db_to_lufs(db: float) -> float:
"""Convert db to LUFS.
According to https://wiki.hydrogenaud.io/index.php?title=
ReplayGain_2.0_specification#Reference_level
"""
return db - 107
def lufs_to_db(db: float) -> float:
"""Convert LUFS to db.
According to https://wiki.hydrogenaud.io/index.php?title=
ReplayGain_2.0_specification#Reference_level
"""
return db + 107
# Backend base and plumbing classes.
@dataclass
class Gain:
# gain: in LU to reference level
gain: float
# peak: part of full scale (FS is 1.0)
peak: float
class PeakMethod(enum.Enum):
true = 1
sample = 2
class RgTask:
"""State and methods for a single replaygain calculation (rg version).
Bundles the state (parameters and results) of a single replaygain
calculation (either for one item, one disk, or one full album).
This class provides methods to store the resulting gains and peaks as plain
old rg tags.
"""
def __init__(
self,
items: Sequence[Item],
album: Optional[Album],
target_level: float,
peak_method: Optional[PeakMethod],
backend_name: str,
log: Logger,
):
self.items = items
self.album = album
self.target_level = target_level
self.peak_method = peak_method
self.backend_name = backend_name
self._log = log
self.album_gain: Optional[Gain] = None
self.track_gains: Optional[List[Gain]] = None
def _store_track_gain(self, item: Item, track_gain: Gain):
"""Store track gain for a single item in the database."""
item.rg_track_gain = track_gain.gain
item.rg_track_peak = track_gain.peak
item.store()
self._log.debug(
"applied track gain {0} LU, peak {1} of FS",
item.rg_track_gain,
item.rg_track_peak,
)
def _store_album_gain(self, item: Item, album_gain: Gain):
"""Store album gain for a single item in the database.
The caller needs to ensure that `self.album_gain is not None`.
"""
item.rg_album_gain = album_gain.gain
item.rg_album_peak = album_gain.peak
item.store()
self._log.debug(
"applied album gain {0} LU, peak {1} of FS",
item.rg_album_gain,
item.rg_album_peak,
)
def _store_track(self, write: bool):
"""Store track gain for the first track of the task in the database."""
item = self.items[0]
if self.track_gains is None or len(self.track_gains) != 1:
# In some cases, backends fail to produce a valid
# `track_gains` without throwing FatalReplayGainError
# => raise non-fatal exception & continue
raise ReplayGainError(
"ReplayGain backend `{}` failed for track {}".format(
self.backend_name, item
)
)
self._store_track_gain(item, self.track_gains[0])
if write:
item.try_write()
self._log.debug("done analyzing {0}", item)
def _store_album(self, write: bool):
"""Store track/album gains for all tracks of the task in the database."""
if (
self.album_gain is None
or self.track_gains is None
or len(self.track_gains) != len(self.items)
):
# In some cases, backends fail to produce a valid
# `album_gain` without throwing FatalReplayGainError
# => raise non-fatal exception & continue
raise ReplayGainError(
"ReplayGain backend `{}` failed "
"for some tracks in album {}".format(
self.backend_name, self.album
)
)
for item, track_gain in zip(self.items, self.track_gains):
self._store_track_gain(item, track_gain)
self._store_album_gain(item, self.album_gain)
if write:
item.try_write()
self._log.debug("done analyzing {0}", item)
def store(self, write: bool):
"""Store computed gains for the items of this task in the database."""
if self.album is not None:
self._store_album(write)
else:
self._store_track(write)
class R128Task(RgTask):
"""State and methods for a single replaygain calculation (r128 version).
Bundles the state (parameters and results) of a single replaygain
calculation (either for one item, one disk, or one full album).
This class provides methods to store the resulting gains and peaks as R128
tags.
"""
def __init__(
self,
items: Sequence[Item],
album: Optional[Album],
target_level: float,
backend_name: str,
log: Logger,
):
# R128_* tags do not store the track/album peak
super().__init__(items, album, target_level, None, backend_name, log)
def _store_track_gain(self, item: Item, track_gain: Gain):
item.r128_track_gain = track_gain.gain
item.store()
self._log.debug("applied r128 track gain {0} LU", item.r128_track_gain)
def _store_album_gain(self, item: Item, album_gain: Gain):
"""
The caller needs to ensure that `self.album_gain is not None`.
"""
item.r128_album_gain = album_gain.gain
item.store()
self._log.debug("applied r128 album gain {0} LU", item.r128_album_gain)
AnyRgTask = TypeVar("AnyRgTask", bound=RgTask)
class Backend(ABC):
"""An abstract class representing engine for calculating RG values."""
NAME = ""
do_parallel = False
def __init__(self, config: ConfigView, log: Logger):
"""Initialize the backend with the configuration view for the
plugin.
"""
self._log = log
@abstractmethod
def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
"""Computes the track gain for the tracks belonging to `task`, and sets
the `track_gains` attribute on the task. Returns `task`.
"""
raise NotImplementedError()
@abstractmethod
def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
"""Computes the album gain for the album belonging to `task`, and sets
the `album_gain` attribute on the task. Returns `task`.
"""
raise NotImplementedError()
# ffmpeg backend
class FfmpegBackend(Backend):
"""A replaygain backend using ffmpeg's ebur128 filter."""
NAME = "ffmpeg"
do_parallel = True
def __init__(self, config: ConfigView, log: Logger):
super().__init__(config, log)
self._ffmpeg_path = "ffmpeg"
# check that ffmpeg is installed
try:
ffmpeg_version_out = call([self._ffmpeg_path, "-version"], log)
except OSError:
raise FatalReplayGainError(
f"could not find ffmpeg at {self._ffmpeg_path}"
)
incompatible_ffmpeg = True
for line in ffmpeg_version_out.stdout.splitlines():
if line.startswith(b"configuration:"):
if b"--enable-libebur128" in line:
incompatible_ffmpeg = False
if line.startswith(b"libavfilter"):
version = line.split(b" ", 1)[1].split(b"/", 1)[0].split(b".")
version = tuple(map(int, version))
if version >= (6, 67, 100):
incompatible_ffmpeg = False
if incompatible_ffmpeg:
raise FatalReplayGainError(
"Installed FFmpeg version does not support ReplayGain."
"calculation. Either libavfilter version 6.67.100 or above or"
"the --enable-libebur128 configuration option is required."
)
def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
"""Computes the track gain for the tracks belonging to `task`, and sets
the `track_gains` attribute on the task. Returns `task`.
"""
task.track_gains = [
self._analyse_item(
item,
task.target_level,
task.peak_method,
count_blocks=False,
)[0] # take only the gain, discarding number of gating blocks
for item in task.items
]
return task
def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
"""Computes the album gain for the album belonging to `task`, and sets
the `album_gain` attribute on the task. Returns `task`.
"""
target_level_lufs = db_to_lufs(task.target_level)
# analyse tracks
# Gives a list of tuples (track_gain, track_n_blocks)
track_results: List[Tuple[Gain, int]] = [
self._analyse_item(
item,
task.target_level,
task.peak_method,
count_blocks=True,
)
for item in task.items
]
track_gains: List[Gain] = [tg for tg, _nb in track_results]
# Album peak is maximum track peak
album_peak = max(tg.peak for tg in track_gains)
# Total number of BS.1770 gating blocks
n_blocks = sum(nb for _tg, nb in track_results)
def sum_of_track_powers(track_gain: Gain, track_n_blocks: int):
# convert `LU to target_level` -> LUFS
loudness = target_level_lufs - track_gain.gain
# This reverses ITU-R BS.1770-4 p. 6 equation (5) to convert
# from loudness to power. The result is the average gating
# block power.
power = 10 ** ((loudness + 0.691) / 10)
# Multiply that average power by the number of gating blocks to get
# the sum of all block powers in this track.
return track_n_blocks * power
# calculate album gain
if n_blocks > 0:
# Sum over all tracks to get the sum of BS.1770 gating block powers
# for the entire album.
sum_powers = sum(
sum_of_track_powers(tg, nb) for tg, nb in track_results
)
# compare ITU-R BS.1770-4 p. 6 equation (5)
# Album gain is the replaygain of the concatenation of all tracks.
album_gain = -0.691 + 10 * math.log10(sum_powers / n_blocks)
else:
album_gain = -70
# convert LUFS -> `LU to target_level`
album_gain = target_level_lufs - album_gain
self._log.debug(
"{}: gain {} LU, peak {}",
task.album,
album_gain,
album_peak,
)
task.album_gain = Gain(album_gain, album_peak)
task.track_gains = track_gains
return task
def _construct_cmd(
self, item: Item, peak_method: Optional[PeakMethod]
) -> List[Union[str, bytes]]:
"""Construct the shell command to analyse items."""
return [
self._ffmpeg_path,
"-nostats",
"-hide_banner",
"-i",
item.path,
"-map",
"a:0",
"-filter",
"ebur128=peak={}".format(
"none" if peak_method is None else peak_method.name
),
"-f",
"null",
"-",
]
def _analyse_item(
self,
item: Item,
target_level: float,
peak_method: Optional[PeakMethod],
count_blocks: bool = True,
) -> Tuple[Gain, int]:
"""Analyse item. Return a pair of a Gain object and the number
of gating blocks above the threshold.
If `count_blocks` is False, the number of gating blocks returned
will be 0.
"""
target_level_lufs = db_to_lufs(target_level)
# call ffmpeg
self._log.debug(f"analyzing {item}")
cmd = self._construct_cmd(item, peak_method)
self._log.debug("executing {0}", " ".join(map(displayable_path, cmd)))
output = call(cmd, self._log).stderr.splitlines()
# parse output
if peak_method is None:
peak = 0.0
else:
line_peak = self._find_line(
output,
# `peak_method` is non-`None` in this arm of the conditional
f" {peak_method.name.capitalize()} peak:".encode(),
start_line=len(output) - 1,
step_size=-1,
)
peak = self._parse_float(
output[
self._find_line(
output,
b" Peak:",
line_peak,
)
]
)
# convert TPFS -> part of FS
peak = 10 ** (peak / 20)
line_integrated_loudness = self._find_line(
output,
b" Integrated loudness:",
start_line=len(output) - 1,
step_size=-1,
)
gain = self._parse_float(
output[
self._find_line(
output,
b" I:",
line_integrated_loudness,
)
]
)
# convert LUFS -> LU from target level
gain = target_level_lufs - gain
# count BS.1770 gating blocks
n_blocks = 0
if count_blocks:
gating_threshold = self._parse_float(
output[
self._find_line(
output,
b" Threshold:",
start_line=line_integrated_loudness,
)
]
)
for line in output:
if not line.startswith(b"[Parsed_ebur128"):
continue
if line.endswith(b"Summary:"):
continue
line = line.split(b"M:", 1)
if len(line) < 2:
continue
if self._parse_float(b"M: " + line[1]) >= gating_threshold:
n_blocks += 1
self._log.debug(
"{}: {} blocks over {} LUFS".format(
item, n_blocks, gating_threshold
)
)
self._log.debug("{}: gain {} LU, peak {}".format(item, gain, peak))
return Gain(gain, peak), n_blocks
def _find_line(
self,
output: Sequence[bytes],
search: bytes,
start_line: int = 0,
step_size: int = 1,
) -> int:
"""Return index of line beginning with `search`.
Begins searching at index `start_line` in `output`.
"""
end_index = len(output) if step_size > 0 else -1
for i in range(start_line, end_index, step_size):
if output[i].startswith(search):
return i
raise ReplayGainError(
"ffmpeg output: missing {} after line {}".format(
repr(search), start_line
)
)
def _parse_float(self, line: bytes) -> float:
"""Extract a float from a key value pair in `line`.
This format is expected: /[^:]:[[:space:]]*value.*/, where `value` is
the float.
"""
# extract value
parts = line.split(b":", 1)
if len(parts) < 2:
raise ReplayGainError(
f"ffmpeg output: expected key value pair, found {line!r}"
)
value = parts[1].lstrip()
# strip unit
value = value.split(b" ", 1)[0]
# cast value to float
try:
return float(value)
except ValueError:
raise ReplayGainError(
f"ffmpeg output: expected float value, found {value!r}"
)
# mpgain/aacgain CLI tool backend.
class CommandBackend(Backend):
NAME = "command"
do_parallel = True
def __init__(self, config: ConfigView, log: Logger):
super().__init__(config, log)
config.add(
{
"command": "",
"noclip": True,
}
)
self.command = cast(str, config["command"].as_str())
if self.command:
# Explicit executable path.
if not os.path.isfile(self.command):
raise FatalReplayGainError(
"replaygain command does not exist: {}".format(self.command)
)
else:
# Check whether the program is in $PATH.
for cmd in ("mp3gain", "aacgain"):
try:
call([cmd, "-v"], self._log)
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)
def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
"""Computes the track gain for the tracks belonging to `task`, and sets
the `track_gains` attribute on the task. Returns `task`.
"""
supported_items = list(filter(self.format_supported, task.items))
output = self.compute_gain(supported_items, task.target_level, False)
task.track_gains = output
return task
def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
"""Computes the album gain for the album belonging to `task`, and sets
the `album_gain` attribute on the task. Returns `task`.
"""
# TODO: What should be done when not all tracks in the album are
# supported?
supported_items = list(filter(self.format_supported, task.items))
if len(supported_items) != len(task.items):
self._log.debug("tracks are of unsupported format")
task.album_gain = None
task.track_gains = None
return task
output = self.compute_gain(supported_items, task.target_level, True)
task.album_gain = output[-1]
task.track_gains = output[:-1]
return task
def format_supported(self, item: Item) -> bool:
"""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: Sequence[Item],
target_level: float,
is_album: bool,
) -> List[Gain]:
"""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 not items:
self._log.debug("no supported tracks to analyze")
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: List[Union[bytes, str]] = [self.command, "-o", "-s", "s"]
if self.noclip:
# Adjust to avoid clipping.
cmd = cmd + ["-k"]
else:
# Disable clipping warning.
cmd = cmd + ["-c"]
cmd = cmd + ["-d", str(int(target_level - 89))]
cmd = cmd + [syspath(i.path) for i in items]
self._log.debug("analyzing {0} files", len(items))
self._log.debug("executing {0}", " ".join(map(displayable_path, cmd)))
output = call(cmd, self._log).stdout
self._log.debug("analysis finished")
return self.parse_tool_output(
output, len(items) + (1 if is_album else 0)
)
def parse_tool_output(self, text: bytes, num_lines: int) -> List[Gain]:
"""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(b"\n")[1 : num_lines + 1]:
parts = line.split(b"\t")
if len(parts) != 6 or parts[0] == b"File":
self._log.debug("bad tool output: {0}", text)
raise ReplayGainError("mp3gain failed")
# _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(gain, peak))
return out
# GStreamer-based backend.
class GStreamerBackend(Backend):
NAME = "gstreamer"
def __init__(self, config: ConfigView, log: Logger):
super().__init__(config, log)
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")
if (
self._src is None
or self._decbin is None
or self._conv is None
or self._res is None
or self._rg is None
):
raise FatalGstreamerPluginReplayGainError(
"Failed to load required GStreamer plugins"
)
# 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._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: List[bytes] = []
def _import_gst(self):
"""Import the necessary GObject-related modules and assign `Gst`
and `GObject` fields on this object.
"""
try:
import gi
except ImportError:
raise FatalReplayGainError(
"Failed to load GStreamer: python-gi not found"
)
try:
gi.require_version("Gst", "1.0")
except ValueError as e:
raise FatalReplayGainError(f"Failed to load GStreamer 1.0: {e}")
from gi.repository import GLib, GObject, Gst
# 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]])
self.GObject = GObject
self.GLib = GLib
self.Gst = Gst
def compute(self, items: Sequence[Item], target_level: float, album: bool):
if len(items) == 0:
return
self._error = None
self._files = [i.path for i in items]
# FIXME: Turn this into DefaultDict[bytes, Gain]
self._file_tags: DefaultDict[bytes, Dict[str, float]] = (
collections.defaultdict(dict)
)
self._rg.set_property("reference-level", target_level)
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, task: AnyRgTask) -> AnyRgTask:
"""Computes the track gain for the tracks belonging to `task`, and sets
the `track_gains` attribute on the task. Returns `task`.
"""
self.compute(task.items, task.target_level, False)
if len(self._file_tags) != len(task.items):
raise ReplayGainError("Some tracks did not receive tags")
ret = []
for item in task.items:
ret.append(
Gain(
self._file_tags[item.path]["TRACK_GAIN"],
self._file_tags[item.path]["TRACK_PEAK"],
)
)
task.track_gains = ret
return task
def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
"""Computes the album gain for the album belonging to `task`, and sets
the `album_gain` attribute on the task. Returns `task`.
"""
items = list(task.items)
self.compute(items, task.target_level, True)
if len(self._file_tags) != len(items):
raise ReplayGainError("Some items in album did not receive tags")
# Collect track gains.
track_gains = []
for item in items:
try:
gain = self._file_tags[item.path]["TRACK_GAIN"]
peak = self._file_tags[item.path]["TRACK_PEAK"]
except KeyError:
raise ReplayGainError("results missing for track")
track_gains.append(Gain(gain, peak))
# Get album gain information from the last track.
last_tags = self._file_tags[items[-1].path]
try:
gain = last_tags["ALBUM_GAIN"]
peak = last_tags["ALBUM_PEAK"]
except KeyError:
raise ReplayGainError("results missing for album")
task.album_gain = Gain(gain, peak)
task.track_gains = track_gains
return task
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(
f"Error {err!r} - {debug!r} on file {f!r}"
)
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) -> bool:
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", os.fsdecode(syspath(self._file)))
self._pipe.set_state(self.Gst.State.PLAYING)
return True
def _set_file(self) -> bool:
"""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)
# 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)
# 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", os.fsdecode(syspath(self._file)))
self._decbin.link(self._conv)
self._pipe.set_state(self.Gst.State.READY)
return True
def _set_next_file(self) -> bool:
"""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
class AudioToolsBackend(Backend):
"""ReplayGain backend that uses `Python Audio Tools
`_ and its capabilities to read more
file formats and compute ReplayGain values using it replaygain module.
"""
NAME = "audiotools"
def __init__(self, config: ConfigView, log: Logger):
super().__init__(config, log)
self._import_audiotools()
def _import_audiotools(self):
"""Check whether it's possible to import the necessary modules.
There is no check on the file formats at runtime.
:raises :exc:`ReplayGainError`: if the modules cannot be imported
"""
try:
import audiotools
import audiotools.replaygain
except ImportError:
raise FatalReplayGainError(
"Failed to load audiotools: audiotools not found"
)
self._mod_audiotools = audiotools
self._mod_replaygain = audiotools.replaygain
def open_audio_file(self, item: Item):
"""Open the file to read the PCM stream from the using
``item.path``.
:return: the audiofile instance
:rtype: :class:`audiotools.AudioFile`
:raises :exc:`ReplayGainError`: if the file is not found or the
file format is not supported
"""
try:
audiofile = self._mod_audiotools.open(
os.fsdecode(syspath(item.path))
)
except OSError:
raise ReplayGainError(f"File {item.path} was not found")
except self._mod_audiotools.UnsupportedFile:
raise ReplayGainError(f"Unsupported file type {item.format}")
return audiofile
def init_replaygain(self, audiofile, item: Item):
"""Return an initialized :class:`audiotools.replaygain.ReplayGain`
instance, which requires the sample rate of the song(s) on which
the ReplayGain values will be computed. The item is passed in case
the sample rate is invalid to log the stored item sample rate.
:return: initialized replagain object
:rtype: :class:`audiotools.replaygain.ReplayGain`
:raises: :exc:`ReplayGainError` if the sample rate is invalid
"""
try:
rg = self._mod_replaygain.ReplayGain(audiofile.sample_rate())
except ValueError:
raise ReplayGainError(f"Unsupported sample rate {item.samplerate}")
return
return rg
def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:
"""Computes the track gain for the tracks belonging to `task`, and sets
the `track_gains` attribute on the task. Returns `task`.
"""
gains = [
self._compute_track_gain(i, task.target_level) for i in task.items
]
task.track_gains = gains
return task
def _with_target_level(self, gain: float, target_level: float):
"""Return `gain` relative to `target_level`.
Assumes `gain` is relative to 89 db.
"""
return gain + (target_level - 89)
def _title_gain(self, rg, audiofile, target_level: float):
"""Get the gain result pair from PyAudioTools using the `ReplayGain`
instance `rg` for the given `audiofile`.
Wraps `rg.title_gain(audiofile.to_pcm())` and throws a
`ReplayGainError` when the library fails.
"""
try:
# The method needs an audiotools.PCMReader instance that can
# be obtained from an audiofile instance.
gain, peak = rg.title_gain(audiofile.to_pcm())
except ValueError as exc:
# `audiotools.replaygain` can raise a `ValueError` if the sample
# rate is incorrect.
self._log.debug("error in rg.title_gain() call: {}", exc)
raise ReplayGainError("audiotools audio data error")
return self._with_target_level(gain, target_level), peak
def _compute_track_gain(self, item: Item, target_level: float):
"""Compute ReplayGain value for the requested item.
:rtype: :class:`Gain`
"""
audiofile = self.open_audio_file(item)
rg = self.init_replaygain(audiofile, item)
# Each call to title_gain on a ReplayGain object returns peak and gain
# of the track.
rg_track_gain, rg_track_peak = self._title_gain(
rg, audiofile, target_level
)
self._log.debug(
"ReplayGain for track {0} - {1}: {2:.2f}, {3:.2f}",
item.artist,
item.title,
rg_track_gain,
rg_track_peak,
)
return Gain(gain=rg_track_gain, peak=rg_track_peak)
def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:
"""Computes the album gain for the album belonging to `task`, and sets
the `album_gain` attribute on the task. Returns `task`.
"""
# The first item is taken and opened to get the sample rate to
# initialize the replaygain object. The object is used for all the
# tracks in the album to get the album values.
item = list(task.items)[0]
audiofile = self.open_audio_file(item)
rg = self.init_replaygain(audiofile, item)
track_gains = []
for item in task.items:
audiofile = self.open_audio_file(item)
rg_track_gain, rg_track_peak = self._title_gain(
rg, audiofile, task.target_level
)
track_gains.append(Gain(gain=rg_track_gain, peak=rg_track_peak))
self._log.debug(
"ReplayGain for track {0}: {1:.2f}, {2:.2f}",
item,
rg_track_gain,
rg_track_peak,
)
# After getting the values for all tracks, it's possible to get the
# album values.
rg_album_gain, rg_album_peak = rg.album_gain()
rg_album_gain = self._with_target_level(
rg_album_gain, task.target_level
)
self._log.debug(
"ReplayGain for album {0}: {1:.2f}, {2:.2f}",
task.items[0].album,
rg_album_gain,
rg_album_peak,
)
task.album_gain = Gain(gain=rg_album_gain, peak=rg_album_peak)
task.track_gains = track_gains
return task
class ExceptionWatcher(Thread):
"""Monitors a queue for exceptions asynchronously.
Once an exception occurs, raise it and execute a callback.
"""
def __init__(self, queue: queue.Queue, callback: Callable[[], None]):
self._queue = queue
self._callback = callback
self._stopevent = Event()
Thread.__init__(self)
def run(self):
while not self._stopevent.is_set():
try:
exc = self._queue.get_nowait()
self._callback()
raise exc
except queue.Empty:
# No exceptions yet, loop back to check
# whether `_stopevent` is set
pass
def join(self, timeout: Optional[float] = None):
self._stopevent.set()
Thread.join(self, timeout)
# Main plugin logic.
BACKEND_CLASSES: List[Type[Backend]] = [
CommandBackend,
GStreamerBackend,
AudioToolsBackend,
FfmpegBackend,
]
BACKENDS: Dict[str, Type[Backend]] = {b.NAME: b for b in BACKEND_CLASSES}
class ReplayGainPlugin(BeetsPlugin):
"""Provides ReplayGain analysis."""
def __init__(self):
super().__init__()
# default backend is 'command' for backward-compatibility.
self.config.add(
{
"overwrite": False,
"auto": True,
"backend": "command",
"threads": os.cpu_count(),
"parallel_on_import": False,
"per_disc": False,
"peak": "true",
"targetlevel": 89,
"r128": ["Opus"],
"r128_targetlevel": lufs_to_db(-23),
}
)
# FIXME: Consider renaming the configuration option and deprecating the
# old name 'overwrite'.
self.force_on_import = cast(bool, self.config["overwrite"].get(bool))
# Remember which backend is used for CLI feedback
self.backend_name = self.config["backend"].as_str()
if self.backend_name not in BACKENDS:
raise ui.UserError(
"Selected ReplayGain backend {} is not supported. "
"Please select one of: {}".format(
self.backend_name, ", ".join(BACKENDS.keys())
)
)
# FIXME: Consider renaming the configuration option to 'peak_method'
# and deprecating the old name 'peak'.
peak_method = self.config["peak"].as_str()
if peak_method not in PeakMethod.__members__:
raise ui.UserError(
"Selected ReplayGain peak method {} is not supported. "
"Please select one of: {}".format(
peak_method, ", ".join(PeakMethod.__members__)
)
)
# This only applies to plain old rg tags, r128 doesn't store peak
# values.
self.peak_method = PeakMethod[peak_method]
# On-import analysis.
if self.config["auto"]:
self.register_listener("import_begin", self.import_begin)
self.register_listener("import", self.import_end)
self.import_stages = [self.imported]
# Formats to use R128.
self.r128_whitelist = self.config["r128"].as_str_seq()
try:
self.backend_instance = BACKENDS[self.backend_name](
self.config, self._log
)
except (ReplayGainError, FatalReplayGainError) as e:
raise ui.UserError(f"replaygain initialization failed: {e}")
# Start threadpool lazily.
self.pool = None
def should_use_r128(self, item: Item) -> bool:
"""Checks the plugin setting to decide whether the calculation
should be done using the EBU R128 standard and use R128_ tags instead.
"""
return item.format in self.r128_whitelist
@staticmethod
def has_r128_track_data(item: Item) -> bool:
return item.r128_track_gain is not None
@staticmethod
def has_rg_track_data(item: Item) -> bool:
return item.rg_track_gain is not None and item.rg_track_peak is not None
def track_requires_gain(self, item: Item) -> bool:
if self.should_use_r128(item):
if not self.has_r128_track_data(item):
return True
else:
if not self.has_rg_track_data(item):
return True
return False
@staticmethod
def has_r128_album_data(item: Item) -> bool:
return (
item.r128_track_gain is not None
and item.r128_album_gain is not None
)
@staticmethod
def has_rg_album_data(item: Item) -> bool:
return item.rg_album_gain is not None and item.rg_album_peak is not None
def album_requires_gain(self, album: Album) -> bool:
# 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.
for item in album.items():
if self.should_use_r128(item):
if not self.has_r128_album_data(item):
return True
else:
if not self.has_rg_album_data(item):
return True
return False
def create_task(
self,
items: Sequence[Item],
use_r128: bool,
album: Optional[Album] = None,
) -> RgTask:
if use_r128:
return R128Task(
items,
album,
self.config["r128_targetlevel"].as_number(),
self.backend_instance.NAME,
self._log,
)
else:
return RgTask(
items,
album,
self.config["targetlevel"].as_number(),
self.peak_method,
self.backend_instance.NAME,
self._log,
)
def handle_album(self, album: Album, write: bool, force: bool = False):
"""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 force and not self.album_requires_gain(album):
self._log.info("Skipping album {0}", album)
return
items_iter = iter(album.items())
use_r128 = self.should_use_r128(next(items_iter))
if any(use_r128 != self.should_use_r128(i) for i in items_iter):
self._log.error(
"Cannot calculate gain for album {0} (incompatible formats)",
album,
)
return
self._log.info("analyzing {0}", album)
discs: Dict[int, List[Item]] = {}
if self.config["per_disc"].get(bool):
for item in album.items():
if discs.get(item.disc) is None:
discs[item.disc] = []
discs[item.disc].append(item)
else:
discs[1] = album.items()
def store_cb(task: RgTask):
task.store(write)
for discnumber, items in discs.items():
task = self.create_task(items, use_r128, album=album)
try:
self._apply(
self.backend_instance.compute_album_gain,
args=[task],
kwds={},
callback=store_cb,
)
except ReplayGainError as e:
self._log.info("ReplayGain error: {0}", e)
except FatalReplayGainError as e:
raise ui.UserError(f"Fatal replay gain error: {e}")
def handle_track(self, item: Item, write: bool, force: bool = False):
"""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 force and not self.track_requires_gain(item):
self._log.info("Skipping track {0}", item)
return
use_r128 = self.should_use_r128(item)
def store_cb(task: RgTask):
task.store(write)
task = self.create_task([item], use_r128)
try:
self._apply(
self.backend_instance.compute_track_gain,
args=[task],
kwds={},
callback=store_cb,
)
except ReplayGainError as e:
self._log.info("ReplayGain error: {0}", e)
except FatalReplayGainError as e:
raise ui.UserError(f"Fatal replay gain error: {e}")
def open_pool(self, threads: int):
"""Open a `ThreadPool` instance in `self.pool`"""
if self.pool is None and self.backend_instance.do_parallel:
self.pool = ThreadPool(threads)
self.exc_queue: queue.Queue = queue.Queue()
signal.signal(signal.SIGINT, self._interrupt)
self.exc_watcher = ExceptionWatcher(
self.exc_queue, # threads push exceptions here
self.terminate_pool, # abort once an exception occurs
)
self.exc_watcher.start()
def _apply(
self,
func: Callable[..., AnyRgTask],
args: List[Any],
kwds: Dict[str, Any],
callback: Callable[[AnyRgTask], Any],
):
if self.pool is not None:
def handle_exc(exc):
"""Handle exceptions in the async work."""
if isinstance(exc, ReplayGainError):
self._log.info(exc.args[0]) # Log non-fatal exceptions.
else:
self.exc_queue.put(exc)
self.pool.apply_async(
func, args, kwds, callback, error_callback=handle_exc
)
else:
callback(func(*args, **kwds))
def terminate_pool(self):
"""Forcibly terminate the `ThreadPool` instance in `self.pool`
Sends SIGTERM to all processes.
"""
if self.pool is not None:
self.pool.terminate()
self.pool.join()
# Terminating the processes leaves the ExceptionWatcher's queues
# in an unknown state, so don't wait for it.
# self.exc_watcher.join()
self.pool = None
def _interrupt(self, signal, frame):
try:
self._log.info("interrupted")
self.terminate_pool()
sys.exit(0)
except SystemExit:
# Silence raised SystemExit ~ exit(0)
pass
def close_pool(self):
"""Regularly close the `ThreadPool` instance in `self.pool`."""
if self.pool is not None:
self.pool.close()
self.pool.join()
self.exc_watcher.join()
self.pool = None
def import_begin(self, session: ImportSession):
"""Handle `import_begin` event -> open pool"""
threads = cast(int, self.config["threads"].get(int))
if (
self.config["parallel_on_import"]
and self.config["auto"]
and threads
):
self.open_pool(threads)
def import_end(self, paths):
"""Handle `import` event -> close pool"""
self.close_pool()
def imported(self, session: ImportSession, task: ImportTask):
"""Add replay gain info to items or albums of ``task``."""
if self.config["auto"]:
if task.is_album:
self.handle_album(task.album, False, self.force_on_import)
else:
# Should be a SingletonImportTask
assert hasattr(task, "item")
self.handle_track(task.item, False, self.force_on_import)
def command_func(
self,
lib: Library,
opts: optparse.Values,
args: List[str],
):
try:
write = ui.should_write(opts.write)
force = opts.force
# Bypass self.open_pool() if called with `--threads 0`
if opts.threads != 0:
threads = opts.threads or cast(
int, self.config["threads"].get(int)
)
self.open_pool(threads)
if opts.album:
albums = lib.albums(ui.decargs(args))
self._log.info(
"Analyzing {} albums ~ {} backend...".format(
len(albums), self.backend_name
)
)
for album in albums:
self.handle_album(album, write, force)
else:
items = lib.items(ui.decargs(args))
self._log.info(
"Analyzing {} tracks ~ {} backend...".format(
len(items), self.backend_name
)
)
for item in items:
self.handle_track(item, write, force)
self.close_pool()
except (SystemExit, KeyboardInterrupt):
# Silence interrupt exceptions
pass
def commands(self) -> List[ui.Subcommand]:
"""Return the "replaygain" ui subcommand."""
cmd = ui.Subcommand("replaygain", help="analyze for ReplayGain")
cmd.parser.add_album_option()
cmd.parser.add_option(
"-t",
"--threads",
dest="threads",
type=int,
help="change the number of threads, \
defaults to maximum available processors",
)
cmd.parser.add_option(
"-f",
"--force",
dest="force",
action="store_true",
default=False,
help="analyze all files, including those that "
"already have ReplayGain metadata",
)
cmd.parser.add_option(
"-w",
"--write",
default=None,
action="store_true",
help="write new metadata to files' tags",
)
cmd.parser.add_option(
"-W",
"--nowrite",
dest="write",
action="store_false",
help="don't write metadata (opposite of -w)",
)
cmd.func = self.command_func
return [cmd]
beetbox-beets-01f1faf/beetsplug/rewrite.py 0000664 0000000 0000000 00000005261 14723254774 0021000 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, 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
from collections import defaultdict
from beets import library, ui
from beets.plugins import BeetsPlugin
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().__init__()
self.config.add({})
# Gather all the rewrite rules for each field.
rules = defaultdict(list)
for key, view in self.config.items():
value = view.as_str()
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
)
self._log.debug("adding template field {0}", 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.items():
getter = rewriter(fieldname, fieldrules)
self.template_fields[fieldname] = getter
if fieldname in library.Album._fields:
self.album_template_fields[fieldname] = getter
beetbox-beets-01f1faf/beetsplug/scrub.py 0000664 0000000 0000000 00000012167 14723254774 0020440 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, 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 mediafile
import mutagen
from beets import config, ui, util
from beets.plugins import BeetsPlugin
_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",
}
class ScrubPlugin(BeetsPlugin):
"""Removes extraneous metadata from files' tags."""
def __init__(self):
super().__init__()
self.config.add(
{
"auto": True,
}
)
if self.config["auto"]:
self.register_listener("import_task_files", self.import_task_files)
def commands(self):
def scrub_func(lib, opts, args):
# Walk through matching files and remove tags.
for item in lib.items(ui.decargs(args)):
self._log.info(
"scrubbing: {0}", util.displayable_path(item.path)
)
self._scrub_item(item, opts.write)
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]
@staticmethod
def _mutagen_classes():
"""Get a list of file type classes from the Mutagen module."""
classes = []
for modname, clsname in _MUTAGEN_FORMATS.items():
mod = __import__(f"mutagen.{modname}", fromlist=[clsname])
classes.append(getattr(mod, clsname))
return classes
def _scrub(self, path):
"""Remove all tags from a file."""
for cls in self._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 (OSError, mutagen.MutagenError) as exc:
self._log.error(
"could not scrub {0}: {1}", util.displayable_path(path), exc
)
def _scrub_item(self, item, restore):
"""Remove tags from an Item's associated file and, if `restore`
is enabled, write the database's tags back to the file.
"""
# Get album art if we need to restore it.
if restore:
try:
mf = mediafile.MediaFile(
util.syspath(item.path), config["id3v23"].get(bool)
)
except mediafile.UnreadableFileError as exc:
self._log.error("could not open file to scrub: {0}", exc)
return
images = mf.images
# Remove all tags.
self._scrub(item.path)
# Restore tags, if enabled.
if restore:
self._log.debug("writing new tags after scrub")
item.try_write()
if images:
self._log.debug("restoring art")
try:
mf = mediafile.MediaFile(
util.syspath(item.path), config["id3v23"].get(bool)
)
mf.images = images
mf.save()
except mediafile.UnreadableFileError as exc:
self._log.error("could not write tags: {0}", exc)
def import_task_files(self, session, task):
"""Automatically scrub imported files."""
for item in task.imported_items():
self._log.debug(
"auto-scrubbing {0}", util.displayable_path(item.path)
)
self._scrub_item(item, ui.should_write())
beetbox-beets-01f1faf/beetsplug/smartplaylist.py 0000664 0000000 0000000 00000031772 14723254774 0022235 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, Dang Mai .
#
# 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."""
import json
import os
from urllib.request import pathname2url
from beets import ui
from beets.dbcore import OrQuery
from beets.dbcore.query import MultipleSort, ParsingError
from beets.library import Album, Item, parse_query_string
from beets.plugins import BeetsPlugin
from beets.plugins import send as send_event
from beets.util import (
bytestring_path,
displayable_path,
mkdirall,
normpath,
path_as_posix,
sanitize_path,
syspath,
)
class SmartPlaylistPlugin(BeetsPlugin):
def __init__(self):
super().__init__()
self.config.add(
{
"relative_to": None,
"playlist_dir": ".",
"auto": True,
"playlists": [],
"uri_format": None,
"fields": [],
"forward_slash": False,
"prefix": "",
"urlencode": False,
"pretend_paths": False,
"output": "m3u",
}
)
self.config["prefix"].redact = True # May contain username/password.
self._matched_playlists = None
self._unmatched_playlists = None
if self.config["auto"]:
self.register_listener("database_change", self.db_change)
def commands(self):
spl_update = ui.Subcommand(
"splupdate",
help="update the smart playlists. Playlist names may be "
"passed as arguments.",
)
spl_update.parser.add_option(
"-p",
"--pretend",
action="store_true",
help="display query results but don't write playlist files.",
)
spl_update.parser.add_option(
"--pretend-paths",
action="store_true",
dest="pretend_paths",
help="in pretend mode, log the playlist item URIs/paths.",
)
spl_update.parser.add_option(
"-d",
"--playlist-dir",
dest="playlist_dir",
metavar="PATH",
type="string",
help="directory to write the generated playlist files to.",
)
spl_update.parser.add_option(
"--relative-to",
dest="relative_to",
metavar="PATH",
type="string",
help="generate playlist item paths relative to this path.",
)
spl_update.parser.add_option(
"--prefix",
type="string",
help="prepend string to every path in the playlist file.",
)
spl_update.parser.add_option(
"--forward-slash",
action="store_true",
dest="forward_slash",
help="force forward slash in paths within playlists.",
)
spl_update.parser.add_option(
"--urlencode",
action="store_true",
help="URL-encode all paths.",
)
spl_update.parser.add_option(
"--uri-format",
dest="uri_format",
type="string",
help="playlist item URI template, e.g. http://beets:8337/item/$id/file.",
)
spl_update.parser.add_option(
"--output",
type="string",
help="specify the playlist format: m3u|extm3u.",
)
spl_update.func = self.update_cmd
return [spl_update]
def update_cmd(self, lib, opts, args):
self.build_queries()
if args:
args = set(ui.decargs(args))
for a in list(args):
if not a.endswith(".m3u"):
args.add(f"{a}.m3u")
playlists = {
(name, q, a_q)
for name, q, a_q in self._unmatched_playlists
if name in args
}
if not playlists:
raise ui.UserError(
"No playlist matching any of {} found".format(
[name for name, _, _ in self._unmatched_playlists]
)
)
self._matched_playlists = playlists
self._unmatched_playlists -= playlists
else:
self._matched_playlists = self._unmatched_playlists
self.__apply_opts_to_config(opts)
self.update_playlists(lib, opts.pretend)
def __apply_opts_to_config(self, opts):
for k, v in opts.__dict__.items():
if v is not None and k in self.config:
self.config[k] = v
def build_queries(self):
"""
Instantiate queries for the playlists.
Each playlist has 2 queries: one or items one for albums, each with a
sort. We must also remember its name. _unmatched_playlists is a set of
tuples (name, (q, q_sort), (album_q, album_q_sort)).
sort may be any sort, or NullSort, or None. None and NullSort are
equivalent and both eval to False.
More precisely
- it will be NullSort when a playlist query ('query' or 'album_query')
is a single item or a list with 1 element
- it will be None when there are multiple items i a query
"""
self._unmatched_playlists = set()
self._matched_playlists = set()
for playlist in self.config["playlists"].get(list):
if "name" not in playlist:
self._log.warning("playlist configuration is missing name")
continue
playlist_data = (playlist["name"],)
try:
for key, model_cls in (("query", Item), ("album_query", Album)):
qs = playlist.get(key)
if qs is None:
query_and_sort = None, None
elif isinstance(qs, str):
query_and_sort = parse_query_string(qs, model_cls)
elif len(qs) == 1:
query_and_sort = parse_query_string(qs[0], model_cls)
else:
# multiple queries and sorts
queries, sorts = zip(
*(parse_query_string(q, model_cls) for q in qs)
)
query = OrQuery(queries)
final_sorts = []
for s in sorts:
if s:
if isinstance(s, MultipleSort):
final_sorts += s.sorts
else:
final_sorts.append(s)
if not final_sorts:
sort = None
elif len(final_sorts) == 1:
(sort,) = final_sorts
else:
sort = MultipleSort(final_sorts)
query_and_sort = query, sort
playlist_data += (query_and_sort,)
except ParsingError as exc:
self._log.warning(
"invalid query in playlist {}: {}", playlist["name"], exc
)
continue
self._unmatched_playlists.add(playlist_data)
def matches(self, model, query, album_query):
if album_query and isinstance(model, Album):
return album_query.match(model)
if query and isinstance(model, Item):
return query.match(model)
return False
def db_change(self, lib, model):
if self._unmatched_playlists is None:
self.build_queries()
for playlist in self._unmatched_playlists:
n, (q, _), (a_q, _) = playlist
if self.matches(model, q, a_q):
self._log.debug("{0} will be updated because of {1}", n, model)
self._matched_playlists.add(playlist)
self.register_listener("cli_exit", self.update_playlists)
self._unmatched_playlists -= self._matched_playlists
def update_playlists(self, lib, pretend=False):
if pretend:
self._log.info(
"Showing query results for {0} smart playlists...",
len(self._matched_playlists),
)
else:
self._log.info(
"Updating {0} smart playlists...", len(self._matched_playlists)
)
playlist_dir = self.config["playlist_dir"].as_filename()
playlist_dir = bytestring_path(playlist_dir)
tpl = self.config["uri_format"].get()
prefix = bytestring_path(self.config["prefix"].as_str())
relative_to = self.config["relative_to"].get()
if relative_to:
relative_to = normpath(relative_to)
# Maps playlist filenames to lists of track filenames.
m3us = {}
for playlist in self._matched_playlists:
name, (query, q_sort), (album_query, a_q_sort) = playlist
if pretend:
self._log.info("Results for playlist {}:", name)
else:
self._log.info("Creating playlist {0}", name)
items = []
if query:
items.extend(lib.items(query, q_sort))
if album_query:
for album in lib.albums(album_query, a_q_sort):
items.extend(album.items())
# 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(name, True)
m3u_name = sanitize_path(m3u_name, lib.replacements)
if m3u_name not in m3us:
m3us[m3u_name] = []
item_uri = item.path
if tpl:
item_uri = tpl.replace("$id", str(item.id)).encode("utf-8")
else:
if relative_to:
item_uri = os.path.relpath(item_uri, relative_to)
if self.config["forward_slash"].get():
item_uri = path_as_posix(item_uri)
if self.config["urlencode"]:
item_uri = bytestring_path(pathname2url(item_uri))
item_uri = prefix + item_uri
if item_uri not in m3us[m3u_name]:
m3us[m3u_name].append(PlaylistItem(item, item_uri))
if pretend and self.config["pretend_paths"]:
print(displayable_path(item_uri))
elif pretend:
print(item)
if not pretend:
# Write all of the accumulated track lists to files.
for m3u in m3us:
m3u_path = normpath(
os.path.join(playlist_dir, bytestring_path(m3u))
)
mkdirall(m3u_path)
pl_format = self.config["output"].get()
if pl_format != "m3u" and pl_format != "extm3u":
msg = "Unsupported output format '{}' provided! "
msg += "Supported: m3u, extm3u"
raise Exception(msg.format(pl_format))
extm3u = pl_format == "extm3u"
with open(syspath(m3u_path), "wb") as f:
keys = []
if extm3u:
keys = self.config["fields"].get(list)
f.write(b"#EXTM3U\n")
for entry in m3us[m3u]:
item = entry.item
comment = ""
if extm3u:
attr = [(k, entry.item[k]) for k in keys]
al = [
f" {a[0]}={json.dumps(str(a[1]))}" for a in attr
]
attrs = "".join(al)
comment = "#EXTINF:{}{},{} - {}\n".format(
int(item.length), attrs, item.artist, item.title
)
f.write(comment.encode("utf-8") + entry.uri + b"\n")
# Send an event when playlists were updated.
send_event("smartplaylist_update")
if pretend:
self._log.info(
"Displayed results for {0} playlists",
len(self._matched_playlists),
)
else:
self._log.info(
"{0} playlists updated", len(self._matched_playlists)
)
class PlaylistItem:
def __init__(self, item, uri):
self.item = item
self.uri = uri
beetbox-beets-01f1faf/beetsplug/sonosupdate.py 0000664 0000000 0000000 00000003110 14723254774 0021652 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2018, Tobias Sauerwein.
#
# 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 a Sonos library whenever the beets library is changed.
This is based on the Kodi Update plugin.
"""
import soco
from beets.plugins import BeetsPlugin
class SonosUpdate(BeetsPlugin):
def __init__(self):
super().__init__()
self.register_listener("database_change", self.listen_for_db_change)
def listen_for_db_change(self, lib, model):
"""Listens for beets db change and register the update"""
self.register_listener("cli_exit", self.update)
def update(self, lib):
"""When the client exists try to send refresh request to a Sonos
controller.
"""
self._log.info("Requesting a Sonos library update...")
device = soco.discovery.any_soco()
if device:
device.music_library.start_library_update()
else:
self._log.warning("Could not find a Sonos device.")
return
self._log.info("Sonos update triggered")
beetbox-beets-01f1faf/beetsplug/spotify.py 0000664 0000000 0000000 00000063703 14723254774 0021021 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2019, Rahul Ahuja.
# Copyright 2022, Alok Saboo.
#
# 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 Spotify release and track search support to the autotagger, along with
Spotify playlist construction.
"""
import base64
import collections
import json
import re
import time
import webbrowser
import confuse
import requests
import unidecode
from beets import ui
from beets.autotag.hooks import AlbumInfo, TrackInfo
from beets.dbcore import types
from beets.library import DateType
from beets.plugins import BeetsPlugin, MetadataSourcePlugin
from beets.util.id_extractors import spotify_id_regex
DEFAULT_WAITING_TIME = 5
class SpotifyAPIError(Exception):
pass
class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin):
data_source = "Spotify"
item_types = {
"spotify_track_popularity": types.INTEGER,
"spotify_acousticness": types.FLOAT,
"spotify_danceability": types.FLOAT,
"spotify_energy": types.FLOAT,
"spotify_instrumentalness": types.FLOAT,
"spotify_key": types.FLOAT,
"spotify_liveness": types.FLOAT,
"spotify_loudness": types.FLOAT,
"spotify_mode": types.INTEGER,
"spotify_speechiness": types.FLOAT,
"spotify_tempo": types.FLOAT,
"spotify_time_signature": types.INTEGER,
"spotify_valence": types.FLOAT,
"spotify_updated": DateType(),
}
# Base URLs for the Spotify API
# Documentation: https://developer.spotify.com/web-api
oauth_token_url = "https://accounts.spotify.com/api/token"
open_track_url = "https://open.spotify.com/track/"
search_url = "https://api.spotify.com/v1/search"
album_url = "https://api.spotify.com/v1/albums/"
track_url = "https://api.spotify.com/v1/tracks/"
audio_features_url = "https://api.spotify.com/v1/audio-features/"
id_regex = spotify_id_regex
spotify_audio_features = {
"acousticness": "spotify_acousticness",
"danceability": "spotify_danceability",
"energy": "spotify_energy",
"instrumentalness": "spotify_instrumentalness",
"key": "spotify_key",
"liveness": "spotify_liveness",
"loudness": "spotify_loudness",
"mode": "spotify_mode",
"speechiness": "spotify_speechiness",
"tempo": "spotify_tempo",
"time_signature": "spotify_time_signature",
"valence": "spotify_valence",
}
def __init__(self):
super().__init__()
self.config.add(
{
"mode": "list",
"tiebreak": "popularity",
"show_failures": False,
"artist_field": "albumartist",
"album_field": "album",
"track_field": "title",
"region_filter": None,
"regex": [],
"client_id": "4e414367a1d14c75a5c5129a627fcab8",
"client_secret": "f82bdc09b2254f1a8286815d02fd46dc",
"tokenfile": "spotify_token.json",
}
)
self.config["client_secret"].redact = True
self.tokenfile = self.config["tokenfile"].get(
confuse.Filename(in_app_dir=True)
) # Path to the JSON file for storing the OAuth access token.
self.setup()
def setup(self):
"""Retrieve previously saved OAuth token or generate a new one."""
try:
with open(self.tokenfile) as f:
token_data = json.load(f)
except OSError:
self._authenticate()
else:
self.access_token = token_data["access_token"]
def _authenticate(self):
"""Request an access token via the Client Credentials Flow:
https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow
"""
headers = {
"Authorization": "Basic {}".format(
base64.b64encode(
":".join(
self.config[k].as_str()
for k in ("client_id", "client_secret")
).encode()
).decode()
)
}
response = requests.post(
self.oauth_token_url,
data={"grant_type": "client_credentials"},
headers=headers,
timeout=10,
)
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
raise ui.UserError(
"Spotify authorization failed: {}\n{}".format(e, response.text)
)
self.access_token = response.json()["access_token"]
# Save the token for later use.
self._log.debug(
"{} access token: {}", self.data_source, self.access_token
)
with open(self.tokenfile, "w") as f:
json.dump({"access_token": self.access_token}, f)
def _handle_response(
self, request_type, url, params=None, retry_count=0, max_retries=3
):
"""Send a request, reauthenticating if necessary.
:param request_type: Type of :class:`Request` constructor,
e.g. ``requests.get``, ``requests.post``, etc.
:type request_type: function
:param url: URL for the new :class:`Request` object.
:type url: str
:param params: (optional) list of tuples or bytes to send
in the query string for the :class:`Request`.
:type params: dict
:return: JSON data for the class:`Response ` object.
:rtype: dict
"""
try:
response = request_type(
url,
headers={"Authorization": f"Bearer {self.access_token}"},
params=params,
timeout=10,
)
response.raise_for_status()
return response.json()
except requests.exceptions.ReadTimeout:
self._log.error("ReadTimeout.")
raise SpotifyAPIError("Request timed out.")
except requests.exceptions.ConnectionError as e:
self._log.error(f"Network error: {e}")
raise SpotifyAPIError("Network error.")
except requests.exceptions.RequestException as e:
if e.response.status_code == 401:
self._log.debug(
f"{self.data_source} access token has expired. "
f"Reauthenticating."
)
self._authenticate()
return self._handle_response(request_type, url, params=params)
elif e.response.status_code == 404:
raise SpotifyAPIError(
f"API Error: {e.response.status_code}\n"
f"URL: {url}\nparams: {params}"
)
elif e.response.status_code == 429:
if retry_count >= max_retries:
raise SpotifyAPIError("Maximum retries reached.")
seconds = response.headers.get(
"Retry-After", DEFAULT_WAITING_TIME
)
self._log.debug(
f"Too many API requests. Retrying after "
f"{seconds} seconds."
)
time.sleep(int(seconds) + 1)
return self._handle_response(
request_type,
url,
params=params,
retry_count=retry_count + 1,
)
elif e.response.status_code == 503:
self._log.error("Service Unavailable.")
raise SpotifyAPIError("Service Unavailable.")
elif e.response.status_code == 502:
self._log.error("Bad Gateway.")
raise SpotifyAPIError("Bad Gateway.")
elif e.response is not None:
raise SpotifyAPIError(
f"{self.data_source} API error:\n{e.response.text}\n"
f"URL:\n{url}\nparams:\n{params}"
)
else:
self._log.error(f"Request failed. Error: {e}")
raise SpotifyAPIError("Request failed.")
def album_for_id(self, album_id):
"""Fetch an album by its Spotify ID or URL and return an
AlbumInfo object or None if the album is not found.
:param album_id: Spotify ID or URL for the album
:type album_id: str
:return: AlbumInfo object for album
:rtype: beets.autotag.hooks.AlbumInfo or None
"""
spotify_id = self._get_id("album", album_id, self.id_regex)
if spotify_id is None:
return None
album_data = self._handle_response(
requests.get, self.album_url + spotify_id
)
if album_data["name"] == "":
self._log.debug("Album removed from Spotify: {}", album_id)
return None
artist, artist_id = self.get_artist(album_data["artists"])
date_parts = [
int(part) for part in album_data["release_date"].split("-")
]
release_date_precision = album_data["release_date_precision"]
if release_date_precision == "day":
year, month, day = date_parts
elif release_date_precision == "month":
year, month = date_parts
day = None
elif release_date_precision == "year":
year = date_parts[0]
month = None
day = None
else:
raise ui.UserError(
"Invalid `release_date_precision` returned "
"by {} API: '{}'".format(
self.data_source, release_date_precision
)
)
tracks_data = album_data["tracks"]
tracks_items = tracks_data["items"]
while tracks_data["next"]:
tracks_data = self._handle_response(
requests.get, tracks_data["next"]
)
tracks_items.extend(tracks_data["items"])
tracks = []
medium_totals = collections.defaultdict(int)
for i, track_data in enumerate(tracks_items, start=1):
track = self._get_track(track_data)
track.index = i
medium_totals[track.medium] += 1
tracks.append(track)
for track in tracks:
track.medium_total = medium_totals[track.medium]
return AlbumInfo(
album=album_data["name"],
album_id=spotify_id,
spotify_album_id=spotify_id,
artist=artist,
artist_id=artist_id,
spotify_artist_id=artist_id,
tracks=tracks,
albumtype=album_data["album_type"],
va=len(album_data["artists"]) == 1
and artist.lower() == "various artists",
year=year,
month=month,
day=day,
label=album_data["label"],
mediums=max(medium_totals.keys()),
data_source=self.data_source,
data_url=album_data["external_urls"]["spotify"],
)
def _get_track(self, track_data):
"""Convert a Spotify track object dict to a TrackInfo object.
:param track_data: Simplified track object
(https://developer.spotify.com/documentation/web-api/reference/object-model/#track-object-simplified)
:type track_data: dict
:return: TrackInfo object for track
:rtype: beets.autotag.hooks.TrackInfo
"""
artist, artist_id = self.get_artist(track_data["artists"])
# Get album information for spotify tracks
try:
album = track_data["album"]["name"]
except (KeyError, TypeError):
album = None
return TrackInfo(
title=track_data["name"],
track_id=track_data["id"],
spotify_track_id=track_data["id"],
artist=artist,
album=album,
artist_id=artist_id,
spotify_artist_id=artist_id,
length=track_data["duration_ms"] / 1000,
index=track_data["track_number"],
medium=track_data["disc_number"],
medium_index=track_data["track_number"],
data_source=self.data_source,
data_url=track_data["external_urls"]["spotify"],
)
def track_for_id(self, track_id=None, track_data=None):
"""Fetch a track by its Spotify ID or URL and return a
TrackInfo object or None if the track is not found.
:param track_id: (Optional) Spotify ID or URL for the track. Either
``track_id`` or ``track_data`` must be provided.
:type track_id: str
:param track_data: (Optional) Simplified track object dict. May be
provided instead of ``track_id`` to avoid unnecessary API calls.
:type track_data: dict
:return: TrackInfo object for track
:rtype: beets.autotag.hooks.TrackInfo or None
"""
if track_data is None:
spotify_id = self._get_id("track", track_id, self.id_regex)
if spotify_id is None:
return None
track_data = self._handle_response(
requests.get, self.track_url + spotify_id
)
track = self._get_track(track_data)
# Get album's tracks to set `track.index` (position on the entire
# release) and `track.medium_total` (total number of tracks on
# the track's disc).
album_data = self._handle_response(
requests.get, self.album_url + track_data["album"]["id"]
)
medium_total = 0
for i, track_data in enumerate(album_data["tracks"]["items"], start=1):
if track_data["disc_number"] == track.medium:
medium_total += 1
if track_data["id"] == track.track_id:
track.index = i
track.medium_total = medium_total
return track
@staticmethod
def _construct_search_query(filters=None, keywords=""):
"""Construct a query string with the specified filters and keywords to
be provided to the Spotify Search API
(https://developer.spotify.com/documentation/web-api/reference/search/search/#writing-a-query---guidelines).
:param filters: (Optional) Field filters to apply.
:type filters: dict
:param keywords: (Optional) Query keywords to use.
:type keywords: str
:return: Query string to be provided to the Search API.
:rtype: str
"""
query_components = [
keywords,
" ".join(":".join((k, v)) for k, v in filters.items()),
]
query = " ".join([q for q in query_components if q])
if not isinstance(query, str):
query = query.decode("utf8")
return unidecode.unidecode(query)
def _search_api(self, query_type, filters=None, keywords=""):
"""Query the Spotify Search API for the specified ``keywords``,
applying the provided ``filters``.
:param query_type: Item type to search across. Valid types are:
'album', 'artist', 'playlist', and 'track'.
:type query_type: str
:param filters: (Optional) Field filters to apply.
:type filters: dict
:param keywords: (Optional) Query keywords to use.
:type keywords: str
:return: JSON data for the class:`Response ` object or None
if no search results are returned.
:rtype: dict or None
"""
query = self._construct_search_query(keywords=keywords, filters=filters)
if not query:
return None
self._log.debug(f"Searching {self.data_source} for '{query}'")
try:
response = self._handle_response(
requests.get,
self.search_url,
params={"q": query, "type": query_type},
)
except SpotifyAPIError as e:
self._log.debug("Spotify API error: {}", e)
return []
response_data = response.get(query_type + "s", {}).get("items", [])
self._log.debug(
"Found {} result(s) from {} for '{}'",
len(response_data),
self.data_source,
query,
)
return response_data
def commands(self):
# autotagger import command
def queries(lib, opts, args):
success = self._parse_opts(opts)
if success:
results = self._match_library_tracks(lib, ui.decargs(args))
self._output_match_results(results)
spotify_cmd = ui.Subcommand(
"spotify", help=f"build a {self.data_source} playlist"
)
spotify_cmd.parser.add_option(
"-m",
"--mode",
action="store",
help='"open" to open {} with playlist, '
'"list" to print (default)'.format(self.data_source),
)
spotify_cmd.parser.add_option(
"-f",
"--show-failures",
action="store_true",
dest="show_failures",
help="list tracks that did not match a {} ID".format(
self.data_source
),
)
spotify_cmd.func = queries
# spotifysync command
sync_cmd = ui.Subcommand(
"spotifysync", help="fetch track attributes from Spotify"
)
sync_cmd.parser.add_option(
"-f",
"--force",
dest="force_refetch",
action="store_true",
default=False,
help="re-download data when already present",
)
def func(lib, opts, args):
items = lib.items(ui.decargs(args))
self._fetch_info(items, ui.should_write(), opts.force_refetch)
sync_cmd.func = func
return [spotify_cmd, sync_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"]:
self._log.warning(
"{0} is not a valid mode", self.config["mode"].get()
)
return False
self.opts = opts
return True
def _match_library_tracks(self, library, keywords):
"""Get a list of simplified track object dicts for library tracks
matching the specified ``keywords``.
:param library: beets library object to query.
:type library: beets.library.Library
:param keywords: Query to match library items against.
:type keywords: str
:return: List of simplified track object dicts for library items
matching the specified query.
:rtype: list[dict]
"""
results = []
failures = []
items = library.items(keywords)
if not items:
self._log.debug(
"Your beets query returned no items, skipping {}.",
self.data_source,
)
return
self._log.info("Processing {} tracks...", 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()]
keywords = item[self.config["track_field"].get()]
# Query the Web API for each track, look for the items' JSON data
query_filters = {"artist": artist, "album": album}
response_data_tracks = self._search_api(
query_type="track", keywords=keywords, filters=query_filters
)
if not response_data_tracks:
query = self._construct_search_query(
keywords=keywords, filters=query_filters
)
failures.append(query)
continue
# Apply market filter if requested
region_filter = self.config["region_filter"].get()
if region_filter:
response_data_tracks = [
track_data
for track_data in response_data_tracks
if region_filter in track_data["available_markets"]
]
if (
len(response_data_tracks) == 1
or self.config["tiebreak"].get() == "first"
):
self._log.debug(
"{} track(s) found, count: {}",
self.data_source,
len(response_data_tracks),
)
chosen_result = response_data_tracks[0]
else:
# Use the popularity filter
self._log.debug(
"Most popular track chosen, count: {}",
len(response_data_tracks),
)
chosen_result = max(
response_data_tracks, key=lambda x: x["popularity"]
)
results.append(chosen_result)
failure_count = len(failures)
if failure_count > 0:
if self.config["show_failures"].get():
self._log.info(
"{} track(s) did not match a {} ID:",
failure_count,
self.data_source,
)
for track in failures:
self._log.info("track: {}", track)
self._log.info("")
else:
self._log.warning(
"{} track(s) did not match a {} ID:\n"
"use --show-failures to display",
failure_count,
self.data_source,
)
return results
def _output_match_results(self, results):
"""Open a playlist or print Spotify URLs for the provided track
object dicts.
:param results: List of simplified track object dicts
(https://developer.spotify.com/documentation/web-api/reference/object-model/#track-object-simplified)
:type results: list[dict]
"""
if results:
spotify_ids = [track_data["id"] for track_data in results]
if self.config["mode"].get() == "open":
self._log.info(
"Attempting to open {} with playlist".format(
self.data_source
)
)
spotify_url = "spotify:trackset:Playlist:" + ",".join(
spotify_ids
)
webbrowser.open(spotify_url)
else:
for spotify_id in spotify_ids:
print(self.open_track_url + spotify_id)
else:
self._log.warning(
f"No {self.data_source} tracks found from beets query"
)
def _fetch_info(self, items, write, force):
"""Obtain track information from Spotify."""
self._log.debug("Total {} tracks", len(items))
for index, item in enumerate(items, start=1):
self._log.info(
"Processing {}/{} tracks - {} ", index, len(items), item
)
# If we're not forcing re-downloading for all tracks, check
# whether the popularity data is already present
if not force:
if "spotify_track_popularity" in item:
self._log.debug("Popularity already present for: {}", item)
continue
try:
spotify_track_id = item.spotify_track_id
except AttributeError:
self._log.debug("No track_id present for: {}", item)
continue
popularity, isrc, ean, upc = self.track_info(spotify_track_id)
item["spotify_track_popularity"] = popularity
item["isrc"] = isrc
item["ean"] = ean
item["upc"] = upc
audio_features = self.track_audio_features(spotify_track_id)
if audio_features is None:
self._log.info("No audio features found for: {}", item)
continue
for feature in audio_features.keys():
if feature in self.spotify_audio_features.keys():
item[self.spotify_audio_features[feature]] = audio_features[
feature
]
item["spotify_updated"] = time.time()
item.store()
if write:
item.try_write()
def track_info(self, track_id=None):
"""Fetch a track's popularity and external IDs using its Spotify ID."""
track_data = self._handle_response(
requests.get, self.track_url + track_id
)
self._log.debug(
"track_popularity: {} and track_isrc: {}",
track_data.get("popularity"),
track_data.get("external_ids").get("isrc"),
)
return (
track_data.get("popularity"),
track_data.get("external_ids").get("isrc"),
track_data.get("external_ids").get("ean"),
track_data.get("external_ids").get("upc"),
)
def track_audio_features(self, track_id=None):
"""Fetch track audio features by its Spotify ID."""
try:
return self._handle_response(
requests.get, self.audio_features_url + track_id
)
except SpotifyAPIError as e:
self._log.debug("Spotify API error: {}", e)
return None
beetbox-beets-01f1faf/beetsplug/subsonicplaylist.py 0000664 0000000 0000000 00000014562 14723254774 0022732 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2019, Joris Jensen
#
# 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 random
import string
from hashlib import md5
from urllib.parse import urlencode
from xml.etree import ElementTree
import requests
from beets.dbcore import AndQuery
from beets.dbcore.query import MatchQuery
from beets.plugins import BeetsPlugin
from beets.ui import Subcommand
__author__ = "https://github.com/MrNuggelz"
def filter_to_be_removed(items, keys):
if len(items) > len(keys):
dont_remove = []
for artist, album, title in keys:
for item in items:
if (
artist == item["artist"]
and album == item["album"]
and title == item["title"]
):
dont_remove.append(item)
return [item for item in items if item not in dont_remove]
else:
def to_be_removed(item):
for artist, album, title in keys:
if (
artist == item["artist"]
and album == item["album"]
and title == item["title"]
):
return False
return True
return [item for item in items if to_be_removed(item)]
class SubsonicPlaylistPlugin(BeetsPlugin):
def __init__(self):
super().__init__()
self.config.add(
{
"delete": False,
"playlist_ids": [],
"playlist_names": [],
"username": "",
"password": "",
}
)
self.config["password"].redact = True
def update_tags(self, playlist_dict, lib):
with lib.transaction():
for query, playlist_tag in playlist_dict.items():
query = AndQuery(
[
MatchQuery("artist", query[0]),
MatchQuery("album", query[1]),
MatchQuery("title", query[2]),
]
)
items = lib.items(query)
if not items:
self._log.warn(
"{} | track not found ({})", playlist_tag, query
)
continue
for item in items:
item.subsonic_playlist = playlist_tag
item.try_sync(write=True, move=False)
def get_playlist(self, playlist_id):
xml = self.send("getPlaylist", {"id": playlist_id}).text
playlist = ElementTree.fromstring(xml)[0]
if playlist.attrib.get("code", "200") != "200":
alt_error = "error getting playlist, but no error message found"
self._log.warn(playlist.attrib.get("message", alt_error))
return
name = playlist.attrib.get("name", "undefined")
tracks = [
(t.attrib["artist"], t.attrib["album"], t.attrib["title"])
for t in playlist
]
return name, tracks
def commands(self):
def build_playlist(lib, opts, args):
self.config.set_args(opts)
ids = self.config["playlist_ids"].as_str_seq()
if self.config["playlist_names"].as_str_seq():
playlists = ElementTree.fromstring(
self.send("getPlaylists").text
)[0]
if playlists.attrib.get("code", "200") != "200":
alt_error = (
"error getting playlists," " but no error message found"
)
self._log.warn(playlists.attrib.get("message", alt_error))
return
for name in self.config["playlist_names"].as_str_seq():
for playlist in playlists:
if name == playlist.attrib["name"]:
ids.append(playlist.attrib["id"])
playlist_dict = self.get_playlists(ids)
# delete old tags
if self.config["delete"]:
existing = list(lib.items('subsonic_playlist:";"'))
to_be_removed = filter_to_be_removed(
existing, playlist_dict.keys()
)
for item in to_be_removed:
item["subsonic_playlist"] = ""
with lib.transaction():
item.try_sync(write=True, move=False)
self.update_tags(playlist_dict, lib)
subsonicplaylist_cmds = Subcommand(
"subsonicplaylist", help="import a subsonic playlist"
)
subsonicplaylist_cmds.parser.add_option(
"-d",
"--delete",
action="store_true",
help="delete tag from items not in any playlist anymore",
)
subsonicplaylist_cmds.func = build_playlist
return [subsonicplaylist_cmds]
def generate_token(self):
salt = "".join(random.choices(string.ascii_lowercase + string.digits))
return (
md5((self.config["password"].get() + salt).encode()).hexdigest(),
salt,
)
def send(self, endpoint, params=None):
if params is None:
params = {}
a, b = self.generate_token()
params["u"] = self.config["username"]
params["t"] = a
params["s"] = b
params["v"] = "1.12.0"
params["c"] = "beets"
resp = requests.get(
"{}/rest/{}?{}".format(
self.config["base_url"].get(), endpoint, urlencode(params)
),
timeout=10,
)
return resp
def get_playlists(self, ids):
output = {}
for playlist_id in ids:
name, tracks = self.get_playlist(playlist_id)
for track in tracks:
if track not in output:
output[track] = ";"
output[track] += name + ";"
return output
beetbox-beets-01f1faf/beetsplug/subsonicupdate.py 0000664 0000000 0000000 00000012373 14723254774 0022351 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, 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 Subsonic library on Beets import
Your Beets configuration file should contain
a "subsonic" section like the following:
subsonic:
url: https://mydomain.com:443/subsonic
user: username
pass: password
auth: token
For older Subsonic versions, token authentication
is not supported, use password instead:
subsonic:
url: https://mydomain.com:443/subsonic
user: username
pass: password
auth: pass
"""
import hashlib
import random
import string
from binascii import hexlify
import requests
from beets import config
from beets.plugins import BeetsPlugin
__author__ = "https://github.com/maffo999"
class SubsonicUpdate(BeetsPlugin):
def __init__(self):
super().__init__()
# Set default configuration values
config["subsonic"].add(
{
"user": "admin",
"pass": "admin",
"url": "http://localhost:4040",
"auth": "token",
}
)
config["subsonic"]["pass"].redact = True
self.register_listener("database_change", self.db_change)
self.register_listener("smartplaylist_update", self.spl_update)
def db_change(self, lib, model):
self.register_listener("cli_exit", self.start_scan)
def spl_update(self):
self.register_listener("cli_exit", self.start_scan)
@staticmethod
def __create_token():
"""Create salt and token from given password.
:return: The generated salt and hashed token
"""
password = config["subsonic"]["pass"].as_str()
# Pick the random sequence and salt the password
r = string.ascii_letters + string.digits
salt = "".join([random.choice(r) for _ in range(6)])
salted_password = password + salt
token = hashlib.md5(salted_password.encode("utf-8")).hexdigest()
# Put together the payload of the request to the server and the URL
return salt, token
@staticmethod
def __format_url(endpoint):
"""Get the Subsonic URL to trigger the given endpoint.
Uses either the url config option or the deprecated host, port,
and context_path config options together.
:return: Endpoint for updating Subsonic
"""
url = config["subsonic"]["url"].as_str()
if url and url.endswith("/"):
url = url[:-1]
# @deprecated("Use url config option instead")
if not url:
host = config["subsonic"]["host"].as_str()
port = config["subsonic"]["port"].get(int)
context_path = config["subsonic"]["contextpath"].as_str()
if context_path == "/":
context_path = ""
url = f"http://{host}:{port}{context_path}"
return url + f"/rest/{endpoint}"
def start_scan(self):
user = config["subsonic"]["user"].as_str()
auth = config["subsonic"]["auth"].as_str()
url = self.__format_url("startScan")
self._log.debug("URL is {0}", url)
self._log.debug("auth type is {0}", config["subsonic"]["auth"])
if auth == "token":
salt, token = self.__create_token()
payload = {
"u": user,
"t": token,
"s": salt,
"v": "1.13.0", # Subsonic 5.3 and newer
"c": "beets",
"f": "json",
}
elif auth == "password":
password = config["subsonic"]["pass"].as_str()
encpass = hexlify(password.encode()).decode()
payload = {
"u": user,
"p": f"enc:{encpass}",
"v": "1.12.0",
"c": "beets",
"f": "json",
}
else:
return
try:
response = requests.get(
url,
params=payload,
timeout=10,
)
json = response.json()
if (
response.status_code == 200
and json["subsonic-response"]["status"] == "ok"
):
count = json["subsonic-response"]["scanStatus"]["count"]
self._log.info(f"Updating Subsonic; scanning {count} tracks")
elif (
response.status_code == 200
and json["subsonic-response"]["status"] == "failed"
):
error_message = json["subsonic-response"]["error"]["message"]
self._log.error(f"Error: {error_message}")
else:
self._log.error("Error: {0}", json)
except Exception as error:
self._log.error(f"Error: {error}")
beetbox-beets-01f1faf/beetsplug/substitute.py 0000664 0000000 0000000 00000003355 14723254774 0021534 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2023, Daniele Ferone.
#
# 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 substitute plugin module.
Uses user-specified substitution rules to canonicalize names for path formats.
"""
import re
from beets.plugins import BeetsPlugin
class Substitute(BeetsPlugin):
"""The substitute plugin class.
Create a template field function that substitute the given field with the
given substitution rules. ``rules`` must be a list of (pattern,
replacement) pairs.
"""
def tmpl_substitute(self, text):
"""Do the actual replacing."""
if text:
for pattern, replacement in self.substitute_rules:
text = pattern.sub(replacement, text)
return text
else:
return ""
def __init__(self):
"""Initialize the substitute plugin.
Get the configuration, register template function and create list of
substitute rules.
"""
super().__init__()
self.template_funcs["substitute"] = self.tmpl_substitute
self.substitute_rules = [
(re.compile(key, flags=re.IGNORECASE), value)
for key, value in self.config.flatten().items()
]
beetbox-beets-01f1faf/beetsplug/the.py 0000664 0000000 0000000 00000006267 14723254774 0020106 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, 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.
"""Moves patterns in path formats (suitable for moving articles)."""
import re
from typing import List
from beets.plugins import BeetsPlugin
__author__ = "baobab@heresiarch.info"
__version__ = "1.1"
PATTERN_THE = "^the\\s"
PATTERN_A = "^[a][n]?\\s"
FORMAT = "{0}, {1}"
class ThePlugin(BeetsPlugin):
patterns: List[str] = []
def __init__(self):
super().__init__()
self.template_funcs["the"] = self.the_template_func
self.config.add(
{
"the": True,
"a": True,
"format": "{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("invalid pattern: {0}", p)
else:
if not (p.startswith("^") or p.endswith("$")):
self._log.warning(
'warning: "{0}" will not ' "match string start/end",
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.warning("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"].as_str()
return fmt.format(r, t.strip()).strip()
else:
return ""
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:
self._log.debug('"{0}" -> "{1}"', text, r)
break
return r
else:
return ""
beetbox-beets-01f1faf/beetsplug/thumbnails.py 0000664 0000000 0000000 00000023164 14723254774 0021467 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, Bruno Cauet
#
# 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.
"""Create freedesktop.org-compliant thumbnails for album folders
This plugin is POSIX-only.
Spec: standards.freedesktop.org/thumbnail-spec/latest/index.html
"""
import ctypes
import ctypes.util
import os
import shutil
from hashlib import md5
from pathlib import PurePosixPath
from xdg import BaseDirectory
from beets import util
from beets.plugins import BeetsPlugin
from beets.ui import Subcommand, decargs
from beets.util import bytestring_path, displayable_path, syspath
from beets.util.artresizer import ArtResizer
BASE_DIR = os.path.join(BaseDirectory.xdg_cache_home, "thumbnails")
NORMAL_DIR = bytestring_path(os.path.join(BASE_DIR, "normal"))
LARGE_DIR = bytestring_path(os.path.join(BASE_DIR, "large"))
class ThumbnailsPlugin(BeetsPlugin):
def __init__(self):
super().__init__()
self.config.add(
{
"auto": True,
"force": False,
"dolphin": False,
}
)
if self.config["auto"] and self._check_local_ok():
self.register_listener("art_set", self.process_album)
def commands(self):
thumbnails_command = Subcommand(
"thumbnails", help="Create album thumbnails"
)
thumbnails_command.parser.add_option(
"-f",
"--force",
dest="force",
action="store_true",
default=False,
help="force regeneration of thumbnails deemed fine (existing & "
"recent enough)",
)
thumbnails_command.parser.add_option(
"--dolphin",
dest="dolphin",
action="store_true",
default=False,
help="create Dolphin-compatible thumbnail information (for KDE)",
)
thumbnails_command.func = self.process_query
return [thumbnails_command]
def process_query(self, lib, opts, args):
self.config.set_args(opts)
if self._check_local_ok():
for album in lib.albums(decargs(args)):
self.process_album(album)
def _check_local_ok(self):
"""Check that everything is ready:
- local capability to resize images
- thumbnail dirs exist (create them if needed)
- detect whether we'll use PIL or IM
- detect whether we'll use GIO or Python to get URIs
"""
if not ArtResizer.shared.local:
self._log.warning(
"No local image resizing capabilities, "
"cannot generate thumbnails"
)
return False
for dir in (NORMAL_DIR, LARGE_DIR):
if not os.path.exists(syspath(dir)):
os.makedirs(syspath(dir))
if not ArtResizer.shared.can_write_metadata:
raise RuntimeError(
f"Thumbnails: ArtResizer backend {ArtResizer.shared.method}"
f" unexpectedly cannot write image metadata."
)
self._log.debug(f"using {ArtResizer.shared.method} to write metadata")
uri_getter = GioURI()
if not uri_getter.available:
uri_getter = PathlibURI()
self._log.debug("using {0.name} to compute URIs", uri_getter)
self.get_uri = uri_getter.uri
return True
def process_album(self, album):
"""Produce thumbnails for the album folder."""
self._log.debug("generating thumbnail for {0}", album)
if not album.artpath:
self._log.info("album {0} has no art", album)
return
if self.config["dolphin"]:
self.make_dolphin_cover_thumbnail(album)
size = ArtResizer.shared.get_size(album.artpath)
if not size:
self._log.warning(
"problem getting the picture size for {0}", album.artpath
)
return
wrote = True
if max(size) >= 256:
wrote &= self.make_cover_thumbnail(album, 256, LARGE_DIR)
wrote &= self.make_cover_thumbnail(album, 128, NORMAL_DIR)
if wrote:
self._log.info("wrote thumbnail for {0}", album)
else:
self._log.info("nothing to do for {0}", album)
def make_cover_thumbnail(self, album, size, target_dir):
"""Make a thumbnail of given size for `album` and put it in
`target_dir`.
"""
target = os.path.join(target_dir, self.thumbnail_file_name(album.path))
if (
os.path.exists(syspath(target))
and os.stat(syspath(target)).st_mtime
> os.stat(syspath(album.artpath)).st_mtime
):
if self.config["force"]:
self._log.debug(
"found a suitable {1}x{1} thumbnail for {0}, "
"forcing regeneration",
album,
size,
)
else:
self._log.debug(
"{1}x{1} thumbnail for {0} exists and is " "recent enough",
album,
size,
)
return False
resized = ArtResizer.shared.resize(size, album.artpath, target)
self.add_tags(album, resized)
shutil.move(syspath(resized), syspath(target))
return True
def thumbnail_file_name(self, path):
"""Compute the thumbnail file name
See https://standards.freedesktop.org/thumbnail-spec/latest/x227.html
"""
uri = self.get_uri(path)
hash = md5(uri.encode("utf-8")).hexdigest()
return bytestring_path(f"{hash}.png")
def add_tags(self, album, image_path):
"""Write required metadata to the thumbnail
See https://standards.freedesktop.org/thumbnail-spec/latest/x142.html
"""
mtime = os.stat(syspath(album.artpath)).st_mtime
metadata = {
"Thumb::URI": self.get_uri(album.artpath),
"Thumb::MTime": str(mtime),
}
try:
ArtResizer.shared.write_metadata(image_path, metadata)
except Exception:
self._log.exception(
"could not write metadata to {0}", displayable_path(image_path)
)
def make_dolphin_cover_thumbnail(self, album):
outfilename = os.path.join(album.path, b".directory")
if os.path.exists(syspath(outfilename)):
return
artfile = os.path.split(album.artpath)[1]
with open(syspath(outfilename), "w") as f:
f.write("[Desktop Entry]\n")
f.write("Icon=./{}".format(artfile.decode("utf-8")))
f.close()
self._log.debug("Wrote file {0}", displayable_path(outfilename))
class URIGetter:
available = False
name = "Abstract base"
def uri(self, path):
raise NotImplementedError()
class PathlibURI(URIGetter):
available = True
name = "Python Pathlib"
def uri(self, path):
return PurePosixPath(os.fsdecode(path)).as_uri()
def copy_c_string(c_string):
"""Copy a `ctypes.POINTER(ctypes.c_char)` value into a new Python
string and return it. The old memory is then safe to free.
"""
# This is a pretty dumb way to get a string copy, but it seems to
# work. A more surefire way would be to allocate a ctypes buffer and copy
# the data with `memcpy` or somesuch.
s = ctypes.cast(c_string, ctypes.c_char_p).value
return b"" + s
class GioURI(URIGetter):
"""Use gio URI function g_file_get_uri. Paths must be utf-8 encoded."""
name = "GIO"
def __init__(self):
self.libgio = self.get_library()
self.available = bool(self.libgio)
if self.available:
self.libgio.g_type_init() # for glib < 2.36
self.libgio.g_file_get_uri.argtypes = [ctypes.c_char_p]
self.libgio.g_file_new_for_path.restype = ctypes.c_void_p
self.libgio.g_file_get_uri.argtypes = [ctypes.c_void_p]
self.libgio.g_file_get_uri.restype = ctypes.POINTER(ctypes.c_char)
self.libgio.g_object_unref.argtypes = [ctypes.c_void_p]
def get_library(self):
lib_name = ctypes.util.find_library("gio-2")
try:
if not lib_name:
return False
return ctypes.cdll.LoadLibrary(lib_name)
except OSError:
return False
def uri(self, path):
g_file_ptr = self.libgio.g_file_new_for_path(path)
if not g_file_ptr:
raise RuntimeError(
"No gfile pointer received for {}".format(
displayable_path(path)
)
)
try:
uri_ptr = self.libgio.g_file_get_uri(g_file_ptr)
finally:
self.libgio.g_object_unref(g_file_ptr)
if not uri_ptr:
self.libgio.g_free(uri_ptr)
raise RuntimeError(
f"No URI received from the gfile pointer for {displayable_path(path)}"
)
try:
uri = copy_c_string(uri_ptr)
finally:
self.libgio.g_free(uri_ptr)
try:
return uri.decode(util._fsencoding())
except UnicodeDecodeError:
raise RuntimeError(f"Could not decode filename from GIO: {uri!r}")
beetbox-beets-01f1faf/beetsplug/types.py 0000664 0000000 0000000 00000003131 14723254774 0020455 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, 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 confuse import ConfigValueError
from beets import library
from beets.dbcore import types
from beets.plugins import BeetsPlugin
class TypesPlugin(BeetsPlugin):
@property
def item_types(self):
return self._types()
@property
def album_types(self):
return self._types()
def _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(
"unknown type '{}' for the '{}' field".format(value, key)
)
return mytypes
beetbox-beets-01f1faf/beetsplug/unimported.py 0000664 0000000 0000000 00000004672 14723254774 0021512 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2019, Joris Jensen
#
# 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 all files in the library folder which are not listed in the
beets library database, including art files
"""
import os
from beets import util
from beets.plugins import BeetsPlugin
from beets.ui import Subcommand, print_
__author__ = "https://github.com/MrNuggelz"
class Unimported(BeetsPlugin):
def __init__(self):
super().__init__()
self.config.add({"ignore_extensions": [], "ignore_subdirectories": []})
def commands(self):
def print_unimported(lib, opts, args):
ignore_exts = [
("." + x).encode()
for x in self.config["ignore_extensions"].as_str_seq()
]
ignore_dirs = [
os.path.join(lib.directory, x.encode())
for x in self.config["ignore_subdirectories"].as_str_seq()
]
in_folder = set()
for root, _, files in os.walk(lib.directory):
# do not traverse if root is a child of an ignored directory
if any(root.startswith(ignored) for ignored in ignore_dirs):
continue
for file in files:
# ignore files with ignored extensions
if any(file.endswith(ext) for ext in ignore_exts):
continue
in_folder.add(os.path.join(root, file))
in_library = {x.path for x in lib.items()}
art_files = {x.artpath for x in lib.albums()}
for f in in_folder - in_library - art_files:
print_(util.displayable_path(f))
unimported = Subcommand(
"unimported",
help="list all files in the library folder which are not listed"
" in the beets library database",
)
unimported.func = print_unimported
return [unimported]
beetbox-beets-01f1faf/beetsplug/web/ 0000775 0000000 0000000 00000000000 14723254774 0017516 5 ustar 00root root 0000000 0000000 beetbox-beets-01f1faf/beetsplug/web/__init__.py 0000664 0000000 0000000 00000037335 14723254774 0021642 0 ustar 00root root 0000000 0000000 # This file is part of beets.
# Copyright 2016, 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."""
import base64
import json
import os
import flask
from flask import g, jsonify
from unidecode import unidecode
from werkzeug.routing import BaseConverter, PathConverter
import beets.library
from beets import ui, util
from beets.plugins import BeetsPlugin
# 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):
if app.config.get("INCLUDE_PATHS", False):
out["path"] = util.displayable_path(out["path"])
else:
del out["path"]
# Filter all bytes attributes and convert them to strings.
for key, value in out.items():
if isinstance(out[key], bytes):
out[key] = base64.b64encode(value).decode("ascii")
# 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):
if app.config.get("INCLUDE_PATHS", False):
out["artpath"] = util.displayable_path(out["artpath"])
else:
del out["artpath"]
if expand:
out["items"] = [_rep(item) for item in obj.items()]
return out
def json_generator(items, root, expand=False):
"""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
:param expand: If true every :class:`Album` contains its items in the json
representation
: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, expand=expand))
yield "]}"
def is_expand():
"""Returns whether the current request is for an expanded response."""
return flask.request.args.get("expand") is not None
def is_delete():
"""Returns whether the current delete request should remove the selected
files.
"""
return flask.request.args.get("delete") is not None
def get_method():
"""Returns the HTTP method of the current request."""
return flask.request.method
def resource(name, patchable=False):
"""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 get_method() == "DELETE":
if app.config.get("READONLY", True):
return flask.abort(405)
for entity in entities:
entity.remove(delete=is_delete())
return flask.make_response(jsonify({"deleted": True}), 200)
elif get_method() == "PATCH" and patchable:
if app.config.get("READONLY", True):
return flask.abort(405)
for entity in entities:
entity.update(flask.request.get_json())
entity.try_sync(True, False) # write, don't move
if len(entities) == 1:
return flask.jsonify(_rep(entities[0], expand=is_expand()))
elif entities:
return app.response_class(
json_generator(entities, root=name),
mimetype="application/json",
)
elif get_method() == "GET":
if len(entities) == 1:
return flask.jsonify(_rep(entities[0], expand=is_expand()))
elif entities:
return app.response_class(
json_generator(entities, root=name),
mimetype="application/json",
)
else:
return flask.abort(404)
else:
return flask.abort(405)
responder.__name__ = f"get_{name}"
return responder
return make_responder
def resource_query(name, patchable=False):
"""Decorates a function to handle RESTful HTTP queries for resources."""
def make_responder(query_func):
def responder(queries):
entities = query_func(queries)
if get_method() == "DELETE":
if app.config.get("READONLY", True):
return flask.abort(405)
for entity in entities:
entity.remove(delete=is_delete())
return flask.make_response(jsonify({"deleted": True}), 200)
elif get_method() == "PATCH" and patchable:
if app.config.get("READONLY", True):
return flask.abort(405)
for entity in entities:
entity.update(flask.request.get_json())
entity.try_sync(True, False) # write, don't move
return app.response_class(
json_generator(entities, root=name),
mimetype="application/json",
)
elif get_method() == "GET":
return app.response_class(
json_generator(
entities, root="results", expand=is_expand()
),
mimetype="application/json",
)
else:
return flask.abort(405)
responder.__name__ = f"query_{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, expand=is_expand()),
mimetype="application/json",
)
responder.__name__ = f"all_{name}"
return responder
return make_responder
def _get_unique_table_field_values(model, field, sort_field):
"""retrieve all unique values belonging to a key from a model"""
if field not in model.all_keys() or sort_field not in model.all_keys():
raise KeyError
with g.lib.transaction() as tx:
rows = tx.query(
"SELECT DISTINCT '{}' FROM '{}' ORDER BY '{}'".format(
field, model._table, sort_field
)
)
return [row[0] for row in rows]
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(str(v) for v in value)
class QueryConverter(PathConverter):
"""Converts slash separated lists of queries in the url to string list."""
def to_python(self, value):
queries = value.split("/")
"""Do not do path substitution on regex value tests"""
return [
query if "::" in query else query.replace("\\", os.sep)
for query in queries
]
def to_url(self, value):
return "/".join([v.replace(os.sep, "\\") for v in value])
class EverythingConverter(PathConverter):
part_isolating = False
regex = ".*?"
# Flask setup.
app = flask.Flask(__name__)
app.url_map.converters["idlist"] = IdListConverter
app.url_map.converters["query"] = QueryConverter
app.url_map.converters["everything"] = EverythingConverter
@app.before_request
def before_request():
g.lib = app.config["lib"]
# Items.
@app.route("/item/", methods=["GET", "DELETE", "PATCH"])
@resource("items", patchable=True)
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//file")
def item_file(item_id):
item = g.lib.get_item(item_id)
# On Windows under Python 2, Flask wants a Unicode path. On Python 3, it
# *always* wants a Unicode path.
if os.name == "nt":
item_path = util.syspath(item.path)
else:
item_path = os.fsdecode(item.path)
base_filename = os.path.basename(item_path)
# FIXME: Arguably, this should just use `displayable_path`: The latter
# tries `_fsencoding()` first, but then falls back to `utf-8`, too.
if isinstance(base_filename, bytes):
try:
unicode_base_filename = base_filename.decode("utf-8")
except UnicodeError:
unicode_base_filename = util.displayable_path(base_filename)
else:
unicode_base_filename = base_filename
try:
# Imitate http.server behaviour
base_filename.encode("latin-1", "strict")
except UnicodeError:
safe_filename = unidecode(base_filename)
else:
safe_filename = unicode_base_filename
response = flask.send_file(
item_path, as_attachment=True, download_name=safe_filename
)
return response
@app.route("/item/query/", methods=["GET", "DELETE", "PATCH"])
@resource_query("items", patchable=True)
def item_query(queries):
return g.lib.items(queries)
@app.route("/item/path/")
def item_at_path(path):
query = beets.library.PathQuery("path", path.encode("utf-8"))
item = g.lib.items(query).get()
if item:
return flask.jsonify(_rep(item))
else:
return flask.abort(404)
@app.route("/item/values/")
def item_unique_field_values(key):
sort_key = flask.request.args.get("sort_key", key)
try:
values = _get_unique_table_field_values(
beets.library.Item, key, sort_key
)
except KeyError:
return flask.abort(404)
return flask.jsonify(values=values)
# Albums.
@app.route("/album/", methods=["GET", "DELETE"])
@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/", methods=["GET", "DELETE"])
@resource_query("albums")
def album_query(queries):
return g.lib.albums(queries)
@app.route("/album//art")
def album_art(album_id):
album = g.lib.get_album(album_id)
if album and album.artpath:
return flask.send_file(album.artpath.decode())
else:
return flask.abort(404)
@app.route("/album/values/")
def album_unique_field_values(key):
sort_key = flask.request.args.get("sort_key", key)
try:
values = _get_unique_table_field_values(
beets.library.Album, key, sort_key
)
except KeyError:
return flask.abort(404)
return flask.jsonify(values=values)
# 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().__init__()
self.config.add(
{
"host": "127.0.0.1",
"port": 8337,
"cors": "",
"cors_supports_credentials": False,
"reverse_proxy": False,
"include_paths": False,
"readonly": True,
}
)
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
# Normalizes json output
app.config["JSONIFY_PRETTYPRINT_REGULAR"] = False
app.config["INCLUDE_PATHS"] = self.config["include_paths"]
app.config["READONLY"] = self.config["readonly"]
# Enable CORS if required.
if self.config["cors"]:
self._log.info(
"Enabling CORS with origin: {0}", self.config["cors"]
)
from flask_cors import CORS
app.config["CORS_ALLOW_HEADERS"] = "Content-Type"
app.config["CORS_RESOURCES"] = {
r"/*": {"origins": self.config["cors"].get(str)}
}
CORS(
app,
supports_credentials=self.config[
"cors_supports_credentials"
].get(bool),
)
# Allow serving behind a reverse proxy
if self.config["reverse_proxy"]:
app.wsgi_app = ReverseProxied(app.wsgi_app)
# Start the web application.
app.run(
host=self.config["host"].as_str(),
port=self.config["port"].get(int),
debug=opts.debug,
threaded=True,
)
cmd.func = func
return [cmd]
class ReverseProxied:
"""Wrap the application in this middleware and configure the
front-end server to add these headers, to let you quietly bind
this to a URL other than / and to an HTTP scheme that is
different than what is used locally.
In nginx:
location /myprefix {
proxy_pass http://192.168.0.1:5001;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Script-Name /myprefix;
}
From: http://flask.pocoo.org/snippets/35/
:param app: the WSGI application
"""
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
script_name = environ.get("HTTP_X_SCRIPT_NAME", "")
if script_name:
environ["SCRIPT_NAME"] = script_name
path_info = environ["PATH_INFO"]
if path_info.startswith(script_name):
environ["PATH_INFO"] = path_info[len(script_name) :]
scheme = environ.get("HTTP_X_SCHEME", "")
if scheme:
environ["wsgi.url_scheme"] = scheme
return self.app(environ, start_response)
beetbox-beets-01f1faf/beetsplug/web/static/ 0000775 0000000 0000000 00000000000 14723254774 0021005 5 ustar 00root root 0000000 0000000 beetbox-beets-01f1faf/beetsplug/web/static/backbone.js 0000664 0000000 0000000 00000123141 14723254774 0023111 0 ustar 00root root 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 overridden,
// 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 = $('').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 preferred 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` properties.
_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,'/');
};
}).call(this);
beetbox-beets-01f1faf/beetsplug/web/static/beets.css 0000664 0000000 0000000 00000005607 14723254774 0022631 0 ustar 00root root 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;
}
beetbox-beets-01f1faf/beetsplug/web/static/beets.js 0000664 0000000 0000000 00000021233 14723254774 0022446 0 ustar 00root root 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.floor(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:
// https://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) {
var queryURL = query.split(/\s+/).map(encodeURIComponent).join('/');
$.getJSON('item/query/' + queryURL, 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/' + encodeURIComponent($('#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();
});
beetbox-beets-01f1faf/beetsplug/web/static/jquery.js 0000664 0000000 0000000 00000744653 14723254774 0022705 0 ustar 00root root 0000000 0000000 /*!
* jQuery JavaScript Library v1.7.1
* http://jquery.com/
*
* Copyright 2016, John Resig
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* Includes Sizzle.js
* http://sizzlejs.com/
* Copyright 2016, 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 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 = "
a";
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>",
// 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 = "
" +
"
" +
"
";
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 = "
t
";
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 = "";
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 separated 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 2016, 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 comparison
// 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 retrieving 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 = "";
// 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 = "";
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 = "";
// 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 = "";
// 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 = /", "" ],
legend: [ 1, "" ],
thead: [ 1, "