35 |
36 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Amazon Cloud Player scrobbler",
4 | "version": "1.1.0",
5 | "description": "Scrobbles songs from Amazon Cloud Player to Last.fm",
6 | "icons": {
7 | "16": "img/icon16.png",
8 | "48": "img/icon48.png",
9 | "64": "img/icon64.png",
10 | "128": "img/icon128.png"
11 | },
12 | "browser_action": {
13 | "default_icon": "img/main-icon.png",
14 | "default_popup": "popup.html"
15 | },
16 | "permissions": [
17 | "http://ws.audioscrobbler.com/",
18 | "https://www.amazon.com/",
19 | "https://www.amazon.de/"
20 | ],
21 | "background" : {"scripts": ["js/md5.js", "js/lastfm.js", "js/jquery-1.7.min.js", "js/background.js"]},
22 | "content_scripts": [
23 | {
24 | "js": ["js/inject.js"],
25 | "matches": ["https://www.amazon.com/gp/dmusic/mp3/*","https://www.amazon.de/gp/dmusic/mp3/*"]
26 | },
27 | {
28 | "js": ["js/contentscript.js"],
29 | "matches": ["https://www.amazon.com/gp/dmusic/mp3/*", "https://www.amazon.de/gp/dmusic/mp3/*"]
30 | },
31 | {
32 | "js": ["js/jquery-1.7.min.js"],
33 | "matches": ["https://www.amazon.com/gp/dmusic/mp3/*", "https://www.amazon.de/gp/dmusic/mp3/*"],
34 | "run_at": "document_start"
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/css/popup.css:
--------------------------------------------------------------------------------
1 | /**
2 | * popup.css
3 | * Style sheet for popup page
4 | * Copyright (c) 2011 Alexey Savartsov
5 | * Licensed under the MIT license
6 | */
7 |
8 | body {
9 | padding: 10px;
10 | margin: 0;
11 | min-width: 300px;
12 |
13 | font-family: "Helvetica", "Arial", sans-serif;
14 | }
15 |
16 | p {
17 | line-height: 100%;
18 | padding: 0;
19 | margin: 0;
20 | }
21 |
22 | a {
23 | color: grey;
24 | }
25 |
26 | a:hover {
27 | color: black;
28 | }
29 |
30 | #cover-box {
31 | float: left;
32 | }
33 |
34 | #cover {
35 | border: none;
36 | }
37 |
38 | #song.nosong {
39 | color: grey;
40 | }
41 |
42 | #song.nosong a {
43 | font-size: 12px;
44 | }
45 |
46 | #song {
47 | margin-left: 75px;
48 | }
49 |
50 | #artist {
51 | font-size: 18px;
52 | }
53 |
54 | #track {
55 | margin-top: 6px;
56 | font-size: 14px;
57 | }
58 |
59 | #lastfm-buttons {
60 | margin-top: 6px;
61 | }
62 |
63 | #bottom {
64 | margin-top: 10px;
65 | font-size: 11px;
66 |
67 | color: grey;
68 | }
69 |
70 | #scrobbling {
71 | float: left;
72 | }
73 |
74 | #lastfm-profile {
75 | float: right;
76 | }
77 |
78 | a.logout {
79 | display: inline-block;
80 | width: 10px;
81 | height: 10px;
82 | background: url('../img/logout.png') 0 0 no-repeat;
83 | margin-left: 4px;
84 | }
85 |
86 | a:hover.logout {
87 | background-position: 0 -10px;
88 | }
89 |
90 | a.loved, a.notloved {
91 | display: inline-block;
92 | width: 16px;
93 | height: 16px;
94 | background-image: url('../img/love.png');
95 | background-repeat: no-repeat;
96 | }
97 |
98 | a.loved {
99 | background-position: 0 0;
100 | }
101 |
102 | a.notloved {
103 | background-position: 0 -16px;
104 | }
105 |
106 | .clear {
107 | clear: both;
108 | }
109 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Amazon Cloud Player scrobbler
2 | -----------------------------
3 |
4 | Google Chrome plugin for scrobbling songs from Amazon Cloud Player to Last.fm
5 |
6 | Features
7 | ========
8 |
9 | * Scrobbles now playing songs at 70% played time
10 | * Song information in popup window
11 | * Cloud Player state in icon
12 | * Love/unlove now playing song
13 | * Option to turn scrobbling off if you do not want to send scrobbles
14 |
15 | Installation
16 | ============
17 |
18 | Install stable release from [Google Web Store](https://chrome.google.com/webstore/detail/nolkhoglpmelgkcljkjlfeledieoahoa)
19 |
20 | Version History
21 | ===============
22 |
23 | **Version 1.1.0**
24 |
25 | * New engine for obtaining the song info. Now playing information now gathering directly from Cloud Player JS objects
26 | * Fixed a problem with long song titles (when some song sometimes were scrobbled like "This is Very Very Very Long Tit...")
27 | * Now the album information is sent too
28 | * Fixed a rare problem with superfluous scrobbles on song switching
29 |
30 | **Version 1.0.3**
31 |
32 | * Fixed link to Cloud Player in popup window (Amazon now returns 403 error for short link used before)
33 |
34 | **Version 1.0.2**
35 |
36 | * Fixed artist detection when playing from Artists section
37 | * Fixed a problem when popup displayed a song info when player stopped playing
38 |
39 | Author
40 | ======
41 |
42 | [Alexey Savartsov](https://github.com/asavartsov), asavartsov@gmail.com
43 |
44 | License
45 | =======
46 |
47 | (The MIT License)
48 |
49 | Copyright (c) 2011 Alexey Savartsov, asavartsov@gmail.com
50 |
51 | Permission is hereby granted, free of charge, to any person obtaining
52 | a copy of this software and associated documentation files (the
53 | 'Software'), to deal in the Software without restriction, including
54 | without limitation the rights to use, copy, modify, merge, publish,
55 | distribute, sublicense, and/or sell copies of the Software, and to
56 | permit persons to whom the Software is furnished to do so, subject to
57 | the following conditions:
58 |
59 | The above copyright notice and this permission notice shall be
60 | included in all copies or substantial portions of the Software.
61 |
62 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
63 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
64 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
65 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
66 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
67 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
68 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
69 |
--------------------------------------------------------------------------------
/js/contentscript.js:
--------------------------------------------------------------------------------
1 | /**
2 | * contentscripts.js
3 | * Parses player page and transmit song information to background page
4 | * Copyright (c) 2011 Alexey Savartsov
5 | * Licensed under the MIT license
6 | */
7 |
8 | /**
9 | * Player class
10 | *
11 | * Cloud Player page parser
12 | */
13 | function Player(parser) {
14 | this.has_song = parser._get_has_song();
15 | this.is_playing = parser._get_is_playing();
16 | this.song = {
17 | position: parser._get_song_position(),
18 | time: parser._get_song_time(),
19 | title: parser._get_song_title(),
20 | artist: parser._get_song_artist(),
21 | album: parser._get_song_album(),
22 | cover: parser._get_song_cover()
23 | };
24 | }
25 |
26 | /**
27 | * Constructor for parser class
28 | * Executes scripts to fetch now playing info from cloudplayer
29 | * @returns {AmazonParser}
30 | */
31 | AmazonParser = function() {
32 | this._player = injectScript(function() {
33 | return amznMusic.widgets.player.getCurrent();
34 | });
35 |
36 | this._time = injectScript(function() {
37 | var position = amznMusic.widgets.playerNative.getCurrentTime() + amznMusic.widgets.playerFlash.getCurrentTime();
38 | return position;
39 | });
40 | };
41 |
42 | /**
43 | * Check whether a song loaded into player widget
44 | *
45 | * @return true if some song is loaded, otherwise false
46 | */
47 | AmazonParser.prototype._get_has_song = function() {
48 | return ($("#noMusicInNowPlaying").length == 0);
49 | };
50 |
51 | /**
52 | * Checks whether song is playing or paused
53 | *
54 | * @return true if song is playing, false if song is paused
55 | */
56 | AmazonParser.prototype._get_is_playing = function() {
57 | return $("#mp3Player .mp3MasterPlayGroup").hasClass("playing");
58 | };
59 |
60 | /**
61 | * Get current song playing position
62 | *
63 | * @return Playing position in seconds
64 | */
65 | AmazonParser.prototype._get_song_position = function() {
66 | return this._time;
67 | };
68 |
69 | /**
70 | * Get current song length
71 | *
72 | * @return Song length in seconds
73 | */
74 | AmazonParser.prototype._get_song_time = function() {
75 | return this._player ? parseInt(this._player.metadata.duration) : 0;
76 | };
77 |
78 | /**
79 | * Get current song title
80 | *
81 | * @return Song title
82 | */
83 | AmazonParser.prototype._get_song_title = function() {
84 | return this._player ? this._player.metadata.title : null;
85 | };
86 |
87 | /**
88 | * Get current song artist
89 | *
90 | * @return Song artist
91 | */
92 | AmazonParser.prototype._get_song_artist = function() {
93 | return this._player ? this._player.metadata.artistName : null;
94 | };
95 |
96 | /**
97 | * Get current song artwork
98 | *
99 | * @return Image URL or default artwork
100 | */
101 | AmazonParser.prototype._get_song_cover = function() {
102 | return this._player ? this._player.metadata.albumCoverImageSmall : null;
103 | };
104 |
105 | /**
106 | * Get current song album name
107 | *
108 | * @return Album name or null
109 | */
110 | AmazonParser.prototype._get_song_album = function() {
111 | return this._player ? this._player.metadata.albumName : null;
112 | };
113 |
114 | var port = chrome.extension.connect({name: "cloudplayer"});
115 |
116 | window.setInterval(function() {
117 | port.postMessage(new Player(new AmazonParser()));
118 | },
119 | 10000);
120 |
--------------------------------------------------------------------------------
/js/inject.js:
--------------------------------------------------------------------------------
1 | //////////////////////////////////////////////////////////////////////////////////////////////
2 | // Copyright(C) 2010 Abdullah Ali, voodooattack@hotmail.com //
3 | //////////////////////////////////////////////////////////////////////////////////////////////
4 | // Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php //
5 | //////////////////////////////////////////////////////////////////////////////////////////////
6 |
7 | // Injects a script into the DOM, the new script gets executed in the original page's
8 | // context instead of the active content-script context.
9 | //
10 | // Parameters:
11 | // source: [string/function]
12 | // (2..n): Function arguments if a function was passed as the first parameter.
13 |
14 |
15 | function injectScript(source)
16 | {
17 |
18 | // Utilities
19 | var isFunction = function (arg) {
20 | return (Object.prototype.toString.call(arg) == "[object Function]");
21 | };
22 |
23 | var jsEscape = function (str) {
24 | // Replaces quotes with numerical escape sequences to
25 | // avoid single-quote-double-quote-hell, also helps by escaping HTML special chars.
26 | if (!str || !str.length) return str;
27 | // use \W in the square brackets if you have trouble with any values.
28 | var r = /['"<>\/]/g, result = "", l = 0, c;
29 | do{ c = r.exec(str);
30 | result += (c ? (str.substring(l, r.lastIndex-1) + "\\x" +
31 | c[0].charCodeAt(0).toString(16)) : (str.substring(l)));
32 | } while (c && ((l = r.lastIndex) > 0))
33 | return (result.length ? result : str);
34 | };
35 |
36 | var bFunction = isFunction(source);
37 | var elem = document.createElement("script"); // create the new script element.
38 | var script, ret, id = "";
39 |
40 | if (bFunction)
41 | {
42 | // We're dealing with a function, prepare the arguments.
43 | var args = [];
44 |
45 | for (var i = 1; i < arguments.length; i++)
46 | {
47 | var raw = arguments[i];
48 | var arg;
49 |
50 | if (isFunction(raw)) // argument is a function.
51 | arg = "eval(\"" + jsEscape("(" + raw.toString() + ")") + "\")";
52 | else if (Object.prototype.toString.call(raw) == '[object Date]') // Date
53 | arg = "(new Date(" + raw.getTime().toString() + "))";
54 | else if (Object.prototype.toString.call(raw) == '[object RegExp]') // RegExp
55 | arg = "(new RegExp(" + raw.toString() + "))";
56 | else if (typeof raw === 'string' || typeof raw === 'object') // String or another object
57 | arg = "JSON.parse(\"" + jsEscape(JSON.stringify(raw)) + "\")";
58 | else
59 | arg = raw.toString(); // Anything else number/boolean
60 |
61 | args.push(arg); // push the new argument on the list
62 | }
63 |
64 | // generate a random id string for the script block
65 | while (id.length < 16) id += String.fromCharCode(((!id.length || Math.random() > 0.5) ?
66 | 0x61 + Math.floor(Math.random() * 0x19) : 0x30 + Math.floor(Math.random() * 0x9 )));
67 |
68 | // build the final script string, wrapping the original in a boot-strapper/proxy:
69 | script = "(function(){var value={callResult: null, throwValue: false};try{value.callResult=(("+
70 | source.toString()+")("+args.join()+"));}catch(e){value.throwValue=true;value.callResult=e;};"+
71 | "document.getElementById('"+id+"').innerText=JSON.stringify(value);})();";
72 |
73 | elem.id = id;
74 | }
75 | else // plain string, just copy it over.
76 | {
77 | script = source;
78 | }
79 |
80 | elem.type = "text/javascript";
81 | elem.innerHTML = script;
82 |
83 | // insert the element into the DOM (it starts to execute instantly)
84 | document.head.appendChild(elem);
85 |
86 | if (bFunction)
87 | {
88 | // get the return value from our function:
89 | ret = JSON.parse(elem.innerText);
90 |
91 | // remove the now-useless clutter.
92 | elem.parentNode.removeChild(elem);
93 |
94 | // make sure the garbage collector picks it instantly. (and hope it does)
95 | delete (elem);
96 |
97 | // see if our returned value was thrown or not
98 | if (ret.throwValue)
99 | throw (ret.callResult);
100 | else
101 | return (ret.callResult);
102 | }
103 | else // plain text insertion, return the new script element.
104 | return (elem);
105 | }
106 |
--------------------------------------------------------------------------------
/js/popup.js:
--------------------------------------------------------------------------------
1 | /**
2 | * popup.js
3 | * Popup page script
4 | * Copyright (c) 2011 Alexey Savartsov
5 | * Licensed under the MIT license
6 | */
7 |
8 | /* Background page */
9 | var bp = chrome.extension.getBackgroundPage();
10 |
11 | /* Render popup when DOM is ready */
12 | $(document).ready(function() {
13 | render_scrobble_link();
14 | render_song();
15 | render_auth_link();
16 | });
17 |
18 | /* Render functions */
19 |
20 | /**
21 | * Renders current song details
22 | */
23 | function render_song() {
24 | if(bp.player.song)
25 | {
26 | $("#artist").text(bp.player.song.artist);
27 | $("#track").text(bp.player.song.title);
28 | $("#cover").attr({ src: bp.player.song.cover || "img/defaultcover.png" });
29 |
30 | if(bp.lastfm_api.session.name && bp.lastfm_api.session.key) {
31 | render_love_button();
32 | }
33 | else {
34 | $("#lastfm-buttons").hide();
35 | }
36 | }
37 | else {
38 | $("#song").addClass("nosong");
39 | $("#artist").text("Nothing is playing");
40 | $("#track").html('');
41 | $("#track a")
42 | .attr({
43 | href: "https://www.amazon.com/gp/dmusic/mp3/player",
44 | target: "_blank"
45 | })
46 | .text("Start Cloud Player");
47 | $("#cover ").attr({ src: "img/defaultcover.png" });
48 | $("#lastfm-buttons").hide();
49 | }
50 | }
51 |
52 | /**
53 | * Renders the link to turn on/off scrobbling
54 | */
55 | function render_scrobble_link() {
56 | $("#scrobbling").html('');
57 | $("#scrobbling a")
58 | .attr({
59 | href: "#"
60 | })
61 | .click(on_toggle_scrobble)
62 | .text(bp.SETTINGS.scrobble ? "Stop scrobbling" : "Resume scrobbling");
63 | }
64 |
65 | /**
66 | * Renders authentication/profile link
67 | */
68 | function render_auth_link() {
69 | if(bp.lastfm_api.session.name && bp.lastfm_api.session.key)
70 | {
71 | $("#lastfm-profile").html("Logged in as " + "");
72 | $("#lastfm-profile a:first")
73 | .attr({
74 | href: "http://last.fm/user/" + bp.lastfm_api.session.name,
75 | target: "_blank"
76 | })
77 | .text(bp.lastfm_api.session.name);
78 |
79 | $("#lastfm-profile a:last")
80 | .attr({
81 | href: "#",
82 | title: "Logout"
83 | })
84 | .click(on_logout)
85 | .addClass("logout");
86 | }
87 | else {
88 | $("#lastfm-profile").html('');
89 | $("#lastfm-profile a")
90 | .attr({
91 | href: "#"
92 | })
93 | .click(on_auth)
94 | .text("Connect to Last.fm");
95 | }
96 | }
97 |
98 | /**
99 | * Renders the love button
100 | */
101 | function render_love_button() {
102 | $("#love-button").html('');
103 |
104 | bp.lastfm_api.is_track_loved(bp.player.song.title,
105 | bp.player.song.artist,
106 | function(result) {
107 | $("#love-button").html('');
108 |
109 | if(result) {
110 | $("#love-button a").attr({ title: "Unlove this song"})
111 | .click(on_unlove)
112 | .addClass("loved");
113 |
114 | }
115 | else {
116 | $("#love-button a").attr({ title: "Love this song" })
117 | .click(on_love)
118 | .addClass("notloved");
119 | }
120 | });
121 | }
122 |
123 | /* Event handlers */
124 |
125 | /**
126 | * Turn on/off scrobbling link was clicked
127 | */
128 | function on_toggle_scrobble() {
129 | bp.toggle_scrobble();
130 | render_scrobble_link();
131 | }
132 |
133 | /**
134 | * Authentication link was clicked
135 | */
136 | function on_auth() {
137 | bp.start_web_auth();
138 | window.close();
139 | }
140 |
141 | /**
142 | * Logout link was clicked
143 | */
144 | function on_logout() {
145 | bp.clear_session();
146 | render_auth_link();
147 | }
148 |
149 | /**
150 | * Love button was clicked
151 | */
152 | function on_love() {
153 | bp.lastfm_api.love_track(bp.player.song.title, bp.player.song.artist,
154 | function(result) {
155 | if(!result.error) {
156 | render_love_button();
157 | }
158 | else {
159 | if(result.error == 9) {
160 | // Session expired
161 | bp.clear_session();
162 | render_auth_link();
163 | }
164 |
165 | chrome.browserAction.setIcon({
166 | 'path': SETTINGS.error_icon });
167 | }
168 | });
169 |
170 | $("#love-button").html('');
171 | }
172 |
173 | /**
174 | * Unlove button was clicked
175 | */
176 | function on_unlove() {
177 | bp.lastfm_api.unlove_track(bp.player.song.title, bp.player.song.artist,
178 | function(result) {
179 | if(!result.error) {
180 | render_love_button();
181 | }
182 | else {
183 | if(result.error == 9) {
184 | // Session expired
185 | bp.clear_session();
186 | render_auth_link();
187 | }
188 |
189 | chrome.browserAction.setIcon({
190 | 'path': SETTINGS.error_icon });
191 | }
192 | });
193 |
194 | $("#love-button").html('');
195 | }
196 |
--------------------------------------------------------------------------------
/js/background.js:
--------------------------------------------------------------------------------
1 | /**
2 | * background.js
3 | * Background page script
4 | * Copyright (c) 2011 Alexey Savartsov
5 | * Licensed under the MIT license
6 | */
7 |
8 | var SETTINGS = {
9 | api_key: "754ae915036422c2134252ffeb1d6cc9",
10 | api_secret: "8fbb8c5f1208e4476b27e03bde8e5c99",
11 |
12 | callback_file: "lastfm_callback.html",
13 |
14 | main_icon: "img/main-icon.png",
15 | playing_icon: "img/main-icon-playing.png",
16 | paused_icon: "img/main-icon-paused.png",
17 | error_icon: "img/main-icon-error.png",
18 | scrobbling_stopped_icon: "img/main-icon-scrobbling-stopped.png"
19 | };
20 |
21 | var player = {}; // Previous player state
22 | var now_playing_sent = false;
23 | var scrobbled = false;
24 | var lastfm_api = new LastFM(SETTINGS.api_key, SETTINGS.api_secret);
25 |
26 | // Load settings from local storage
27 | lastfm_api.session.key = localStorage["session_key"] || null;
28 | lastfm_api.session.name = localStorage["session_name"] || null;
29 |
30 | // This enables scrobbling by default
31 | SETTINGS.scrobble = !(localStorage["scrobble"] == "false");
32 |
33 | if(!SETTINGS.scrobble) {
34 | chrome.browserAction.setIcon({ 'path': SETTINGS.scrobbling_stopped_icon });
35 | }
36 |
37 | // Connect event handlers
38 | chrome.extension.onConnect.addListener(port_on_connect);
39 |
40 | /**
41 | * Content script has connected to the extension
42 | */
43 | function port_on_connect(port) {
44 | console.assert(port.name == "cloudplayer");
45 |
46 | // Connect another port event handlers
47 | port.onMessage.addListener(port_on_message);
48 | port.onDisconnect.addListener(port_on_disconnect);
49 | }
50 |
51 | /**
52 | * New message arrives to the port
53 | */
54 | function port_on_message(message) {
55 | // Current player state
56 | var _p = message;
57 |
58 | if(!SETTINGS.scrobble) {
59 | chrome.browserAction.setIcon({
60 | 'path': SETTINGS.scrobbling_stopped_icon });
61 |
62 | player = _p;
63 | return;
64 | }
65 |
66 | if(_p.has_song) {
67 | if(_p.is_playing) {
68 | chrome.browserAction.setIcon({
69 | 'path': SETTINGS.playing_icon });
70 |
71 | // Last.fm recommends to scrobble a song at least at 50%
72 | // TODO: Setting for 0.7?
73 | var time_to_scrobble = _p.song.time * 0.7 - _p.song.position;
74 |
75 | // Check for valid timings and for that the now playing status was reported at least once
76 | // This intended to fix an issue with invalid timings that Amazon accidentally reports on
77 | // song start
78 | if(time_to_scrobble <= 0 && _p.song.position > 0 && _p.song.time > 0) {
79 | // TODO: Another way?
80 | // if(scrobbled && _p.song.position > player.song.position)
81 |
82 | if(now_playing_sent && !scrobbled) {
83 | // Scrobble this song
84 | lastfm_api.scrobble(_p.song.title,
85 | /* Song start time */
86 | Math.round(new Date().getTime() / 1000) - _p.song.position,
87 | _p.song.artist,
88 | _p.song.album,
89 | function(response) {
90 | if(!response.error) {
91 | // Song was scrobled, waiting for the next song
92 | scrobbled = true;
93 | now_playing_sent = false;
94 | }
95 | else {
96 | if(response.error == 9) {
97 | // Session expired
98 | clear_session();
99 | }
100 |
101 | chrome.browserAction.setIcon({
102 | 'path': SETTINGS.error_icon });
103 | }
104 | });
105 | }
106 | }
107 | else {
108 | // Set now playing status
109 | // TODO: Maybe there is no need to do it so frequent?
110 | lastfm_api.now_playing(_p.song.title,
111 | _p.song.artist,
112 | _p.song.album,
113 | function(response) {
114 | // TODO:
115 | });
116 |
117 | now_playing_sent = true;
118 | scrobbled = false;
119 | }
120 |
121 | // Save player state
122 | player = _p; // TODO: Save here?
123 | }
124 | else {
125 | // The player is paused
126 | chrome.browserAction.setIcon({
127 | 'path': SETTINGS.paused_icon });
128 | }
129 | }
130 | else {
131 | chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon });
132 | player = {};
133 | scrobbled = false;
134 | now_playing_sent = false;
135 | }
136 | }
137 |
138 | /**
139 | * Content script has disconnected
140 | */
141 | function port_on_disconnect() {
142 | player = {}; // Clear player state
143 | scrobbled = false;
144 | now_playing_sent = false;
145 | chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon });
146 | }
147 |
148 |
149 | /**
150 | * Authentication link from popup window
151 | */
152 | function start_web_auth() {
153 | var callback_url = chrome.extension.getURL(SETTINGS.callback_file);
154 | chrome.tabs.create({
155 | 'url':
156 | "http://www.last.fm/api/auth?api_key=" +
157 | SETTINGS.api_key +
158 | "&cb=" +
159 | callback_url });
160 | }
161 |
162 | /**
163 | * Clears last.fm session
164 | */
165 | function clear_session() {
166 | lastfm_api.session = {};
167 |
168 | localStorage.removeItem("session_key");
169 | localStorage.removeItem("session_name");
170 | }
171 |
172 | /**
173 | * Toggles setting to scrobble songs or not
174 | */
175 | function toggle_scrobble() {
176 | SETTINGS.scrobble = !SETTINGS.scrobble;
177 | localStorage["scrobble"] = SETTINGS.scrobble;
178 |
179 | // Set the icon corresponding the current scrobble state
180 | var icon = SETTINGS.scrobble ? SETTINGS.main_icon : SETTINGS.scrobbling_stopped_icon;
181 | chrome.browserAction.setIcon({ 'path': icon });
182 | }
183 |
184 | /**
185 | * Last.fm session request
186 | */
187 | function get_lastfm_session(token) {
188 | lastfm_api.authorize(token, function(response) {
189 | // Save session
190 | if(response.session)
191 | {
192 | localStorage["session_key"] = response.session.key;
193 | localStorage["session_name"] = response.session.name;
194 | }
195 | });
196 | }
197 |
--------------------------------------------------------------------------------
/js/lastfm.js:
--------------------------------------------------------------------------------
1 | /**
2 | * lastfm.js
3 | * Last.fm authorization and scrobbling XHR requests
4 | * Copyright (c) 2011 Alexey Savartsov
5 | * Licensed under the MIT license
6 | */
7 |
8 | /**
9 | * LastFM class constructor
10 | *
11 | * @param api_key Last.fm API key
12 | * @param api_secret Last.fm API secret
13 | */
14 | function LastFM(api_key, api_secret) {
15 | this.API_KEY = api_key || "";
16 | this.API_SECRET = api_secret || "";
17 | this.API_ROOT = "http://ws.audioscrobbler.com/2.0/";
18 |
19 | this.session = {};
20 | }
21 |
22 | /**
23 | * Makes an authorization request
24 | *
25 | * @param token Authorization token
26 | * @param callback Callback function for the request. Sends a parameter with
27 | * reply decoded as JS object from JSON on null on error
28 | */
29 | LastFM.prototype.authorize = function(token, callback) {
30 | var params = {
31 | 'api_key': this.API_KEY,
32 | 'method': "auth.getSession",
33 | 'token': token
34 | };
35 |
36 | params.api_sig = this._req_sign(params);
37 | params.format = "json";
38 |
39 | var self = this;
40 |
41 | this._xhr("GET", params,
42 | function(reply) {
43 | if(reply) {
44 | self.session.key = reply.session.key;
45 | self.session.name = reply.session.name;
46 | callback(reply);
47 | }
48 | else {
49 | callback();
50 | }
51 | });
52 | };
53 |
54 | /**
55 | * Sets Now Playing status for a track
56 | *
57 | * @param track Track title
58 | * @param artist Track artist
59 | * @param callback Callback function for the request. Sends a parameter with
60 | * reply decoded as JS object from JSON on null on error
61 | */
62 | LastFM.prototype.now_playing = function(track, artist, album, callback) {
63 | var params = {
64 | 'api_key': this.API_KEY,
65 | 'method': "track.updateNowPlaying",
66 | 'track': track,
67 | 'artist': artist,
68 | 'album': album,
69 | 'sk': this.session.key
70 | };
71 |
72 | params.api_sig = this._req_sign(params);
73 | params.format = "json";
74 |
75 | this._xhr("POST", params,
76 | function(result) {
77 | callback(result);
78 | });
79 | };
80 |
81 | /**
82 | * Scrobbles a track
83 | *
84 | * @param track Track title
85 | * @param timestamp The time the track starts playing in UNIX format
86 | * @param artist Track artist
87 | * @param callback Callback function for the request. Sends a parameter with
88 | * reply decoded as JS object from JSON on null on error
89 | */
90 | LastFM.prototype.scrobble = function(track, timestamp, artist, album, callback) {
91 | var params = {
92 | 'api_key': this.API_KEY,
93 | 'method': "track.scrobble",
94 | 'track': track,
95 | 'timestamp': timestamp,
96 | 'artist': artist,
97 | 'album': album,
98 | 'sk': this.session.key
99 | };
100 |
101 | params.api_sig = this._req_sign(params);
102 | params.format = "json";
103 |
104 | this._xhr("POST", params,
105 | function(result) {
106 | callback(result);
107 | });
108 | };
109 |
110 | /**
111 | * Loves a track
112 | *
113 | * @param track Track title
114 | * @param artist Track artist
115 | * @param callback Callback function for the request. Sends a parameter with
116 | * reply decoded as JS object from JSON on null on error
117 | */
118 | LastFM.prototype.love_track = function(track, artist, callback) {
119 | var params = {
120 | 'api_key': this.API_KEY,
121 | 'method': "track.love",
122 | 'track': track,
123 | 'artist': artist,
124 | 'sk': this.session.key
125 | };
126 |
127 | params.api_sig = this._req_sign(params);
128 | params.format = "json";
129 |
130 | this._xhr("POST", params,
131 | function(result) {
132 | callback(result);
133 | });
134 | };
135 |
136 | /**
137 | * Unloves a track
138 | *
139 | * @param track Track title
140 | * @param artist Track artist
141 | * @param callback Callback function for the request. Sends a parameter with
142 | * reply decoded as JS object from JSON on null on error
143 | */
144 | LastFM.prototype.unlove_track = function(track, artist, callback) {
145 | var params = {
146 | 'api_key': this.API_KEY,
147 | 'method': "track.unlove",
148 | 'track': track,
149 | 'artist': artist,
150 | 'sk': this.session.key
151 | };
152 |
153 | params.api_sig = this._req_sign(params);
154 | params.format = "json";
155 |
156 | this._xhr("POST", params,
157 | function(result) {
158 | callback(result);
159 | });
160 | };
161 |
162 | /**
163 | * Checks whether a track loved by current user
164 | *
165 | * @param track Track title
166 | * @param artist Track artist
167 | * @param callback Callback function for the request. Sends a boolean
168 | * parameter which is true if track loved, otherwise false
169 | */
170 | LastFM.prototype.is_track_loved = function(track, artist, callback) {
171 | if(!this.session.name) {
172 | callback(false);
173 | return;
174 | }
175 |
176 | var params = {
177 | 'api_key': this.API_KEY,
178 | 'method': "track.getInfo",
179 | 'track': track,
180 | 'artist': artist,
181 | 'username': this.session.name
182 | };
183 |
184 | params.format = 'json';
185 |
186 | this._xhr("GET", params, function(result) {
187 | if(!result.error && result.track) {
188 | callback(result.track.userloved == 1);
189 | }
190 | else {
191 | callback(false);
192 | }
193 | });
194 | };
195 |
196 | /**
197 | * Makes a signature of request
198 | *
199 | * @param params Request values
200 | * @return Signature string
201 | */
202 | LastFM.prototype._req_sign = function(params) {
203 | var keys = [];
204 | var key, i;
205 | var signature = "";
206 |
207 | for(key in params) {
208 | keys.push(key);
209 | }
210 |
211 | for(i in keys.sort()) {
212 | key = keys[i];
213 | signature += key + params[key];
214 | }
215 |
216 | signature += this.API_SECRET;
217 | return hex_md5(signature);
218 | };
219 |
220 | /**
221 | * Performs an XMLHTTP request and expects JSON as reply
222 | *
223 | * @param method Request method (GET or POST)
224 | * @param params Hash with request values. All request fields will be
225 | * automatically urlencoded
226 | * @param callback Callback function for the request. Sends a parameter with
227 | * reply decoded as JS object from JSON on null on error
228 | */
229 | LastFM.prototype._xhr = function(method, params, callback) {
230 | var uri = this.API_ROOT;
231 | var _data = "";
232 | var _params = [];
233 | var xhr = new XMLHttpRequest();
234 |
235 | for(param in params) {
236 | _params.push(encodeURIComponent(param) + "="
237 | + encodeURIComponent(params[param]));
238 | }
239 |
240 | switch(method) {
241 | case "GET":
242 | uri += '?' + _params.join('&').replace(/%20/, '+');
243 | break;
244 | case "POST":
245 | _data = _params.join('&');
246 | break;
247 | default:
248 | return;
249 | }
250 |
251 | xhr.open(method, uri);
252 |
253 | // TODO: Better error handling
254 | xhr.onreadystatechange = function() {
255 | if (xhr.readyState == 4) {
256 | var reply;
257 |
258 | try {
259 | reply = JSON.parse(xhr.responseText);
260 | }
261 | catch (e) {
262 | reply = null;
263 | }
264 |
265 | callback(reply);
266 | }
267 | };
268 |
269 | xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
270 | xhr.setRequestHeader("Pragma", "no-cache"); // The cache is a lie!
271 | xhr.send(_data || null);
272 | };
273 |
--------------------------------------------------------------------------------
/js/md5.js:
--------------------------------------------------------------------------------
1 | /*
2 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
3 | * Digest Algorithm, as defined in RFC 1321.
4 | * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
5 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
6 | * Distributed under the BSD License
7 | * See http://pajhome.org.uk/crypt/md5 for more info.
8 | */
9 |
10 | /*
11 | * Configurable variables. You may need to tweak these to be compatible with
12 | * the server-side, but the defaults work in most cases.
13 | */
14 | var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
15 | var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
16 |
17 | /*
18 | * These are the functions you'll usually want to call
19 | * They take string arguments and return either hex or base-64 encoded strings
20 | */
21 | function hex_md5(s) { return rstr2hex(rstr_md5(str2rstr_utf8(s))); }
22 | function b64_md5(s) { return rstr2b64(rstr_md5(str2rstr_utf8(s))); }
23 | function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); }
24 | function hex_hmac_md5(k, d)
25 | { return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
26 | function b64_hmac_md5(k, d)
27 | { return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
28 | function any_hmac_md5(k, d, e)
29 | { return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); }
30 |
31 | /*
32 | * Perform a simple self-test to see if the VM is working
33 | */
34 | function md5_vm_test()
35 | {
36 | return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72";
37 | }
38 |
39 | /*
40 | * Calculate the MD5 of a raw string
41 | */
42 | function rstr_md5(s)
43 | {
44 | return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
45 | }
46 |
47 | /*
48 | * Calculate the HMAC-MD5, of a key and some data (raw strings)
49 | */
50 | function rstr_hmac_md5(key, data)
51 | {
52 | var bkey = rstr2binl(key);
53 | if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);
54 |
55 | var ipad = Array(16), opad = Array(16);
56 | for(var i = 0; i < 16; i++)
57 | {
58 | ipad[i] = bkey[i] ^ 0x36363636;
59 | opad[i] = bkey[i] ^ 0x5C5C5C5C;
60 | }
61 |
62 | var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
63 | return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
64 | }
65 |
66 | /*
67 | * Convert a raw string to a hex string
68 | */
69 | function rstr2hex(input)
70 | {
71 | try { hexcase } catch(e) { hexcase=0; }
72 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
73 | var output = "";
74 | var x;
75 | for(var i = 0; i < input.length; i++)
76 | {
77 | x = input.charCodeAt(i);
78 | output += hex_tab.charAt((x >>> 4) & 0x0F)
79 | + hex_tab.charAt( x & 0x0F);
80 | }
81 | return output;
82 | }
83 |
84 | /*
85 | * Convert a raw string to a base-64 string
86 | */
87 | function rstr2b64(input)
88 | {
89 | try { b64pad } catch(e) { b64pad=''; }
90 | var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
91 | var output = "";
92 | var len = input.length;
93 | for(var i = 0; i < len; i += 3)
94 | {
95 | var triplet = (input.charCodeAt(i) << 16)
96 | | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0)
97 | | (i + 2 < len ? input.charCodeAt(i+2) : 0);
98 | for(var j = 0; j < 4; j++)
99 | {
100 | if(i * 8 + j * 6 > input.length * 8) output += b64pad;
101 | else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
102 | }
103 | }
104 | return output;
105 | }
106 |
107 | /*
108 | * Convert a raw string to an arbitrary string encoding
109 | */
110 | function rstr2any(input, encoding)
111 | {
112 | var divisor = encoding.length;
113 | var i, j, q, x, quotient;
114 |
115 | /* Convert to an array of 16-bit big-endian values, forming the dividend */
116 | var dividend = Array(Math.ceil(input.length / 2));
117 | for(i = 0; i < dividend.length; i++)
118 | {
119 | dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
120 | }
121 |
122 | /*
123 | * Repeatedly perform a long division. The binary array forms the dividend,
124 | * the length of the encoding is the divisor. Once computed, the quotient
125 | * forms the dividend for the next step. All remainders are stored for later
126 | * use.
127 | */
128 | var full_length = Math.ceil(input.length * 8 /
129 | (Math.log(encoding.length) / Math.log(2)));
130 | var remainders = Array(full_length);
131 | for(j = 0; j < full_length; j++)
132 | {
133 | quotient = Array();
134 | x = 0;
135 | for(i = 0; i < dividend.length; i++)
136 | {
137 | x = (x << 16) + dividend[i];
138 | q = Math.floor(x / divisor);
139 | x -= q * divisor;
140 | if(quotient.length > 0 || q > 0)
141 | quotient[quotient.length] = q;
142 | }
143 | remainders[j] = x;
144 | dividend = quotient;
145 | }
146 |
147 | /* Convert the remainders to the output string */
148 | var output = "";
149 | for(i = remainders.length - 1; i >= 0; i--)
150 | output += encoding.charAt(remainders[i]);
151 |
152 | return output;
153 | }
154 |
155 | /*
156 | * Encode a string as utf-8.
157 | * For efficiency, this assumes the input is valid utf-16.
158 | */
159 | function str2rstr_utf8(input)
160 | {
161 | var output = "";
162 | var i = -1;
163 | var x, y;
164 |
165 | while(++i < input.length)
166 | {
167 | /* Decode utf-16 surrogate pairs */
168 | x = input.charCodeAt(i);
169 | y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
170 | if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF)
171 | {
172 | x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);
173 | i++;
174 | }
175 |
176 | /* Encode output as utf-8 */
177 | if(x <= 0x7F)
178 | output += String.fromCharCode(x);
179 | else if(x <= 0x7FF)
180 | output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F),
181 | 0x80 | ( x & 0x3F));
182 | else if(x <= 0xFFFF)
183 | output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),
184 | 0x80 | ((x >>> 6 ) & 0x3F),
185 | 0x80 | ( x & 0x3F));
186 | else if(x <= 0x1FFFFF)
187 | output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),
188 | 0x80 | ((x >>> 12) & 0x3F),
189 | 0x80 | ((x >>> 6 ) & 0x3F),
190 | 0x80 | ( x & 0x3F));
191 | }
192 | return output;
193 | }
194 |
195 | /*
196 | * Encode a string as utf-16
197 | */
198 | function str2rstr_utf16le(input)
199 | {
200 | var output = "";
201 | for(var i = 0; i < input.length; i++)
202 | output += String.fromCharCode( input.charCodeAt(i) & 0xFF,
203 | (input.charCodeAt(i) >>> 8) & 0xFF);
204 | return output;
205 | }
206 |
207 | function str2rstr_utf16be(input)
208 | {
209 | var output = "";
210 | for(var i = 0; i < input.length; i++)
211 | output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF,
212 | input.charCodeAt(i) & 0xFF);
213 | return output;
214 | }
215 |
216 | /*
217 | * Convert a raw string to an array of little-endian words
218 | * Characters >255 have their high-byte silently ignored.
219 | */
220 | function rstr2binl(input)
221 | {
222 | var output = Array(input.length >> 2);
223 | for(var i = 0; i < output.length; i++)
224 | output[i] = 0;
225 | for(var i = 0; i < input.length * 8; i += 8)
226 | output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32);
227 | return output;
228 | }
229 |
230 | /*
231 | * Convert an array of little-endian words to a string
232 | */
233 | function binl2rstr(input)
234 | {
235 | var output = "";
236 | for(var i = 0; i < input.length * 32; i += 8)
237 | output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF);
238 | return output;
239 | }
240 |
241 | /*
242 | * Calculate the MD5 of an array of little-endian words, and a bit length.
243 | */
244 | function binl_md5(x, len)
245 | {
246 | /* append padding */
247 | x[len >> 5] |= 0x80 << ((len) % 32);
248 | x[(((len + 64) >>> 9) << 4) + 14] = len;
249 |
250 | var a = 1732584193;
251 | var b = -271733879;
252 | var c = -1732584194;
253 | var d = 271733878;
254 |
255 | for(var i = 0; i < x.length; i += 16)
256 | {
257 | var olda = a;
258 | var oldb = b;
259 | var oldc = c;
260 | var oldd = d;
261 |
262 | a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
263 | d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
264 | c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
265 | b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
266 | a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
267 | d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
268 | c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
269 | b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
270 | a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
271 | d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
272 | c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
273 | b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
274 | a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
275 | d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
276 | c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
277 | b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
278 |
279 | a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
280 | d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
281 | c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
282 | b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
283 | a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
284 | d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
285 | c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
286 | b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
287 | a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
288 | d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
289 | c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
290 | b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
291 | a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
292 | d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
293 | c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
294 | b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
295 |
296 | a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
297 | d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
298 | c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
299 | b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
300 | a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
301 | d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
302 | c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
303 | b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
304 | a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
305 | d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
306 | c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
307 | b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
308 | a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
309 | d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
310 | c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
311 | b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
312 |
313 | a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
314 | d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
315 | c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
316 | b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
317 | a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
318 | d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
319 | c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
320 | b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
321 | a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
322 | d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
323 | c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
324 | b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
325 | a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
326 | d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
327 | c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
328 | b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
329 |
330 | a = safe_add(a, olda);
331 | b = safe_add(b, oldb);
332 | c = safe_add(c, oldc);
333 | d = safe_add(d, oldd);
334 | }
335 | return Array(a, b, c, d);
336 | }
337 |
338 | /*
339 | * These functions implement the four basic operations the algorithm uses.
340 | */
341 | function md5_cmn(q, a, b, x, s, t)
342 | {
343 | return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
344 | }
345 | function md5_ff(a, b, c, d, x, s, t)
346 | {
347 | return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
348 | }
349 | function md5_gg(a, b, c, d, x, s, t)
350 | {
351 | return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
352 | }
353 | function md5_hh(a, b, c, d, x, s, t)
354 | {
355 | return md5_cmn(b ^ c ^ d, a, b, x, s, t);
356 | }
357 | function md5_ii(a, b, c, d, x, s, t)
358 | {
359 | return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
360 | }
361 |
362 | /*
363 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally
364 | * to work around bugs in some JS interpreters.
365 | */
366 | function safe_add(x, y)
367 | {
368 | var lsw = (x & 0xFFFF) + (y & 0xFFFF);
369 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
370 | return (msw << 16) | (lsw & 0xFFFF);
371 | }
372 |
373 | /*
374 | * Bitwise rotate a 32-bit number to the left.
375 | */
376 | function bit_rol(num, cnt)
377 | {
378 | return (num << cnt) | (num >>> (32 - cnt));
379 | }
--------------------------------------------------------------------------------
/js/jquery-1.7.min.js:
--------------------------------------------------------------------------------
1 | /*! jQuery v1.7 jquery.com | jquery.org/license */
2 | (function(a,b){function cA(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cx(a){if(!cm[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cn||(cn=c.createElement("iframe"),cn.frameBorder=cn.width=cn.height=0),b.appendChild(cn);if(!co||!cn.createElement)co=(cn.contentWindow||cn.contentDocument).document,co.write((c.compatMode==="CSS1Compat"?"":"")+""),co.close();d=co.createElement(a),co.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cn)}cm[a]=e}return cm[a]}function cw(a,b){var c={};f.each(cs.concat.apply([],cs.slice(0,b)),function(){c[this]=a});return c}function cv(){ct=b}function cu(){setTimeout(cv,0);return ct=f.now()}function cl(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ck(){try{return new a.XMLHttpRequest}catch(b){}}function ce(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bB(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function br(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bi,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bq(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bp(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bp)}function bp(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bo(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bn(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bm(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(){return!0}function M(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=/-([a-z]|[0-9])/ig,x=/^-ms-/,y=function(a,b){return(b+"").toUpperCase()},z=d.userAgent,A,B,C,D=Object.prototype.toString,E=Object.prototype.hasOwnProperty,F=Array.prototype.push,G=Array.prototype.slice,H=String.prototype.trim,I=Array.prototype.indexOf,J={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7",length:0,size:function(){return this.length},toArray:function(){return G.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?F.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),B.add(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(G.apply(this,arguments),"slice",G.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:F,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;B.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!B){B=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",C,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",C),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&K()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return a!=null&&m.test(a)&&!isNaN(a)},type:function(a){return a==null?String(a):J[D.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!E.call(a,"constructor")&&!E.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||E.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(x,"ms-").replace(w,y)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
a",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,unknownElems:!!a.getElementsByTagName("nav").length,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",enctype:!!c.createElement("form").enctype,submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.lastChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},m&&f.extend(p,{position:"absolute",left:"-999px",top:"-999px"});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="
t
",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;f(function(){var a,b,d,e,g,h,i=1,j="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",l="visibility:hidden;border:0;",n="style='"+j+"border:5px solid #000;padding:0;'",p="