From 1258d9bd6cbb321f666f2858d4d502ba18f396d5 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Thu, 2 May 2013 23:22:57 -0400 Subject: [PATCH 01/74] Made the display pretty with room for controls. Organized code in folders. --- background.html | 10 ------ css/popup.css | 4 +-- html/background.html | 10 ++++++ .../lastfm_callback.html | 2 +- html/popup.html | 35 +++++++++++++++++++ js/background.js | 12 +++---- js/contentscript.js | 7 ++-- js/popup.js | 28 ++++++++------- manifest.json | 20 +++-------- popup.html | 35 ------------------- 10 files changed, 79 insertions(+), 84 deletions(-) delete mode 100644 background.html create mode 100644 html/background.html rename lastfm_callback.html => html/lastfm_callback.html (79%) create mode 100644 html/popup.html delete mode 100644 popup.html diff --git a/background.html b/background.html deleted file mode 100644 index 2e7e715..0000000 --- a/background.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - \ No newline at end of file diff --git a/css/popup.css b/css/popup.css index 8244bfc..038e3a5 100644 --- a/css/popup.css +++ b/css/popup.css @@ -13,7 +13,7 @@ body { font-family: "Helvetica", "Arial", sans-serif; } -p { +span { line-height: 100%; padding: 0; margin: 0; @@ -48,7 +48,7 @@ a:hover { } #artist { - font-size: 18px; + font-weight: bold; } #track { diff --git a/html/background.html b/html/background.html new file mode 100644 index 0000000..9bce66a --- /dev/null +++ b/html/background.html @@ -0,0 +1,10 @@ + + + + + diff --git a/lastfm_callback.html b/html/lastfm_callback.html similarity index 79% rename from lastfm_callback.html rename to html/lastfm_callback.html index b36954d..a57209b 100644 --- a/lastfm_callback.html +++ b/html/lastfm_callback.html @@ -4,4 +4,4 @@ Copyright (c) 2011 Alexey Savartsov Licensed under the MIT license --> - \ No newline at end of file + diff --git a/html/popup.html b/html/popup.html new file mode 100644 index 0000000..3a1f8ad --- /dev/null +++ b/html/popup.html @@ -0,0 +1,35 @@ + + + + + + + + + + +
+
+ +
+
+ +  -  + + +
+ +
+
+
+
+
+
+
+
+ diff --git a/js/background.js b/js/background.js index 8bf9b87..c47b805 100644 --- a/js/background.js +++ b/js/background.js @@ -9,13 +9,13 @@ var SETTINGS = { api_key: "d00dce85051b7dbcbfcc165eaebfc6d2", api_secret: "bdfcae3563763ece1b6d3dcdd56a7ab8", - callback_file: "lastfm_callback.html", + callback_file: "html/lastfm_callback.html", - main_icon: "img/main-icon.png", - playing_icon: "img/main-icon-playing.png", - paused_icon: "img/main-icon-paused.png", - error_icon: "img/main-icon-error.png", - scrobbling_stopped_icon: "img/main-icon-scrobbling-stopped.png", + main_icon: "../img/main-icon.png", + playing_icon: "../img/main-icon-playing.png", + paused_icon: "../img/main-icon-paused.png", + error_icon: "../img/main-icon-error.png", + scrobbling_stopped_icon: "../img/main-icon-scrobbling-stopped.png", scrobble_threshold: .70 }; diff --git a/js/contentscript.js b/js/contentscript.js index 991104e..a18cb59 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -107,7 +107,10 @@ GoogleMusicParser.prototype._get_song_artist = function() { * @return Image URL or default artwork */ GoogleMusicParser.prototype._get_song_cover = function() { - return ("http:" + $("#playingAlbumArt").attr("src")); + var albumImg = $("#playingAlbumArt").attr("src"); + if (albumImg) + return ("http:" + albumImg); + return null; }; /** @@ -124,4 +127,4 @@ var port = chrome.extension.connect({name: "cloudplayer"}); window.setInterval(function() { port.postMessage(new Player(new GoogleMusicParser())); }, -10000); +5000); diff --git a/js/popup.js b/js/popup.js index 889a19d..36e74db 100644 --- a/js/popup.js +++ b/js/popup.js @@ -22,9 +22,10 @@ $(document).ready(function() { function render_song() { if(bp.player.song) { + console.log(bp.player.song.cover); $("#artist").text(bp.player.song.artist); $("#track").text(bp.player.song.title); - $("#cover").attr({ src: bp.player.song.cover || "img/defaultcover.png", width: "60", height: "60" }); + $("#cover").attr({ src: bp.player.song.cover || "../img/defaultcover.png", width: "60", height: "60" }); if(bp.lastfm_api.session.name && bp.lastfm_api.session.key) { render_love_button(); @@ -35,17 +36,18 @@ function render_song() { } else { $("#song").addClass("nosong"); - $("#artist").text("Nothing is playing"); - $("#track").html(''); - $("#track a") - .attr({ - href: "http://play.google.com/music/listen", - target: "_blank" - }) - .text("Go to Google Music"); - $("#cover ").attr({ src: "img/defaultcover.png" }); + $("#artist").text(""); + $("#track").html(''); + $("#cover ").attr({ src: "../img/defaultcover.png" }); $("#lastfm-buttons").hide(); } + // check if we need to marquee + var songElem = $("#now-playing"); + if (songElem.get(0).scrollWidth > songElem.width()) { + songElem.attr('scrollamount', '1'); + } else { + songElem.attr('scrollamount', '0'); + } } /** @@ -98,7 +100,7 @@ function render_auth_link() { * Renders the love button */ function render_love_button() { - $("#love-button").html(''); + $("#love-button").html(''); bp.lastfm_api.is_track_loved(bp.player.song.title, bp.player.song.artist, @@ -166,7 +168,7 @@ function on_love() { } }); - $("#love-button").html(''); + $("#love-button").html(''); } /** @@ -190,5 +192,5 @@ function on_unlove() { } }); - $("#love-button").html(''); + $("#love-button").html(''); } diff --git a/manifest.json b/manifest.json index 00f2ef6..8b7d762 100644 --- a/manifest.json +++ b/manifest.json @@ -7,7 +7,7 @@ "version" : "1.2.8", "manifest_version": 2, "background": { - "page": "background.html", + "page": "html/background.html", "persistent": true }, "name" : "Google Play Scrobbler", @@ -19,33 +19,23 @@ }, "description" : "Scrobbles songs from Google Play to Last.fm", "browser_action" : { - "default_popup" : "popup.html", + "default_popup" : "html/popup.html", "default_icon" : "img/main-icon.png" }, "content_scripts" : [ { "matches" : [ - "http://play.google.com/music/listen*", - "https://play.google.com/music/listen*" - ], - "js" : [ - "js/inject.js" - ] - }, - { - "matches" : [ - "http://play.google.com/music/listen*", - "https://play.google.com/music/listen*" + "*://play.google.com/music/listen*" ], "js" : [ + "js/inject.js", "js/contentscript.js" ] }, { "run_at" : "document_start", "matches" : [ - "http://play.google.com/music/listen*", - "https://play.google.com/music/listen*" + "*://play.google.com/music/listen*" ], "js" : [ "js/jquery-1.7.min.js" diff --git a/popup.html b/popup.html deleted file mode 100644 index 3818a5f..0000000 --- a/popup.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - -
-
- -
-
-
-

-

-
-
- -
-
-
-
-
-
-
-
- From 4e6a0546f441fd70ca4ad6bc0abfb63e3d7c6417 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Tue, 7 May 2013 00:53:09 -0400 Subject: [PATCH 02/74] Supports songs over an hour long --- img/defaultcover.png | Bin 3829 -> 60637 bytes js/background.js | 17 ++++++++--------- js/contentscript.js | 21 +++++++++++++-------- manifest.json | 2 +- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/img/defaultcover.png b/img/defaultcover.png index 1634c592a2d23c40872ad5413b99fc78edab3489..304419b47fd99780ebef84ac8dd6807712e0643e 100644 GIT binary patch literal 60637 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 JlQ0bUzX0nQltcgk literal 3829 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 player.song.position) - if(now_playing_sent && !scrobbled) { // Scrobble this song lastfm_api.scrobble(_p.song.title, @@ -89,7 +84,8 @@ function port_on_message(message) { scrobbled = true; now_playing_sent = false; } - else { + else + { if(response.error == 9) { // Session expired clear_session(); @@ -101,7 +97,8 @@ function port_on_message(message) { }); } } - else { + else + { // Set now playing status // TODO: Maybe there is no need to do it so frequent? lastfm_api.now_playing(_p.song.title, @@ -118,13 +115,15 @@ function port_on_message(message) { // Save player state player = _p; // TODO: Save here? } - else { + else + { // The player is paused chrome.browserAction.setIcon({ 'path': SETTINGS.paused_icon }); } } - else { + else + { chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); player = {}; scrobbled = false; diff --git a/js/contentscript.js b/js/contentscript.js index a18cb59..8b219ff 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -58,12 +58,15 @@ GoogleMusicParser.prototype._get_is_playing = function() { GoogleMusicParser.prototype._get_song_position = function() { var _time = $("#currentTime").text(); _time = $.trim(_time).split(':'); - if(_time.length == 2) { - return (parseInt(_time[0], 10) * 60 + parseInt(_time[1], 10)); + if(_time.length == 2) + { + return (parseInt(_time[0]) * 60 + parseInt(_time[1])); } - else { - return 0; + else if (_time.length == 3) + { + return (parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2])); } + return null; }; /** @@ -75,11 +78,13 @@ GoogleMusicParser.prototype._get_song_time = function() { var _time = $("#duration").text(); _time = $.trim(_time).split(':'); if(_time.length == 2) { - return (parseInt(_time[0], 10) * 60 + parseInt(_time[1], 10)); - } - else { - return 0; + return (parseInt(_time[0]) * 60 + parseInt(_time[1])); + } + else if (_time.length == 3) + { + return (parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2])); } + return null; }; /** diff --git a/manifest.json b/manifest.json index 8b7d762..4709650 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.2.8", + "version" : "1.2.9", "manifest_version": 2, "background": { "page": "html/background.html", From a6e12e7fb8400ff3e73be40c983c9be342ee4221 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sat, 18 May 2013 10:34:42 -0400 Subject: [PATCH 03/74] Support new Google Play interface. --- js/contentscript.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/contentscript.js b/js/contentscript.js index 8b219ff..7d32bc5 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -103,7 +103,7 @@ GoogleMusicParser.prototype._get_song_title = function() { * @return Song artist */ GoogleMusicParser.prototype._get_song_artist = function() { - return $("#player-artist div").text(); + return $("#player-artist").text(); }; /** From 2cfa0fe4883efbae4a7c5e583d23cc78effe6177 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sat, 18 May 2013 12:14:56 -0400 Subject: [PATCH 04/74] Scrobbles album. Continues to submit now playing after scrobble. --- css/popup.css | 2 ++ js/background.js | 43 ++++++++++++++++++------------------------- js/contentscript.js | 2 +- js/popup.js | 5 ++--- manifest.json | 2 +- 5 files changed, 24 insertions(+), 30 deletions(-) diff --git a/css/popup.css b/css/popup.css index 038e3a5..89d6b2d 100644 --- a/css/popup.css +++ b/css/popup.css @@ -33,6 +33,8 @@ a:hover { #cover { border: none; + width: 60px; + height: 60px; } #song.nosong { diff --git a/js/background.js b/js/background.js index 23caa86..5888452 100644 --- a/js/background.js +++ b/js/background.js @@ -21,7 +21,6 @@ var SETTINGS = { }; var player = {}; // Previous player state -var now_playing_sent = false; var scrobbled = false; var lastfm_api = new LastFM(SETTINGS.api_key, SETTINGS.api_secret); @@ -43,11 +42,11 @@ chrome.extension.onConnect.addListener(port_on_connect); * Content script has connected to the extension */ function port_on_connect(port) { - console.assert(port.name == "cloudplayer"); - - // Connect another port event handlers - port.onMessage.addListener(port_on_message); - port.onDisconnect.addListener(port_on_disconnect); + if (port.name == "cloudplayer") + { + port.onMessage.addListener(port_on_message); + port.onDisconnect.addListener(port_on_disconnect); + } } /** @@ -71,7 +70,7 @@ function port_on_message(message) { var time_to_scrobble = _p.song.time * SETTINGS.scrobble_threshold - _p.song.position; if(time_to_scrobble <= 0) { - if(now_playing_sent && !scrobbled) { + if(!scrobbled) { // Scrobble this song lastfm_api.scrobble(_p.song.title, /* Song start time */ @@ -80,9 +79,7 @@ function port_on_message(message) { _p.song.album, function(response) { if(!response.error) { - // Song was scrobled, waiting for the next song scrobbled = true; - now_playing_sent = false; } else { @@ -99,19 +96,17 @@ function port_on_message(message) { } else { - // Set now playing status - // TODO: Maybe there is no need to do it so frequent? - lastfm_api.now_playing(_p.song.title, - _p.song.artist, - _p.song.album, - function(response) { - // TODO: - }); - - now_playing_sent = true; scrobbled = false; } - + + lastfm_api.now_playing(_p.song.title, + _p.song.artist, + _p.song.album, + function(response) { + // TODO: + } + ); + // Save player state player = _p; // TODO: Save here? } @@ -127,17 +122,15 @@ function port_on_message(message) { chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); player = {}; scrobbled = false; - now_playing_sent = false; } } - /** - * Content script has disconnected - */ +/** +* Content script has disconnected +*/ function port_on_disconnect() { player = {}; // Clear player state scrobbled = false; - now_playing_sent = false; chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); } diff --git a/js/contentscript.js b/js/contentscript.js index 7d32bc5..c6fe8ec 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -124,7 +124,7 @@ GoogleMusicParser.prototype._get_song_cover = function() { * @return Album name or null */ GoogleMusicParser.prototype._get_song_album = function() { - return null; + return $("#playerSongInfo .player-artist-album-wrapper .player-album").text(); }; var port = chrome.extension.connect({name: "cloudplayer"}); diff --git a/js/popup.js b/js/popup.js index 36e74db..2541ed3 100644 --- a/js/popup.js +++ b/js/popup.js @@ -22,11 +22,10 @@ $(document).ready(function() { function render_song() { if(bp.player.song) { - console.log(bp.player.song.cover); $("#artist").text(bp.player.song.artist); $("#track").text(bp.player.song.title); - $("#cover").attr({ src: bp.player.song.cover || "../img/defaultcover.png", width: "60", height: "60" }); - + $("#cover").attr({ src: bp.player.song.cover || "../img/defaultcover.png", + alt:bp.player.song.album}); if(bp.lastfm_api.session.name && bp.lastfm_api.session.key) { render_love_button(); } diff --git a/manifest.json b/manifest.json index 4709650..639f033 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.2.9", + "version" : "1.3.1", "manifest_version": 2, "background": { "page": "html/background.html", From 076176b9837c11b0b93314dad949aacffe74eccf Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sat, 18 May 2013 17:26:22 -0400 Subject: [PATCH 05/74] Play link switches to an existing Google Play tab if one is already open. --- css/popup.css | 6 ++++++ html/popup.html | 2 +- js/popup.js | 32 ++++++++++++++++++++++++++++++++ manifest.json | 3 ++- 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/css/popup.css b/css/popup.css index 89d6b2d..f6e4a80 100644 --- a/css/popup.css +++ b/css/popup.css @@ -29,12 +29,18 @@ a:hover { #cover-box { float: left; + border-color: #FFC200; + border-width: 2px; + border-style: solid; + border-radius: 5px; } #cover { border: none; width: 60px; height: 60px; + cursor: hand; + cursor: pointer; } #song.nosong { diff --git a/html/popup.html b/html/popup.html index 3a1f8ad..eb707c0 100644 --- a/html/popup.html +++ b/html/popup.html @@ -15,7 +15,7 @@ diff --git a/js/contentscript.js b/js/contentscript.js index 88b3d64..532099d 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -47,9 +47,12 @@ GoogleMusicParser.prototype._get_has_song = function() { * @return true if song is playing, false if song is paused */ GoogleMusicParser.prototype._get_is_playing = function() { - return $(".player-middle button[data-id='play-pause']").hasClass("playing"); + return is_playing(); }; +function is_playing() { + return $(".player-middle button[data-id='play-pause']").hasClass("playing"); +} /** * Get current song playing position * @@ -133,3 +136,23 @@ window.setInterval(function() { port.postMessage(new Player(new GoogleMusicParser())); }, 5000); + + +chrome.extension.onMessage.addListener(toggle_play); +chrome.extension.onMessage.addListener(next_song); + +function toggle_play(request, sndr, callback) { + if (request.cmd == "toggle_play") { + $(".player-middle button[data-id='play-pause']").click(); + port.postMessage(new Player(new GoogleMusicParser())); + callback(is_playing()); + } +} + +function next_song(request, sndr, callback) { + if (request.cmd == "next_song") { + $(".player-middle button[data-id='forward']").click(); + port.postMessage(new Player(new GoogleMusicParser())); + callback(new Player(new GoogleMusicParser())); + } +} diff --git a/js/popup.js b/js/popup.js index 098d843..18ea817 100644 --- a/js/popup.js +++ b/js/popup.js @@ -15,52 +15,96 @@ $(document).ready(function() { render_auth_link(); }); -function FindPlayTab(callback) { - chrome.windows.getAll( - {populate: true}, - function(windows) { - var pattern = 'https?\:\/\/play\.google\.com\/music\/listen.*'; - for (var window = 0; window < windows.length; window++) { - for (var i = 0; i < windows[window].tabs.length; i++) { - if (windows[window].tabs[i].url.match(pattern)) { - callback(windows[window].tabs[i].id) - return; - } - } +function find_play_tab(callback) { + chrome.tabs.query({url: '*://play.google.com/music/listen*'}, + function(tabs) { + if (tabs.length > 0) { + callback(tabs[0]); + } else { + callback(null); + } + }); +} + +function open_play_tab() { + find_play_tab( + function(tab) { + if (tab) { + chrome.tabs.update(tab.id, {selected: true}); + } else { + chrome.tabs.create({url: 'https://play.google.com/music/listen', + selected: true}); } - callback(null); - }); + } + ); +} + +function toggle_play() { + find_play_tab( + function(tab) { + chrome.tabs.sendMessage(tab.id, {cmd: "toggle_play"}, toggle_play_btn); + } + ); } -function openPlayTab() { - FindPlayTab(function(tab_id) { - if (tab_id) { - chrome.tabs.update(tab_id, {selected: true}); - } else { - chrome.tabs.create({url: 'https://play.google.com/music/listen', - selected: true}); +function next_song() { + find_play_tab( + function(tab) { + chrome.tabs.sendMessage(tab.id, {cmd: "next_song"}, update_song_info); } - }); + ); } function set_play_link() { - $("#cover").click(openPlayTab); + $("#cover").click(open_play_tab); } /* Render functions */ +/* +* TODO +* - Update song info when restarting playlist +* - Reset marquee position when changing songs +* - Get real button icons +* - Cache player elements in a single place in parser. +*/ +function update_song_info(player) { + $("#artist").text(player.song.artist); + $("#track").text(player.song.title); + $("#cover").attr({ src: player.song.cover || "../img/defaultcover.png", + alt:player.song.album}); + $("#album").text(player.song.album); + // check if we need to marquee + var songElem = $("#now-playing"); + if (songElem.get(0).scrollWidth > songElem.width() + 10) { + songElem.attr('scrollamount', '1'); + } else { + songElem.attr('scrollamount', '0'); + } + + if(bp.lastfm_api.session.name && bp.lastfm_api.session.key) { + render_love_button(); + toggle_play_btn(player.is_playing); + } +} + +function toggle_play_btn(is_playing) { + if (is_playing) { + $("#play-pause-btn").text("Pause"); + } else { + $("#play-pause-btn").text("Play"); + } +} /** * Renders current song details */ function render_song() { - if(bp.player.song) + if(bp.player.has_song) { - $("#artist").text(bp.player.song.artist); - $("#track").text(bp.player.song.title); - $("#cover").attr({ src: bp.player.song.cover || "../img/defaultcover.png", - alt:bp.player.song.album}); - $("#album").text(bp.player.song.album); + update_song_info(bp.player); + if(bp.lastfm_api.session.name && bp.lastfm_api.session.key) { - render_love_button(); + $("#play-pause-btn").click(toggle_play); + $("#next-btn").click(next_song); } else { $("#lastfm-buttons").hide(); @@ -73,13 +117,6 @@ function render_song() { $("#cover ").attr({ src: "../img/defaultcover.png" }); $("#lastfm-buttons").hide(); } - // check if we need to marquee - var songElem = $("#now-playing"); - if (songElem.get(0).scrollWidth > songElem.width() + 10) { - songElem.attr('scrollamount', '1'); - } else { - songElem.attr('scrollamount', '0'); - } } /** From bc12f40d8b3258d001df149a7cd010b32b871c8c Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Thu, 27 Jun 2013 15:56:06 -0400 Subject: [PATCH 14/74] Finished player control buttons --- README.md | 6 +++++ css/popup.css | 24 ++++++++++++++++++++ html/popup.html | 6 +++-- img/next.png | Bin 0 -> 1274 bytes img/play_pause.png | Bin 0 -> 326 bytes js/contentscript.js | 9 +++----- js/popup.js | 54 +++++++++++++++++++++++++------------------- manifest.json | 2 +- 8 files changed, 69 insertions(+), 32 deletions(-) create mode 100644 img/next.png create mode 100644 img/play_pause.png diff --git a/README.md b/README.md index 0118fce..da85bd2 100755 --- a/README.md +++ b/README.md @@ -30,6 +30,11 @@ Thanks to contributors: [Caleb Brown](https://github.com/kayluhb) +Icons +======= +www.glyphicons.com +[CC by](http://creativecommons.org/licenses/by/3.0/us/) + License ======= @@ -55,3 +60,4 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/css/popup.css b/css/popup.css index 6819fa7..2b03d4f 100644 --- a/css/popup.css +++ b/css/popup.css @@ -117,6 +117,30 @@ a.notloved { background-position: 0 -16px; } +a.play, a.pause { + display: inline-block; + width: 16px; + height: 18; + background-image: url('../img/play_pause.png'); + background-repeat: no-repeat; +} + +a.play { + background-position: 0 0; +} + +a.pause { + background-position: 0 -18px; +} + +#next-btn { + display: inline-block; + width: 25px; + height: 18px; + background-image: url('../img/next.png'); + background-repeat: no-repeat; +} + .clear { clear: both; } diff --git a/html/popup.html b/html/popup.html index 659f40c..6dd803a 100644 --- a/html/popup.html +++ b/html/popup.html @@ -25,8 +25,10 @@
- Pause - Next +     + +     +
diff --git a/img/next.png b/img/next.png new file mode 100644 index 0000000000000000000000000000000000000000..6dd545bfafb32d1f92a4e695a77302cd5be40b1e GIT binary patch literal 1274 zcmaJ>Pi)&%7;E)kzuD+Ofw1Kv7MousPS~JURujjcck}OnI1SBJ)0->pE z$P!IiE*X|)=qo}g50*PP38Wg%rC7ZJ1^H1-H1eKH;RF=iNLdU+wsC&@m$`?5m%geuVJ^6 zL;)DKLiHZNK!7qGv=Yrxr0GL?l8Y5f)U~1^Sr(Gm5!O9XvT^1V@IS0oLZ&7ZO+hjs z%TqG0qY{t!cr>G$PezjO<#VF?cG#f>DY$ss?)ij0-SG!;*K?vUUFGu`^gRj~N=I1! zgvX)!#L$i{=_cgj@e0zz@e<>ZZf-(XbWORc(e?hUW@^ps*Y?)d*56Ouo;f@`?124R z^PhukW0)(NBv4ygRY5duI(ie%UO1D%^ZQzx@h$ z<~Q$?{kiH7??3X^LF@XPzhic`I?m7Fmzq__*zax2wLbs(RatghV#M{WpKttSxV7=y{VDq|U)6tX@$UD! zkHWFvpB*=h{TOInCeyB0bLrb*oAY%(U*p#&SKZ5u6tjI5Vi8c`*=la<8~pxw)Q?A8 z+ua^MTYdN5Q~7}3pZmj0>K#r!nxP_mG}A$?>vDsS$a2P+8h*@1fpgeW7FqC~a4D3U l(D}$jfq;T8`vtFmG5anF5u4QWqaNr@22WQ%mvv4FO#s+cfAatU literal 0 HcmV?d00001 diff --git a/js/contentscript.js b/js/contentscript.js index 532099d..f441552 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -47,12 +47,9 @@ GoogleMusicParser.prototype._get_has_song = function() { * @return true if song is playing, false if song is paused */ GoogleMusicParser.prototype._get_is_playing = function() { - return is_playing(); + return $(".player-middle button[data-id='play-pause']").hasClass("playing"); }; -function is_playing() { - return $(".player-middle button[data-id='play-pause']").hasClass("playing"); -} /** * Get current song playing position * @@ -145,7 +142,7 @@ function toggle_play(request, sndr, callback) { if (request.cmd == "toggle_play") { $(".player-middle button[data-id='play-pause']").click(); port.postMessage(new Player(new GoogleMusicParser())); - callback(is_playing()); + callback(); } } @@ -153,6 +150,6 @@ function next_song(request, sndr, callback) { if (request.cmd == "next_song") { $(".player-middle button[data-id='forward']").click(); port.postMessage(new Player(new GoogleMusicParser())); - callback(new Player(new GoogleMusicParser())); + callback(); } } diff --git a/js/popup.js b/js/popup.js index 18ea817..d97b54a 100644 --- a/js/popup.js +++ b/js/popup.js @@ -32,17 +32,28 @@ function open_play_tab() { if (tab) { chrome.tabs.update(tab.id, {selected: true}); } else { - chrome.tabs.create({url: 'https://play.google.com/music/listen', - selected: true}); + chrome.tabs.create({url: + 'https://play.google.com/music/listen', + selected: true}); } } ); } function toggle_play() { + var has_song = bp.player.has_song; find_play_tab( function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: "toggle_play"}, toggle_play_btn); + chrome.tabs.sendMessage(tab.id, {cmd: "toggle_play"}, + function() { + if (has_song) { + toggle_play_btn(); + } else { + update_song_info(); + toggle_play_btn(); + } + } + ); } ); } @@ -50,7 +61,8 @@ function toggle_play() { function next_song() { find_play_tab( function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: "next_song"}, update_song_info); + chrome.tabs.sendMessage(tab.id, {cmd: "next_song"}, + update_song_info); } ); } @@ -59,19 +71,12 @@ function set_play_link() { $("#cover").click(open_play_tab); } /* Render functions */ -/* -* TODO -* - Update song info when restarting playlist -* - Reset marquee position when changing songs -* - Get real button icons -* - Cache player elements in a single place in parser. -*/ -function update_song_info(player) { - $("#artist").text(player.song.artist); - $("#track").text(player.song.title); - $("#cover").attr({ src: player.song.cover || "../img/defaultcover.png", - alt:player.song.album}); - $("#album").text(player.song.album); +function update_song_info() { + $("#artist").text(bp.player.song.artist); + $("#track").text(bp.player.song.title); + $("#cover").attr({ src: bp.player.song.cover || "../img/defaultcover.png", + alt:bp.player.song.album}); + $("#album").text(bp.player.song.album); // check if we need to marquee var songElem = $("#now-playing"); if (songElem.get(0).scrollWidth > songElem.width() + 10) { @@ -82,15 +87,18 @@ function update_song_info(player) { if(bp.lastfm_api.session.name && bp.lastfm_api.session.key) { render_love_button(); - toggle_play_btn(player.is_playing); + toggle_play_btn(); } } -function toggle_play_btn(is_playing) { - if (is_playing) { - $("#play-pause-btn").text("Pause"); +function toggle_play_btn() { + var play_btn = $("#play-pause-btn"); + if (bp.player.is_playing) { + play_btn.removeClass(); + play_btn.addClass("pause"); } else { - $("#play-pause-btn").text("Play"); + play_btn.removeClass(); + play_btn.addClass("play"); } } @@ -100,7 +108,7 @@ function toggle_play_btn(is_playing) { function render_song() { if(bp.player.has_song) { - update_song_info(bp.player); + update_song_info(); if(bp.lastfm_api.session.name && bp.lastfm_api.session.key) { $("#play-pause-btn").click(toggle_play); diff --git a/manifest.json b/manifest.json index c88ebac..c88be2f 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.3.8", + "version" : "1.3.9", "manifest_version": 2, "background": { "page": "html/background.html", From 669622815268aa1878810cd122e83cb66caa6ce6 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Thu, 27 Jun 2013 16:24:33 -0400 Subject: [PATCH 15/74] woops, play/pause toggles icon now when scrobbling. --- js/background.js | 5 ++--- manifest.json | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/js/background.js b/js/background.js index 5888452..30d59a4 100644 --- a/js/background.js +++ b/js/background.js @@ -63,6 +63,8 @@ function port_on_message(message) { player = _p; return; } + // Save player state + player = _p; if(_p.has_song) { if(_p.is_playing) { chrome.browserAction.setIcon({ @@ -106,9 +108,6 @@ function port_on_message(message) { // TODO: } ); - - // Save player state - player = _p; // TODO: Save here? } else { diff --git a/manifest.json b/manifest.json index c88be2f..e221a35 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.3.9", + "version" : "1.4.0", "manifest_version": 2, "background": { "page": "html/background.html", From 4034826eca4d513152129e619187d364ea46a9a3 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Thu, 27 Jun 2013 16:29:03 -0400 Subject: [PATCH 16/74] Removed redundant line --- js/background.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/js/background.js b/js/background.js index 30d59a4..319ff93 100644 --- a/js/background.js +++ b/js/background.js @@ -55,16 +55,17 @@ function port_on_connect(port) { function port_on_message(message) { // Current player state var _p = message; + + // Save player state + player = _p; if(!SETTINGS.scrobble) { chrome.browserAction.setIcon({ 'path': SETTINGS.scrobbling_stopped_icon }); - player = _p; return; } - // Save player state - player = _p; + if(_p.has_song) { if(_p.is_playing) { chrome.browserAction.setIcon({ From f8a8f1e57c63e131a37c48e4c8acf7203fa73550 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Thu, 27 Jun 2013 16:53:26 -0400 Subject: [PATCH 17/74] Restarts playlists correctly when scrobbling and using 'next' button --- js/background.js | 1 - manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/js/background.js b/js/background.js index 319ff93..6a866aa 100644 --- a/js/background.js +++ b/js/background.js @@ -120,7 +120,6 @@ function port_on_message(message) { else { chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); - player = {}; scrobbled = false; } } diff --git a/manifest.json b/manifest.json index e221a35..2efa806 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.4.0", + "version" : "1.4.1", "manifest_version": 2, "background": { "page": "html/background.html", From 6ef1c85bc2998caafb87e0b1bdfa6c85a0fd19db Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Fri, 16 Aug 2013 14:40:12 -0400 Subject: [PATCH 18/74] Working on scrobbling long mixes multiple times. --- js/background.js | 67 ++++++++++++++++++++++++++++-------------------- js/popup.js | 54 +++++++++++++++++++------------------- 2 files changed, 66 insertions(+), 55 deletions(-) diff --git a/js/background.js b/js/background.js index 6a866aa..aac736f 100644 --- a/js/background.js +++ b/js/background.js @@ -17,11 +17,13 @@ var SETTINGS = { error_icon: "../img/main-icon-error.png", scrobbling_stopped_icon: "../img/main-icon-scrobbling-stopped.png", - scrobble_threshold: .70 + scrobble_threshold: .70, + scrobble_interval: 240 // 4 minutes }; var player = {}; // Previous player state var scrobbled = false; +var time_played = 0; var lastfm_api = new LastFM(SETTINGS.api_key, SETTINGS.api_secret); // Load settings from local storage @@ -31,7 +33,7 @@ lastfm_api.session.name = localStorage["session_name"] || null; // This enables scrobbling by default SETTINGS.scrobble = !(localStorage["scrobble"] == "false"); -if(!SETTINGS.scrobble) { +if (!SETTINGS.scrobble) { chrome.browserAction.setIcon({ 'path': SETTINGS.scrobbling_stopped_icon }); } @@ -71,34 +73,22 @@ function port_on_message(message) { chrome.browserAction.setIcon({ 'path': SETTINGS.playing_icon }); - var time_to_scrobble = _p.song.time * SETTINGS.scrobble_threshold - _p.song.position; - if(time_to_scrobble <= 0) { + var scrobble_point = _p.song.time * SETTINGS.scrobble_threshold; + + var time_to_scrobble = scrobble_point - _p.song.position; + if (time_to_scrobble <= 0) { if(!scrobbled) { - // Scrobble this song - lastfm_api.scrobble(_p.song.title, - /* Song start time */ - Math.round(new Date().getTime() / 1000) - _p.song.position, - _p.song.artist, - _p.song.album, - function(response) { - if(!response.error) { - scrobbled = true; - } - else - { - if(response.error == 9) { - // Session expired - clear_session(); - } - - chrome.browserAction.setIcon({ - 'path': SETTINGS.error_icon }); - } - }); + scrobble_song(_p.song.artist, _p.song.album, _p.song.title, + Math.round(new Date().getTime() / 1000) - _p.song.position); } - } - else - { + } else if (time_played >= SETTINGS.scrobble_interval && + (_p.song.position < scrobble_point - SETTINGS.scrobble_interval || + _p.song.position > scrobble_point + SETTINGS.scrobble_interval)) { + time_played = 0; + scrobble_song(_p.song.artist, _p.song.album, _p.song.title, + Math.round(new Date().getTime() / 1000) - SETTINGS.scrobble_interval); + } else { + time_played += 5; scrobbled = false; } @@ -124,6 +114,27 @@ function port_on_message(message) { } } + +function scrobble_song(artist, album, title, time) { + // Scrobble this song + lastfm_api.scrobble(title, time, artist, album, + function(response) { + if(!response.error) { + scrobbled = true; + } + else + { + if(response.error == 9) { + // Session expired + clear_session(); + } + + chrome.browserAction.setIcon({ + 'path': SETTINGS.error_icon }); + } + }); +} + /** * Content script has disconnected */ diff --git a/js/popup.js b/js/popup.js index d97b54a..7217fb3 100644 --- a/js/popup.js +++ b/js/popup.js @@ -40,33 +40,6 @@ function open_play_tab() { ); } -function toggle_play() { - var has_song = bp.player.has_song; - find_play_tab( - function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: "toggle_play"}, - function() { - if (has_song) { - toggle_play_btn(); - } else { - update_song_info(); - toggle_play_btn(); - } - } - ); - } - ); -} - -function next_song() { - find_play_tab( - function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: "next_song"}, - update_song_info); - } - ); -} - function set_play_link() { $("#cover").click(open_play_tab); } @@ -200,6 +173,33 @@ function render_love_button() { /* Event handlers */ +function toggle_play() { + var has_song = bp.player.has_song; + find_play_tab( + function(tab) { + chrome.tabs.sendMessage(tab.id, {cmd: "toggle_play"}, + function() { + if (has_song) { + toggle_play_btn(); + } else { + update_song_info(); + toggle_play_btn(); + } + } + ); + } + ); +} + +function next_song() { + find_play_tab( + function(tab) { + chrome.tabs.sendMessage(tab.id, {cmd: "next_song"}, + update_song_info); + } + ); +} + /** * Turn on/off scrobbling link was clicked */ From c6003a597bcf3ead2704d0542fcd61b2eff16a80 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Fri, 16 Aug 2013 15:35:42 -0400 Subject: [PATCH 19/74] Display player controls when not logged in. Only display scrobble link when logged in. --- css/popup.css | 4 +++ html/popup.html | 4 ++- js/background.js | 6 ++-- js/contentscript.js | 4 ++- js/popup.js | 88 +++++++++++++++++++++------------------------ manifest.json | 2 +- 6 files changed, 54 insertions(+), 54 deletions(-) diff --git a/css/popup.css b/css/popup.css index 2b03d4f..029ca5f 100644 --- a/css/popup.css +++ b/css/popup.css @@ -72,6 +72,7 @@ a:hover { #lastfm-buttons { margin-top: 6px; + float: left; } #bottom { @@ -117,6 +118,9 @@ a.notloved { background-position: 0 -16px; } +#player-controls { + margin-top: 3px; +} a.play, a.pause { display: inline-block; width: 16px; diff --git a/html/popup.html b/html/popup.html index 6dd803a..1bfa8b9 100644 --- a/html/popup.html +++ b/html/popup.html @@ -26,9 +26,11 @@
    +
+
    - +
diff --git a/js/background.js b/js/background.js index 6a866aa..bb77645 100644 --- a/js/background.js +++ b/js/background.js @@ -80,12 +80,10 @@ function port_on_message(message) { Math.round(new Date().getTime() / 1000) - _p.song.position, _p.song.artist, _p.song.album, - function(response) { + function (response) { if(!response.error) { scrobbled = true; - } - else - { + } else { if(response.error == 9) { // Session expired clear_session(); diff --git a/js/contentscript.js b/js/contentscript.js index f441552..be886d0 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -134,7 +134,9 @@ window.setInterval(function() { }, 5000); - +/* +* Listeners for player control buttons +*/ chrome.extension.onMessage.addListener(toggle_play); chrome.extension.onMessage.addListener(next_song); diff --git a/js/popup.js b/js/popup.js index d97b54a..0c1c5ea 100644 --- a/js/popup.js +++ b/js/popup.js @@ -10,8 +10,10 @@ var bp = chrome.extension.getBackgroundPage(); /* Render popup when DOM is ready */ $(document).ready(function() { set_play_link(); - render_scrobble_link(); render_song(); + if (bp.lastfm_api.session.name && bp.lastfm_api.session.key) { + render_scrobble_link(); + } render_auth_link(); }); @@ -40,33 +42,6 @@ function open_play_tab() { ); } -function toggle_play() { - var has_song = bp.player.has_song; - find_play_tab( - function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: "toggle_play"}, - function() { - if (has_song) { - toggle_play_btn(); - } else { - update_song_info(); - toggle_play_btn(); - } - } - ); - } - ); -} - -function next_song() { - find_play_tab( - function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: "next_song"}, - update_song_info); - } - ); -} - function set_play_link() { $("#cover").click(open_play_tab); } @@ -85,10 +60,10 @@ function update_song_info() { songElem.attr('scrollamount', '0'); } - if(bp.lastfm_api.session.name && bp.lastfm_api.session.key) { + if (bp.lastfm_api.session.name && bp.lastfm_api.session.key) { render_love_button(); - toggle_play_btn(); } + toggle_play_btn(); } function toggle_play_btn() { @@ -109,12 +84,9 @@ function render_song() { if(bp.player.has_song) { update_song_info(); - - if(bp.lastfm_api.session.name && bp.lastfm_api.session.key) { - $("#play-pause-btn").click(toggle_play); - $("#next-btn").click(next_song); - } - else { + $("#play-pause-btn").click(toggle_play); + $("#next-btn").click(next_song); + if(!(bp.lastfm_api.session.name && bp.lastfm_api.session.key)) { $("#lastfm-buttons").hide(); } } @@ -124,6 +96,7 @@ function render_song() { $("#track").html(''); $("#cover ").attr({ src: "../img/defaultcover.png" }); $("#lastfm-buttons").hide(); + $("#player-controls").hide(); } } @@ -133,9 +106,7 @@ function render_song() { function render_scrobble_link() { $("#scrobbling").html(''); $("#scrobbling a") - .attr({ - href: "#" - }) + .attr("href", "#") .click(on_toggle_scrobble) .text(bp.SETTINGS.scrobble ? "Stop scrobbling" : "Resume scrobbling"); } @@ -144,8 +115,8 @@ function render_scrobble_link() { * Renders authentication/profile link */ function render_auth_link() { - if(bp.lastfm_api.session.name && bp.lastfm_api.session.key) - { + if (bp.lastfm_api.session.name && bp.lastfm_api.session.key) { + render_scrobble_link(); $("#lastfm-profile").html("Logged in as " + ""); $("#lastfm-profile a:first") .attr({ @@ -161,13 +132,9 @@ function render_auth_link() { }) .click(on_logout) .addClass("logout"); - } - else { + } else { $("#lastfm-profile").html(''); - $("#lastfm-profile a") - .attr({ - href: "#" - }) + $("#lastfm-profile a").attr("href", "#") .click(on_auth) .text("Connect to Last.fm"); } @@ -200,6 +167,33 @@ function render_love_button() { /* Event handlers */ +function toggle_play() { + var has_song = bp.player.has_song; + find_play_tab( + function(tab) { + chrome.tabs.sendMessage(tab.id, {cmd: "toggle_play"}, + function() { + if (has_song) { + toggle_play_btn(); + } else { // if pressing FF on previous song reached end of play queue + update_song_info(); + toggle_play_btn(); + } + } + ); + } + ); +} + +function next_song() { + find_play_tab( + function(tab) { + chrome.tabs.sendMessage(tab.id, {cmd: "next_song"}, + update_song_info); + } + ); +} + /** * Turn on/off scrobbling link was clicked */ diff --git a/manifest.json b/manifest.json index 2efa806..000fc5a 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.4.1", + "version" : "1.4.2", "manifest_version": 2, "background": { "page": "html/background.html", From 48162a729174a79a3540e067cede4daf13b3f8e0 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Fri, 23 Aug 2013 15:59:24 -0400 Subject: [PATCH 20/74] Some work on new scrobbling method. --- js/background.js | 22 +++++++++++++++++----- js/contentscript.js | 1 + 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/js/background.js b/js/background.js index 231c7a5..64fcf0f 100644 --- a/js/background.js +++ b/js/background.js @@ -18,7 +18,9 @@ var SETTINGS = { scrobbling_stopped_icon: "../img/main-icon-scrobbling-stopped.png", scrobble_threshold: .70, - scrobble_interval: 240 // 4 minutes + scrobble_interval: 300, // 5 minutes + // NOTE: This should be exactly the same as the portMessage interval in contentscript.js. + refresh_interval: 5 }; var player = {}; // Previous player state @@ -81,13 +83,24 @@ function port_on_message(message) { var scrobble_point = _p.song.time * SETTINGS.scrobble_threshold; + /* + * Scrobble via passing the 'scrobble_threshold' mark. + */ var time_to_scrobble = scrobble_point - _p.song.position; if (time_to_scrobble <= 0) { - if(!scrobbled) { + if (!scrobbled) { scrobble_song(_p.song.artist, _p.song.album, _p.song.title, Math.round(new Date().getTime() / 1000) - _p.song.position); + time_played = 0; } - } else if (time_played >= SETTINGS.scrobble_interval && + } else { + scrobbled = false; + } + + /* + * Scrobble every 'scrobble_interval' seconds if it's not too close to 'scrobble_threshold'. + */ + if (time_played >= SETTINGS.scrobble_interval && (_p.song.position < scrobble_point - SETTINGS.scrobble_interval || _p.song.position > scrobble_point + SETTINGS.scrobble_interval)) { time_played = 0; @@ -97,8 +110,7 @@ function port_on_message(message) { new Date().getTime() / 1000) - SETTINGS.scrobble_interval ); } else { - time_played += 5; - scrobbled = false; + time_played += SETTINGS.refresh_interval; } lastfm_api.now_playing(_p.song.title, diff --git a/js/contentscript.js b/js/contentscript.js index be886d0..e3c8fd6 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -129,6 +129,7 @@ GoogleMusicParser.prototype._get_song_album = function() { var port = chrome.extension.connect({name: "cloudplayer"}); +// NOTE: If you update this, you must also update SETTINGS.refresh_interval accordingly in background.js. window.setInterval(function() { port.postMessage(new Player(new GoogleMusicParser())); }, From b368c5ab1c974cf23763a624515de99b980c62cd Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Fri, 23 Aug 2013 16:37:20 -0400 Subject: [PATCH 21/74] Scrobbles based solely on time played rather than song position. --- js/background.js | 39 +++++++-------------------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/js/background.js b/js/background.js index 64fcf0f..851d02b 100644 --- a/js/background.js +++ b/js/background.js @@ -24,7 +24,6 @@ var SETTINGS = { }; var player = {}; // Previous player state -var scrobbled = false; var time_played = 0; var curr_song_title = ""; var lastfm_api = new LastFM(SETTINGS.api_key, SETTINGS.api_secret); @@ -80,35 +79,12 @@ function port_on_message(message) { if(_p.is_playing) { chrome.browserAction.setIcon({ 'path': SETTINGS.playing_icon }); - - var scrobble_point = _p.song.time * SETTINGS.scrobble_threshold; - - /* - * Scrobble via passing the 'scrobble_threshold' mark. - */ - var time_to_scrobble = scrobble_point - _p.song.position; - if (time_to_scrobble <= 0) { - if (!scrobbled) { - scrobble_song(_p.song.artist, _p.song.album, _p.song.title, - Math.round(new Date().getTime() / 1000) - _p.song.position); - time_played = 0; - } - } else { - scrobbled = false; - } - - /* - * Scrobble every 'scrobble_interval' seconds if it's not too close to 'scrobble_threshold'. - */ - if (time_played >= SETTINGS.scrobble_interval && - (_p.song.position < scrobble_point - SETTINGS.scrobble_interval || - _p.song.position > scrobble_point + SETTINGS.scrobble_interval)) { - time_played = 0; - console.log("Scrobbled " + _p.song.title); + + if (time_played >= _p.song.time * SETTINGS.scrobble_threshold || + time_played >= SETTINGS.scrobble_interval) { scrobble_song(_p.song.artist, _p.song.album, _p.song.title, - Math.round( - new Date().getTime() / 1000) - SETTINGS.scrobble_interval - ); + Math.round(new Date().getTime() / 1000) - time_played); + time_played = 0; } else { time_played += SETTINGS.refresh_interval; } @@ -129,7 +105,6 @@ function port_on_message(message) { else { chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); - scrobbled = false; } } @@ -139,7 +114,7 @@ function scrobble_song(artist, album, title, time) { lastfm_api.scrobble(title, time, artist, album, function(response) { if(!response.error) { - scrobbled = true; + } else { if(response.error == 9) { // Session expired @@ -157,7 +132,7 @@ function scrobble_song(artist, album, title, time) { */ function port_on_disconnect() { player = {}; // Clear player state - scrobbled = false; + time_played = 0 chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); } From aa93922209bf0d4b91b27ffaa1c395f269bcc0b5 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sat, 24 Aug 2013 18:32:29 -0400 Subject: [PATCH 22/74] Fixed image sprite. Images weren't isolated on Linux client. --- css/popup.css | 2 +- img/play_pause.png | Bin 326 -> 247 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/css/popup.css b/css/popup.css index 029ca5f..e2fd010 100644 --- a/css/popup.css +++ b/css/popup.css @@ -134,7 +134,7 @@ a.play { } a.pause { - background-position: 0 -18px; + background-position: 0 -19px; } #next-btn { diff --git a/img/play_pause.png b/img/play_pause.png index 1aab2441f96da1dfb703a1d9832815727931d4e0..a08f5e735e3dd17404703ab20a93491c747efd9b 100644 GIT binary patch delta 212 zcmV;_04x8-0`~!s7YZc^0ssI2#3ueIkv5KcAxKvl;Wgs>>3cnv|e28vqi0~bMDiFkC*Ir^` zo-U3d7XDK&-=5m!AmS2etia*~LMk9qQN<}C%Kzo6)RmXZ>tAYC9b><@E!X<|=T~Lf zZHW=rw|>6ym*LjNZ}+F{zkF5yvBkUJ>plv{et&k{F!p1hb(u`NUd^R%hi%T+`FxFE zpImh>Gg8d6p+yDRo From 211db532da7852fe6b6df8fe1870d5e3e2058a22 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sun, 25 Aug 2013 15:40:01 -0400 Subject: [PATCH 23/74] Scrobble interval increased to 8 minutes. --- js/background.js | 3 +-- manifest.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/js/background.js b/js/background.js index 851d02b..4551eb8 100644 --- a/js/background.js +++ b/js/background.js @@ -18,7 +18,7 @@ var SETTINGS = { scrobbling_stopped_icon: "../img/main-icon-scrobbling-stopped.png", scrobble_threshold: .70, - scrobble_interval: 300, // 5 minutes + scrobble_interval: 480, // 8 minutes // NOTE: This should be exactly the same as the portMessage interval in contentscript.js. refresh_interval: 5 }; @@ -79,7 +79,6 @@ function port_on_message(message) { if(_p.is_playing) { chrome.browserAction.setIcon({ 'path': SETTINGS.playing_icon }); - if (time_played >= _p.song.time * SETTINGS.scrobble_threshold || time_played >= SETTINGS.scrobble_interval) { scrobble_song(_p.song.artist, _p.song.album, _p.song.title, diff --git a/manifest.json b/manifest.json index d8a056d..e8abe5d 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.4.3", + "version" : "1.4.4", "manifest_version": 2, "background": { "page": "html/background.html", From 1528b64c5ebc45ef8332a0c72da090f9209f4a15 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Mon, 26 Aug 2013 17:00:21 -0400 Subject: [PATCH 24/74] Moved the player controls down a bit. --- css/popup.css | 4 ++-- manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/css/popup.css b/css/popup.css index e2fd010..6c46a2c 100644 --- a/css/popup.css +++ b/css/popup.css @@ -71,7 +71,7 @@ a:hover { } #lastfm-buttons { - margin-top: 6px; + margin-top: 10px; float: left; } @@ -119,7 +119,7 @@ a.notloved { } #player-controls { - margin-top: 3px; + margin-top: 8px; } a.play, a.pause { display: inline-block; diff --git a/manifest.json b/manifest.json index e8abe5d..ce0379a 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.4.4", + "version" : "1.4.5", "manifest_version": 2, "background": { "page": "html/background.html", From a64a2736191cb9293103d8dcf51a2b967f49e1b3 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Tue, 27 Aug 2013 17:26:25 -0400 Subject: [PATCH 25/74] Reset time_played when at beginning of song to prevent over-scrobbling when playing a song on repeat. --- js/background.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/js/background.js b/js/background.js index 4551eb8..20e4078 100644 --- a/js/background.js +++ b/js/background.js @@ -17,7 +17,7 @@ var SETTINGS = { error_icon: "../img/main-icon-error.png", scrobbling_stopped_icon: "../img/main-icon-scrobbling-stopped.png", - scrobble_threshold: .70, + scrobble_threshold: .7, scrobble_interval: 480, // 8 minutes // NOTE: This should be exactly the same as the portMessage interval in contentscript.js. refresh_interval: 5 @@ -63,21 +63,22 @@ function port_on_message(message) { // Save player state player = _p; - if(!SETTINGS.scrobble) { + if (!SETTINGS.scrobble) { chrome.browserAction.setIcon({ 'path': SETTINGS.scrobbling_stopped_icon }); return; } - if(_p.has_song) { - if (_p.song.title != curr_song_title) { + if (_p.has_song) { + if (_p.song.title != curr_song_title || + _p.song.position <= SETTINGS.refresh_interval) { curr_song_title = _p.song.title; time_played = 0; } - if(_p.is_playing) { - chrome.browserAction.setIcon({ + if (_p.is_playing) { + chrome.browserAction.setIcon({ 'path': SETTINGS.playing_icon }); if (time_played >= _p.song.time * SETTINGS.scrobble_threshold || time_played >= SETTINGS.scrobble_interval) { @@ -100,22 +101,20 @@ function port_on_message(message) { chrome.browserAction.setIcon({ 'path': SETTINGS.paused_icon }); } - } - else - { + } else { chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); } } - + function scrobble_song(artist, album, title, time) { // Scrobble this song lastfm_api.scrobble(title, time, artist, album, function(response) { - if(!response.error) { + if (!response.error) { } else { - if(response.error == 9) { + if (response.error == 9) { // Session expired clear_session(); } @@ -177,8 +176,7 @@ function toggle_scrobble() { function get_lastfm_session(token) { lastfm_api.authorize(token, function(response) { // Save session - if(response.session) - { + if (response.session) { localStorage["session_key"] = response.session.key; localStorage["session_name"] = response.session.name; } From b479b190cc1c6c76604948fd0ce5193e95b79fa7 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Tue, 10 Sep 2013 17:00:43 -0400 Subject: [PATCH 26/74] Background page to event page. Use chrome.runtime. Converted the background page to an event page, saving memory when not listening to music. See: http://developer.chrome.com/extensions/event_pages.html Use newer chrome.runtime API instead of chrome.extension. --- js/background.js | 12 ++++------- js/contentscript.js | 14 ++++++------- js/lastfm_callback.js | 8 ++++---- js/popup.js | 46 +++++++++++++++++++++---------------------- manifest.json | 4 ++-- 5 files changed, 39 insertions(+), 45 deletions(-) diff --git a/js/background.js b/js/background.js index 20e4078..b5555be 100644 --- a/js/background.js +++ b/js/background.js @@ -4,7 +4,6 @@ * Copyright (c) 2011 Alexey Savartsov * Licensed under the MIT license */ - var SETTINGS = { api_key: "d00dce85051b7dbcbfcc165eaebfc6d2", api_secret: "bdfcae3563763ece1b6d3dcdd56a7ab8", @@ -40,17 +39,14 @@ if (!SETTINGS.scrobble) { } // Connect event handlers -chrome.extension.onConnect.addListener(port_on_connect); +chrome.runtime.onConnect.addListener(port_on_connect); /** * Content script has connected to the extension */ function port_on_connect(port) { - if (port.name == "cloudplayer") - { - port.onMessage.addListener(port_on_message); - port.onDisconnect.addListener(port_on_disconnect); - } + port.onMessage.addListener(port_on_message); + port.onDisconnect.addListener(port_on_disconnect); } /** @@ -139,7 +135,7 @@ function port_on_disconnect() { * Authentication link from popup window */ function start_web_auth() { - var callback_url = chrome.extension.getURL(SETTINGS.callback_file); + var callback_url = chrome.runtime.getURL(SETTINGS.callback_file); chrome.tabs.create({ 'url': "http://www.last.fm/api/auth?api_key=" + diff --git a/js/contentscript.js b/js/contentscript.js index e3c8fd6..3c40a0e 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -127,7 +127,7 @@ GoogleMusicParser.prototype._get_song_album = function() { return $("#playerSongInfo .player-artist-album-wrapper .player-album").text(); }; -var port = chrome.extension.connect({name: "cloudplayer"}); +var port = chrome.runtime.connect(); // NOTE: If you update this, you must also update SETTINGS.refresh_interval accordingly in background.js. window.setInterval(function() { @@ -138,19 +138,19 @@ window.setInterval(function() { /* * Listeners for player control buttons */ -chrome.extension.onMessage.addListener(toggle_play); -chrome.extension.onMessage.addListener(next_song); +chrome.runtime.onMessage.addListener(toggle_play); +chrome.runtime.onMessage.addListener(next_song); -function toggle_play(request, sndr, callback) { - if (request.cmd == "toggle_play") { +function toggle_play(msg, sndr, callback) { + if (msg.cmd == "tgl") { $(".player-middle button[data-id='play-pause']").click(); port.postMessage(new Player(new GoogleMusicParser())); callback(); } } -function next_song(request, sndr, callback) { - if (request.cmd == "next_song") { +function next_song(msg, sndr, callback) { + if (msg.cmd == "nxt") { $(".player-middle button[data-id='forward']").click(); port.postMessage(new Player(new GoogleMusicParser())); callback(); diff --git a/js/lastfm_callback.js b/js/lastfm_callback.js index 8c1bf23..560f396 100644 --- a/js/lastfm_callback.js +++ b/js/lastfm_callback.js @@ -9,7 +9,7 @@ function _url_param(name, url) { '(.+?)(&|$)').exec(url) || [,null])[1]); } -var background = chrome.extension.getBackgroundPage(); -location.href = "http://last.fm/"; - -background.get_lastfm_session(_url_param("token", location.search)); \ No newline at end of file +chrome.runtime.getBackgroundPage(function(background) { + location.href = "http://last.fm/"; + background.get_lastfm_session(_url_param("token", location.search)); +}); diff --git a/js/popup.js b/js/popup.js index 0c1c5ea..3caa0aa 100644 --- a/js/popup.js +++ b/js/popup.js @@ -5,16 +5,19 @@ * Licensed under the MIT license */ /* Background page */ -var bp = chrome.extension.getBackgroundPage(); +var bp; /* Render popup when DOM is ready */ $(document).ready(function() { - set_play_link(); - render_song(); - if (bp.lastfm_api.session.name && bp.lastfm_api.session.key) { - render_scrobble_link(); - } - render_auth_link(); + chrome.runtime.getBackgroundPage(function(backgroundPage) { + bp = backgroundPage; + set_play_link(); + render_song(); + if (bp.lastfm_api.session.name && bp.lastfm_api.session.key) { + render_scrobble_link(); + } + render_auth_link(); + }); }); function find_play_tab(callback) { @@ -81,16 +84,14 @@ function toggle_play_btn() { * Renders current song details */ function render_song() { - if(bp.player.has_song) - { + if (bp.player.has_song) { update_song_info(); $("#play-pause-btn").click(toggle_play); $("#next-btn").click(next_song); - if(!(bp.lastfm_api.session.name && bp.lastfm_api.session.key)) { + if (!(bp.lastfm_api.session.name && bp.lastfm_api.session.key)) { $("#lastfm-buttons").hide(); } - } - else { + } else { $("#song").addClass("nosong"); $("#artist").text(""); $("#track").html(''); @@ -150,14 +151,12 @@ function render_love_button() { bp.player.song.artist, function(result) { $("#love-button").html(''); - if(result) { + if (result) { $("#love-button a").attr({ title: "Unlove this song"}) .click(on_unlove) .addClass("loved"); - } - else - { + } else { $("#love-button a").attr({ title: "Love this song" }) .click(on_love) .addClass("notloved"); @@ -171,7 +170,7 @@ function toggle_play() { var has_song = bp.player.has_song; find_play_tab( function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: "toggle_play"}, + chrome.tabs.sendMessage(tab.id, {cmd: "tgl"}, function() { if (has_song) { toggle_play_btn(); @@ -188,7 +187,7 @@ function toggle_play() { function next_song() { find_play_tab( function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: "next_song"}, + chrome.tabs.sendMessage(tab.id, {cmd: "nxt"}, update_song_info); } ); @@ -224,11 +223,11 @@ function on_logout() { function on_love() { bp.lastfm_api.love_track(bp.player.song.title, bp.player.song.artist, function(result) { - if(!result.error) { + if (!result.error) { render_love_button(); } else { - if(result.error == 9) { + if (result.error == 9) { // Session expired bp.clear_session(); render_auth_link(); @@ -248,11 +247,10 @@ function on_love() { function on_unlove() { bp.lastfm_api.unlove_track(bp.player.song.title, bp.player.song.artist, function(result) { - if(!result.error) { + if (!result.error) { render_love_button(); - } - else { - if(result.error == 9) { + } else { + if (result.error == 9) { // Session expired bp.clear_session(); render_auth_link(); diff --git a/manifest.json b/manifest.json index ce0379a..f640709 100644 --- a/manifest.json +++ b/manifest.json @@ -5,11 +5,11 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.4.5", + "version" : "1.4.6", "manifest_version": 2, "background": { "page": "html/background.html", - "persistent": true + "persistent": false }, "name" : "Google Play Last.fm Scrobbler", "icons" : { From 8af2ce983d44c250f052e1191a751ec8d5d614f4 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Wed, 2 Oct 2013 00:55:13 -0400 Subject: [PATCH 27/74] Reduced scrobble interval to 7 minutes. --- js/background.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/js/background.js b/js/background.js index b5555be..bbb4a83 100644 --- a/js/background.js +++ b/js/background.js @@ -17,7 +17,7 @@ var SETTINGS = { scrobbling_stopped_icon: "../img/main-icon-scrobbling-stopped.png", scrobble_threshold: .7, - scrobble_interval: 480, // 8 minutes + scrobble_interval: 420, // 7 minutes // NOTE: This should be exactly the same as the portMessage interval in contentscript.js. refresh_interval: 5 }; @@ -107,14 +107,11 @@ function scrobble_song(artist, album, title, time) { // Scrobble this song lastfm_api.scrobble(title, time, artist, album, function(response) { - if (!response.error) { - - } else { + if (response.error) { if (response.error == 9) { // Session expired clear_session(); } - chrome.browserAction.setIcon({ 'path': SETTINGS.error_icon }); } From 891e008d6ff00b200935d80affd2bd878464b4e0 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Mon, 7 Oct 2013 12:05:15 -0400 Subject: [PATCH 28/74] Error logging for last.fm calls --- js/lastfm.js | 9 +++++++-- manifest.json | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/js/lastfm.js b/js/lastfm.js index c91bcee..812e582 100644 --- a/js/lastfm.js +++ b/js/lastfm.js @@ -251,18 +251,23 @@ LastFM.prototype._xhr = function(method, params, callback) { xhr.open(method, uri); - // TODO: Better error handling xhr.onreadystatechange = function() { if (xhr.readyState == 4) { var reply; try { reply = JSON.parse(xhr.responseText); + if (reply.error) { + console.log('Last.fm ' + params.method + + ' error: ' + reply.error) + } } catch (e) { reply = null; + console.log('Error parsing JSON response.'); + console.log('Request params:'); + console.log(params); } - callback(reply); } }; diff --git a/manifest.json b/manifest.json index f640709..769d317 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.4.6", + "version" : "1.4.7", "manifest_version": 2, "background": { "page": "html/background.html", From 9d480277ec6ec07eb897559a9218dde2d20445b3 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Fri, 11 Oct 2013 23:47:35 -0400 Subject: [PATCH 29/74] Added web_accessible_resources to manifest --- manifest.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/manifest.json b/manifest.json index 769d317..26e8c12 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.4.7", + "version" : "1.4.8", "manifest_version": 2, "background": { "page": "html/background.html", @@ -42,5 +42,8 @@ "js/jquery-1.7.min.js" ] } - ] + ], + "web_accessible_resources": [ + "html/lastfm_callback.html" + ] } From 0c6af94c61d4948382f9f28d59609070284ff8a0 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Fri, 1 Nov 2013 14:36:56 -0400 Subject: [PATCH 30/74] Controls stay on bottom when no album present --- css/popup.css | 11 +---------- html/popup.html | 18 ++++++++++-------- manifest.json | 2 +- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/css/popup.css b/css/popup.css index 6c46a2c..f1406f1 100644 --- a/css/popup.css +++ b/css/popup.css @@ -15,8 +15,6 @@ body { span { line-height: 100%; - padding: 0; - margin: 0; } a { @@ -60,6 +58,7 @@ a:hover { } #album { + min-height: 25px; font-style: italic; color: #999; font-size: 80%; @@ -70,11 +69,6 @@ a:hover { font-size: 14px; } -#lastfm-buttons { - margin-top: 10px; - float: left; -} - #bottom { margin-top: 10px; font-size: 11px; @@ -118,9 +112,6 @@ a.notloved { background-position: 0 -16px; } -#player-controls { - margin-top: 8px; -} a.play, a.pause { display: inline-block; width: 16px; diff --git a/html/popup.html b/html/popup.html index 1bfa8b9..c608b0e 100644 --- a/html/popup.html +++ b/html/popup.html @@ -23,14 +23,16 @@
-
- -     -
-
- -     - +
+ + +     + + + +     + +
diff --git a/manifest.json b/manifest.json index 26e8c12..a5bf851 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.4.8", + "version" : "1.4.9", "manifest_version": 2, "background": { "page": "html/background.html", From 913718e90bee4d3020da9c2469a6f581c8ed8583 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sat, 2 Nov 2013 17:30:31 -0400 Subject: [PATCH 31/74] Options page.Cleanup manifest.Settings in seperate file. --- html/background.html | 10 ---------- js/background.js | 39 +++++++++++++++------------------------ js/contentscript.js | 3 +-- js/options.js | 30 ++++++++++++++++++++++++++++++ js/settings.js | 18 ++++++++++++++++++ manifest.json | 13 ++++++++----- 6 files changed, 72 insertions(+), 41 deletions(-) delete mode 100644 html/background.html create mode 100644 js/options.js create mode 100644 js/settings.js diff --git a/html/background.html b/html/background.html deleted file mode 100644 index 9bce66a..0000000 --- a/html/background.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/js/background.js b/js/background.js index bbb4a83..7da7d49 100644 --- a/js/background.js +++ b/js/background.js @@ -4,33 +4,20 @@ * Copyright (c) 2011 Alexey Savartsov * Licensed under the MIT license */ -var SETTINGS = { - api_key: "d00dce85051b7dbcbfcc165eaebfc6d2", - api_secret: "bdfcae3563763ece1b6d3dcdd56a7ab8", - - callback_file: "html/lastfm_callback.html", - - main_icon: "../img/main-icon.png", - playing_icon: "../img/main-icon-playing.png", - paused_icon: "../img/main-icon-paused.png", - error_icon: "../img/main-icon-error.png", - scrobbling_stopped_icon: "../img/main-icon-scrobbling-stopped.png", - - scrobble_threshold: .7, - scrobble_interval: 420, // 7 minutes - // NOTE: This should be exactly the same as the portMessage interval in contentscript.js. - refresh_interval: 5 -}; var player = {}; // Previous player state var time_played = 0; -var curr_song_title = ""; +var num_scrobbles = 0; +var curr_song_title = ''; var lastfm_api = new LastFM(SETTINGS.api_key, SETTINGS.api_secret); // Load settings from local storage -lastfm_api.session.key = localStorage["session_key"] || null; -lastfm_api.session.name = localStorage["session_name"] || null; +lastfm_api.session.key = localStorage['session_key'] || null; +lastfm_api.session.name = localStorage['session_name'] || null; +SETTINGS.max_scrobbles = localStorage['max_scrobbles'] && + parseInt(localStorage['max_scrobbles']) || + SETTINGS.max_scrobbles; // This enables scrobbling by default SETTINGS.scrobble = !(localStorage["scrobble"] == "false"); @@ -55,7 +42,7 @@ function port_on_connect(port) { function port_on_message(message) { // Current player state var _p = message; - + // Save player state player = _p; @@ -71,16 +58,19 @@ function port_on_message(message) { _p.song.position <= SETTINGS.refresh_interval) { curr_song_title = _p.song.title; time_played = 0; + num_scrobbles = 0; } if (_p.is_playing) { chrome.browserAction.setIcon({ 'path': SETTINGS.playing_icon }); - if (time_played >= _p.song.time * SETTINGS.scrobble_threshold || - time_played >= SETTINGS.scrobble_interval) { + if ((time_played >= _p.song.time * SETTINGS.scrobble_point || + time_played >= SETTINGS.scrobble_interval) && + num_scrobbles < SETTINGS.max_scrobbles) { scrobble_song(_p.song.artist, _p.song.album, _p.song.title, Math.round(new Date().getTime() / 1000) - time_played); time_played = 0; + num_scrobbles += 1; } else { time_played += SETTINGS.refresh_interval; } @@ -123,7 +113,8 @@ function scrobble_song(artist, album, title, time) { */ function port_on_disconnect() { player = {}; // Clear player state - time_played = 0 + time_played = 0; + num_scrobbles = 0; chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); } diff --git a/js/contentscript.js b/js/contentscript.js index 3c40a0e..ef6dc3c 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -129,11 +129,10 @@ GoogleMusicParser.prototype._get_song_album = function() { var port = chrome.runtime.connect(); -// NOTE: If you update this, you must also update SETTINGS.refresh_interval accordingly in background.js. window.setInterval(function() { port.postMessage(new Player(new GoogleMusicParser())); }, -5000); +SETTINGS.refresh_interval * 1000); /* * Listeners for player control buttons diff --git a/js/options.js b/js/options.js new file mode 100644 index 0000000..180fead --- /dev/null +++ b/js/options.js @@ -0,0 +1,30 @@ +// Saves options to localStorage. +function save_options() { + var scrobble_mult = document.getElementById('scrobble_mult').checked; + if (!scrobble_mult) { + localStorage['max_scrobbles'] = 1; + } else { + localStorage.removeItem('max_scrobbles') + } + + chrome.runtime.getBackgroundPage(function(backgroundPage) { + backgroundPage.location.reload(); + // Update status to let user know options were saved. + var status = document.getElementById('status'); + status.innerHTML = 'Options saved, please reload the Google Play page.'; + setTimeout(function() { + status.innerHTML = ''; + }, 3500); + }); +} + +// Restores select box state to saved value from localStorage. +function restore_options() { + var scrobble_mult = SETTINGS.max_scrobbles > 1; + if (localStorage['max_scrobbles']) { + scrobble_mult = parseInt(localStorage['max_scrobbles']) > 1; + } + document.getElementById('scrobble_mult').checked = scrobble_mult; +} +document.addEventListener('DOMContentLoaded', restore_options); +document.querySelector('#save').addEventListener('click', save_options); \ No newline at end of file diff --git a/js/settings.js b/js/settings.js new file mode 100644 index 0000000..bd507ec --- /dev/null +++ b/js/settings.js @@ -0,0 +1,18 @@ +var SETTINGS = { + api_key: 'd00dce85051b7dbcbfcc165eaebfc6d2', + api_secret: 'bdfcae3563763ece1b6d3dcdd56a7ab8', + + callback_file: 'html/lastfm_callback.html', + + main_icon: '../img/main-icon.png', + playing_icon: '../img/main-icon-playing.png', + paused_icon: '../img/main-icon-paused.png', + error_icon: '../img/main-icon-error.png', + scrobbling_stopped_icon: '../img/main-icon-scrobbling-stopped.png', + + scrobble_point: .7, + scrobble_interval: 420, // 7 minutes + max_scrobbles: Number.POSITIVE_INFINITY, + + refresh_interval: 5 +}; \ No newline at end of file diff --git a/manifest.json b/manifest.json index a5bf851..d28c086 100644 --- a/manifest.json +++ b/manifest.json @@ -1,24 +1,25 @@ { + "name" : "Google Play Last.fm Scrobbler", + "description" : "Scrobbles songs from Google Play to Last.fm", + "version" : "1.5.0", + "manifest_version": 2, "permissions" : [ "tabs", "http://ws.audioscrobbler.com/", "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.4.9", - "manifest_version": 2, "background": { - "page": "html/background.html", + "scripts": ["js/md5.js", "js/lastfm.js", + "js/settings.js", "js/background.js"], "persistent": false }, - "name" : "Google Play Last.fm Scrobbler", "icons" : { "128" : "img/icon128.png", "64" : "img/icon64.png", "16" : "img/icon16.png", "48" : "img/icon48.png" }, - "description" : "Scrobbles songs from Google Play to Last.fm", "browser_action" : { "default_popup" : "html/popup.html", "default_icon" : "img/main-icon.png" @@ -30,6 +31,7 @@ ], "js" : [ "js/inject.js", + "js/settings.js", "js/contentscript.js" ] }, @@ -43,6 +45,7 @@ ] } ], + "options_page": "html/options.html", "web_accessible_resources": [ "html/lastfm_callback.html" ] From 370b91aee917ec62130fefa7d536ba2be853d82b Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sat, 2 Nov 2013 17:42:18 -0400 Subject: [PATCH 32/74] 'time_played' resets to 0 each interval even when multiple scrobbles turned off --- js/background.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/js/background.js b/js/background.js index 7da7d49..075c9c9 100644 --- a/js/background.js +++ b/js/background.js @@ -42,7 +42,7 @@ function port_on_connect(port) { function port_on_message(message) { // Current player state var _p = message; - + // Save player state player = _p; @@ -65,12 +65,13 @@ function port_on_message(message) { chrome.browserAction.setIcon({ 'path': SETTINGS.playing_icon }); if ((time_played >= _p.song.time * SETTINGS.scrobble_point || - time_played >= SETTINGS.scrobble_interval) && - num_scrobbles < SETTINGS.max_scrobbles) { - scrobble_song(_p.song.artist, _p.song.album, _p.song.title, - Math.round(new Date().getTime() / 1000) - time_played); + time_played >= SETTINGS.scrobble_interval)) { + if (num_scrobbles < SETTINGS.max_scrobbles) { + scrobble_song(_p.song.artist, _p.song.album, _p.song.title, + Math.round(new Date().getTime() / 1000) - time_played); + num_scrobbles += 1; + } time_played = 0; - num_scrobbles += 1; } else { time_played += SETTINGS.refresh_interval; } From f26d25628ab133a62983edc1d9945d3cde9acab7 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sat, 2 Nov 2013 17:49:13 -0400 Subject: [PATCH 33/74] Nah, cleaner the old way --- js/background.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/js/background.js b/js/background.js index 075c9c9..6e44727 100644 --- a/js/background.js +++ b/js/background.js @@ -65,13 +65,12 @@ function port_on_message(message) { chrome.browserAction.setIcon({ 'path': SETTINGS.playing_icon }); if ((time_played >= _p.song.time * SETTINGS.scrobble_point || - time_played >= SETTINGS.scrobble_interval)) { - if (num_scrobbles < SETTINGS.max_scrobbles) { - scrobble_song(_p.song.artist, _p.song.album, _p.song.title, - Math.round(new Date().getTime() / 1000) - time_played); - num_scrobbles += 1; - } + time_played >= SETTINGS.scrobble_interval) && + num_scrobbles < SETTINGS.max_scrobbles) { + scrobble_song(_p.song.artist, _p.song.album, _p.song.title, + Math.round(new Date().getTime() / 1000) - time_played); time_played = 0; + num_scrobbles += 1; } else { time_played += SETTINGS.refresh_interval; } From 1e23fbd2b2bbd7630a893d407b8d1cf972aab6e0 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sat, 2 Nov 2013 22:11:26 -0400 Subject: [PATCH 34/74] Added options.html --- html/options.html | 16 ++++++++++++++++ js/options.js | 2 ++ 2 files changed, 18 insertions(+) create mode 100644 html/options.html diff --git a/html/options.html b/html/options.html new file mode 100644 index 0000000..915c6d8 --- /dev/null +++ b/html/options.html @@ -0,0 +1,16 @@ + + + Google Play Last.fm Scrobbler Options + + + Scrobbler Options: +

+ Scrobble long tracks multiple times (once every + minutes)?:
+
+
+ + + + + diff --git a/js/options.js b/js/options.js index 180fead..e20c55a 100644 --- a/js/options.js +++ b/js/options.js @@ -25,6 +25,8 @@ function restore_options() { scrobble_mult = parseInt(localStorage['max_scrobbles']) > 1; } document.getElementById('scrobble_mult').checked = scrobble_mult; + document.getElementById('minute_field').innerHTML = + Math.round(SETTINGS.scrobble_interval / 60); } document.addEventListener('DOMContentLoaded', restore_options); document.querySelector('#save').addEventListener('click', save_options); \ No newline at end of file From cb5da2f4a57accb83dd17f2a38491489710280ed Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sat, 2 Nov 2013 22:40:27 -0400 Subject: [PATCH 35/74] Moved some initialization to settings.js --- js/background.js | 6 ------ js/options.js | 5 +---- js/settings.js | 9 ++++++++- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/js/background.js b/js/background.js index 6e44727..fc1e69f 100644 --- a/js/background.js +++ b/js/background.js @@ -15,12 +15,6 @@ var lastfm_api = new LastFM(SETTINGS.api_key, SETTINGS.api_secret); lastfm_api.session.key = localStorage['session_key'] || null; lastfm_api.session.name = localStorage['session_name'] || null; -SETTINGS.max_scrobbles = localStorage['max_scrobbles'] && - parseInt(localStorage['max_scrobbles']) || - SETTINGS.max_scrobbles; -// This enables scrobbling by default -SETTINGS.scrobble = !(localStorage["scrobble"] == "false"); - if (!SETTINGS.scrobble) { chrome.browserAction.setIcon({ 'path': SETTINGS.scrobbling_stopped_icon }); } diff --git a/js/options.js b/js/options.js index e20c55a..b04a27c 100644 --- a/js/options.js +++ b/js/options.js @@ -21,12 +21,9 @@ function save_options() { // Restores select box state to saved value from localStorage. function restore_options() { var scrobble_mult = SETTINGS.max_scrobbles > 1; - if (localStorage['max_scrobbles']) { - scrobble_mult = parseInt(localStorage['max_scrobbles']) > 1; - } document.getElementById('scrobble_mult').checked = scrobble_mult; document.getElementById('minute_field').innerHTML = - Math.round(SETTINGS.scrobble_interval / 60); + Math.round((SETTINGS.scrobble_interval / 60) * 100) / 100; } document.addEventListener('DOMContentLoaded', restore_options); document.querySelector('#save').addEventListener('click', save_options); \ No newline at end of file diff --git a/js/settings.js b/js/settings.js index bd507ec..230341f 100644 --- a/js/settings.js +++ b/js/settings.js @@ -15,4 +15,11 @@ var SETTINGS = { max_scrobbles: Number.POSITIVE_INFINITY, refresh_interval: 5 -}; \ No newline at end of file +}; + +SETTINGS.max_scrobbles = localStorage['max_scrobbles'] && + parseInt(localStorage['max_scrobbles']) || + SETTINGS.max_scrobbles; + +// This enables scrobbling by default +SETTINGS.scrobble = !(localStorage["scrobble"] == "false"); \ No newline at end of file From fe784b9d1a615609963ea0e046cac5c0416efa79 Mon Sep 17 00:00:00 2001 From: Queile Laiho Date: Fri, 16 May 2014 20:59:50 +0900 Subject: [PATCH 36/74] Fix If-Modified-Since Header bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In my Japanese environment, new Date(0).toString is "Thu Jan 01 1970 09:00:00 GMT+0900 (東京 (標準時))" and error occurred. I fix it. --- js/lastfm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/lastfm.js b/js/lastfm.js index 812e582..e1206b9 100644 --- a/js/lastfm.js +++ b/js/lastfm.js @@ -274,7 +274,7 @@ LastFM.prototype._xhr = function(method, params, callback) { xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); // The cache is a lie! - xhr.setRequestHeader("If-Modified-Since", new Date(0)); + xhr.setRequestHeader("If-Modified-Since", "Thu, 01 Jun 1970 00:00:00 GMT"); xhr.setRequestHeader("Pragma", "no-cache"); xhr.send(_data || null); }; From c5976f042f402b3bd60cbd0991cc4df9838a07c4 Mon Sep 17 00:00:00 2001 From: Chrissy Weeks Date: Mon, 26 May 2014 03:36:00 -0400 Subject: [PATCH 37/74] Added rewind/previous song capability --- css/popup.css | 8 ++++++++ html/popup.html | 4 +++- img/prev.png | Bin 0 -> 443 bytes js/contentscript.js | 9 +++++++++ js/popup.js | 42 ++++++++++++++++++++++++++---------------- manifest.json | 2 +- 6 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 img/prev.png diff --git a/css/popup.css b/css/popup.css index f1406f1..9c4609a 100644 --- a/css/popup.css +++ b/css/popup.css @@ -128,6 +128,14 @@ a.pause { background-position: 0 -19px; } +#prev-btn { + display: inline-block; + width: 25px; + height: 18px; + background-image: url('../img/prev.png'); + background-repeat: no-repeat; +} + #next-btn { display: inline-block; width: 25px; diff --git a/html/popup.html b/html/popup.html index c608b0e..b3eb6ea 100644 --- a/html/popup.html +++ b/html/popup.html @@ -29,9 +29,11 @@     + +         - + diff --git a/img/prev.png b/img/prev.png new file mode 100644 index 0000000000000000000000000000000000000000..64a1833bed91ec5d9648d8ef88d21748b1895d37 GIT binary patch literal 443 zcmeAS@N?(olHy`uVBq!ia0vp^l0YoP!2%>%)c0zGsB}kPM+SzC{oH>NS%G}U;vjb? zhIQv;UIIA^$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZEE z?e4>+sSM@Afw;Y#W6(V{M^Zbf(;4+tl`2E2RYm}H1YN% zuq>81sNrJ3q;`NmP*DC1$M?g!n~lra8;&N*78*PiHm#&(-J(uk_FOy!8 zXB&iknOobw#_n0R-RZa(zen(T1GcSpikYpCpRfAQ)o-++JJ`3`tvBbs$}N4VP3P4r zrv|%eRv7FPnY`Gw-~R7Wp)U-d6BE9-Bt1F}bed|3YeY#(Vo9o1a#1RfVlXl=G|@FM z)ip8;F)*|;GPg1`*EKM)GB8;2zS9szLvDUbW?Cg~4L3I#_5w9Xf@}!RPb(=;EJ|f? iOvz75Rq)JBOiv9;O-!jQJedj#L'); - + bp.lastfm_api.is_track_loved(bp.player.song.title, - bp.player.song.artist, + bp.player.song.artist, function(result) { $("#love-button").html(''); if (result) { $("#love-button a").attr({ title: "Unlove this song"}) .click(on_unlove) .addClass("loved"); - + } else { $("#love-button a").attr({ title: "Love this song" }) .click(on_love) @@ -170,7 +171,7 @@ function toggle_play() { var has_song = bp.player.has_song; find_play_tab( function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: "tgl"}, + chrome.tabs.sendMessage(tab.id, {cmd: "tgl"}, function() { if (has_song) { toggle_play_btn(); @@ -179,7 +180,16 @@ function toggle_play() { toggle_play_btn(); } } - ); + ); + } + ); +} + +function prev_song() { + find_play_tab( + function(tab) { + chrome.tabs.sendMessage(tab.id, {cmd: "prv"}, + update_song_info); } ); } @@ -187,8 +197,8 @@ function toggle_play() { function next_song() { find_play_tab( function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: "nxt"}, - update_song_info); + chrome.tabs.sendMessage(tab.id, {cmd: "nxt"}, + update_song_info); } ); } @@ -221,7 +231,7 @@ function on_logout() { * Love button was clicked */ function on_love() { - bp.lastfm_api.love_track(bp.player.song.title, bp.player.song.artist, + bp.lastfm_api.love_track(bp.player.song.title, bp.player.song.artist, function(result) { if (!result.error) { render_love_button(); @@ -232,7 +242,7 @@ function on_love() { bp.clear_session(); render_auth_link(); } - + chrome.browserAction.setIcon({ 'path': SETTINGS.error_icon }); } @@ -245,7 +255,7 @@ function on_love() { * Unlove button was clicked */ function on_unlove() { - bp.lastfm_api.unlove_track(bp.player.song.title, bp.player.song.artist, + bp.lastfm_api.unlove_track(bp.player.song.title, bp.player.song.artist, function(result) { if (!result.error) { render_love_button(); @@ -255,7 +265,7 @@ function on_unlove() { bp.clear_session(); render_auth_link(); } - + chrome.browserAction.setIcon({ 'path': SETTINGS.error_icon }); } diff --git a/manifest.json b/manifest.json index d28c086..7245da4 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.5.0", + "version" : "1.5.1", "manifest_version": 2, "permissions" : [ "tabs", From 35343c7104496987056cd6ac38851385a9e13adb Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Wed, 30 Jul 2014 14:24:32 -0700 Subject: [PATCH 38/74] Toggle play button polls for 3 seconds. Remove whitespace. --- js/background.js | 30 ++++++++++++------------- js/contentscript.js | 14 ++++++------ js/lastfm.js | 54 ++++++++++++++++++++++----------------------- js/popup.js | 20 ++++++++++++----- js/settings.js | 14 ++++++------ manifest.json | 4 ++-- 6 files changed, 72 insertions(+), 64 deletions(-) diff --git a/js/background.js b/js/background.js index fc1e69f..abda485 100644 --- a/js/background.js +++ b/js/background.js @@ -29,7 +29,7 @@ function port_on_connect(port) { port.onMessage.addListener(port_on_message); port.onDisconnect.addListener(port_on_disconnect); } - + /** * New message arrives to the port */ @@ -39,7 +39,7 @@ function port_on_message(message) { // Save player state player = _p; - + if (!SETTINGS.scrobble) { chrome.browserAction.setIcon({ 'path': SETTINGS.scrobbling_stopped_icon }); @@ -48,17 +48,17 @@ function port_on_message(message) { } if (_p.has_song) { - if (_p.song.title != curr_song_title || + if (_p.song.title != curr_song_title || _p.song.position <= SETTINGS.refresh_interval) { curr_song_title = _p.song.title; time_played = 0; num_scrobbles = 0; } - + if (_p.is_playing) { chrome.browserAction.setIcon({ 'path': SETTINGS.playing_icon }); - if ((time_played >= _p.song.time * SETTINGS.scrobble_point || + if ((time_played >= _p.song.time * SETTINGS.scrobble_point || time_played >= SETTINGS.scrobble_interval) && num_scrobbles < SETTINGS.max_scrobbles) { scrobble_song(_p.song.artist, _p.song.album, _p.song.title, @@ -68,24 +68,24 @@ function port_on_message(message) { } else { time_played += SETTINGS.refresh_interval; } - + lastfm_api.now_playing(_p.song.title, _p.song.artist, _p.song.album, function(response) { - // TODO: + // TODO: } ); } else { // The player is paused - chrome.browserAction.setIcon({ + chrome.browserAction.setIcon({ 'path': SETTINGS.paused_icon }); } } else { chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); } } - + function scrobble_song(artist, album, title, time) { // Scrobble this song @@ -119,10 +119,10 @@ function port_on_disconnect() { function start_web_auth() { var callback_url = chrome.runtime.getURL(SETTINGS.callback_file); chrome.tabs.create({ - 'url': - "http://www.last.fm/api/auth?api_key=" + - SETTINGS.api_key + - "&cb=" + + 'url': + "http://www.last.fm/api/auth?api_key=" + + SETTINGS.api_key + + "&cb=" + callback_url }); } @@ -131,7 +131,7 @@ function start_web_auth() { */ function clear_session() { lastfm_api.session = {}; - + localStorage.removeItem("session_key"); localStorage.removeItem("session_name"); } @@ -142,7 +142,7 @@ function clear_session() { function toggle_scrobble() { SETTINGS.scrobble = !SETTINGS.scrobble; localStorage["scrobble"] = SETTINGS.scrobble; - + // Set the icon corresponding the current scrobble state var icon = SETTINGS.scrobble ? SETTINGS.main_icon : SETTINGS.scrobbling_stopped_icon; chrome.browserAction.setIcon({ 'path': icon }); diff --git a/js/contentscript.js b/js/contentscript.js index 9ee6850..967a6ba 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -10,7 +10,7 @@ * * Cloud Player page parser */ -function Player(parser) { +function Player(parser) { this.has_song = parser._get_has_song(); this.is_playing = parser._get_is_playing(); this.song = { @@ -58,11 +58,11 @@ GoogleMusicParser.prototype._get_is_playing = function() { GoogleMusicParser.prototype._get_song_position = function() { var _time = $("#time_container_current").text(); _time = $.trim(_time).split(':'); - if(_time.length == 2) + if(_time.length == 2) { return (parseInt(_time[0]) * 60 + parseInt(_time[1])); } - else if (_time.length == 3) + else if (_time.length == 3) { return (parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2])); } @@ -79,8 +79,8 @@ GoogleMusicParser.prototype._get_song_time = function() { _time = $.trim(_time).split(':'); if(_time.length == 2) { return (parseInt(_time[0]) * 60 + parseInt(_time[1])); - } - else if (_time.length == 3) + } + else if (_time.length == 3) { return (parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2])); } @@ -131,8 +131,8 @@ var port = chrome.runtime.connect(); window.setInterval(function() { port.postMessage(new Player(new GoogleMusicParser())); -}, -SETTINGS.refresh_interval * 1000); +}, +SETTINGS.refresh_interval * 1000); /* * Listeners for player control buttons diff --git a/js/lastfm.js b/js/lastfm.js index e1206b9..3e8d3bf 100644 --- a/js/lastfm.js +++ b/js/lastfm.js @@ -15,7 +15,7 @@ function LastFM(api_key, api_secret) { this.API_KEY = api_key || ""; this.API_SECRET = api_secret || ""; this.API_ROOT = "http://ws.audioscrobbler.com/2.0/"; - + this.session = {}; } @@ -35,10 +35,10 @@ LastFM.prototype.authorize = function(token, callback) { params.api_sig = this._req_sign(params); params.format = "json"; - + var self = this; - this._xhr("GET", params, + this._xhr("GET", params, function(reply) { if(reply) { self.session.key = reply.session.key; @@ -72,10 +72,10 @@ LastFM.prototype.now_playing = function(track, artist, album, callback) { params.api_sig = this._req_sign(params); params.format = "json"; - this._xhr("POST", params, + this._xhr("POST", params, function(result) { callback(result); - }); + }); }; /** @@ -97,11 +97,11 @@ LastFM.prototype.scrobble = function(track, timestamp, artist, album, callback) 'album': album || "", 'sk': this.session.key }; - + params.api_sig = this._req_sign(params); params.format = "json"; - - this._xhr("POST", params, + + this._xhr("POST", params, function(result) { callback(result); }); @@ -123,11 +123,11 @@ LastFM.prototype.love_track = function(track, artist, callback) { 'artist': artist, 'sk': this.session.key }; - + params.api_sig = this._req_sign(params); params.format = "json"; - - this._xhr("POST", params, + + this._xhr("POST", params, function(result) { callback(result); }); @@ -149,11 +149,11 @@ LastFM.prototype.unlove_track = function(track, artist, callback) { 'artist': artist, 'sk': this.session.key }; - + params.api_sig = this._req_sign(params); params.format = "json"; - - this._xhr("POST", params, + + this._xhr("POST", params, function(result) { callback(result); }); @@ -172,7 +172,7 @@ LastFM.prototype.is_track_loved = function(track, artist, callback) { callback(false); return; } - + var params = { 'api_key': this.API_KEY, 'method': "track.getInfo", @@ -180,14 +180,14 @@ LastFM.prototype.is_track_loved = function(track, artist, callback) { 'artist': artist, 'username': this.session.name }; - + params.format = 'json'; - + this._xhr("GET", params, function(result) { if(!result.error && result.track) { callback(result.track.userloved == 1); } - else + else { callback(false); } @@ -204,16 +204,16 @@ LastFM.prototype._req_sign = function(params) { var keys = []; var key, i; var signature = ""; - + for(key in params) { keys.push(key); } - + for(i in keys.sort()) { key = keys[i]; signature += key + params[key]; } - + signature += this.API_SECRET; return hex_md5(signature); }; @@ -232,12 +232,12 @@ LastFM.prototype._xhr = function(method, params, callback) { var _data = ""; var _params = []; var xhr = new XMLHttpRequest(); - + for(param in params) { _params.push(encodeURIComponent(param) + "=" + encodeURIComponent(params[param])); } - + switch(method) { case "GET": uri += '?' + _params.join('&').replace(/%20/, '+'); @@ -248,17 +248,17 @@ LastFM.prototype._xhr = function(method, params, callback) { default: return; } - + xhr.open(method, uri); - + xhr.onreadystatechange = function() { if (xhr.readyState == 4) { var reply; - + try { reply = JSON.parse(xhr.responseText); if (reply.error) { - console.log('Last.fm ' + params.method + + console.log('Last.fm ' + params.method + ' error: ' + reply.error) } } diff --git a/js/popup.js b/js/popup.js index a8b4845..6b6c224 100644 --- a/js/popup.js +++ b/js/popup.js @@ -71,12 +71,20 @@ function update_song_info() { function toggle_play_btn() { var play_btn = $("#play-pause-btn"); - if (bp.player.is_playing) { - play_btn.removeClass(); - play_btn.addClass("pause"); - } else { - play_btn.removeClass(); - play_btn.addClass("play"); + play_btn.toggle = function() { + if (bp.player.is_playing) { + play_btn.removeClass(); + play_btn.addClass("pause"); + } else { + play_btn.removeClass(); + play_btn.addClass("play"); + } + } + // TODO kind of hackish + // This callback can be called too early (before bp.player is updated) + // Thus, just try a few times for 3 seconds + for (var i = 0; i < 30; i++) { + setTimeout(play_btn.toggle, i * 100); } } diff --git a/js/settings.js b/js/settings.js index 230341f..edab61b 100644 --- a/js/settings.js +++ b/js/settings.js @@ -1,25 +1,25 @@ var SETTINGS = { api_key: 'd00dce85051b7dbcbfcc165eaebfc6d2', api_secret: 'bdfcae3563763ece1b6d3dcdd56a7ab8', - + callback_file: 'html/lastfm_callback.html', - + main_icon: '../img/main-icon.png', playing_icon: '../img/main-icon-playing.png', paused_icon: '../img/main-icon-paused.png', error_icon: '../img/main-icon-error.png', scrobbling_stopped_icon: '../img/main-icon-scrobbling-stopped.png', - + scrobble_point: .7, scrobble_interval: 420, // 7 minutes max_scrobbles: Number.POSITIVE_INFINITY, - refresh_interval: 5 + refresh_interval: 2 }; -SETTINGS.max_scrobbles = localStorage['max_scrobbles'] && - parseInt(localStorage['max_scrobbles']) || +SETTINGS.max_scrobbles = localStorage['max_scrobbles'] && + parseInt(localStorage['max_scrobbles']) || SETTINGS.max_scrobbles; // This enables scrobbling by default -SETTINGS.scrobble = !(localStorage["scrobble"] == "false"); \ No newline at end of file +SETTINGS.scrobble = !(localStorage["scrobble"] == "false"); diff --git a/manifest.json b/manifest.json index 7245da4..7338f27 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.5.1", + "version" : "1.5.2", "manifest_version": 2, "permissions" : [ "tabs", @@ -10,7 +10,7 @@ "https://play.google.com/" ], "background": { - "scripts": ["js/md5.js", "js/lastfm.js", + "scripts": ["js/md5.js", "js/lastfm.js", "js/settings.js", "js/background.js"], "persistent": false }, From 498219d8f1280fdaf379d097a9ad4802b001ecf8 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sun, 3 Aug 2014 17:33:48 -0700 Subject: [PATCH 39/74] Keyboard shortcuts --- html/popup.html | 3 ++- js/background.js | 47 ++++++++++++++++++++++++++++++++++++++++------- js/popup.js | 25 ------------------------- js/util.js | 28 ++++++++++++++++++++++++++++ manifest.json | 39 ++++++++++++++++++++++++++++++++++++--- 5 files changed, 106 insertions(+), 36 deletions(-) create mode 100644 js/util.js diff --git a/html/popup.html b/html/popup.html index b3eb6ea..a0b202e 100644 --- a/html/popup.html +++ b/html/popup.html @@ -6,10 +6,11 @@ --> - + + diff --git a/js/background.js b/js/background.js index abda485..97f7de8 100644 --- a/js/background.js +++ b/js/background.js @@ -22,6 +22,8 @@ if (!SETTINGS.scrobble) { // Connect event handlers chrome.runtime.onConnect.addListener(port_on_connect); +bind_keyboard_shortcuts(); + /** * Content script has connected to the extension */ @@ -120,9 +122,9 @@ function start_web_auth() { var callback_url = chrome.runtime.getURL(SETTINGS.callback_file); chrome.tabs.create({ 'url': - "http://www.last.fm/api/auth?api_key=" + + 'http://www.last.fm/api/auth?api_key=' + SETTINGS.api_key + - "&cb=" + + '&cb=' + callback_url }); } @@ -132,8 +134,8 @@ function start_web_auth() { function clear_session() { lastfm_api.session = {}; - localStorage.removeItem("session_key"); - localStorage.removeItem("session_name"); + localStorage.removeItem('session_key'); + localStorage.removeItem('session_name'); } /** @@ -141,7 +143,7 @@ function clear_session() { */ function toggle_scrobble() { SETTINGS.scrobble = !SETTINGS.scrobble; - localStorage["scrobble"] = SETTINGS.scrobble; + localStorage['scrobble'] = SETTINGS.scrobble; // Set the icon corresponding the current scrobble state var icon = SETTINGS.scrobble ? SETTINGS.main_icon : SETTINGS.scrobbling_stopped_icon; @@ -155,8 +157,39 @@ function get_lastfm_session(token) { lastfm_api.authorize(token, function(response) { // Save session if (response.session) { - localStorage["session_key"] = response.session.key; - localStorage["session_name"] = response.session.name; + localStorage['session_key'] = response.session.key; + localStorage['session_name'] = response.session.name; } }); } + +function bind_keyboard_shortcuts() { + chrome.commands.onCommand.addListener( + function(command) { + switch (command) { + case 'toggle_play': + send_cmd_to_play_tab('tgl'); + break; + case 'prev_song': + send_cmd_to_play_tab('prv'); + break; + case 'next_song': + send_cmd_to_play_tab('nxt'); + break; + case 'goto_play_tab': + open_play_tab(); + break; + default: + console.error("No handler for command '" + command + "'"); + } + } + ); +} + +function send_cmd_to_play_tab(cmd) { + find_play_tab( + function(tab) { + chrome.tabs.sendMessage(tab.id, {cmd: cmd}, function() {}); + } + ); +} diff --git a/js/popup.js b/js/popup.js index 6b6c224..4f9a7b6 100644 --- a/js/popup.js +++ b/js/popup.js @@ -20,31 +20,6 @@ $(document).ready(function() { }); }); -function find_play_tab(callback) { - chrome.tabs.query({url: '*://play.google.com/music/listen*'}, - function(tabs) { - if (tabs.length > 0) { - callback(tabs[0]); - } else { - callback(null); - } - }); -} - -function open_play_tab() { - find_play_tab( - function(tab) { - if (tab) { - chrome.tabs.update(tab.id, {selected: true}); - } else { - chrome.tabs.create({url: - 'https://play.google.com/music/listen', - selected: true}); - } - } - ); -} - function set_play_link() { $("#cover").click(open_play_tab); } diff --git a/js/util.js b/js/util.js new file mode 100644 index 0000000..1843bcb --- /dev/null +++ b/js/util.js @@ -0,0 +1,28 @@ +/** +* Various utility functions +*/ + +function find_play_tab(callback) { + chrome.tabs.query({url: '*://play.google.com/music/listen*'}, + function(tabs) { + if (tabs.length > 0) { + callback(tabs[0]); + } else { + callback(null); + } + }); +} + +function open_play_tab() { + find_play_tab( + function(tab) { + if (tab) { + chrome.tabs.update(tab.id, {selected: true}); + } else { + chrome.tabs.create({url: + 'https://play.google.com/music/listen', + selected: true}); + } + } + ); +} \ No newline at end of file diff --git a/manifest.json b/manifest.json index 7338f27..86401e8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.5.2", + "version" : "1.5.3", "manifest_version": 2, "permissions" : [ "tabs", @@ -11,7 +11,7 @@ ], "background": { "scripts": ["js/md5.js", "js/lastfm.js", - "js/settings.js", "js/background.js"], + "js/settings.js", "js/util.js", "js/background.js"], "persistent": false }, "icons" : { @@ -48,5 +48,38 @@ "options_page": "html/options.html", "web_accessible_resources": [ "html/lastfm_callback.html" - ] + ], + "commands": { + "toggle_play": { + "suggested_key": { + "default": "MediaPlayPause", + "linux": "Ctrl+Shift+2" + }, + "description": "Toggle play/pause", + "global": true + }, + "prev_song": { + "suggested_key": { + "default": "MediaPrevTrack", + "linux": "Ctrl+Shift+1" + }, + "description": "Previous song", + "global": true + }, + "next_song": { + "suggested_key": { + "default": "MediaNextTrack", + "linux": "Ctrl+Shift+3" + }, + "description": "Next song", + "global": true + }, + "goto_play_tab": { + "suggested_key": { + "default": "Ctrl+Shift+4" + }, + "description": "Go to Google Play tab", + "global": true + } + } } From c1bebfc08824e3607135d08f092e260761c2789a Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sun, 3 Aug 2014 22:23:35 -0700 Subject: [PATCH 40/74] Add first-run message --- css/popup.css | 18 ++++++++++---- html/options.html | 3 +++ html/popup.html | 62 ++++++++++++++++++++++++++--------------------- js/background.js | 4 +++ js/popup.js | 19 +++++++++++++++ manifest.json | 2 +- 6 files changed, 75 insertions(+), 33 deletions(-) diff --git a/css/popup.css b/css/popup.css index 9c4609a..25dc5b8 100644 --- a/css/popup.css +++ b/css/popup.css @@ -2,14 +2,14 @@ * popup.css * Style sheet for popup page * Copyright (c) 2011 Alexey Savartsov - * Licensed under the MIT license + * Licensed under the MIT license */ body { padding: 10px; margin: 0; min-width: 300px; - + font-family: "Helvetica", "Arial", sans-serif; } @@ -37,7 +37,7 @@ a:hover { border: none; width: 60px; height: 60px; - cursor: hand; + cursor: hand; cursor: pointer; } @@ -72,10 +72,10 @@ a:hover { #bottom { margin-top: 10px; font-size: 11px; - + color: grey; } - + #scrobbling { float: left; } @@ -147,3 +147,11 @@ a.pause { .clear { clear: both; } + +.hidden { + display: none; +} + +#alert { + font-size: 14px; +} \ No newline at end of file diff --git a/html/options.html b/html/options.html index 915c6d8..490e0e1 100644 --- a/html/options.html +++ b/html/options.html @@ -10,6 +10,9 @@
+
+ To edit keyboard shortcuts, go to chrome://extensions and click
+ keyboard shortcuts on the bottom right of the page. diff --git a/html/popup.html b/html/popup.html index a0b202e..fca2b41 100644 --- a/html/popup.html +++ b/html/popup.html @@ -14,34 +14,42 @@ -
-
- -
-
- -  -  - - -
-
- - -     - - - -     - -     - - + +
+
+
+ +
+
+ +  -  + + +
+
+ + +     + + + +     + +     + + +
-
-
-
-
-
+
+
+
+
+
diff --git a/js/background.js b/js/background.js index 97f7de8..156b763 100644 --- a/js/background.js +++ b/js/background.js @@ -193,3 +193,7 @@ function send_cmd_to_play_tab(cmd) { } ); } + +function open_extensions_page() { + chrome.tabs.create({url: 'chrome://extensions/'}); +} diff --git a/js/popup.js b/js/popup.js index 4f9a7b6..d488f5d 100644 --- a/js/popup.js +++ b/js/popup.js @@ -11,6 +11,9 @@ var bp; $(document).ready(function() { chrome.runtime.getBackgroundPage(function(backgroundPage) { bp = backgroundPage; + if (localStorage.getItem("seen_alert") === null) { + show_alert(); + } set_play_link(); render_song(); if (bp.lastfm_api.session.name && bp.lastfm_api.session.key) { @@ -256,3 +259,19 @@ function on_unlove() { $("#love-button").html(''); } + +/** +* Show temporary msg from me to user <3 +*/ +function show_alert() { + $("#content").addClass("hidden"); + $("#alert").removeClass("hidden"); + $("#extns_link").click(function() { + bp.open_extensions_page(); + }); + $("#dismiss_alert").click(function() { + $("#content").removeClass("hidden"); + $("#alert").addClass("hidden"); + localStorage.setItem("seen_alert", "1"); + }); +} \ No newline at end of file diff --git a/manifest.json b/manifest.json index 86401e8..f584c78 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.5.3", + "version" : "1.5.4", "manifest_version": 2, "permissions" : [ "tabs", From 07fedddf508d448196c594129ec1a0bcf60e7e82 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sun, 3 Aug 2014 22:46:40 -0700 Subject: [PATCH 41/74] Don't hide main content with alert message --- css/popup.css | 5 ++++- html/options.html | 2 +- js/popup.js | 2 -- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/css/popup.css b/css/popup.css index 25dc5b8..7603e16 100644 --- a/css/popup.css +++ b/css/popup.css @@ -153,5 +153,8 @@ a.pause { } #alert { - font-size: 14px; + font-size: 12px; + border-bottom: 1px dotted #000; + padding-bottom: 10px; + margin-bottom: 10px; } \ No newline at end of file diff --git a/html/options.html b/html/options.html index 490e0e1..23da8f0 100644 --- a/html/options.html +++ b/html/options.html @@ -10,7 +10,7 @@
-
+

To edit keyboard shortcuts, go to chrome://extensions and click
keyboard shortcuts on the bottom right of the page. diff --git a/js/popup.js b/js/popup.js index d488f5d..c4d8b7a 100644 --- a/js/popup.js +++ b/js/popup.js @@ -264,13 +264,11 @@ function on_unlove() { * Show temporary msg from me to user <3 */ function show_alert() { - $("#content").addClass("hidden"); $("#alert").removeClass("hidden"); $("#extns_link").click(function() { bp.open_extensions_page(); }); $("#dismiss_alert").click(function() { - $("#content").removeClass("hidden"); $("#alert").addClass("hidden"); localStorage.setItem("seen_alert", "1"); }); From ccf19220ece9bfd18b53690915bbd0f1b0361b88 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Thu, 13 Nov 2014 22:32:19 -0800 Subject: [PATCH 42/74] Grabs album cover correctly. --- js/contentscript.js | 2 +- manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/contentscript.js b/js/contentscript.js index 967a6ba..061bf4f 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -114,7 +114,7 @@ GoogleMusicParser.prototype._get_song_artist = function() { GoogleMusicParser.prototype._get_song_cover = function() { var albumImg = $("#playingAlbumArt").attr("src"); if (albumImg) - return ("http:" + albumImg); + return albumImg; return null; }; diff --git a/manifest.json b/manifest.json index f584c78..c2eaddb 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.5.4", + "version" : "1.5.5", "manifest_version": 2, "permissions" : [ "tabs", From ab0a34f609c942546121387b1c239fb05d426b87 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Wed, 19 Nov 2014 14:11:31 -0800 Subject: [PATCH 43/74] Don't shadow Mac keyboard shortcuts. --- manifest.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/manifest.json b/manifest.json index c2eaddb..0cacd1f 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.5.5", + "version" : "1.5.6", "manifest_version": 2, "permissions" : [ "tabs", @@ -53,7 +53,7 @@ "toggle_play": { "suggested_key": { "default": "MediaPlayPause", - "linux": "Ctrl+Shift+2" + "linux": "Ctrl+Shift+8" }, "description": "Toggle play/pause", "global": true @@ -61,7 +61,7 @@ "prev_song": { "suggested_key": { "default": "MediaPrevTrack", - "linux": "Ctrl+Shift+1" + "linux": "Ctrl+Shift+7" }, "description": "Previous song", "global": true @@ -69,14 +69,14 @@ "next_song": { "suggested_key": { "default": "MediaNextTrack", - "linux": "Ctrl+Shift+3" + "linux": "Ctrl+Shift+9" }, "description": "Next song", "global": true }, "goto_play_tab": { "suggested_key": { - "default": "Ctrl+Shift+4" + "default": "Ctrl+Shift+0" }, "description": "Go to Google Play tab", "global": true From 2187bac7a01b21fb1a00efa3396083f2b82f5a90 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Tue, 20 Jan 2015 10:08:23 -0800 Subject: [PATCH 44/74] Upload album artist when scrobbling --- js/background.js | 3 ++- js/contentscript.js | 10 ++++++++++ js/lastfm.js | 4 +++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/js/background.js b/js/background.js index 156b763..2261373 100644 --- a/js/background.js +++ b/js/background.js @@ -63,7 +63,8 @@ function port_on_message(message) { if ((time_played >= _p.song.time * SETTINGS.scrobble_point || time_played >= SETTINGS.scrobble_interval) && num_scrobbles < SETTINGS.max_scrobbles) { - scrobble_song(_p.song.artist, _p.song.album, _p.song.title, + scrobble_song(_p.song.artist,_p.song.album_artist, + _p.song.album, _p.song.title, Math.round(new Date().getTime() / 1000) - time_played); time_played = 0; num_scrobbles += 1; diff --git a/js/contentscript.js b/js/contentscript.js index 061bf4f..85673b5 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -18,6 +18,7 @@ function Player(parser) { time: parser._get_song_time(), title: parser._get_song_title(), artist: parser._get_song_artist(), + album_artist: parser._get_album_artist(), album: parser._get_song_album(), cover: parser._get_song_cover() }; @@ -106,6 +107,15 @@ GoogleMusicParser.prototype._get_song_artist = function() { return $("#player-artist").text(); }; +/** + * Get current song album artist + * + * @return The album artist + */ +GoogleMusicParser.prototype._get_album_artist = function() { + return decodeURIComponent($(".player-album").attr('data-id').split('/')[1]); +}; + /** * Get current song artwork * diff --git a/js/lastfm.js b/js/lastfm.js index 3e8d3bf..c874c10 100644 --- a/js/lastfm.js +++ b/js/lastfm.js @@ -87,13 +87,15 @@ LastFM.prototype.now_playing = function(track, artist, album, callback) { * @param callback Callback function for the request. Sends a parameter with * reply decoded as JS object from JSON on null on error */ -LastFM.prototype.scrobble = function(track, timestamp, artist, album, callback) { +LastFM.prototype.scrobble = function(track, timestamp, artist, album_artist, + album, callback) { var params = { 'api_key': this.API_KEY, 'method': "track.scrobble", 'track': track, 'timestamp': timestamp, 'artist': artist, + 'albumArtist': album_artist, 'album': album || "", 'sk': this.session.key }; From 0bcd7a399c0712e5de26b8b779d868a37fb55609 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Tue, 20 Jan 2015 11:37:55 -0800 Subject: [PATCH 45/74] Fixed argument order for scrobble --- js/background.js | 4 ++-- js/lastfm.js | 4 ++-- manifest.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/js/background.js b/js/background.js index 2261373..22df949 100644 --- a/js/background.js +++ b/js/background.js @@ -90,9 +90,9 @@ function port_on_message(message) { } -function scrobble_song(artist, album, title, time) { +function scrobble_song(artist, album_artist, album, title, time) { // Scrobble this song - lastfm_api.scrobble(title, time, artist, album, + lastfm_api.scrobble(artist, album_artist, album, title, time, function(response) { if (response.error) { if (response.error == 9) { diff --git a/js/lastfm.js b/js/lastfm.js index c874c10..43518ca 100644 --- a/js/lastfm.js +++ b/js/lastfm.js @@ -87,8 +87,8 @@ LastFM.prototype.now_playing = function(track, artist, album, callback) { * @param callback Callback function for the request. Sends a parameter with * reply decoded as JS object from JSON on null on error */ -LastFM.prototype.scrobble = function(track, timestamp, artist, album_artist, - album, callback) { +LastFM.prototype.scrobble = function(artist, album_artist, album, track, + timestamp, callback) { var params = { 'api_key': this.API_KEY, 'method': "track.scrobble", diff --git a/manifest.json b/manifest.json index 0cacd1f..7aea70c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.5.6", + "version" : "1.5.7", "manifest_version": 2, "permissions" : [ "tabs", From 424aaa02f1a2f97400104c40f053ccb727386e4b Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Tue, 20 Jan 2015 22:37:34 -0800 Subject: [PATCH 46/74] Properly convert spaces when getting album artist --- js/contentscript.js | 5 ++++- manifest.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/js/contentscript.js b/js/contentscript.js index 85673b5..bec1e92 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -113,7 +113,10 @@ GoogleMusicParser.prototype._get_song_artist = function() { * @return The album artist */ GoogleMusicParser.prototype._get_album_artist = function() { - return decodeURIComponent($(".player-album").attr('data-id').split('/')[1]); + var album_artist = $(".player-album").attr('data-id'); + if (album_artist) + return decodeURIComponent(album_artist.split('/')[1].replace('+', ' ')); + return null; }; /** diff --git a/manifest.json b/manifest.json index 7aea70c..1cc6da5 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.5.7", + "version" : "1.6.0", "manifest_version": 2, "permissions" : [ "tabs", From 8cf23b7947c9737fa7e96833561e710216d372fb Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sun, 25 Jan 2015 12:51:46 -0800 Subject: [PATCH 47/74] stupid replace function --- js/contentscript.js | 3 ++- manifest.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/js/contentscript.js b/js/contentscript.js index bec1e92..47786c1 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -115,7 +115,8 @@ GoogleMusicParser.prototype._get_song_artist = function() { GoogleMusicParser.prototype._get_album_artist = function() { var album_artist = $(".player-album").attr('data-id'); if (album_artist) - return decodeURIComponent(album_artist.split('/')[1].replace('+', ' ')); + return decodeURIComponent( + album_artist.split('/')[1].replace(/\+/g, ' ')); return null; }; diff --git a/manifest.json b/manifest.json index 1cc6da5..955dc85 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.6.0", + "version" : "1.6.1", "manifest_version": 2, "permissions" : [ "tabs", From f34705cab7604855131c6163c7ca17cb66ca3916 Mon Sep 17 00:00:00 2001 From: gzhang1337 Date: Sun, 8 Feb 2015 01:49:45 -0500 Subject: [PATCH 48/74] marquee changed to span --- html/popup.html | 4 ++-- js/popup.js | 7 ------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/html/popup.html b/html/popup.html index fca2b41..54d6b69 100644 --- a/html/popup.html +++ b/html/popup.html @@ -26,10 +26,10 @@
- +  -  - +
diff --git a/js/popup.js b/js/popup.js index c4d8b7a..19f8c78 100644 --- a/js/popup.js +++ b/js/popup.js @@ -33,13 +33,6 @@ function update_song_info() { $("#cover").attr({ src: bp.player.song.cover || "../img/defaultcover.png", alt:bp.player.song.album}); $("#album").text(bp.player.song.album); - // check if we need to marquee - var songElem = $("#now-playing"); - if (songElem.get(0).scrollWidth > songElem.width() + 10) { - songElem.attr('scrollamount', '1'); - } else { - songElem.attr('scrollamount', '0'); - } if (bp.lastfm_api.session.name && bp.lastfm_api.session.key) { render_love_button(); From efcfcd3d218f80c449620072cb3bbe94654f3483 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Mon, 9 Feb 2015 21:46:26 -0800 Subject: [PATCH 49/74] Update version number --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 955dc85..e7a22b1 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.6.1", + "version" : "1.6.2", "manifest_version": 2, "permissions" : [ "tabs", From b59beabc17dd203bb27464a38e6184f38573728c Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 23 Apr 2015 22:46:00 +0100 Subject: [PATCH 50/74] Upd: Moves to or opens Google Play on Last.fm authentication. --- html/lastfm_callback.html | 1 + js/lastfm_callback.js | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/html/lastfm_callback.html b/html/lastfm_callback.html index a57209b..766a3fa 100644 --- a/html/lastfm_callback.html +++ b/html/lastfm_callback.html @@ -4,4 +4,5 @@ Copyright (c) 2011 Alexey Savartsov Licensed under the MIT license --> + diff --git a/js/lastfm_callback.js b/js/lastfm_callback.js index 560f396..f2bb43b 100644 --- a/js/lastfm_callback.js +++ b/js/lastfm_callback.js @@ -10,6 +10,9 @@ function _url_param(name, url) { } chrome.runtime.getBackgroundPage(function(background) { - location.href = "http://last.fm/"; background.get_lastfm_session(_url_param("token", location.search)); + open_play_tab(); + setTimeout(function() { + window.close(); + }, 100); }); From 668470b5bbeb083c2a65bf9bcbb73cef915b3c50 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 24 Apr 2015 10:46:54 +0100 Subject: [PATCH 51/74] Oops: Tidy up indentation. --- js/lastfm_callback.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/lastfm_callback.js b/js/lastfm_callback.js index f2bb43b..3c32cca 100644 --- a/js/lastfm_callback.js +++ b/js/lastfm_callback.js @@ -12,7 +12,7 @@ function _url_param(name, url) { chrome.runtime.getBackgroundPage(function(background) { background.get_lastfm_session(_url_param("token", location.search)); open_play_tab(); - setTimeout(function() { - window.close(); - }, 100); + setTimeout(function() { + window.close(); + }, 100); }); From b25d9486828463b7ae6a44a27f3687bddb0f8e01 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Tue, 5 May 2015 23:08:17 -0700 Subject: [PATCH 52/74] Update version to 1.6.3 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index e7a22b1..f29ef5e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.6.2", + "version" : "1.6.3", "manifest_version": 2, "permissions" : [ "tabs", From c95bbcc93052c42c30c327d92cc72d4cc2d38a03 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Thu, 14 May 2015 20:52:18 -0700 Subject: [PATCH 53/74] Fix scrobbling with material UI. --- js/contentscript.js | 12 ++++++------ manifest.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/js/contentscript.js b/js/contentscript.js index 47786c1..aefe02f 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -39,7 +39,7 @@ GoogleMusicParser = function() { * @return true if some song is loaded, otherwise false */ GoogleMusicParser.prototype._get_has_song = function() { - return $("#playerSongInfo div").hasClass("now-playing-menu-wrapper"); + return $("#playerSongInfo").children().length > 0; }; /** @@ -48,7 +48,7 @@ GoogleMusicParser.prototype._get_has_song = function() { * @return true if song is playing, false if song is paused */ GoogleMusicParser.prototype._get_is_playing = function() { - return $(".player-middle button[data-id='play-pause']").hasClass("playing"); + return $(".material-player-middle sj-icon-button[data-id='play-pause']").hasClass("playing"); }; /** @@ -95,7 +95,7 @@ GoogleMusicParser.prototype._get_song_time = function() { */ GoogleMusicParser.prototype._get_song_title = function() { // the text inside the div located inside element with id="playerSongTitle" - return $("#playerSongTitle").text(); + return $("#player-song-title").text(); }; /** @@ -157,7 +157,7 @@ chrome.runtime.onMessage.addListener(next_song); function toggle_play(msg, sndr, callback) { if (msg.cmd == "tgl") { - $(".player-middle button[data-id='play-pause']").click(); + $(".material-player-middle sj-icon-button[data-id='play-pause']").click(); port.postMessage(new Player(new GoogleMusicParser())); callback(); } @@ -165,7 +165,7 @@ function toggle_play(msg, sndr, callback) { function prev_song(msg, sndr, callback) { if (msg.cmd == "prv") { - $(".player-middle button[data-id='rewind']").click(); + $(".material-player-middle sj-icon-button[data-id='rewind']").click(); port.postMessage(new Player(new GoogleMusicParser())); callback(); } @@ -173,7 +173,7 @@ function prev_song(msg, sndr, callback) { function next_song(msg, sndr, callback) { if (msg.cmd == "nxt") { - $(".player-middle button[data-id='forward']").click(); + $(".material-player-middle sj-icon-button[data-id='forward']").click(); port.postMessage(new Player(new GoogleMusicParser())); callback(); } diff --git a/manifest.json b/manifest.json index f29ef5e..c18965b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.6.3", + "version" : "1.6.4", "manifest_version": 2, "permissions" : [ "tabs", From 294add7840f3baa8c8fa1e13ed4fd2bbe96155eb Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Fri, 15 May 2015 00:35:53 -0700 Subject: [PATCH 54/74] Song info updates properly when you press player controls. --- js/contentscript.js | 36 ++++++++++++++----- js/popup.js | 85 +++++++++++++++++++++++---------------------- manifest.json | 2 +- 3 files changed, 72 insertions(+), 51 deletions(-) diff --git a/js/contentscript.js b/js/contentscript.js index aefe02f..4cc36d6 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -155,26 +155,44 @@ chrome.runtime.onMessage.addListener(toggle_play); chrome.runtime.onMessage.addListener(prev_song); chrome.runtime.onMessage.addListener(next_song); -function toggle_play(msg, sndr, callback) { +function toggle_play(msg, sndr, send_response) { if (msg.cmd == "tgl") { $(".material-player-middle sj-icon-button[data-id='play-pause']").click(); - port.postMessage(new Player(new GoogleMusicParser())); - callback(); + /* + * Wait a little for the UI to update before sending a response + * with the updated state. + */ + setTimeout(function() { + send_response(new Player(new GoogleMusicParser())); + }, 100); + /* + * Return true keeps the message channel open so the timeout function + * will be called, otherwise send_response will not work per + * https://developer.chrome.com/extensions/runtime#event-onMessage. + */ + return true; } + return false; } -function prev_song(msg, sndr, callback) { +function prev_song(msg, sndr, send_response) { if (msg.cmd == "prv") { $(".material-player-middle sj-icon-button[data-id='rewind']").click(); - port.postMessage(new Player(new GoogleMusicParser())); - callback(); + setTimeout(function() { + send_response(new Player(new GoogleMusicParser())); + }, 100); + return true; } + return false; } -function next_song(msg, sndr, callback) { +function next_song(msg, sndr, send_response) { if (msg.cmd == "nxt") { $(".material-player-middle sj-icon-button[data-id='forward']").click(); - port.postMessage(new Player(new GoogleMusicParser())); - callback(); + setTimeout(function() { + send_response(new Player(new GoogleMusicParser())); + }, 100); + return true; } + return false; } diff --git a/js/popup.js b/js/popup.js index 19f8c78..5e3511c 100644 --- a/js/popup.js +++ b/js/popup.js @@ -26,36 +26,29 @@ $(document).ready(function() { function set_play_link() { $("#cover").click(open_play_tab); } + /* Render functions */ -function update_song_info() { - $("#artist").text(bp.player.song.artist); - $("#track").text(bp.player.song.title); - $("#cover").attr({ src: bp.player.song.cover || "../img/defaultcover.png", - alt:bp.player.song.album}); - $("#album").text(bp.player.song.album); +function update_song_info(player) { + $("#artist").text(player.song.artist); + $("#track").text(player.song.title); + $("#cover").attr({ src: player.song.cover || "../img/defaultcover.png", + alt:player.song.album}); + $("#album").text(player.song.album); if (bp.lastfm_api.session.name && bp.lastfm_api.session.key) { - render_love_button(); + render_love_button(player); } - toggle_play_btn(); + toggle_play_btn(player); } -function toggle_play_btn() { +function toggle_play_btn(player) { var play_btn = $("#play-pause-btn"); - play_btn.toggle = function() { - if (bp.player.is_playing) { - play_btn.removeClass(); - play_btn.addClass("pause"); - } else { - play_btn.removeClass(); - play_btn.addClass("play"); - } - } - // TODO kind of hackish - // This callback can be called too early (before bp.player is updated) - // Thus, just try a few times for 3 seconds - for (var i = 0; i < 30; i++) { - setTimeout(play_btn.toggle, i * 100); + if (player.is_playing) { + play_btn.removeClass(); + play_btn.addClass("pause"); + } else { + play_btn.removeClass(); + play_btn.addClass("play"); } } @@ -64,7 +57,7 @@ function toggle_play_btn() { */ function render_song() { if (bp.player.has_song) { - update_song_info(); + update_song_info(bp.player); $("#play-pause-btn").click(toggle_play); $("#next-btn").click(next_song); $("#prev-btn").click(prev_song); @@ -124,21 +117,21 @@ function render_auth_link() { /** * Renders the love button */ -function render_love_button() { +function render_love_button(player) { $("#love-button").html(''); - bp.lastfm_api.is_track_loved(bp.player.song.title, - bp.player.song.artist, + bp.lastfm_api.is_track_loved(player.song.title, + player.song.artist, function(result) { $("#love-button").html(''); if (result) { $("#love-button a").attr({ title: "Unlove this song"}) - .click(on_unlove) + .click(function() {on_unlove(player)}) .addClass("loved"); } else { $("#love-button a").attr({ title: "Love this song" }) - .click(on_love) + .click(function() {on_love(player)}) .addClass("notloved"); } }); @@ -151,12 +144,12 @@ function toggle_play() { find_play_tab( function(tab) { chrome.tabs.sendMessage(tab.id, {cmd: "tgl"}, - function() { + function(player) { if (has_song) { - toggle_play_btn(); + toggle_play_btn(player); } else { // if pressing FF on previous song reached end of play queue - update_song_info(); - toggle_play_btn(); + update_song_info(player); + toggle_play_btn(player); } } ); @@ -168,7 +161,14 @@ function prev_song() { find_play_tab( function(tab) { chrome.tabs.sendMessage(tab.id, {cmd: "prv"}, - update_song_info); + function(player) { + /* The player state is in a disabled state as it loads the + * song initially, but we should display it as playing since + * hitting next or previous always starts a song. + */ + player.is_playing = true; + update_song_info(player) + }); } ); } @@ -177,7 +177,10 @@ function next_song() { find_play_tab( function(tab) { chrome.tabs.sendMessage(tab.id, {cmd: "nxt"}, - update_song_info); + function(player) { + player.is_playing = true; + update_song_info(player) + }); } ); } @@ -209,11 +212,11 @@ function on_logout() { /** * Love button was clicked */ -function on_love() { - bp.lastfm_api.love_track(bp.player.song.title, bp.player.song.artist, +function on_love(player) { + bp.lastfm_api.love_track(player.song.title, player.song.artist, function(result) { if (!result.error) { - render_love_button(); + render_love_button(player); } else { if (result.error == 9) { @@ -233,11 +236,11 @@ function on_love() { /** * Unlove button was clicked */ -function on_unlove() { - bp.lastfm_api.unlove_track(bp.player.song.title, bp.player.song.artist, +function on_unlove(player) { + bp.lastfm_api.unlove_track(player.song.title, player.song.artist, function(result) { if (!result.error) { - render_love_button(); + render_love_button(player); } else { if (result.error == 9) { // Session expired diff --git a/manifest.json b/manifest.json index c18965b..a85e222 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.6.4", + "version" : "1.6.5", "manifest_version": 2, "permissions" : [ "tabs", From ae767ee448ab53a27cd3cfc1132363ccc2261612 Mon Sep 17 00:00:00 2001 From: dhnishi Date: Tue, 19 May 2015 13:34:21 -0700 Subject: [PATCH 55/74] Instead of assuming that the refresh is every 2 seconds, calculate the time since the last refresh. --- js/background.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/js/background.js b/js/background.js index 22df949..1b813b1 100644 --- a/js/background.js +++ b/js/background.js @@ -7,6 +7,7 @@ var player = {}; // Previous player state var time_played = 0; +var last_refresh = (new Date()).getTime(); var num_scrobbles = 0; var curr_song_title = ''; var lastfm_api = new LastFM(SETTINGS.api_key, SETTINGS.api_secret); @@ -55,6 +56,7 @@ function port_on_message(message) { curr_song_title = _p.song.title; time_played = 0; num_scrobbles = 0; + last_refresh = (new Date()).getTime() - _p.song.position * 1000; } if (_p.is_playing) { @@ -65,11 +67,15 @@ function port_on_message(message) { num_scrobbles < SETTINGS.max_scrobbles) { scrobble_song(_p.song.artist,_p.song.album_artist, _p.song.album, _p.song.title, - Math.round(new Date().getTime() / 1000) - time_played); + Math.round(new Date().getTime() / 1000 - time_played)); time_played = 0; num_scrobbles += 1; } else { - time_played += SETTINGS.refresh_interval; + var now = (new Date()).getTime(); + if (last_refresh != 0) { + time_played += (now - last_refresh) / 1000; + } + last_refresh = now; } lastfm_api.now_playing(_p.song.title, @@ -83,6 +89,7 @@ function port_on_message(message) { // The player is paused chrome.browserAction.setIcon({ 'path': SETTINGS.paused_icon }); + last_refresh = (new Date()).getTime(); } } else { chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); From 01d35fd18abb8b20c028e20a64fd667afb1420ec Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Thu, 21 May 2015 11:31:57 -0700 Subject: [PATCH 56/74] Move some stuff around. --- js/background.js | 23 ++++++++++++++--------- manifest.json | 2 +- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/js/background.js b/js/background.js index 1b813b1..7f133c3 100644 --- a/js/background.js +++ b/js/background.js @@ -39,6 +39,7 @@ function port_on_connect(port) { function port_on_message(message) { // Current player state var _p = message; + var now = (new Date()).getTime(); // Save player state player = _p; @@ -51,17 +52,17 @@ function port_on_message(message) { } if (_p.has_song) { + // if the song changed or looped if (_p.song.title != curr_song_title || _p.song.position <= SETTINGS.refresh_interval) { curr_song_title = _p.song.title; time_played = 0; num_scrobbles = 0; - last_refresh = (new Date()).getTime() - _p.song.position * 1000; + last_refresh = now - SETTINGS.refresh_interval*1000; } if (_p.is_playing) { - chrome.browserAction.setIcon({ - 'path': SETTINGS.playing_icon }); + chrome.browserAction.setIcon({'path': SETTINGS.playing_icon }); if ((time_played >= _p.song.time * SETTINGS.scrobble_point || time_played >= SETTINGS.scrobble_interval) && num_scrobbles < SETTINGS.max_scrobbles) { @@ -71,11 +72,14 @@ function port_on_message(message) { time_played = 0; num_scrobbles += 1; } else { - var now = (new Date()).getTime(); - if (last_refresh != 0) { - time_played += (now - last_refresh) / 1000; - } - last_refresh = now; + /* + * Don't depend on the SETTINGS.refresh_interval to + * calculate time_played since there can be a significant delay + * between the time the message was sent from the contentscript + * to when it's recieved here. + * See: https://github.com/newgiin/cloudplayer-scrobbler/issues/23 + */ + time_played += (now - last_refresh) / 1000; } lastfm_api.now_playing(_p.song.title, @@ -89,11 +93,11 @@ function port_on_message(message) { // The player is paused chrome.browserAction.setIcon({ 'path': SETTINGS.paused_icon }); - last_refresh = (new Date()).getTime(); } } else { chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); } + last_refresh = now; } @@ -119,6 +123,7 @@ function port_on_disconnect() { player = {}; // Clear player state time_played = 0; num_scrobbles = 0; + curr_song_title = ''; chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); } diff --git a/manifest.json b/manifest.json index a85e222..221b475 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.6.5", + "version" : "1.6.6", "manifest_version": 2, "permissions" : [ "tabs", From 1cb4384d8086b0cedc92afc2316cefab4b9792b4 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Mon, 26 Oct 2015 11:38:38 -0700 Subject: [PATCH 57/74] Handle if HTML has paper-icon elements instead of sj-icon elements. --- js/contentscript.js | 24 ++++++++++++++++++++---- manifest.json | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/js/contentscript.js b/js/contentscript.js index 4cc36d6..c47b715 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -48,7 +48,11 @@ GoogleMusicParser.prototype._get_has_song = function() { * @return true if song is playing, false if song is paused */ GoogleMusicParser.prototype._get_is_playing = function() { - return $(".material-player-middle sj-icon-button[data-id='play-pause']").hasClass("playing"); + var play_btn = $(".material-player-middle paper-icon-button[data-id='play-pause']"); + if (play_btn.length == 0) { + play_btn = $(".material-player-middle sj-icon-button[data-id='play-pause']") + } + return play_btn.hasClass("playing"); }; /** @@ -157,7 +161,11 @@ chrome.runtime.onMessage.addListener(next_song); function toggle_play(msg, sndr, send_response) { if (msg.cmd == "tgl") { - $(".material-player-middle sj-icon-button[data-id='play-pause']").click(); + var play_btn = $(".material-player-middle paper-icon-button[data-id='play-pause']"); + if (play_btn.length == 0) { + play_btn = $(".material-player-middle sj-icon-button[data-id='play-pause']") + } + play_btn.click(); /* * Wait a little for the UI to update before sending a response * with the updated state. @@ -177,7 +185,11 @@ function toggle_play(msg, sndr, send_response) { function prev_song(msg, sndr, send_response) { if (msg.cmd == "prv") { - $(".material-player-middle sj-icon-button[data-id='rewind']").click(); + var prev_btn = $(".material-player-middle paper-icon-button[data-id='rewind']"); + if (prev_btn.length == 0) { + prev_btn = $(".material-player-middle sj-icon-button[data-id='rewind']"); + } + prev_btn.click(); setTimeout(function() { send_response(new Player(new GoogleMusicParser())); }, 100); @@ -188,7 +200,11 @@ function prev_song(msg, sndr, send_response) { function next_song(msg, sndr, send_response) { if (msg.cmd == "nxt") { - $(".material-player-middle sj-icon-button[data-id='forward']").click(); + var next_btn = $(".material-player-middle paper-icon-button[data-id='forward']"); + if (next_btn.length == 0) { + next_btn = $(".material-player-middle sj-icon-button[data-id='forward']"); + } + next_btn.click(); setTimeout(function() { send_response(new Player(new GoogleMusicParser())); }, 100); diff --git a/manifest.json b/manifest.json index 221b475..2974417 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.6.6", + "version" : "1.6.7", "manifest_version": 2, "permissions" : [ "tabs", From 0e000304f07d123fb533db67247e43d561d00950 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Fri, 11 Dec 2015 13:06:38 -0800 Subject: [PATCH 58/74] update contentscript --- js/contentscript.js | 8 ++++---- manifest.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/js/contentscript.js b/js/contentscript.js index c47b715..8de37c8 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -99,7 +99,7 @@ GoogleMusicParser.prototype._get_song_time = function() { */ GoogleMusicParser.prototype._get_song_title = function() { // the text inside the div located inside element with id="playerSongTitle" - return $("#player-song-title").text(); + return $("#currently-playing-title").text(); }; /** @@ -117,7 +117,7 @@ GoogleMusicParser.prototype._get_song_artist = function() { * @return The album artist */ GoogleMusicParser.prototype._get_album_artist = function() { - var album_artist = $(".player-album").attr('data-id'); + var album_artist = $("#playerSongInfo .player-album").attr('data-id'); if (album_artist) return decodeURIComponent( album_artist.split('/')[1].replace(/\+/g, ' ')); @@ -130,7 +130,7 @@ GoogleMusicParser.prototype._get_album_artist = function() { * @return Image URL or default artwork */ GoogleMusicParser.prototype._get_song_cover = function() { - var albumImg = $("#playingAlbumArt").attr("src"); + var albumImg = $("#playerBarArt").attr("src"); if (albumImg) return albumImg; return null; @@ -142,7 +142,7 @@ GoogleMusicParser.prototype._get_song_cover = function() { * @return Album name or null */ GoogleMusicParser.prototype._get_song_album = function() { - return $("#playerSongInfo .player-artist-album-wrapper .player-album").text(); + return $("#playerSongInfo .player-album").text(); }; var port = chrome.runtime.connect(); diff --git a/manifest.json b/manifest.json index 2974417..39d7125 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.6.7", + "version" : "1.6.8", "manifest_version": 2, "permissions" : [ "tabs", From b35716b1a84d544649d35ee6b4deb4b37fb39208 Mon Sep 17 00:00:00 2001 From: byted Date: Wed, 27 Jan 2016 21:43:55 -0800 Subject: [PATCH 59/74] do not scrobble google music advertisments --- js/background.js | 8 +++++++- js/settings.js | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/js/background.js b/js/background.js index 7f133c3..2c351ec 100644 --- a/js/background.js +++ b/js/background.js @@ -65,7 +65,8 @@ function port_on_message(message) { chrome.browserAction.setIcon({'path': SETTINGS.playing_icon }); if ((time_played >= _p.song.time * SETTINGS.scrobble_point || time_played >= SETTINGS.scrobble_interval) && - num_scrobbles < SETTINGS.max_scrobbles) { + num_scrobbles < SETTINGS.max_scrobbles && + !is_advertisment(_p.song)) { scrobble_song(_p.song.artist,_p.song.album_artist, _p.song.album, _p.song.title, Math.round(new Date().getTime() / 1000 - time_played)); @@ -116,6 +117,11 @@ function scrobble_song(artist, album_artist, album, title, time) { }); } +function is_advertisment(song) { + return song.title === SETTINGS.gmusic_ads_metadata.title && + song.artist === SETTINGS.gmusic_ads_metadata.artist +} + /** * Content script has disconnected */ diff --git a/js/settings.js b/js/settings.js index edab61b..f7243cd 100644 --- a/js/settings.js +++ b/js/settings.js @@ -14,7 +14,12 @@ var SETTINGS = { scrobble_interval: 420, // 7 minutes max_scrobbles: Number.POSITIVE_INFINITY, - refresh_interval: 2 + refresh_interval: 2, + + gmusic_ads_metadata: { + title: 'We\'ll be right back', + artist: 'Subscribe to go ad-free' + } }; SETTINGS.max_scrobbles = localStorage['max_scrobbles'] && From 2f17fd0610e897aa174dcf9e31e1aa3b281d7c81 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Mon, 15 Feb 2016 22:11:49 -0800 Subject: [PATCH 60/74] Bump manifest to 1.6.9 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 39d7125..c38a024 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.6.8", + "version" : "1.6.9", "manifest_version": 2, "permissions" : [ "tabs", From 2da06b5f4820a9f6a89adfcae292dc43186a28d3 Mon Sep 17 00:00:00 2001 From: Tim Sell Date: Wed, 21 Jun 2017 03:18:41 +0800 Subject: [PATCH 61/74] closes #38, send now playing request only once with the correct duration --- js/background.js | 17 +++++++++-------- js/lastfm.js | 3 ++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/js/background.js b/js/background.js index 2c351ec..7b278ec 100644 --- a/js/background.js +++ b/js/background.js @@ -59,6 +59,15 @@ function port_on_message(message) { time_played = 0; num_scrobbles = 0; last_refresh = now - SETTINGS.refresh_interval*1000; + + lastfm_api.now_playing(_p.song.title, + _p.song.artist, + _p.song.album, + _p.song.time, + function(response) { + // TODO: + } + ); } if (_p.is_playing) { @@ -82,14 +91,6 @@ function port_on_message(message) { */ time_played += (now - last_refresh) / 1000; } - - lastfm_api.now_playing(_p.song.title, - _p.song.artist, - _p.song.album, - function(response) { - // TODO: - } - ); } else { // The player is paused chrome.browserAction.setIcon({ diff --git a/js/lastfm.js b/js/lastfm.js index 43518ca..e5a277b 100644 --- a/js/lastfm.js +++ b/js/lastfm.js @@ -59,12 +59,13 @@ LastFM.prototype.authorize = function(token, callback) { * @param callback Callback function for the request. Sends a parameter with * reply decoded as JS object from JSON on null on error */ -LastFM.prototype.now_playing = function(track, artist, album, callback) { +LastFM.prototype.now_playing = function(track, artist, album, duration, callback) { var params = { 'api_key': this.API_KEY, 'method': "track.updateNowPlaying", 'track': track, 'artist': artist, + 'duration': duration, 'album': album || "", 'sk': this.session.key }; From e07033ccc5e11b96540aa2b550174e9b7d2aacf7 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sat, 29 Jul 2017 14:19:09 -0700 Subject: [PATCH 62/74] bump version to 1.7.0 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index c38a024..39a36fa 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.6.9", + "version" : "1.7.0", "manifest_version": 2, "permissions" : [ "tabs", From 8d99df2c75018baf0be4841b90cb56e5fd3dc12f Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sun, 21 Jan 2018 22:59:23 -0800 Subject: [PATCH 63/74] Set indent to two spaces. Cleanup style in background.js --- js/background.js | 279 ++++++++++++++++++++++++----------------------- 1 file changed, 142 insertions(+), 137 deletions(-) diff --git a/js/background.js b/js/background.js index 7b278ec..1087cac 100644 --- a/js/background.js +++ b/js/background.js @@ -4,7 +4,6 @@ * Copyright (c) 2011 Alexey Savartsov * Licensed under the MIT license */ - var player = {}; // Previous player state var time_played = 0; var last_refresh = (new Date()).getTime(); @@ -13,125 +12,126 @@ var curr_song_title = ''; var lastfm_api = new LastFM(SETTINGS.api_key, SETTINGS.api_secret); // Load settings from local storage -lastfm_api.session.key = localStorage['session_key'] || null; -lastfm_api.session.name = localStorage['session_name'] || null; +lastfm_api.session.key = localStorage.getItem('session_key'); +lastfm_api.session.name = localStorage.getItem('session_name'); + if (!SETTINGS.scrobble) { - chrome.browserAction.setIcon({ 'path': SETTINGS.scrobbling_stopped_icon }); + chrome.browserAction.setIcon({'path': SETTINGS.scrobbling_stopped_icon}); } // Connect event handlers chrome.runtime.onConnect.addListener(port_on_connect); - bind_keyboard_shortcuts(); + /** * Content script has connected to the extension */ function port_on_connect(port) { - port.onMessage.addListener(port_on_message); - port.onDisconnect.addListener(port_on_disconnect); + port.onMessage.addListener(port_on_message); + port.onDisconnect.addListener(port_on_disconnect); } - /** - * New message arrives to the port - */ -function port_on_message(message) { - // Current player state - var _p = message; - var now = (new Date()).getTime(); - - // Save player state - player = _p; - - if (!SETTINGS.scrobble) { - chrome.browserAction.setIcon({ - 'path': SETTINGS.scrobbling_stopped_icon }); - - return; - } - if (_p.has_song) { - // if the song changed or looped - if (_p.song.title != curr_song_title || - _p.song.position <= SETTINGS.refresh_interval) { - curr_song_title = _p.song.title; - time_played = 0; - num_scrobbles = 0; - last_refresh = now - SETTINGS.refresh_interval*1000; - - lastfm_api.now_playing(_p.song.title, - _p.song.artist, - _p.song.album, - _p.song.time, - function(response) { - // TODO: - } - ); +/** + * New message arrives to the port + */ +function port_on_message(message) { + // Current player state + var _p = message; + var now = (new Date()).getTime(); + + // Save player state + player = _p; + + if (!SETTINGS.scrobble) { + chrome.browserAction.setIcon({'path': SETTINGS.scrobbling_stopped_icon}); + + return; + } + + if (_p.has_song) { + // if the song changed or looped + if (_p.song.title != curr_song_title || + _p.song.position <= SETTINGS.refresh_interval) { + curr_song_title = _p.song.title; + time_played = 0; + num_scrobbles = 0; + last_refresh = now - SETTINGS.refresh_interval*1000; + + lastfm_api.now_playing(_p.song.title, + _p.song.artist, + _p.song.album, + _p.song.time, + function(response) { + // TODO: } + ); + } - if (_p.is_playing) { - chrome.browserAction.setIcon({'path': SETTINGS.playing_icon }); - if ((time_played >= _p.song.time * SETTINGS.scrobble_point || - time_played >= SETTINGS.scrobble_interval) && - num_scrobbles < SETTINGS.max_scrobbles && - !is_advertisment(_p.song)) { - scrobble_song(_p.song.artist,_p.song.album_artist, - _p.song.album, _p.song.title, - Math.round(new Date().getTime() / 1000 - time_played)); - time_played = 0; - num_scrobbles += 1; - } else { - /* - * Don't depend on the SETTINGS.refresh_interval to - * calculate time_played since there can be a significant delay - * between the time the message was sent from the contentscript - * to when it's recieved here. - * See: https://github.com/newgiin/cloudplayer-scrobbler/issues/23 - */ - time_played += (now - last_refresh) / 1000; - } - } else { - // The player is paused - chrome.browserAction.setIcon({ - 'path': SETTINGS.paused_icon }); - } + if (_p.is_playing) { + chrome.browserAction.setIcon({'path': SETTINGS.playing_icon }); + if ((time_played >= _p.song.time * SETTINGS.scrobble_point || + time_played >= SETTINGS.scrobble_interval) && + num_scrobbles < SETTINGS.max_scrobbles && + !is_advertisment(_p.song)) { + scrobble_song(_p.song.artist,_p.song.album_artist, + _p.song.album, _p.song.title, + Math.round(new Date().getTime() / 1000 - time_played)); + time_played = 0; + num_scrobbles += 1; + } else { + /* + * Don't depend on the SETTINGS.refresh_interval to + * calculate time_played since there can be a significant delay + * between the time the message was sent from the contentscript + * to when it's recieved here. + * See: https://github.com/newgiin/cloudplayer-scrobbler/issues/23 + */ + time_played += (now - last_refresh) / 1000; + } } else { - chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); + // The player is paused + chrome.browserAction.setIcon({'path': SETTINGS.paused_icon}); } - last_refresh = now; + } else { + chrome.browserAction.setIcon({'path': SETTINGS.main_icon}); + } + last_refresh = now; } function scrobble_song(artist, album_artist, album, title, time) { - // Scrobble this song - lastfm_api.scrobble(artist, album_artist, album, title, time, - function(response) { - if (response.error) { - if (response.error == 9) { - // Session expired - clear_session(); - } - chrome.browserAction.setIcon({ - 'path': SETTINGS.error_icon }); - } - }); + // Scrobble this song + lastfm_api.scrobble(artist, album_artist, album, title, time, + function(response) { + if (response.error) { + if (response.error == 9) { + // Session expired + clear_session(); + } + chrome.browserAction.setIcon({'path': SETTINGS.error_icon}); + } + }); } + function is_advertisment(song) { - return song.title === SETTINGS.gmusic_ads_metadata.title && - song.artist === SETTINGS.gmusic_ads_metadata.artist + return (song.title === SETTINGS.gmusic_ads_metadata.title && + song.artist === SETTINGS.gmusic_ads_metadata.artist); } + /** -* Content script has disconnected -*/ + * Content script has disconnected + */ function port_on_disconnect() { - player = {}; // Clear player state - time_played = 0; - num_scrobbles = 0; - curr_song_title = ''; - chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); + player = {}; // Clear player state + time_played = 0; + num_scrobbles = 0; + curr_song_title = ''; + chrome.browserAction.setIcon({'path': SETTINGS.main_icon}); } @@ -139,81 +139,86 @@ function port_on_disconnect() { * Authentication link from popup window */ function start_web_auth() { - var callback_url = chrome.runtime.getURL(SETTINGS.callback_file); - chrome.tabs.create({ - 'url': - 'http://www.last.fm/api/auth?api_key=' + - SETTINGS.api_key + - '&cb=' + - callback_url }); + var callback_url = chrome.runtime.getURL(SETTINGS.callback_file); + chrome.tabs.create({ + 'url': ('http://www.last.fm/api/auth?api_key=' + SETTINGS.api_key + '&cb=' + + callback_url) + }); } + /** * Clears last.fm session */ function clear_session() { - lastfm_api.session = {}; + lastfm_api.session = {}; - localStorage.removeItem('session_key'); - localStorage.removeItem('session_name'); + localStorage.removeItem('session_key'); + localStorage.removeItem('session_name'); } + /** * Toggles setting to scrobble songs or not */ function toggle_scrobble() { - SETTINGS.scrobble = !SETTINGS.scrobble; - localStorage['scrobble'] = SETTINGS.scrobble; + SETTINGS.scrobble = !SETTINGS.scrobble; + localStorage.setItem('scrobble', SETTINGS.scrobble); - // Set the icon corresponding the current scrobble state - var icon = SETTINGS.scrobble ? SETTINGS.main_icon : SETTINGS.scrobbling_stopped_icon; - chrome.browserAction.setIcon({ 'path': icon }); + // Set the icon corresponding the current scrobble state + var icon = (SETTINGS.scrobble ? + SETTINGS.main_icon : SETTINGS.scrobbling_stopped_icon); + chrome.browserAction.setIcon({'path': icon}); } + /** * Last.fm session request */ function get_lastfm_session(token) { - lastfm_api.authorize(token, function(response) { - // Save session - if (response.session) { - localStorage['session_key'] = response.session.key; - localStorage['session_name'] = response.session.name; - } - }); + lastfm_api.authorize(token, function(response) { + // Save session + if (response.session) { + localStorage.setItem('session_key', response.session.key); + localStorage.setItem('session_name', response.session.name); + } + }); } + function bind_keyboard_shortcuts() { - chrome.commands.onCommand.addListener( - function(command) { - switch (command) { - case 'toggle_play': - send_cmd_to_play_tab('tgl'); - break; - case 'prev_song': - send_cmd_to_play_tab('prv'); - break; - case 'next_song': - send_cmd_to_play_tab('nxt'); - break; - case 'goto_play_tab': - open_play_tab(); - break; - default: - console.error("No handler for command '" + command + "'"); - } - } - ); + chrome.commands.onCommand.addListener( + function(command) { + switch (command) { + case 'toggle_play': + send_cmd_to_play_tab('tgl'); + break; + case 'prev_song': + send_cmd_to_play_tab('prv'); + break; + case 'next_song': + send_cmd_to_play_tab('nxt'); + break; + case 'goto_play_tab': + open_play_tab(); + break; + default: + console.error("No handler for command '" + command + "'"); + } + } + ); } + function send_cmd_to_play_tab(cmd) { - find_play_tab( - function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: cmd}, function() {}); - } - ); + find_play_tab( + function(tab) { + chrome.tabs.sendMessage(tab.id, {cmd: cmd}, function() {}); + } + ); } + function open_extensions_page() { chrome.tabs.create({url: 'chrome://extensions/'}); } From c0ebfe0d092d5874969c163716ac1e6ed088cf2b Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Fri, 16 Feb 2018 00:11:30 -0800 Subject: [PATCH 64/74] - Add logging option. - Change some localstorage calls to use getItem(). - various linting. --- html/options.html | 1 + js/background.js | 2 ++ js/contentscript.js | 4 ++-- js/logging.js | 5 +++++ js/options.js | 19 ++++++++++++++++--- js/popup.js | 8 ++++---- js/settings.js | 11 +++++++---- manifest.json | 10 ++++++++-- 8 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 js/logging.js diff --git a/html/options.html b/html/options.html index 23da8f0..3751fe1 100644 --- a/html/options.html +++ b/html/options.html @@ -7,6 +7,7 @@

Scrobble long tracks multiple times (once every minutes)?:
+ Enable logging (for debugging):

diff --git a/js/background.js b/js/background.js index 1087cac..59bb82a 100644 --- a/js/background.js +++ b/js/background.js @@ -4,6 +4,7 @@ * Copyright (c) 2011 Alexey Savartsov * Licensed under the MIT license */ +log("background.js loaded"); var player = {}; // Previous player state var time_played = 0; var last_refresh = (new Date()).getTime(); @@ -29,6 +30,7 @@ bind_keyboard_shortcuts(); * Content script has connected to the extension */ function port_on_connect(port) { + log("Content script connected"); port.onMessage.addListener(port_on_message); port.onDisconnect.addListener(port_on_disconnect); } diff --git a/js/contentscript.js b/js/contentscript.js index 8de37c8..2ad7070 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -50,7 +50,7 @@ GoogleMusicParser.prototype._get_has_song = function() { GoogleMusicParser.prototype._get_is_playing = function() { var play_btn = $(".material-player-middle paper-icon-button[data-id='play-pause']"); if (play_btn.length == 0) { - play_btn = $(".material-player-middle sj-icon-button[data-id='play-pause']") + play_btn = $(".material-player-middle sj-icon-button[data-id='play-pause']"); } return play_btn.hasClass("playing"); }; @@ -163,7 +163,7 @@ function toggle_play(msg, sndr, send_response) { if (msg.cmd == "tgl") { var play_btn = $(".material-player-middle paper-icon-button[data-id='play-pause']"); if (play_btn.length == 0) { - play_btn = $(".material-player-middle sj-icon-button[data-id='play-pause']") + play_btn = $(".material-player-middle sj-icon-button[data-id='play-pause']"); } play_btn.click(); /* diff --git a/js/logging.js b/js/logging.js new file mode 100644 index 0000000..164d2b9 --- /dev/null +++ b/js/logging.js @@ -0,0 +1,5 @@ +function log(message) { + if (SETTINGS.logs_enabled) { + console.log(message); + } +} \ No newline at end of file diff --git a/js/options.js b/js/options.js index b04a27c..4a465dc 100644 --- a/js/options.js +++ b/js/options.js @@ -2,9 +2,18 @@ function save_options() { var scrobble_mult = document.getElementById('scrobble_mult').checked; if (!scrobble_mult) { - localStorage['max_scrobbles'] = 1; + // TODO localStorage actually stores this as a string. Find out + // if this causes bugs. + localStorage.setItem('max_scrobbles', 1); } else { - localStorage.removeItem('max_scrobbles') + localStorage.removeItem('max_scrobbles'); + } + + var logs_enabled = document.getElementById('log_checkbox').checked; + if (logs_enabled) { + localStorage.setItem('logs_enabled', 'true'); + } else { + localStorage.removeItem('logs_enabled'); } chrome.runtime.getBackgroundPage(function(backgroundPage) { @@ -18,12 +27,16 @@ function save_options() { }); } + // Restores select box state to saved value from localStorage. function restore_options() { var scrobble_mult = SETTINGS.max_scrobbles > 1; document.getElementById('scrobble_mult').checked = scrobble_mult; - document.getElementById('minute_field').innerHTML = + document.getElementById('logs_enabled').checked = SETTINGS.logs_enabled; + document.getElementById('minute_field').innerHTML = Math.round((SETTINGS.scrobble_interval / 60) * 100) / 100; } + + document.addEventListener('DOMContentLoaded', restore_options); document.querySelector('#save').addEventListener('click', save_options); \ No newline at end of file diff --git a/js/popup.js b/js/popup.js index 5e3511c..06c8b6f 100644 --- a/js/popup.js +++ b/js/popup.js @@ -126,12 +126,12 @@ function render_love_button(player) { $("#love-button").html(''); if (result) { $("#love-button a").attr({ title: "Unlove this song"}) - .click(function() {on_unlove(player)}) + .click(function() {on_unlove(player);}) .addClass("loved"); } else { $("#love-button a").attr({ title: "Love this song" }) - .click(function() {on_love(player)}) + .click(function() {on_love(player);}) .addClass("notloved"); } }); @@ -167,7 +167,7 @@ function prev_song() { * hitting next or previous always starts a song. */ player.is_playing = true; - update_song_info(player) + update_song_info(player); }); } ); @@ -179,7 +179,7 @@ function next_song() { chrome.tabs.sendMessage(tab.id, {cmd: "nxt"}, function(player) { player.is_playing = true; - update_song_info(player) + update_song_info(player); }); } ); diff --git a/js/settings.js b/js/settings.js index f7243cd..fa60718 100644 --- a/js/settings.js +++ b/js/settings.js @@ -10,7 +10,7 @@ var SETTINGS = { error_icon: '../img/main-icon-error.png', scrobbling_stopped_icon: '../img/main-icon-scrobbling-stopped.png', - scrobble_point: .7, + scrobble_point: 0.7, scrobble_interval: 420, // 7 minutes max_scrobbles: Number.POSITIVE_INFINITY, @@ -22,9 +22,12 @@ var SETTINGS = { } }; -SETTINGS.max_scrobbles = localStorage['max_scrobbles'] && - parseInt(localStorage['max_scrobbles']) || +SETTINGS.max_scrobbles = localStorage.getItem('max_scrobbles') && + parseInt(localStorage.getItem('max_scrobbles')) || SETTINGS.max_scrobbles; +SETTINGS.logs_enabled = localStorage.getItem('logs_enabled') && + localStorage.getItem('logs_enabled') == 'true'; + // This enables scrobbling by default -SETTINGS.scrobble = !(localStorage["scrobble"] == "false"); +SETTINGS.scrobble = localStorage.getItem("scrobble") != "false"; diff --git a/manifest.json b/manifest.json index 39a36fa..b141ec1 100644 --- a/manifest.json +++ b/manifest.json @@ -10,8 +10,14 @@ "https://play.google.com/" ], "background": { - "scripts": ["js/md5.js", "js/lastfm.js", - "js/settings.js", "js/util.js", "js/background.js"], + "scripts": [ + "js/md5.js", + "js/lastfm.js", + "js/settings.js", + "js/util.js", + "js/logging.js", + "js/background.js" + ], "persistent": false }, "icons" : { From b5103bf7fc6e7126bcde2df059b52ae6d149aada Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Wed, 31 Jul 2019 23:30:08 -0700 Subject: [PATCH 65/74] Log vars that trigger scrobble. Fix options page checkbox. Bump version number. --- js/background.js | 6 ++++++ js/options.js | 4 ++-- manifest.json | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/js/background.js b/js/background.js index 59bb82a..2a8f48b 100644 --- a/js/background.js +++ b/js/background.js @@ -57,6 +57,7 @@ function port_on_message(message) { // if the song changed or looped if (_p.song.title != curr_song_title || _p.song.position <= SETTINGS.refresh_interval) { + log("Started playing: " + _p.song.artist + " - " + _p.song.title); curr_song_title = _p.song.title; time_played = 0; num_scrobbles = 0; @@ -78,6 +79,11 @@ function port_on_message(message) { time_played >= SETTINGS.scrobble_interval) && num_scrobbles < SETTINGS.max_scrobbles && !is_advertisment(_p.song)) { + log("Scrobbled: " + _p.song.artist + " - " + _p.song.title); + log("time_played: " + time_played); + log("scrobble point: " + (_p.song.time * SETTINGS.scrobble_point)); + log("num_scrobbles: " + num_scrobbles); + scrobble_song(_p.song.artist,_p.song.album_artist, _p.song.album, _p.song.title, Math.round(new Date().getTime() / 1000 - time_played)); diff --git a/js/options.js b/js/options.js index 4a465dc..bf26f46 100644 --- a/js/options.js +++ b/js/options.js @@ -32,11 +32,11 @@ function save_options() { function restore_options() { var scrobble_mult = SETTINGS.max_scrobbles > 1; document.getElementById('scrobble_mult').checked = scrobble_mult; - document.getElementById('logs_enabled').checked = SETTINGS.logs_enabled; + document.getElementById('log_checkbox').checked = SETTINGS.logs_enabled; document.getElementById('minute_field').innerHTML = Math.round((SETTINGS.scrobble_interval / 60) * 100) / 100; } document.addEventListener('DOMContentLoaded', restore_options); -document.querySelector('#save').addEventListener('click', save_options); \ No newline at end of file +document.querySelector('#save').addEventListener('click', save_options); diff --git a/manifest.json b/manifest.json index b141ec1..f178b4b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.7.0", + "version" : "1.7.1", "manifest_version": 2, "permissions" : [ "tabs", From f7b9f193dc17124b502f26f59f71eb927f946620 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Thu, 26 Sep 2019 22:21:27 -0700 Subject: [PATCH 66/74] Check if song length is non-null/zero before scrobbling. Not sure how this happens, but it does. --- js/background.js | 3 ++- manifest.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/js/background.js b/js/background.js index 2a8f48b..6fb8010 100644 --- a/js/background.js +++ b/js/background.js @@ -75,7 +75,8 @@ function port_on_message(message) { if (_p.is_playing) { chrome.browserAction.setIcon({'path': SETTINGS.playing_icon }); - if ((time_played >= _p.song.time * SETTINGS.scrobble_point || + if ((_p.song.time && + time_played >= _p.song.time * SETTINGS.scrobble_point || time_played >= SETTINGS.scrobble_interval) && num_scrobbles < SETTINGS.max_scrobbles && !is_advertisment(_p.song)) { diff --git a/manifest.json b/manifest.json index f178b4b..692e39d 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "Google Play Last.fm Scrobbler", "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.7.1", + "version" : "1.7.2", "manifest_version": 2, "permissions" : [ "tabs", From 51bf71c4dbda7a67530cb6db88f3b1b45f8c4d84 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Tue, 26 May 2020 00:51:43 -0700 Subject: [PATCH 67/74] YouTube Music integration. --- js/background.js | 16 +++- js/settings.js | 2 +- js/util.js | 42 +++++---- js/ytm_contentscript.js | 198 ++++++++++++++++++++++++++++++++++++++++ manifest.json | 24 +++-- 5 files changed, 254 insertions(+), 28 deletions(-) create mode 100644 js/ytm_contentscript.js diff --git a/js/background.js b/js/background.js index 6fb8010..5578438 100644 --- a/js/background.js +++ b/js/background.js @@ -85,11 +85,15 @@ function port_on_message(message) { log("scrobble point: " + (_p.song.time * SETTINGS.scrobble_point)); log("num_scrobbles: " + num_scrobbles); - scrobble_song(_p.song.artist,_p.song.album_artist, - _p.song.album, _p.song.title, - Math.round(new Date().getTime() / 1000 - time_played)); + var time_scrobbled = + Math.round(new Date().getTime() / 1000 - time_played); + // Reset the time_played before the RPC to prevent repeat scrobbling + // in the case the Scrobble RPC takes a long time. time_played = 0; num_scrobbles += 1; + scrobble_song(_p.song.artist,_p.song.album_artist, + _p.song.album, _p.song.title, + time_scrobbled); } else { /* * Don't depend on the SETTINGS.refresh_interval to @@ -222,7 +226,11 @@ function bind_keyboard_shortcuts() { function send_cmd_to_play_tab(cmd) { find_play_tab( function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: cmd}, function() {}); + if (tab) { + chrome.tabs.sendMessage(tab.id, {cmd: cmd}, function() {}); + } else { + log("Unable to find Play tab"); + } } ); } diff --git a/js/settings.js b/js/settings.js index fa60718..d100187 100644 --- a/js/settings.js +++ b/js/settings.js @@ -14,7 +14,7 @@ var SETTINGS = { scrobble_interval: 420, // 7 minutes max_scrobbles: Number.POSITIVE_INFINITY, - refresh_interval: 2, + refresh_interval: 4, gmusic_ads_metadata: { title: 'We\'ll be right back', diff --git a/js/util.js b/js/util.js index 1843bcb..35339a5 100644 --- a/js/util.js +++ b/js/util.js @@ -3,26 +3,34 @@ */ function find_play_tab(callback) { - chrome.tabs.query({url: '*://play.google.com/music/listen*'}, - function(tabs) { + chrome.tabs.query({url: '*://music.youtube.com/*'}, + function(tabs) { + if (tabs.length > 0) { + callback(tabs[0]); + } else { + // Fallback to GPM if no YT Music tab. + chrome.tabs.query({url: '*://play.google.com/music/listen*'}, + function(tabs) { if (tabs.length > 0) { - callback(tabs[0]); + callback(tabs[0]); } else { - callback(null); + callback(null); } - }); + }); + } + }); } function open_play_tab() { - find_play_tab( - function(tab) { - if (tab) { - chrome.tabs.update(tab.id, {selected: true}); - } else { - chrome.tabs.create({url: - 'https://play.google.com/music/listen', - selected: true}); - } - } - ); -} \ No newline at end of file + find_play_tab( + function(tab) { + if (tab) { + chrome.tabs.update(tab.id, {selected: true}); + } else { + chrome.tabs.create({url: + 'https://music.youtube.com', + selected: true}); + } + } + ); +} diff --git a/js/ytm_contentscript.js b/js/ytm_contentscript.js new file mode 100644 index 0000000..423ae41 --- /dev/null +++ b/js/ytm_contentscript.js @@ -0,0 +1,198 @@ +/** + * contentscripts.js + * Parses player page and transmit song information to background page + * Copyright (c) 2011 Alexey Savartsov, , Brad Lambeth + * Licensed under the MIT license + */ + +/** + * Player class + * + * Cloud Player page parser + */ +function Player(parser) { + this.has_song = parser._get_has_song(); + this.is_playing = parser._get_is_playing(); + this.song = { + position: parser._get_song_position(), + time: parser._get_song_time(), + title: parser._get_song_title(), + artist: parser._get_song_artist(), + album_artist: parser._get_album_artist(), + album: parser._get_song_album(), + cover: parser._get_song_cover() + }; +} + +/** + * Constructor for parser class + * Executes scripts to fetch now playing info from cloudplayer + * @returns {YtMusicParser} + */ +YtMusicParser = function() { + +}; + +/** + * Check whether a song loaded into player widget + * + * @return true if some song is loaded, otherwise false + */ +YtMusicParser.prototype._get_has_song = function() { + return $("yt-formatted-string.title.ytmusic-player-bar").text().length > 0; +}; + +/** + * Checks whether song is playing or paused + * + * @return true if song is playing, false if song is paused + */ +YtMusicParser.prototype._get_is_playing = function() { + return play_btn = $("#play-pause-button").attr("title") === "Pause"; +}; + +/** + * Get current song playing position + * + * @return Playing position in seconds + */ +YtMusicParser.prototype._get_song_position = function() { + var _time = $("span.time-info").text().split("/")[0]; + _time = $.trim(_time).split(":"); + if(_time.length == 2) + { + return (parseInt(_time[0]) * 60 + parseInt(_time[1])); + } + else if (_time.length == 3) + { + return (parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2])); + } + return null; +}; + +/** + * Get current song length + * + * @return Song length in seconds + */ +YtMusicParser.prototype._get_song_time = function() { + var _time = $("span.time-info").text().split("/")[1]; + _time = $.trim(_time).split(":"); + if(_time.length == 2) { + return (parseInt(_time[0]) * 60 + parseInt(_time[1])); + } + else if (_time.length == 3) + { + return (parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2])); + } + return null; +}; + +/** + * Get current song title + * + * @return Song title + */ +YtMusicParser.prototype._get_song_title = function() { + // the text inside the div located inside element with id="playerSongTitle" + return $("yt-formatted-string.title.ytmusic-player-bar").text(); +}; + +/** + * Get current song artist + * + * @return Song artist + */ +YtMusicParser.prototype._get_song_artist = function() { + return $("span.subtitle.ytmusic-player-bar>yt-formatted-string>a").first().text(); +}; + +/** + * Get current song album artist + * + * @return The album artist + */ +YtMusicParser.prototype._get_album_artist = function() { + // TODO: Check if album artist is actually available. + return $("span.subtitle.ytmusic-player-bar>yt-formatted-string>a").first().text(); +}; + +/** + * Get current song artwork + * + * @return Image URL or default artwork + */ +YtMusicParser.prototype._get_song_cover = function() { + var albumImg = $("div.middle-controls.ytmusic-player-bar>img").attr("src"); + if (albumImg) + return albumImg; + return null; +}; + +/** + * Get current song album name + * + * @return Album name or null + */ +YtMusicParser.prototype._get_song_album = function() { + return $("span.subtitle.style-scope.ytmusic-player-bar>yt-formatted-string>a").last().text(); +}; + +var port = chrome.runtime.connect(); + +window.setInterval(function() { + port.postMessage(new Player(new YtMusicParser())); +}, +SETTINGS.refresh_interval * 1000); + +/* +* Listeners for player control buttons +*/ +chrome.runtime.onMessage.addListener(toggle_play); +chrome.runtime.onMessage.addListener(prev_song); +chrome.runtime.onMessage.addListener(next_song); + +function toggle_play(msg, sndr, send_response) { + if (msg.cmd == "tgl") { + var play_btn = $("#play-pause-button"); + play_btn.click(); + /* + * Wait a little for the UI to update before sending a response + * with the updated state. + */ + setTimeout(function() { + send_response(new Player(new YtMusicParser())); + }, 100); + /* + * Return true keeps the message channel open so the timeout function + * will be called, otherwise send_response will not work per + * https://developer.chrome.com/extensions/runtime#event-onMessage. + */ + return true; + } + return false; +} + +function prev_song(msg, sndr, send_response) { + if (msg.cmd == "prv") { + var prev_btn = $("div.left-controls-buttons>.previous-button"); + prev_btn.click(); + setTimeout(function() { + send_response(new Player(new YtMusicParser())); + }, 100); + return true; + } + return false; +} + +function next_song(msg, sndr, send_response) { + if (msg.cmd == "nxt") { + var next_btn = $("div.left-controls-buttons>.next-button"); + next_btn.click(); + setTimeout(function() { + send_response(new Player(new YtMusicParser())); + }, 100); + return true; + } + return false; +} diff --git a/manifest.json b/manifest.json index 692e39d..891143a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,13 +1,14 @@ { - "name" : "Google Play Last.fm Scrobbler", - "description" : "Scrobbles songs from Google Play to Last.fm", - "version" : "1.7.2", + "name" : "YouTube Music Last.fm Scrobbler", + "description" : "Scrobbles songs from YouTube Music or Google Play to Last.fm", + "version" : "2.0.0", "manifest_version": 2, "permissions" : [ "tabs", "http://ws.audioscrobbler.com/", "http://play.google.com/", - "https://play.google.com/" + "https://play.google.com/", + "https://music.youtube.com/" ], "background": { "scripts": [ @@ -41,10 +42,21 @@ "js/contentscript.js" ] }, + { + "matches" : [ + "*://music.youtube.com/*" + ], + "js" : [ + "js/inject.js", + "js/settings.js", + "js/ytm_contentscript.js" + ] + }, { "run_at" : "document_start", "matches" : [ - "*://play.google.com/music/listen*" + "*://play.google.com/music/listen*", + "*://music.youtube.com/*" ], "js" : [ "js/jquery-1.7.min.js" @@ -84,7 +96,7 @@ "suggested_key": { "default": "Ctrl+Shift+0" }, - "description": "Go to Google Play tab", + "description": "Go to YT Music/Google Music tab", "global": true } } From 6d5b9f1e9e0db17221cb982f038bc47e62a6ee42 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Tue, 26 May 2020 01:06:35 -0700 Subject: [PATCH 68/74] Reverting some unintended changes. --- js/background.js | 10 +++------- js/settings.js | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/js/background.js b/js/background.js index 5578438..399f7ee 100644 --- a/js/background.js +++ b/js/background.js @@ -85,15 +85,11 @@ function port_on_message(message) { log("scrobble point: " + (_p.song.time * SETTINGS.scrobble_point)); log("num_scrobbles: " + num_scrobbles); - var time_scrobbled = - Math.round(new Date().getTime() / 1000 - time_played); - // Reset the time_played before the RPC to prevent repeat scrobbling - // in the case the Scrobble RPC takes a long time. - time_played = 0; - num_scrobbles += 1; scrobble_song(_p.song.artist,_p.song.album_artist, _p.song.album, _p.song.title, - time_scrobbled); + Math.round(new Date().getTime() / 1000 - time_played)); + time_played = 0; + num_scrobbles += 1; } else { /* * Don't depend on the SETTINGS.refresh_interval to diff --git a/js/settings.js b/js/settings.js index d100187..fa60718 100644 --- a/js/settings.js +++ b/js/settings.js @@ -14,7 +14,7 @@ var SETTINGS = { scrobble_interval: 420, // 7 minutes max_scrobbles: Number.POSITIVE_INFINITY, - refresh_interval: 4, + refresh_interval: 2, gmusic_ads_metadata: { title: 'We\'ll be right back', From 5187e5ffafacd0ce72ae87a55736a19225809792 Mon Sep 17 00:00:00 2001 From: Michal Zgraja Date: Sat, 6 Feb 2021 20:43:50 -0800 Subject: [PATCH 69/74] Fix YtMusic is_playing detection - check browser tab title instead of button title, which is translated into user's locale, so it's not always "Pause". --- js/ytm_contentscript.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/ytm_contentscript.js b/js/ytm_contentscript.js index 423ae41..3bd045c 100644 --- a/js/ytm_contentscript.js +++ b/js/ytm_contentscript.js @@ -48,7 +48,9 @@ YtMusicParser.prototype._get_has_song = function() { * @return true if song is playing, false if song is paused */ YtMusicParser.prototype._get_is_playing = function() { - return play_btn = $("#play-pause-button").attr("title") === "Pause"; + // YT app prepends current song's name to the tab title + var songTitle = this._get_song_title() + return songTitle.length > 0 && window.document.title.startsWith(songTitle) }; /** From 35f1571000741865c9de19a8a88d28d7a52eb279 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sat, 19 Jun 2021 11:20:59 -0700 Subject: [PATCH 70/74] Remove 'tabs' permission Remove 'tabs' permission to pass Play store privacy restrictions. --- manifest.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/manifest.json b/manifest.json index 891143a..7d8485e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,9 @@ { "name" : "YouTube Music Last.fm Scrobbler", "description" : "Scrobbles songs from YouTube Music or Google Play to Last.fm", - "version" : "2.0.0", + "version" : "2.0.1", "manifest_version": 2, "permissions" : [ - "tabs", "http://ws.audioscrobbler.com/", "http://play.google.com/", "https://play.google.com/", From 2629b72523f5804d90c7962484aa9b173867f292 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Wed, 26 Jan 2022 23:01:49 -0500 Subject: [PATCH 71/74] Update album art selector. --- js/ytm_contentscript.js | 2 +- manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/ytm_contentscript.js b/js/ytm_contentscript.js index 3bd045c..3e3faa5 100644 --- a/js/ytm_contentscript.js +++ b/js/ytm_contentscript.js @@ -125,7 +125,7 @@ YtMusicParser.prototype._get_album_artist = function() { * @return Image URL or default artwork */ YtMusicParser.prototype._get_song_cover = function() { - var albumImg = $("div.middle-controls.ytmusic-player-bar>img").attr("src"); + var albumImg = $("div.thumbnail-image-wrapper.ytmusic-player-bar>img").attr("src"); if (albumImg) return albumImg; return null; diff --git a/manifest.json b/manifest.json index 7d8485e..9237842 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "YouTube Music Last.fm Scrobbler", "description" : "Scrobbles songs from YouTube Music or Google Play to Last.fm", - "version" : "2.0.1", + "version" : "2.0.2", "manifest_version": 2, "permissions" : [ "http://ws.audioscrobbler.com/", From 3a32f38a1d4ef471b9ecdae60be1bd14c3db5b26 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Tue, 7 Oct 2025 11:14:49 -0400 Subject: [PATCH 72/74] Update manifest to v3. --- manifest.json | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/manifest.json b/manifest.json index 9237842..e6b7752 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { "name" : "YouTube Music Last.fm Scrobbler", "description" : "Scrobbles songs from YouTube Music or Google Play to Last.fm", - "version" : "2.0.2", - "manifest_version": 2, - "permissions" : [ + "version" : "2.0.3", + "manifest_version": 3, + "host_permissions" : [ "http://ws.audioscrobbler.com/", "http://play.google.com/", "https://play.google.com/", @@ -64,7 +64,14 @@ ], "options_page": "html/options.html", "web_accessible_resources": [ - "html/lastfm_callback.html" + { + "resources": [ + "html/lastfm_callback.html" + ], + "matches": [ + "*://*/*" + ] + } ], "commands": { "toggle_play": { From 760a9fc4ac9e7aaf73e15995d373584fefd8f213 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Tue, 17 Feb 2026 21:16:45 -0500 Subject: [PATCH 73/74] Update rest of code to be manifest v3 compatible. --- html/lastfm_callback.html | 17 +- html/options.html | 6 +- html/popup.html | 18 +- js/background.js | 388 +++++++++++++++++++------------------- js/contentscript.js | 229 +++++++--------------- js/lastfm.js | 74 +++----- js/lastfm_callback.js | 28 +-- js/logging.js | 8 +- js/options.js | 62 +++--- js/popup.js | 349 ++++++++++++++++------------------ js/settings.js | 53 ++++-- js/util.js | 46 ++--- js/ytm_contentscript.js | 221 +++++++--------------- manifest.json | 41 ++-- 14 files changed, 666 insertions(+), 874 deletions(-) diff --git a/html/lastfm_callback.html b/html/lastfm_callback.html index 766a3fa..4f2cb37 100644 --- a/html/lastfm_callback.html +++ b/html/lastfm_callback.html @@ -1,8 +1,13 @@ - - + +Last.fm Auth + +

Authenticating with Last.fm, please wait...

+ + + diff --git a/html/options.html b/html/options.html index 3751fe1..ef0255c 100644 --- a/html/options.html +++ b/html/options.html @@ -1,6 +1,6 @@ - Google Play Last.fm Scrobbler Options + YouTube Music Last.fm Scrobbler Options Scrobbler Options: @@ -14,7 +14,7 @@

To edit keyboard shortcuts, go to chrome://extensions and click
keyboard shortcuts on the bottom right of the page. + + - - diff --git a/html/popup.html b/html/popup.html index 54d6b69..04c2e6b 100644 --- a/html/popup.html +++ b/html/popup.html @@ -1,17 +1,11 @@ + - - - - - -
+ + + + diff --git a/js/background.js b/js/background.js index 399f7ee..96c7c97 100644 --- a/js/background.js +++ b/js/background.js @@ -1,237 +1,239 @@ /** - * background.js - * Background page script + * background.js — MV3 Service Worker * Copyright (c) 2011 Alexey Savartsov * Licensed under the MIT license */ -log("background.js loaded"); -var player = {}; // Previous player state + +importScripts( + 'md5.js', + 'lastfm.js', + 'settings.js', + 'util.js', + 'logging.js' +); + +// --------------------------------------------------------------------------- +// State (lives only while the service worker is alive) +// --------------------------------------------------------------------------- +var player = {}; var time_played = 0; -var last_refresh = (new Date()).getTime(); +var last_refresh = Date.now(); var num_scrobbles = 0; var curr_song_title = ''; -var lastfm_api = new LastFM(SETTINGS.api_key, SETTINGS.api_secret); -// Load settings from local storage -lastfm_api.session.key = localStorage.getItem('session_key'); -lastfm_api.session.name = localStorage.getItem('session_name'); +var lastfm_api = null; // initialised after settings load +// --------------------------------------------------------------------------- +// Bootstrap — load persisted settings then wire everything up +// --------------------------------------------------------------------------- +loadSettings().then(function(stored) { + lastfm_api = new LastFM(SETTINGS.api_key, SETTINGS.api_secret); + lastfm_api.session.key = stored.session_key || null; + lastfm_api.session.name = stored.session_name || null; -if (!SETTINGS.scrobble) { - chrome.browserAction.setIcon({'path': SETTINGS.scrobbling_stopped_icon}); -} + log('background.js loaded (service worker)'); -// Connect event handlers -chrome.runtime.onConnect.addListener(port_on_connect); -bind_keyboard_shortcuts(); + if (!SETTINGS.scrobble) { + chrome.action.setIcon({ path: SETTINGS.scrobbling_stopped_icon }); + } + // Messages from content scripts and popup/callback pages + chrome.runtime.onMessage.addListener(on_message); -/** - * Content script has connected to the extension - */ -function port_on_connect(port) { - log("Content script connected"); - port.onMessage.addListener(port_on_message); - port.onDisconnect.addListener(port_on_disconnect); -} + bind_keyboard_shortcuts(); +}); +// --------------------------------------------------------------------------- +// Player state handler (called by content scripts via sendMessage) +// --------------------------------------------------------------------------- +function handle_player_state(_p) { + var now = Date.now(); -/** - * New message arrives to the port - */ -function port_on_message(message) { - // Current player state - var _p = message; - var now = (new Date()).getTime(); - - // Save player state - player = _p; - - if (!SETTINGS.scrobble) { - chrome.browserAction.setIcon({'path': SETTINGS.scrobbling_stopped_icon}); - - return; - } - - if (_p.has_song) { - // if the song changed or looped - if (_p.song.title != curr_song_title || - _p.song.position <= SETTINGS.refresh_interval) { - log("Started playing: " + _p.song.artist + " - " + _p.song.title); - curr_song_title = _p.song.title; - time_played = 0; - num_scrobbles = 0; - last_refresh = now - SETTINGS.refresh_interval*1000; - - lastfm_api.now_playing(_p.song.title, - _p.song.artist, - _p.song.album, - _p.song.time, - function(response) { - // TODO: - } - ); + player = _p; + + if (!SETTINGS.scrobble) { + chrome.action.setIcon({ path: SETTINGS.scrobbling_stopped_icon }); + return; } - if (_p.is_playing) { - chrome.browserAction.setIcon({'path': SETTINGS.playing_icon }); - if ((_p.song.time && - time_played >= _p.song.time * SETTINGS.scrobble_point || - time_played >= SETTINGS.scrobble_interval) && - num_scrobbles < SETTINGS.max_scrobbles && - !is_advertisment(_p.song)) { - log("Scrobbled: " + _p.song.artist + " - " + _p.song.title); - log("time_played: " + time_played); - log("scrobble point: " + (_p.song.time * SETTINGS.scrobble_point)); - log("num_scrobbles: " + num_scrobbles); - - scrobble_song(_p.song.artist,_p.song.album_artist, - _p.song.album, _p.song.title, - Math.round(new Date().getTime() / 1000 - time_played)); - time_played = 0; - num_scrobbles += 1; - } else { - /* - * Don't depend on the SETTINGS.refresh_interval to - * calculate time_played since there can be a significant delay - * between the time the message was sent from the contentscript - * to when it's recieved here. - * See: https://github.com/newgiin/cloudplayer-scrobbler/issues/23 - */ - time_played += (now - last_refresh) / 1000; - } + if (_p.has_song) { + if (_p.song.title !== curr_song_title || + _p.song.position <= SETTINGS.refresh_interval) { + + log('Started playing: ' + _p.song.artist + ' - ' + _p.song.title); + curr_song_title = _p.song.title; + time_played = 0; + num_scrobbles = 0; + last_refresh = now - SETTINGS.refresh_interval * 1000; + + lastfm_api.now_playing( + _p.song.title, _p.song.artist, _p.song.album, _p.song.time, + function() {} + ); + } + + if (_p.is_playing) { + chrome.action.setIcon({ path: SETTINGS.playing_icon }); + + if ((_p.song.time && + time_played >= _p.song.time * SETTINGS.scrobble_point || + time_played >= SETTINGS.scrobble_interval) && + num_scrobbles < SETTINGS.max_scrobbles && + !is_advertisement(_p.song)) { + + log('Scrobbled: ' + _p.song.artist + ' - ' + _p.song.title); + scrobble_song( + _p.song.artist, _p.song.album_artist, + _p.song.album, _p.song.title, + Math.round(Date.now() / 1000 - time_played) + ); + time_played = 0; + num_scrobbles += 1; + } else { + time_played += (now - last_refresh) / 1000; + } + } else { + chrome.action.setIcon({ path: SETTINGS.paused_icon }); + } } else { - // The player is paused - chrome.browserAction.setIcon({'path': SETTINGS.paused_icon}); + chrome.action.setIcon({ path: SETTINGS.main_icon }); } - } else { - chrome.browserAction.setIcon({'path': SETTINGS.main_icon}); - } - last_refresh = now; + last_refresh = now; } +// --------------------------------------------------------------------------- +// Message handler — content scripts + popup/callback pages +// --------------------------------------------------------------------------- +function on_message(message, sender, sendResponse) { + switch (message.type) { + case 'player_state': + handle_player_state(message.state); + return false; + + case 'get_state': + sendResponse({ + player: player, + session: lastfm_api ? lastfm_api.session : {}, + scrobble: SETTINGS.scrobble + }); + return false; + + case 'start_web_auth': + start_web_auth(); + return false; + + case 'clear_session': + clear_session(); + sendResponse({ ok: true }); + return false; + + case 'toggle_scrobble': + toggle_scrobble(); + sendResponse({ scrobble: SETTINGS.scrobble }); + return false; + + case 'get_lastfm_session': + get_lastfm_session(message.token, function() { + sendResponse({ ok: true }); + }); + return true; // async + + case 'open_extensions_page': + chrome.tabs.create({ url: 'chrome://extensions/' }); + return false; + + case 'love_track': + lastfm_api.love_track(message.track, message.artist, function(result) { + sendResponse(result); + }); + return true; + + case 'unlove_track': + lastfm_api.unlove_track(message.track, message.artist, function(result) { + sendResponse(result); + }); + return true; + + case 'is_track_loved': + lastfm_api.is_track_loved(message.track, message.artist, function(result) { + sendResponse(result); + }); + return true; + } + return false; +} +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- function scrobble_song(artist, album_artist, album, title, time) { - // Scrobble this song - lastfm_api.scrobble(artist, album_artist, album, title, time, - function(response) { - if (response.error) { - if (response.error == 9) { - // Session expired - clear_session(); + lastfm_api.scrobble(artist, album_artist, album, title, time, + function(response) { + if (response && response.error) { + if (response.error === 9) clear_session(); + chrome.action.setIcon({ path: SETTINGS.error_icon }); + } } - chrome.browserAction.setIcon({'path': SETTINGS.error_icon}); - } - }); -} - - -function is_advertisment(song) { - return (song.title === SETTINGS.gmusic_ads_metadata.title && - song.artist === SETTINGS.gmusic_ads_metadata.artist); + ); } - -/** - * Content script has disconnected - */ -function port_on_disconnect() { - player = {}; // Clear player state - time_played = 0; - num_scrobbles = 0; - curr_song_title = ''; - chrome.browserAction.setIcon({'path': SETTINGS.main_icon}); +function is_advertisement(song) { + return (song.title === SETTINGS.gmusic_ads_metadata.title && + song.artist === SETTINGS.gmusic_ads_metadata.artist); } - -/** - * Authentication link from popup window - */ function start_web_auth() { - var callback_url = chrome.runtime.getURL(SETTINGS.callback_file); - chrome.tabs.create({ - 'url': ('http://www.last.fm/api/auth?api_key=' + SETTINGS.api_key + '&cb=' + - callback_url) - }); + var callback_url = chrome.runtime.getURL(SETTINGS.callback_file); + chrome.tabs.create({ + url: 'http://www.last.fm/api/auth?api_key=' + SETTINGS.api_key + + '&cb=' + callback_url + }); } - -/** - * Clears last.fm session - */ function clear_session() { - lastfm_api.session = {}; - - localStorage.removeItem('session_key'); - localStorage.removeItem('session_name'); + if (lastfm_api) lastfm_api.session = {}; + chrome.storage.local.remove(['session_key', 'session_name']); } - -/** - * Toggles setting to scrobble songs or not - */ function toggle_scrobble() { - SETTINGS.scrobble = !SETTINGS.scrobble; - localStorage.setItem('scrobble', SETTINGS.scrobble); - - // Set the icon corresponding the current scrobble state - var icon = (SETTINGS.scrobble ? - SETTINGS.main_icon : SETTINGS.scrobbling_stopped_icon); - chrome.browserAction.setIcon({'path': icon}); + SETTINGS.scrobble = !SETTINGS.scrobble; + chrome.storage.local.set({ scrobble: SETTINGS.scrobble }); + chrome.action.setIcon({ + path: SETTINGS.scrobble ? SETTINGS.main_icon : SETTINGS.scrobbling_stopped_icon + }); } - -/** - * Last.fm session request - */ -function get_lastfm_session(token) { - lastfm_api.authorize(token, function(response) { - // Save session - if (response.session) { - localStorage.setItem('session_key', response.session.key); - localStorage.setItem('session_name', response.session.name); - } - }); +function get_lastfm_session(token, callback) { + lastfm_api.authorize(token, function(response) { + if (response && response.session) { + chrome.storage.local.set({ + session_key: response.session.key, + session_name: response.session.name + }); + } + if (callback) callback(); + }); } - function bind_keyboard_shortcuts() { - chrome.commands.onCommand.addListener( - function(command) { - switch (command) { - case 'toggle_play': - send_cmd_to_play_tab('tgl'); - break; - case 'prev_song': - send_cmd_to_play_tab('prv'); - break; - case 'next_song': - send_cmd_to_play_tab('nxt'); - break; - case 'goto_play_tab': - open_play_tab(); - break; - default: - console.error("No handler for command '" + command + "'"); - } - } - ); + chrome.commands.onCommand.addListener(function(command) { + switch (command) { + case 'toggle_play': send_cmd_to_play_tab('tgl'); break; + case 'prev_song': send_cmd_to_play_tab('prv'); break; + case 'next_song': send_cmd_to_play_tab('nxt'); break; + case 'goto_play_tab': open_play_tab(); break; + default: + console.error("No handler for command '" + command + "'"); + } + }); } - function send_cmd_to_play_tab(cmd) { - find_play_tab( - function(tab) { - if (tab) { - chrome.tabs.sendMessage(tab.id, {cmd: cmd}, function() {}); - } else { - log("Unable to find Play tab"); - } - } - ); -} - - -function open_extensions_page() { - chrome.tabs.create({url: 'chrome://extensions/'}); + find_play_tab(function(tab) { + if (tab) { + chrome.tabs.sendMessage(tab.id, { cmd: cmd }, function() { void chrome.runtime.lastError; }); + } else { + log('Unable to find Play tab'); + } + }); } diff --git a/js/contentscript.js b/js/contentscript.js index 2ad7070..ed3a8cd 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -1,214 +1,119 @@ /** - * contentscripts.js - * Parses player page and transmit song information to background page - * Copyright (c) 2011 Alexey Savartsov, , Brad Lambeth - * Licensed under the MIT license + * contentscript.js — Google Play Music content script (MV3) */ -/** - * Player class - * - * Cloud Player page parser - */ +var CS_SETTINGS = { + refresh_interval: 2 +}; + +// --------------------------------------------------------------------------- +// Player / Parser classes +// --------------------------------------------------------------------------- + function Player(parser) { - this.has_song = parser._get_has_song(); + this.has_song = parser._get_has_song(); this.is_playing = parser._get_is_playing(); this.song = { - position: parser._get_song_position(), - time: parser._get_song_time(), - title: parser._get_song_title(), - artist: parser._get_song_artist(), + position: parser._get_song_position(), + time: parser._get_song_time(), + title: parser._get_song_title(), + artist: parser._get_song_artist(), album_artist: parser._get_album_artist(), - album: parser._get_song_album(), - cover: parser._get_song_cover() + album: parser._get_song_album(), + cover: parser._get_song_cover() }; } -/** - * Constructor for parser class - * Executes scripts to fetch now playing info from cloudplayer - * @returns {GoogleMusicParser} - */ -GoogleMusicParser = function() { +var GoogleMusicParser = function() {}; -}; - -/** - * Check whether a song loaded into player widget - * - * @return true if some song is loaded, otherwise false - */ GoogleMusicParser.prototype._get_has_song = function() { - return $("#playerSongInfo").children().length > 0; + return $('#playerSongInfo').children().length > 0; }; -/** - * Checks whether song is playing or paused - * - * @return true if song is playing, false if song is paused - */ GoogleMusicParser.prototype._get_is_playing = function() { - var play_btn = $(".material-player-middle paper-icon-button[data-id='play-pause']"); - if (play_btn.length == 0) { - play_btn = $(".material-player-middle sj-icon-button[data-id='play-pause']"); + var play_btn = $('.material-player-middle paper-icon-button[data-id="play-pause"]'); + if (play_btn.length === 0) { + play_btn = $('.material-player-middle sj-icon-button[data-id="play-pause"]'); } - return play_btn.hasClass("playing"); + return play_btn.hasClass('playing'); }; -/** - * Get current song playing position - * - * @return Playing position in seconds - */ GoogleMusicParser.prototype._get_song_position = function() { - var _time = $("#time_container_current").text(); - _time = $.trim(_time).split(':'); - if(_time.length == 2) - { - return (parseInt(_time[0]) * 60 + parseInt(_time[1])); - } - else if (_time.length == 3) - { - return (parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2])); - } + var _time = $.trim($('#time_container_current').text()).split(':'); + if (_time.length === 2) return parseInt(_time[0]) * 60 + parseInt(_time[1]); + if (_time.length === 3) return parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2]); return null; }; -/** - * Get current song length - * - * @return Song length in seconds - */ GoogleMusicParser.prototype._get_song_time = function() { - var _time = $("#time_container_duration").text(); - _time = $.trim(_time).split(':'); - if(_time.length == 2) { - return (parseInt(_time[0]) * 60 + parseInt(_time[1])); - } - else if (_time.length == 3) - { - return (parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2])); - } + var _time = $.trim($('#time_container_duration').text()).split(':'); + if (_time.length === 2) return parseInt(_time[0]) * 60 + parseInt(_time[1]); + if (_time.length === 3) return parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2]); return null; }; -/** - * Get current song title - * - * @return Song title - */ GoogleMusicParser.prototype._get_song_title = function() { - // the text inside the div located inside element with id="playerSongTitle" - return $("#currently-playing-title").text(); + return $('#currently-playing-title').text(); }; -/** - * Get current song artist - * - * @return Song artist - */ GoogleMusicParser.prototype._get_song_artist = function() { - return $("#player-artist").text(); + return $('#player-artist').text(); }; -/** - * Get current song album artist - * - * @return The album artist - */ GoogleMusicParser.prototype._get_album_artist = function() { - var album_artist = $("#playerSongInfo .player-album").attr('data-id'); - if (album_artist) - return decodeURIComponent( - album_artist.split('/')[1].replace(/\+/g, ' ')); + var album_artist = $('#playerSongInfo .player-album').attr('data-id'); + if (album_artist) return decodeURIComponent(album_artist.split('/')[1].replace(/\+/g, ' ')); return null; }; -/** - * Get current song artwork - * - * @return Image URL or default artwork - */ GoogleMusicParser.prototype._get_song_cover = function() { - var albumImg = $("#playerBarArt").attr("src"); - if (albumImg) - return albumImg; - return null; + return $('#playerBarArt').attr('src') || null; }; -/** - * Get current song album name - * - * @return Album name or null - */ GoogleMusicParser.prototype._get_song_album = function() { - return $("#playerSongInfo .player-album").text(); + return $('#playerSongInfo .player-album').text(); }; -var port = chrome.runtime.connect(); - -window.setInterval(function() { - port.postMessage(new Player(new GoogleMusicParser())); -}, -SETTINGS.refresh_interval * 1000); - -/* -* Listeners for player control buttons -*/ -chrome.runtime.onMessage.addListener(toggle_play); -chrome.runtime.onMessage.addListener(prev_song); -chrome.runtime.onMessage.addListener(next_song); - -function toggle_play(msg, sndr, send_response) { - if (msg.cmd == "tgl") { - var play_btn = $(".material-player-middle paper-icon-button[data-id='play-pause']"); - if (play_btn.length == 0) { - play_btn = $(".material-player-middle sj-icon-button[data-id='play-pause']"); - } +// --------------------------------------------------------------------------- +// Poll via sendMessage — wakes the service worker on demand each interval +// --------------------------------------------------------------------------- + +chrome.storage.local.get('refresh_interval', function(items) { + var interval = (items.refresh_interval || CS_SETTINGS.refresh_interval) * 1000; + window.setInterval(function() { + chrome.runtime.sendMessage( + { type: 'player_state', state: new Player(new GoogleMusicParser()) }, + function() { void chrome.runtime.lastError; } + ); + }, interval); +}); + +// --------------------------------------------------------------------------- +// Player control listeners +// --------------------------------------------------------------------------- + +chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) { + var play_btn, prev_btn, next_btn; + if (msg.cmd === 'tgl') { + play_btn = $('.material-player-middle paper-icon-button[data-id="play-pause"]'); + if (play_btn.length === 0) play_btn = $('.material-player-middle sj-icon-button[data-id="play-pause"]'); play_btn.click(); - /* - * Wait a little for the UI to update before sending a response - * with the updated state. - */ - setTimeout(function() { - send_response(new Player(new GoogleMusicParser())); - }, 100); - /* - * Return true keeps the message channel open so the timeout function - * will be called, otherwise send_response will not work per - * https://developer.chrome.com/extensions/runtime#event-onMessage. - */ + setTimeout(function() { sendResponse(new Player(new GoogleMusicParser())); }, 100); return true; } - return false; -} - -function prev_song(msg, sndr, send_response) { - if (msg.cmd == "prv") { - var prev_btn = $(".material-player-middle paper-icon-button[data-id='rewind']"); - if (prev_btn.length == 0) { - prev_btn = $(".material-player-middle sj-icon-button[data-id='rewind']"); - } + if (msg.cmd === 'prv') { + prev_btn = $('.material-player-middle paper-icon-button[data-id="rewind"]'); + if (prev_btn.length === 0) prev_btn = $('.material-player-middle sj-icon-button[data-id="rewind"]'); prev_btn.click(); - setTimeout(function() { - send_response(new Player(new GoogleMusicParser())); - }, 100); + setTimeout(function() { sendResponse(new Player(new GoogleMusicParser())); }, 100); return true; } - return false; -} - -function next_song(msg, sndr, send_response) { - if (msg.cmd == "nxt") { - var next_btn = $(".material-player-middle paper-icon-button[data-id='forward']"); - if (next_btn.length == 0) { - next_btn = $(".material-player-middle sj-icon-button[data-id='forward']"); - } + if (msg.cmd === 'nxt') { + next_btn = $('.material-player-middle paper-icon-button[data-id="forward"]'); + if (next_btn.length === 0) next_btn = $('.material-player-middle sj-icon-button[data-id="forward"]'); next_btn.click(); - setTimeout(function() { - send_response(new Player(new GoogleMusicParser())); - }, 100); + setTimeout(function() { sendResponse(new Player(new GoogleMusicParser())); }, 100); return true; } return false; -} +}); diff --git a/js/lastfm.js b/js/lastfm.js index e5a277b..2af675e 100644 --- a/js/lastfm.js +++ b/js/lastfm.js @@ -222,62 +222,50 @@ LastFM.prototype._req_sign = function(params) { }; /** - * Performs an XMLHTTP request and expects JSON as reply + * Performs a fetch request and expects JSON as reply * * @param method Request method (GET or POST) * @param params Hash with request values. All request fields will be * automatically urlencoded * @param callback Callback function for the request. Sends a parameter with - * reply decoded as JS object from JSON on null on error + * reply decoded as JS object from JSON or null on error */ LastFM.prototype._xhr = function(method, params, callback) { var uri = this.API_ROOT; - var _data = ""; var _params = []; - var xhr = new XMLHttpRequest(); - - for(param in params) { - _params.push(encodeURIComponent(param) + "=" - + encodeURIComponent(params[param])); - } - switch(method) { - case "GET": - uri += '?' + _params.join('&').replace(/%20/, '+'); - break; - case "POST": - _data = _params.join('&'); - break; - default: - return; + for (var param in params) { + _params.push(encodeURIComponent(param) + "=" + encodeURIComponent(params[param])); } - xhr.open(method, uri); + var options = { + method: method, + headers: { + "Content-type": "application/x-www-form-urlencoded; charset=UTF-8", + "If-Modified-Since": "Thu, 01 Jun 1970 00:00:00 GMT", + "Pragma": "no-cache" + } + }; - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - var reply; + if (method === "GET") { + uri += '?' + _params.join('&').replace(/%20/, '+'); + } else if (method === "POST") { + options.body = _params.join('&'); + } else { + return; + } - try { - reply = JSON.parse(xhr.responseText); - if (reply.error) { - console.log('Last.fm ' + params.method + - ' error: ' + reply.error) - } - } - catch (e) { - reply = null; - console.log('Error parsing JSON response.'); - console.log('Request params:'); - console.log(params); + fetch(uri, options) + .then(function(response) { return response.json(); }) + .then(function(reply) { + if (reply.error) { + console.log('Last.fm ' + params.method + ' error: ' + reply.error); } callback(reply); - } - }; - - xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); - // The cache is a lie! - xhr.setRequestHeader("If-Modified-Since", "Thu, 01 Jun 1970 00:00:00 GMT"); - xhr.setRequestHeader("Pragma", "no-cache"); - xhr.send(_data || null); -}; + }) + .catch(function(e) { + console.log('Error parsing JSON response.'); + console.log('Request params:', params); + callback(null); + }); +}; \ No newline at end of file diff --git a/js/lastfm_callback.js b/js/lastfm_callback.js index 3c32cca..91ddba6 100644 --- a/js/lastfm_callback.js +++ b/js/lastfm_callback.js @@ -1,18 +1,22 @@ /** - * lastfm_callback.js - * LastFM callback script - * Copyright (c) 2011 Alexey Savartsov - * Licensed under the MIT license + * lastfm_callback.js — MV3 compatible + * Uses chrome.runtime.sendMessage instead of getBackgroundPage() */ + function _url_param(name, url) { - return unescape((RegExp(name + '=' + - '(.+?)(&|$)').exec(url) || [,null])[1]); + return unescape((RegExp(name + '=(.+?)(&|$)').exec(url) || [, null])[1]); } -chrome.runtime.getBackgroundPage(function(background) { - background.get_lastfm_session(_url_param("token", location.search)); - open_play_tab(); - setTimeout(function() { - window.close(); - }, 100); +var token = _url_param('token', location.search); + +chrome.runtime.sendMessage({ type: 'get_lastfm_session', token: token }, function() { + // Navigate to the music tab, then close this callback tab + chrome.tabs.query({ url: '*://music.youtube.com/*' }, function(tabs) { + if (tabs.length > 0) { + chrome.tabs.update(tabs[0].id, { active: true }); + } else { + chrome.tabs.create({ url: 'https://music.youtube.com', active: true }); + } + setTimeout(function() { window.close(); }, 100); + }); }); diff --git a/js/logging.js b/js/logging.js index 164d2b9..43f7d7e 100644 --- a/js/logging.js +++ b/js/logging.js @@ -1,5 +1,5 @@ function log(message) { - if (SETTINGS.logs_enabled) { - console.log(message); - } -} \ No newline at end of file + if (SETTINGS.logs_enabled) { + console.log(message); + } +} diff --git a/js/options.js b/js/options.js index bf26f46..b57bc78 100644 --- a/js/options.js +++ b/js/options.js @@ -1,42 +1,50 @@ -// Saves options to localStorage. +/** + * options.js — MV3 compatible + * Uses chrome.storage.local instead of localStorage + */ + function save_options() { var scrobble_mult = document.getElementById('scrobble_mult').checked; - if (!scrobble_mult) { - // TODO localStorage actually stores this as a string. Find out - // if this causes bugs. - localStorage.setItem('max_scrobbles', 1); - } else { - localStorage.removeItem('max_scrobbles'); - } + var logs_enabled = document.getElementById('log_checkbox').checked; - var logs_enabled = document.getElementById('log_checkbox').checked; - if (logs_enabled) { - localStorage.setItem('logs_enabled', 'true'); + var updates = { logs_enabled: logs_enabled }; + if (!scrobble_mult) { + updates.max_scrobbles = 1; } else { - localStorage.removeItem('logs_enabled'); + // Remove the limit so background uses POSITIVE_INFINITY default + updates.max_scrobbles = null; } - chrome.runtime.getBackgroundPage(function(backgroundPage) { - backgroundPage.location.reload(); - // Update status to let user know options were saved. - var status = document.getElementById('status'); - status.innerHTML = 'Options saved, please reload the Google Play page.'; - setTimeout(function() { - status.innerHTML = ''; - }, 3500); + // Store all at once; null values removed manually + chrome.storage.local.set({ logs_enabled: logs_enabled }, function() { + if (!scrobble_mult) { + chrome.storage.local.set({ max_scrobbles: 1 }, after_save); + } else { + chrome.storage.local.remove('max_scrobbles', after_save); + } }); } +function after_save() { + // Service worker will pick up new settings on next activation; + // user just needs to reload the music tab. + var status = document.getElementById('status'); + status.textContent = 'Options saved — please reload the YouTube Music tab.'; + setTimeout(function() { status.textContent = ''; }, 3500); +} -// Restores select box state to saved value from localStorage. function restore_options() { - var scrobble_mult = SETTINGS.max_scrobbles > 1; - document.getElementById('scrobble_mult').checked = scrobble_mult; - document.getElementById('log_checkbox').checked = SETTINGS.logs_enabled; - document.getElementById('minute_field').innerHTML = - Math.round((SETTINGS.scrobble_interval / 60) * 100) / 100; -} + chrome.storage.local.get(['max_scrobbles', 'logs_enabled'], function(items) { + var max = items.max_scrobbles; + var scrobble_mult = (max === undefined || max === null || max > 1); + document.getElementById('scrobble_mult').checked = scrobble_mult; + document.getElementById('log_checkbox').checked = items.logs_enabled === true; + // Show scrobble interval in minutes (from SETTINGS default) + var minutes = Math.round((420 / 60) * 100) / 100; + document.getElementById('minute_field').textContent = minutes; + }); +} document.addEventListener('DOMContentLoaded', restore_options); document.querySelector('#save').addEventListener('click', save_options); diff --git a/js/popup.js b/js/popup.js index 06c8b6f..317e6b9 100644 --- a/js/popup.js +++ b/js/popup.js @@ -1,22 +1,23 @@ /** - * popup.js - * Popup page script - * Copyright (c) 2011 Alexey Savartsov - * Licensed under the MIT license + * popup.js — MV3 compatible + * Uses chrome.runtime.sendMessage instead of getBackgroundPage() */ -/* Background page */ -var bp; -/* Render popup when DOM is ready */ +var _state = {}; // cached background state + $(document).ready(function() { - chrome.runtime.getBackgroundPage(function(backgroundPage) { - bp = backgroundPage; - if (localStorage.getItem("seen_alert") === null) { - show_alert(); - } + // Fetch current state from the service worker + chrome.runtime.sendMessage({ type: 'get_state' }, function(state) { + _state = state || {}; + + chrome.storage.local.get('seen_alert', function(items) { + if (items.seen_alert === undefined) show_alert(); + }); + set_play_link(); render_song(); - if (bp.lastfm_api.session.name && bp.lastfm_api.session.key) { + + if (_state.session && _state.session.name && _state.session.key) { render_scrobble_link(); } render_auth_link(); @@ -24,248 +25,220 @@ $(document).ready(function() { }); function set_play_link() { - $("#cover").click(open_play_tab); + $('#cover').click(function() { + chrome.runtime.sendMessage({ type: 'open_play_tab' }); + // util open_play_tab is in background; we just send the message + find_play_tab(function(tab) { + if (tab) chrome.tabs.update(tab.id, { active: true }); + else chrome.tabs.create({ url: 'https://music.youtube.com', active: true }); + }); + }); } -/* Render functions */ +/* ---- Render helpers ---- */ + function update_song_info(player) { - $("#artist").text(player.song.artist); - $("#track").text(player.song.title); - $("#cover").attr({ src: player.song.cover || "../img/defaultcover.png", - alt:player.song.album}); - $("#album").text(player.song.album); + $('#artist').text(player.song.artist); + $('#track').text(player.song.title); + $('#cover').attr({ + src: player.song.cover || '../img/defaultcover.png', + alt: player.song.album + }); + $('#album').text(player.song.album); - if (bp.lastfm_api.session.name && bp.lastfm_api.session.key) { + if (_state.session && _state.session.name && _state.session.key) { render_love_button(player); } toggle_play_btn(player); } function toggle_play_btn(player) { - var play_btn = $("#play-pause-btn"); - if (player.is_playing) { - play_btn.removeClass(); - play_btn.addClass("pause"); - } else { - play_btn.removeClass(); - play_btn.addClass("play"); - } + var play_btn = $('#play-pause-btn'); + play_btn.removeClass(); + play_btn.addClass(player.is_playing ? 'pause' : 'play'); } -/** - * Renders current song details - */ function render_song() { - if (bp.player.has_song) { - update_song_info(bp.player); - $("#play-pause-btn").click(toggle_play); - $("#next-btn").click(next_song); - $("#prev-btn").click(prev_song); - if (!(bp.lastfm_api.session.name && bp.lastfm_api.session.key)) { - $("#lastfm-buttons").hide(); + var player = _state.player || {}; + if (player.has_song) { + update_song_info(player); + $('#play-pause-btn').click(toggle_play); + $('#next-btn').click(next_song); + $('#prev-btn').click(prev_song); + if (!(_state.session && _state.session.name && _state.session.key)) { + $('#lastfm-buttons').hide(); } } else { - $("#song").addClass("nosong"); - $("#artist").text(""); - $("#track").html(''); - $("#cover ").attr({ src: "../img/defaultcover.png" }); - $("#lastfm-buttons").hide(); - $("#player-controls").hide(); + $('#song').addClass('nosong'); + $('#artist').text(''); + $('#track').html(''); + $('#cover').attr({ src: '../img/defaultcover.png' }); + $('#lastfm-buttons').hide(); + $('#player-controls').hide(); } } -/** - * Renders the link to turn on/off scrobbling - */ function render_scrobble_link() { - $("#scrobbling").html(''); - $("#scrobbling a") - .attr("href", "#") - .click(on_toggle_scrobble) - .text(bp.SETTINGS.scrobble ? "Stop scrobbling" : "Resume scrobbling"); + $('#scrobbling').html(''); + $('#scrobbling a') + .attr('href', '#') + .click(on_toggle_scrobble) + .text(_state.scrobble ? 'Stop scrobbling' : 'Resume scrobbling'); } -/** - * Renders authentication/profile link - */ function render_auth_link() { - if (bp.lastfm_api.session.name && bp.lastfm_api.session.key) { + if (_state.session && _state.session.name && _state.session.key) { render_scrobble_link(); - $("#lastfm-profile").html("Logged in as " + ""); - $("#lastfm-profile a:first") - .attr({ - href: "http://last.fm/user/" + bp.lastfm_api.session.name, - target: "_blank" - }) - .text(bp.lastfm_api.session.name); - - $("#lastfm-profile a:last") - .attr({ - href: "#", - title: "Logout" - }) - .click(on_logout) - .addClass("logout"); + $('#lastfm-profile').html('Logged in as '); + $('#lastfm-profile a:first') + .attr({ href: 'http://last.fm/user/' + _state.session.name, target: '_blank' }) + .text(_state.session.name); + $('#lastfm-profile a:last') + .attr({ href: '#', title: 'Logout' }) + .click(on_logout) + .addClass('logout'); } else { - $("#lastfm-profile").html(''); - $("#lastfm-profile a").attr("href", "#") - .click(on_auth) - .text("Connect to Last.fm"); + $('#lastfm-profile').html(''); + $('#lastfm-profile a') + .attr('href', '#') + .click(on_auth) + .text('Connect to Last.fm'); } } -/** - * Renders the love button - */ function render_love_button(player) { - $("#love-button").html(''); - - bp.lastfm_api.is_track_loved(player.song.title, - player.song.artist, - function(result) { - $("#love-button").html(''); - if (result) { - $("#love-button a").attr({ title: "Unlove this song"}) - .click(function() {on_unlove(player);}) - .addClass("loved"); - - } else { - $("#love-button a").attr({ title: "Love this song" }) - .click(function() {on_love(player);}) - .addClass("notloved"); - } - }); + $('#love-button').html(''); + chrome.runtime.sendMessage( + { type: 'is_track_loved', track: player.song.title, artist: player.song.artist }, + function(result) { + $('#love-button').html(''); + if (result) { + $('#love-button a') + .attr({ title: 'Unlove this song' }) + .click(function() { on_unlove(player); }) + .addClass('loved'); + } else { + $('#love-button a') + .attr({ title: 'Love this song' }) + .click(function() { on_love(player); }) + .addClass('notloved'); + } + } + ); } -/* Event handlers */ +/* ---- Controls ---- */ function toggle_play() { - var has_song = bp.player.has_song; - find_play_tab( - function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: "tgl"}, - function(player) { - if (has_song) { - toggle_play_btn(player); - } else { // if pressing FF on previous song reached end of play queue - update_song_info(player); - toggle_play_btn(player); - } - } - ); - } - ); + find_play_tab(function(tab) { + if (!tab) return; + chrome.tabs.sendMessage(tab.id, { cmd: 'tgl' }, function(player) { + if (player) toggle_play_btn(player); + }); + }); } function prev_song() { - find_play_tab( - function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: "prv"}, - function(player) { - /* The player state is in a disabled state as it loads the - * song initially, but we should display it as playing since - * hitting next or previous always starts a song. - */ - player.is_playing = true; - update_song_info(player); - }); - } - ); + find_play_tab(function(tab) { + if (!tab) return; + chrome.tabs.sendMessage(tab.id, { cmd: 'prv' }, function(player) { + if (player) { + player.is_playing = true; + update_song_info(player); + } + }); + }); } function next_song() { - find_play_tab( - function(tab) { - chrome.tabs.sendMessage(tab.id, {cmd: "nxt"}, - function(player) { - player.is_playing = true; - update_song_info(player); - }); - } - ); + find_play_tab(function(tab) { + if (!tab) return; + chrome.tabs.sendMessage(tab.id, { cmd: 'nxt' }, function(player) { + if (player) { + player.is_playing = true; + update_song_info(player); + } + }); + }); } -/** - * Turn on/off scrobbling link was clicked - */ +/* ---- Event handlers ---- */ + function on_toggle_scrobble() { - bp.toggle_scrobble(); - render_scrobble_link(); + chrome.runtime.sendMessage({ type: 'toggle_scrobble' }, function(resp) { + if (resp) _state.scrobble = resp.scrobble; + render_scrobble_link(); + }); } -/** - * Authentication link was clicked - */ function on_auth() { - bp.start_web_auth(); + chrome.runtime.sendMessage({ type: 'start_web_auth' }); window.close(); } -/** - * Logout link was clicked - */ function on_logout() { - bp.clear_session(); - render_auth_link(); + chrome.runtime.sendMessage({ type: 'clear_session' }, function() { + _state.session = {}; + render_auth_link(); + }); } -/** - * Love button was clicked - */ function on_love(player) { - bp.lastfm_api.love_track(player.song.title, player.song.artist, + $('#love-button').html(''); + chrome.runtime.sendMessage( + { type: 'love_track', track: player.song.title, artist: player.song.artist }, function(result) { - if (!result.error) { + if (!result || !result.error) { render_love_button(player); - } - else { - if (result.error == 9) { - // Session expired - bp.clear_session(); + } else { + if (result.error === 9) { + chrome.runtime.sendMessage({ type: 'clear_session' }); + _state.session = {}; render_auth_link(); } - - chrome.browserAction.setIcon({ - 'path': SETTINGS.error_icon }); } - }); - - $("#love-button").html(''); + } + ); } -/** - * Unlove button was clicked - */ function on_unlove(player) { - bp.lastfm_api.unlove_track(player.song.title, player.song.artist, + $('#love-button').html(''); + chrome.runtime.sendMessage( + { type: 'unlove_track', track: player.song.title, artist: player.song.artist }, function(result) { - if (!result.error) { + if (!result || !result.error) { render_love_button(player); } else { - if (result.error == 9) { - // Session expired - bp.clear_session(); + if (result.error === 9) { + chrome.runtime.sendMessage({ type: 'clear_session' }); + _state.session = {}; render_auth_link(); } - - chrome.browserAction.setIcon({ - 'path': SETTINGS.error_icon }); } - }); - - $("#love-button").html(''); + } + ); } -/** -* Show temporary msg from me to user <3 -*/ function show_alert() { - $("#alert").removeClass("hidden"); - $("#extns_link").click(function() { - bp.open_extensions_page(); + $('#alert').removeClass('hidden'); + $('#extns_link').click(function() { + chrome.runtime.sendMessage({ type: 'open_extensions_page' }); }); - $("#dismiss_alert").click(function() { - $("#alert").addClass("hidden"); - localStorage.setItem("seen_alert", "1"); + $('#dismiss_alert').click(function() { + $('#alert').addClass('hidden'); + chrome.storage.local.set({ seen_alert: '1' }); }); -} \ No newline at end of file +} + +/* ---- Inline copy of find_play_tab (util.js not loaded in popup) ---- */ +function find_play_tab(callback) { + chrome.tabs.query({ url: '*://music.youtube.com/*' }, function(tabs) { + if (tabs.length > 0) { + callback(tabs[0]); + } else { + chrome.tabs.query({ url: '*://play.google.com/music/listen*' }, function(tabs2) { + callback(tabs2.length > 0 ? tabs2[0] : null); + }); + } + }); +} diff --git a/js/settings.js b/js/settings.js index fa60718..16eb96c 100644 --- a/js/settings.js +++ b/js/settings.js @@ -1,14 +1,19 @@ +/** + * settings.js + * Extension settings — MV3 compatible (chrome.storage.local instead of localStorage) + */ + var SETTINGS = { api_key: 'd00dce85051b7dbcbfcc165eaebfc6d2', api_secret: 'bdfcae3563763ece1b6d3dcdd56a7ab8', callback_file: 'html/lastfm_callback.html', - main_icon: '../img/main-icon.png', - playing_icon: '../img/main-icon-playing.png', - paused_icon: '../img/main-icon-paused.png', - error_icon: '../img/main-icon-error.png', - scrobbling_stopped_icon: '../img/main-icon-scrobbling-stopped.png', + main_icon: chrome.runtime.getURL('img/main-icon.png'), + playing_icon: chrome.runtime.getURL('img/main-icon-playing.png'), + paused_icon: chrome.runtime.getURL('img/main-icon-paused.png'), + error_icon: chrome.runtime.getURL('img/main-icon-error.png'), + scrobbling_stopped_icon: chrome.runtime.getURL('img/main-icon-scrobbling-stopped.png'), scrobble_point: 0.7, scrobble_interval: 420, // 7 minutes @@ -17,17 +22,33 @@ var SETTINGS = { refresh_interval: 2, gmusic_ads_metadata: { - title: 'We\'ll be right back', + title: "We'll be right back", artist: 'Subscribe to go ad-free' - } -}; + }, -SETTINGS.max_scrobbles = localStorage.getItem('max_scrobbles') && - parseInt(localStorage.getItem('max_scrobbles')) || - SETTINGS.max_scrobbles; - -SETTINGS.logs_enabled = localStorage.getItem('logs_enabled') && - localStorage.getItem('logs_enabled') == 'true'; + // Defaults (overwritten by loadSettings) + scrobble: true, + logs_enabled: false +}; -// This enables scrobbling by default -SETTINGS.scrobble = localStorage.getItem("scrobble") != "false"; +/** + * Load persisted settings from chrome.storage.local. + * Returns a Promise that resolves once SETTINGS has been updated. + */ +function loadSettings() { + return new Promise(function(resolve) { + chrome.storage.local.get( + ['session_key', 'session_name', 'max_scrobbles', 'logs_enabled', 'scrobble'], + function(items) { + if (items.max_scrobbles !== undefined) { + var parsed = parseInt(items.max_scrobbles); + if (!isNaN(parsed)) SETTINGS.max_scrobbles = parsed; + } + SETTINGS.logs_enabled = items.logs_enabled === true; + // scrobble defaults to true; only false if explicitly stored as false + SETTINGS.scrobble = items.scrobble !== false; + resolve(items); + } + ); + }); +} \ No newline at end of file diff --git a/js/util.js b/js/util.js index 35339a5..e83ceb8 100644 --- a/js/util.js +++ b/js/util.js @@ -1,36 +1,26 @@ /** -* Various utility functions -*/ + * util.js — Various utility functions + */ function find_play_tab(callback) { - chrome.tabs.query({url: '*://music.youtube.com/*'}, - function(tabs) { - if (tabs.length > 0) { - callback(tabs[0]); - } else { - // Fallback to GPM if no YT Music tab. - chrome.tabs.query({url: '*://play.google.com/music/listen*'}, - function(tabs) { - if (tabs.length > 0) { - callback(tabs[0]); - } else { - callback(null); - } - }); - } + chrome.tabs.query({ url: '*://music.youtube.com/*' }, function(tabs) { + if (tabs.length > 0) { + callback(tabs[0]); + } else { + chrome.tabs.query({ url: '*://play.google.com/music/listen*' }, function(tabs2) { + callback(tabs2.length > 0 ? tabs2[0] : null); + }); + } }); } function open_play_tab() { - find_play_tab( - function(tab) { - if (tab) { - chrome.tabs.update(tab.id, {selected: true}); - } else { - chrome.tabs.create({url: - 'https://music.youtube.com', - selected: true}); - } - } - ); + find_play_tab(function(tab) { + if (tab) { + // MV3: use 'active' + 'highlighted' instead of deprecated 'selected' + chrome.tabs.update(tab.id, { active: true, highlighted: true }); + } else { + chrome.tabs.create({ url: 'https://music.youtube.com', active: true }); + } + }); } diff --git a/js/ytm_contentscript.js b/js/ytm_contentscript.js index 3e3faa5..d6adcd1 100644 --- a/js/ytm_contentscript.js +++ b/js/ytm_contentscript.js @@ -1,200 +1,115 @@ /** - * contentscripts.js - * Parses player page and transmit song information to background page - * Copyright (c) 2011 Alexey Savartsov, , Brad Lambeth - * Licensed under the MIT license + * ytm_contentscript.js — YouTube Music content script (MV3) */ -/** - * Player class - * - * Cloud Player page parser - */ +// Default settings used in content script context +var CS_SETTINGS = { + refresh_interval: 2 +}; + +// --------------------------------------------------------------------------- +// Player / Parser classes +// --------------------------------------------------------------------------- + function Player(parser) { - this.has_song = parser._get_has_song(); + this.has_song = parser._get_has_song(); this.is_playing = parser._get_is_playing(); this.song = { - position: parser._get_song_position(), - time: parser._get_song_time(), - title: parser._get_song_title(), - artist: parser._get_song_artist(), + position: parser._get_song_position(), + time: parser._get_song_time(), + title: parser._get_song_title(), + artist: parser._get_song_artist(), album_artist: parser._get_album_artist(), - album: parser._get_song_album(), - cover: parser._get_song_cover() + album: parser._get_song_album(), + cover: parser._get_song_cover() }; } -/** - * Constructor for parser class - * Executes scripts to fetch now playing info from cloudplayer - * @returns {YtMusicParser} - */ -YtMusicParser = function() { +var YtMusicParser = function() {}; -}; - -/** - * Check whether a song loaded into player widget - * - * @return true if some song is loaded, otherwise false - */ YtMusicParser.prototype._get_has_song = function() { - return $("yt-formatted-string.title.ytmusic-player-bar").text().length > 0; + return $('yt-formatted-string.title.ytmusic-player-bar').text().length > 0; }; -/** - * Checks whether song is playing or paused - * - * @return true if song is playing, false if song is paused - */ YtMusicParser.prototype._get_is_playing = function() { - // YT app prepends current song's name to the tab title - var songTitle = this._get_song_title() - return songTitle.length > 0 && window.document.title.startsWith(songTitle) + var songTitle = this._get_song_title(); + return songTitle.length > 0 && window.document.title.startsWith(songTitle); }; -/** - * Get current song playing position - * - * @return Playing position in seconds - */ YtMusicParser.prototype._get_song_position = function() { - var _time = $("span.time-info").text().split("/")[0]; - _time = $.trim(_time).split(":"); - if(_time.length == 2) - { - return (parseInt(_time[0]) * 60 + parseInt(_time[1])); - } - else if (_time.length == 3) - { - return (parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2])); - } + var _time = $('span.time-info').text().split('/')[0]; + _time = $.trim(_time).split(':'); + if (_time.length === 2) return parseInt(_time[0]) * 60 + parseInt(_time[1]); + if (_time.length === 3) return parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2]); return null; }; -/** - * Get current song length - * - * @return Song length in seconds - */ YtMusicParser.prototype._get_song_time = function() { - var _time = $("span.time-info").text().split("/")[1]; - _time = $.trim(_time).split(":"); - if(_time.length == 2) { - return (parseInt(_time[0]) * 60 + parseInt(_time[1])); - } - else if (_time.length == 3) - { - return (parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2])); - } + var _time = $('span.time-info').text().split('/')[1]; + _time = $.trim(_time).split(':'); + if (_time.length === 2) return parseInt(_time[0]) * 60 + parseInt(_time[1]); + if (_time.length === 3) return parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2]); return null; }; -/** - * Get current song title - * - * @return Song title - */ YtMusicParser.prototype._get_song_title = function() { - // the text inside the div located inside element with id="playerSongTitle" - return $("yt-formatted-string.title.ytmusic-player-bar").text(); + return $('yt-formatted-string.title.ytmusic-player-bar').text(); }; -/** - * Get current song artist - * - * @return Song artist - */ YtMusicParser.prototype._get_song_artist = function() { - return $("span.subtitle.ytmusic-player-bar>yt-formatted-string>a").first().text(); + return $('span.subtitle.ytmusic-player-bar>yt-formatted-string>a').first().text(); }; -/** - * Get current song album artist - * - * @return The album artist - */ YtMusicParser.prototype._get_album_artist = function() { - // TODO: Check if album artist is actually available. - return $("span.subtitle.ytmusic-player-bar>yt-formatted-string>a").first().text(); + return $('span.subtitle.ytmusic-player-bar>yt-formatted-string>a').first().text(); }; -/** - * Get current song artwork - * - * @return Image URL or default artwork - */ YtMusicParser.prototype._get_song_cover = function() { - var albumImg = $("div.thumbnail-image-wrapper.ytmusic-player-bar>img").attr("src"); - if (albumImg) - return albumImg; - return null; + var albumImg = $('div.thumbnail-image-wrapper.ytmusic-player-bar>img').attr('src'); + return albumImg || null; }; -/** - * Get current song album name - * - * @return Album name or null - */ YtMusicParser.prototype._get_song_album = function() { - return $("span.subtitle.style-scope.ytmusic-player-bar>yt-formatted-string>a").last().text(); + return $('span.subtitle.style-scope.ytmusic-player-bar>yt-formatted-string>a').last().text(); }; -var port = chrome.runtime.connect(); - -window.setInterval(function() { - port.postMessage(new Player(new YtMusicParser())); -}, -SETTINGS.refresh_interval * 1000); - -/* -* Listeners for player control buttons -*/ -chrome.runtime.onMessage.addListener(toggle_play); -chrome.runtime.onMessage.addListener(prev_song); -chrome.runtime.onMessage.addListener(next_song); - -function toggle_play(msg, sndr, send_response) { - if (msg.cmd == "tgl") { - var play_btn = $("#play-pause-button"); - play_btn.click(); - /* - * Wait a little for the UI to update before sending a response - * with the updated state. - */ - setTimeout(function() { - send_response(new Player(new YtMusicParser())); - }, 100); - /* - * Return true keeps the message channel open so the timeout function - * will be called, otherwise send_response will not work per - * https://developer.chrome.com/extensions/runtime#event-onMessage. - */ +// --------------------------------------------------------------------------- +// Poll via sendMessage — wakes the service worker on demand each interval +// --------------------------------------------------------------------------- + +chrome.storage.local.get('refresh_interval', function(items) { + var interval = (items.refresh_interval || CS_SETTINGS.refresh_interval) * 1000; + window.setInterval(function() { + chrome.runtime.sendMessage( + { type: 'player_state', state: new Player(new YtMusicParser()) }, + function() { void chrome.runtime.lastError; } + ); + }, interval); +}); + +// --------------------------------------------------------------------------- +// Player control listeners +// --------------------------------------------------------------------------- + +chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) { + var btn; + if (msg.cmd === 'tgl') { + btn = $('#play-pause-button')[0]; + if (btn) btn.click(); + setTimeout(function() { sendResponse(new Player(new YtMusicParser())); }, 100); return true; } - return false; -} - -function prev_song(msg, sndr, send_response) { - if (msg.cmd == "prv") { - var prev_btn = $("div.left-controls-buttons>.previous-button"); - prev_btn.click(); - setTimeout(function() { - send_response(new Player(new YtMusicParser())); - }, 100); + if (msg.cmd === 'prv') { + btn = $('div.left-controls-buttons>.previous-button')[0]; + if (btn) btn.click(); + setTimeout(function() { sendResponse(new Player(new YtMusicParser())); }, 100); return true; } - return false; -} - -function next_song(msg, sndr, send_response) { - if (msg.cmd == "nxt") { - var next_btn = $("div.left-controls-buttons>.next-button"); - next_btn.click(); - setTimeout(function() { - send_response(new Player(new YtMusicParser())); - }, 100); + if (msg.cmd === 'nxt') { + btn = $('div.left-controls-buttons>.next-button')[0]; + if (btn) btn.click(); + setTimeout(function() { sendResponse(new Player(new YtMusicParser())); }, 100); return true; } return false; -} +}); diff --git a/manifest.json b/manifest.json index e6b7752..4de963f 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name" : "YouTube Music Last.fm Scrobbler", "description" : "Scrobbles songs from YouTube Music or Google Play to Last.fm", - "version" : "2.0.3", + "version" : "3.0.0", "manifest_version": 3, "host_permissions" : [ "http://ws.audioscrobbler.com/", @@ -9,26 +9,21 @@ "https://play.google.com/", "https://music.youtube.com/" ], + "permissions": [ + "storage" + ], "background": { - "scripts": [ - "js/md5.js", - "js/lastfm.js", - "js/settings.js", - "js/util.js", - "js/logging.js", - "js/background.js" - ], - "persistent": false + "service_worker": "js/background.js" }, "icons" : { "128" : "img/icon128.png", - "64" : "img/icon64.png", - "16" : "img/icon16.png", - "48" : "img/icon48.png" + "64" : "img/icon64.png", + "16" : "img/icon16.png", + "48" : "img/icon48.png" }, - "browser_action" : { + "action" : { "default_popup" : "html/popup.html", - "default_icon" : "img/main-icon.png" + "default_icon" : "img/main-icon.png" }, "content_scripts" : [ { @@ -36,8 +31,7 @@ "*://play.google.com/music/listen*" ], "js" : [ - "js/inject.js", - "js/settings.js", + "js/jquery-1.7.min.js", "js/contentscript.js" ] }, @@ -46,20 +40,9 @@ "*://music.youtube.com/*" ], "js" : [ - "js/inject.js", - "js/settings.js", + "js/jquery-1.7.min.js", "js/ytm_contentscript.js" ] - }, - { - "run_at" : "document_start", - "matches" : [ - "*://play.google.com/music/listen*", - "*://music.youtube.com/*" - ], - "js" : [ - "js/jquery-1.7.min.js" - ] } ], "options_page": "html/options.html", From c47ec7f28b1fa7c9ae7fddfc98e22a7f9c21c38d Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Tue, 17 Feb 2026 21:28:43 -0500 Subject: [PATCH 74/74] Remove old Google Music integration. --- js/contentscript.js | 119 -------------------------------------------- manifest.json | 15 +----- 2 files changed, 2 insertions(+), 132 deletions(-) delete mode 100644 js/contentscript.js diff --git a/js/contentscript.js b/js/contentscript.js deleted file mode 100644 index ed3a8cd..0000000 --- a/js/contentscript.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * contentscript.js — Google Play Music content script (MV3) - */ - -var CS_SETTINGS = { - refresh_interval: 2 -}; - -// --------------------------------------------------------------------------- -// Player / Parser classes -// --------------------------------------------------------------------------- - -function Player(parser) { - this.has_song = parser._get_has_song(); - this.is_playing = parser._get_is_playing(); - this.song = { - position: parser._get_song_position(), - time: parser._get_song_time(), - title: parser._get_song_title(), - artist: parser._get_song_artist(), - album_artist: parser._get_album_artist(), - album: parser._get_song_album(), - cover: parser._get_song_cover() - }; -} - -var GoogleMusicParser = function() {}; - -GoogleMusicParser.prototype._get_has_song = function() { - return $('#playerSongInfo').children().length > 0; -}; - -GoogleMusicParser.prototype._get_is_playing = function() { - var play_btn = $('.material-player-middle paper-icon-button[data-id="play-pause"]'); - if (play_btn.length === 0) { - play_btn = $('.material-player-middle sj-icon-button[data-id="play-pause"]'); - } - return play_btn.hasClass('playing'); -}; - -GoogleMusicParser.prototype._get_song_position = function() { - var _time = $.trim($('#time_container_current').text()).split(':'); - if (_time.length === 2) return parseInt(_time[0]) * 60 + parseInt(_time[1]); - if (_time.length === 3) return parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2]); - return null; -}; - -GoogleMusicParser.prototype._get_song_time = function() { - var _time = $.trim($('#time_container_duration').text()).split(':'); - if (_time.length === 2) return parseInt(_time[0]) * 60 + parseInt(_time[1]); - if (_time.length === 3) return parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2]); - return null; -}; - -GoogleMusicParser.prototype._get_song_title = function() { - return $('#currently-playing-title').text(); -}; - -GoogleMusicParser.prototype._get_song_artist = function() { - return $('#player-artist').text(); -}; - -GoogleMusicParser.prototype._get_album_artist = function() { - var album_artist = $('#playerSongInfo .player-album').attr('data-id'); - if (album_artist) return decodeURIComponent(album_artist.split('/')[1].replace(/\+/g, ' ')); - return null; -}; - -GoogleMusicParser.prototype._get_song_cover = function() { - return $('#playerBarArt').attr('src') || null; -}; - -GoogleMusicParser.prototype._get_song_album = function() { - return $('#playerSongInfo .player-album').text(); -}; - -// --------------------------------------------------------------------------- -// Poll via sendMessage — wakes the service worker on demand each interval -// --------------------------------------------------------------------------- - -chrome.storage.local.get('refresh_interval', function(items) { - var interval = (items.refresh_interval || CS_SETTINGS.refresh_interval) * 1000; - window.setInterval(function() { - chrome.runtime.sendMessage( - { type: 'player_state', state: new Player(new GoogleMusicParser()) }, - function() { void chrome.runtime.lastError; } - ); - }, interval); -}); - -// --------------------------------------------------------------------------- -// Player control listeners -// --------------------------------------------------------------------------- - -chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) { - var play_btn, prev_btn, next_btn; - if (msg.cmd === 'tgl') { - play_btn = $('.material-player-middle paper-icon-button[data-id="play-pause"]'); - if (play_btn.length === 0) play_btn = $('.material-player-middle sj-icon-button[data-id="play-pause"]'); - play_btn.click(); - setTimeout(function() { sendResponse(new Player(new GoogleMusicParser())); }, 100); - return true; - } - if (msg.cmd === 'prv') { - prev_btn = $('.material-player-middle paper-icon-button[data-id="rewind"]'); - if (prev_btn.length === 0) prev_btn = $('.material-player-middle sj-icon-button[data-id="rewind"]'); - prev_btn.click(); - setTimeout(function() { sendResponse(new Player(new GoogleMusicParser())); }, 100); - return true; - } - if (msg.cmd === 'nxt') { - next_btn = $('.material-player-middle paper-icon-button[data-id="forward"]'); - if (next_btn.length === 0) next_btn = $('.material-player-middle sj-icon-button[data-id="forward"]'); - next_btn.click(); - setTimeout(function() { sendResponse(new Player(new GoogleMusicParser())); }, 100); - return true; - } - return false; -}); diff --git a/manifest.json b/manifest.json index 4de963f..161cd75 100644 --- a/manifest.json +++ b/manifest.json @@ -1,12 +1,10 @@ { "name" : "YouTube Music Last.fm Scrobbler", "description" : "Scrobbles songs from YouTube Music or Google Play to Last.fm", - "version" : "3.0.0", + "version" : "3.0.1", "manifest_version": 3, "host_permissions" : [ "http://ws.audioscrobbler.com/", - "http://play.google.com/", - "https://play.google.com/", "https://music.youtube.com/" ], "permissions": [ @@ -26,15 +24,6 @@ "default_icon" : "img/main-icon.png" }, "content_scripts" : [ - { - "matches" : [ - "*://play.google.com/music/listen*" - ], - "js" : [ - "js/jquery-1.7.min.js", - "js/contentscript.js" - ] - }, { "matches" : [ "*://music.youtube.com/*" @@ -85,7 +74,7 @@ "suggested_key": { "default": "Ctrl+Shift+0" }, - "description": "Go to YT Music/Google Music tab", + "description": "Go to YT Music", "global": true } }
- +
diff --git a/js/popup.js b/js/popup.js index 2541ed3..dcea44f 100644 --- a/js/popup.js +++ b/js/popup.js @@ -9,11 +9,43 @@ var bp = chrome.extension.getBackgroundPage(); /* Render popup when DOM is ready */ $(document).ready(function() { + set_play_link(); render_scrobble_link(); render_song(); render_auth_link(); }); +function FindPlayTab(callback) { + chrome.windows.getAll( + {populate: true}, + function(windows) { + var pattern = 'https?\:\/\/play\.google\.com\/music\/listen.*'; + for (var window = 0; window < windows.length; window++) { + for (var i = 0; i < windows[window].tabs.length; i++) { + if (windows[window].tabs[i].url.match(pattern)) { + callback(windows[window].tabs[i].id) + return; + } + } + } + callback(null); + }); +} + +function openPlayTab() { + FindPlayTab(function(tab_id) { + if (tab_id) { + chrome.tabs.update(tab_id, {selected: true}); + } else { + chrome.tabs.create({url: 'https://play.google.com/music/listen', + selected: true}); + } + }); +} + +function set_play_link() { + $("#cover").click(openPlayTab); +} /* Render functions */ /** diff --git a/manifest.json b/manifest.json index 639f033..885d9c8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,11 @@ { "permissions" : [ + "tabs", "http://ws.audioscrobbler.com/", "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.3.1", + "version" : "1.3.2", "manifest_version": 2, "background": { "page": "html/background.html", From aa0ba8ca63cdfcdd872000738bbfed1dc75dac41 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Tue, 21 May 2013 10:45:37 -0400 Subject: [PATCH 06/74] Display album name --- css/popup.css | 6 ++++++ html/popup.html | 1 + js/popup.js | 1 + manifest.json | 2 +- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/css/popup.css b/css/popup.css index f6e4a80..6819fa7 100644 --- a/css/popup.css +++ b/css/popup.css @@ -59,6 +59,12 @@ a:hover { font-weight: bold; } +#album { + font-style: italic; + color: #999; + font-size: 80%; +} + #track { margin-top: 6px; font-size: 14px; diff --git a/html/popup.html b/html/popup.html index eb707c0..d8372a3 100644 --- a/html/popup.html +++ b/html/popup.html @@ -22,6 +22,7 @@  -  +
diff --git a/js/popup.js b/js/popup.js index dcea44f..992e460 100644 --- a/js/popup.js +++ b/js/popup.js @@ -58,6 +58,7 @@ function render_song() { $("#track").text(bp.player.song.title); $("#cover").attr({ src: bp.player.song.cover || "../img/defaultcover.png", alt:bp.player.song.album}); + $("#album").text(bp.player.song.album); if(bp.lastfm_api.session.name && bp.lastfm_api.session.key) { render_love_button(); } diff --git a/manifest.json b/manifest.json index 885d9c8..db2259e 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.3.2", + "version" : "1.3.3", "manifest_version": 2, "background": { "page": "html/background.html", From 28f34a0905170ed58971157c48d4ae67013d5e58 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Fri, 24 May 2013 13:57:44 -0400 Subject: [PATCH 07/74] Stop caching GET requests. Love button works now. --- js/lastfm.js | 7 +++++-- js/popup.js | 6 +++--- manifest.json | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/js/lastfm.js b/js/lastfm.js index 987d0be..c91bcee 100644 --- a/js/lastfm.js +++ b/js/lastfm.js @@ -187,7 +187,8 @@ LastFM.prototype.is_track_loved = function(track, artist, callback) { if(!result.error && result.track) { callback(result.track.userloved == 1); } - else { + else + { callback(false); } }); @@ -267,6 +268,8 @@ LastFM.prototype._xhr = function(method, params, callback) { }; xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); - xhr.setRequestHeader("Pragma", "no-cache"); // The cache is a lie! + // The cache is a lie! + xhr.setRequestHeader("If-Modified-Since", new Date(0)); + xhr.setRequestHeader("Pragma", "no-cache"); xhr.send(_data || null); }; diff --git a/js/popup.js b/js/popup.js index 992e460..25c4e02 100644 --- a/js/popup.js +++ b/js/popup.js @@ -131,21 +131,21 @@ function render_auth_link() { /** * Renders the love button */ -function render_love_button() { +function render_love_button() { $("#love-button").html(''); bp.lastfm_api.is_track_loved(bp.player.song.title, bp.player.song.artist, function(result) { $("#love-button").html(''); - if(result) { $("#love-button a").attr({ title: "Unlove this song"}) .click(on_unlove) .addClass("loved"); } - else { + else + { $("#love-button a").attr({ title: "Love this song" }) .click(on_love) .addClass("notloved"); diff --git a/manifest.json b/manifest.json index db2259e..367aae8 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.3.3", + "version" : "1.3.4", "manifest_version": 2, "background": { "page": "html/background.html", From 9e30a67134e87b36a50d39600be59a6d8db32f9b Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sat, 25 May 2013 19:15:04 -0400 Subject: [PATCH 08/74] Updated README --- README.md | 34 +++++----------------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 683de7c..30b658f 100755 --- a/README.md +++ b/README.md @@ -6,44 +6,20 @@ Google Chrome plugin for scrobbling songs from Google Music Player to Last.fm Features ======== -* Scrobbles now playing songs at 70% played time * Song information in popup window -* Cloud Player state in icon -* Love/unlove now playing song -* Option to turn scrobbling off if you do not want to send scrobbles +* Love/unlove tracks +* Toggle scrobbling Installation ============ -Install stable release from [Google Web Store](https://chrome.google.com/webstore/detail/lhlmaloocaogaldcbpimhlbimmhaonep) - -Version History -=============== - -**Version 1.2.4** - -* Spanish fixes - -**Version 1.2.3** - -* Google Play - -**Version 1.2.2** - -* Timestamp fixes -* HTTPS support - -**Version 1.2.1** - -* Album art image size fixed to 60x60px - -**Version 1.2.0** - -* Port scrobbler to Google Music Beta +Install stable release from [Google Web Store](https://chrome.google.com/webstore/detail/google-play-scrobbler/llpepekkleeoeiloijhcafgpjdnhhcbl) Authors ======= +[Andrew Nguyen](https://github.com/newgiin) + [Alexey Savartsov](https://github.com/asavartsov), asavartsov@gmail.com Google Music support by [Brad Lambeth](https://github.com/bradlambeth), brad@lambeth.us From bc055b8436ea37d590ce6655a788d704ccafe4eb Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Thu, 30 May 2013 09:55:06 -0400 Subject: [PATCH 09/74] Added last.fm to project name. --- README.md | 2 +- manifest.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 30b658f..0118fce 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Google Music Player scrobbler +Google Play Last.fm scrobbler ----------------------------- Google Chrome plugin for scrobbling songs from Google Music Player to Last.fm diff --git a/manifest.json b/manifest.json index 367aae8..59111a2 100644 --- a/manifest.json +++ b/manifest.json @@ -5,13 +5,13 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.3.4", + "version" : "1.3.5", "manifest_version": 2, "background": { "page": "html/background.html", "persistent": true }, - "name" : "Google Play Scrobbler", + "name" : "Google Play Last.fm Scrobbler", "icons" : { "128" : "img/icon128.png", "64" : "img/icon64.png", From c9feaa0bff553d39c2e2c09464eb5dd34f07ddbe Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Thu, 13 Jun 2013 14:15:30 -0400 Subject: [PATCH 10/74] Higher marquee threshold to prevent jittering. --- js/popup.js | 2 +- manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/popup.js b/js/popup.js index 25c4e02..098d843 100644 --- a/js/popup.js +++ b/js/popup.js @@ -75,7 +75,7 @@ function render_song() { } // check if we need to marquee var songElem = $("#now-playing"); - if (songElem.get(0).scrollWidth > songElem.width()) { + if (songElem.get(0).scrollWidth > songElem.width() + 10) { songElem.attr('scrollamount', '1'); } else { songElem.attr('scrollamount', '0'); diff --git a/manifest.json b/manifest.json index 59111a2..71893c4 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.3.5", + "version" : "1.3.6", "manifest_version": 2, "background": { "page": "html/background.html", From 01f3760d7ae373de5bd3b3e37a4876bccc8b2875 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Fri, 21 Jun 2013 23:38:21 -0400 Subject: [PATCH 11/74] Works with Play Music update --- js/contentscript.js | 6 +++--- manifest.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/js/contentscript.js b/js/contentscript.js index c6fe8ec..005bf75 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -38,7 +38,7 @@ GoogleMusicParser = function() { * @return true if some song is loaded, otherwise false */ GoogleMusicParser.prototype._get_has_song = function() { - return $("#playerSongInfo div").hasClass("goog-inline-block goog-flat-button"); + return $("#playerSongInfo div").hasClass("now-playing-menu-wrapper"); }; /** @@ -47,7 +47,7 @@ GoogleMusicParser.prototype._get_has_song = function() { * @return true if song is playing, false if song is paused */ GoogleMusicParser.prototype._get_is_playing = function() { - return ($("#playPause").attr("aria-pressed") == "true"); + return $(".player-middle button[data-id='play-pause']").hasClass("playing"); }; /** @@ -94,7 +94,7 @@ GoogleMusicParser.prototype._get_song_time = function() { */ GoogleMusicParser.prototype._get_song_title = function() { // the text inside the div located inside element with id="playerSongTitle" - return $("#playerSongTitle div").text(); + return $("#playerSongTitle").text(); }; /** diff --git a/manifest.json b/manifest.json index 71893c4..a43b454 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.3.6", + "version" : "1.3.7", "manifest_version": 2, "background": { "page": "html/background.html", From 1b4532c225ffa20eea8387af46ef2e12da100a20 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Sat, 22 Jun 2013 01:10:40 -0400 Subject: [PATCH 12/74] gets time correctly --- js/contentscript.js | 4 ++-- manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/js/contentscript.js b/js/contentscript.js index 005bf75..88b3d64 100644 --- a/js/contentscript.js +++ b/js/contentscript.js @@ -56,7 +56,7 @@ GoogleMusicParser.prototype._get_is_playing = function() { * @return Playing position in seconds */ GoogleMusicParser.prototype._get_song_position = function() { - var _time = $("#currentTime").text(); + var _time = $("#time_container_current").text(); _time = $.trim(_time).split(':'); if(_time.length == 2) { @@ -75,7 +75,7 @@ GoogleMusicParser.prototype._get_song_position = function() { * @return Song length in seconds */ GoogleMusicParser.prototype._get_song_time = function() { - var _time = $("#duration").text(); + var _time = $("#time_container_duration").text(); _time = $.trim(_time).split(':'); if(_time.length == 2) { return (parseInt(_time[0]) * 60 + parseInt(_time[1])); diff --git a/manifest.json b/manifest.json index a43b454..c88ebac 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "http://play.google.com/", "https://play.google.com/" ], - "version" : "1.3.7", + "version" : "1.3.8", "manifest_version": 2, "background": { "page": "html/background.html", From f87d2808418283c652a60e235e494bf1603c2cb2 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Thu, 27 Jun 2013 03:35:11 -0400 Subject: [PATCH 13/74] Mostly finished button implementation. Some minor bugs left. --- html/popup.html | 2 + js/contentscript.js | 25 +++++++++- js/popup.js | 111 +++++++++++++++++++++++++++++--------------- 3 files changed, 100 insertions(+), 38 deletions(-) diff --git a/html/popup.html b/html/popup.html index d8372a3..659f40c 100644 --- a/html/popup.html +++ b/html/popup.html @@ -25,6 +25,8 @@
+ Pause + Next