diff --git a/README.md b/README.md index 683de7c..da85bd2 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 @@ -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 @@ -54,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 ======= @@ -79,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/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..7603e16 100644 --- a/css/popup.css +++ b/css/popup.css @@ -2,21 +2,19 @@ * 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; } -p { +span { line-height: 100%; - padding: 0; - margin: 0; } a { @@ -29,10 +27,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 { @@ -48,25 +54,28 @@ a:hover { } #artist { - font-size: 18px; + font-weight: bold; } -#track { - margin-top: 6px; - font-size: 14px; +#album { + min-height: 25px; + font-style: italic; + color: #999; + font-size: 80%; } -#lastfm-buttons { +#track { margin-top: 6px; + font-size: 14px; } #bottom { margin-top: 10px; font-size: 11px; - + color: grey; } - + #scrobbling { float: left; } @@ -103,6 +112,49 @@ 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 -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; + height: 18px; + background-image: url('../img/next.png'); + background-repeat: no-repeat; +} + .clear { clear: both; } + +.hidden { + display: none; +} + +#alert { + font-size: 12px; + border-bottom: 1px dotted #000; + padding-bottom: 10px; + margin-bottom: 10px; +} \ No newline at end of file diff --git a/html/lastfm_callback.html b/html/lastfm_callback.html new file mode 100644 index 0000000..4f2cb37 --- /dev/null +++ b/html/lastfm_callback.html @@ -0,0 +1,13 @@ + + +Last.fm Auth + +

Authenticating with Last.fm, please wait...

+ + + diff --git a/html/options.html b/html/options.html new file mode 100644 index 0000000..ef0255c --- /dev/null +++ b/html/options.html @@ -0,0 +1,20 @@ + + + YouTube Music Last.fm Scrobbler Options + + + Scrobbler Options: +

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

+ 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 new file mode 100644 index 0000000..04c2e6b --- /dev/null +++ b/html/popup.html @@ -0,0 +1,53 @@ + + + + + + + +
+
+
+ +
+
+ +  -  + + +
+
+ + +     + + + +     + +     + + +
+
+
+
+
+
+
+
+
+ + + + + diff --git a/img/defaultcover.png b/img/defaultcover.png index 1634c59..304419b 100644 Binary files a/img/defaultcover.png and b/img/defaultcover.png differ diff --git a/img/next.png b/img/next.png new file mode 100644 index 0000000..6dd545b Binary files /dev/null and b/img/next.png differ diff --git a/img/play_pause.png b/img/play_pause.png new file mode 100644 index 0000000..a08f5e7 Binary files /dev/null and b/img/play_pause.png differ diff --git a/img/prev.png b/img/prev.png new file mode 100644 index 0000000..64a1833 Binary files /dev/null and b/img/prev.png differ diff --git a/js/background.js b/js/background.js index 8bf9b87..96c7c97 100644 --- a/js/background.js +++ b/js/background.js @@ -1,193 +1,239 @@ /** - * background.js - * Background page script + * background.js — MV3 Service Worker * Copyright (c) 2011 Alexey Savartsov * Licensed under the MIT license */ -var SETTINGS = { - api_key: "d00dce85051b7dbcbfcc165eaebfc6d2", - api_secret: "bdfcae3563763ece1b6d3dcdd56a7ab8", - - callback_file: "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: .70 -}; - -var player = {}; // Previous player state -var now_playing_sent = false; -var scrobbled = false; -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; - -// This enables scrobbling by default -SETTINGS.scrobble = !(localStorage["scrobble"] == "false"); - -if(!SETTINGS.scrobble) { - chrome.browserAction.setIcon({ 'path': SETTINGS.scrobbling_stopped_icon }); -} +importScripts( + 'md5.js', + 'lastfm.js', + 'settings.js', + 'util.js', + 'logging.js' +); -// Connect event handlers -chrome.extension.onConnect.addListener(port_on_connect); +// --------------------------------------------------------------------------- +// State (lives only while the service worker is alive) +// --------------------------------------------------------------------------- +var player = {}; +var time_played = 0; +var last_refresh = Date.now(); +var num_scrobbles = 0; +var curr_song_title = ''; -/** - * Content script has connected to the extension - */ -function port_on_connect(port) { - console.assert(port.name == "cloudplayer"); +var lastfm_api = null; // initialised after settings load - // Connect another port event handlers - 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; - - if(!SETTINGS.scrobble) { - chrome.browserAction.setIcon({ - 'path': SETTINGS.scrobbling_stopped_icon }); - - player = _p; +// --------------------------------------------------------------------------- +// 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; + + log('background.js loaded (service worker)'); + + 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); + + bind_keyboard_shortcuts(); +}); + +// --------------------------------------------------------------------------- +// Player state handler (called by content scripts via sendMessage) +// --------------------------------------------------------------------------- +function handle_player_state(_p) { + var now = Date.now(); + + player = _p; + + if (!SETTINGS.scrobble) { + chrome.action.setIcon({ path: SETTINGS.scrobbling_stopped_icon }); return; } - - if(_p.has_song) { - if(_p.is_playing) { - 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) { - // TODO: Another way? - // if(scrobbled && _p.song.position > player.song.position) - - if(now_playing_sent && !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) { - // Song was scrobled, waiting for the next song - scrobbled = true; - now_playing_sent = false; - } - else { - if(response.error == 9) { - // Session expired - clear_session(); - } - - chrome.browserAction.setIcon({ - 'path': SETTINGS.error_icon }); - } - }); - } - } - 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; - } - - // Save player state - player = _p; // TODO: Save here? + + 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() {} + ); } - else { - // The player is paused - chrome.browserAction.setIcon({ - 'path': SETTINGS.paused_icon }); + + 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 { + chrome.action.setIcon({ path: SETTINGS.main_icon }); } - else { - chrome.browserAction.setIcon({ 'path': SETTINGS.main_icon }); - player = {}; - scrobbled = false; - now_playing_sent = false; + 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; } - - /** - * 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 }); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +function scrobble_song(artist, album_artist, album, title, time) { + 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 }); + } + } + ); } +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.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=" + - SETTINGS.api_key + - "&cb=" + - callback_url }); + 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["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 }); + 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) { +function get_lastfm_session(token, callback) { lastfm_api.authorize(token, function(response) { - // Save session - if(response.session) - { - localStorage["session_key"] = response.session.key; - localStorage["session_name"] = response.session.name; + 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 + "'"); + } + }); +} + +function send_cmd_to_play_tab(cmd) { + 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 deleted file mode 100644 index 991104e..0000000 --- a/js/contentscript.js +++ /dev/null @@ -1,127 +0,0 @@ -/** - * 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: 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() { - -}; - -/** - * 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 div").hasClass("goog-inline-block goog-flat-button"); -}; - -/** - * Checks whether song is playing or paused - * - * @return true if song is playing, false if song is paused - */ -GoogleMusicParser.prototype._get_is_playing = function() { - return ($("#playPause").attr("aria-pressed") == "true"); -}; - -/** - * Get current song playing position - * - * @return Playing position in seconds - */ -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)); - } - else { - return 0; - } -}; - -/** - * Get current song length - * - * @return Song length in seconds - */ -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; - } -}; - -/** - * 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 $("#playerSongTitle div").text(); -}; - -/** - * Get current song artist - * - * @return Song artist - */ -GoogleMusicParser.prototype._get_song_artist = function() { - return $("#player-artist div").text(); -}; - -/** - * Get current song artwork - * - * @return Image URL or default artwork - */ -GoogleMusicParser.prototype._get_song_cover = function() { - return ("http:" + $("#playingAlbumArt").attr("src")); -}; - -/** - * Get current song album name - * - * @return Album name or null - */ -GoogleMusicParser.prototype._get_song_album = function() { - return null; -}; - -var port = chrome.extension.connect({name: "cloudplayer"}); - -window.setInterval(function() { - port.postMessage(new Player(new GoogleMusicParser())); -}, -10000); diff --git a/js/lastfm.js b/js/lastfm.js index 987d0be..2af675e 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; @@ -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 }; @@ -72,10 +73,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); - }); + }); }; /** @@ -87,21 +88,23 @@ 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(artist, album_artist, album, track, + timestamp, 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 }; - + params.api_sig = this._req_sign(params); params.format = "json"; - - this._xhr("POST", params, + + this._xhr("POST", params, function(result) { callback(result); }); @@ -123,11 +126,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 +152,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 +175,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 +183,15 @@ 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); } }); @@ -203,70 +207,65 @@ 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); }; /** - * 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); - - // TODO: Better error handling - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - var reply; - - try { - reply = JSON.parse(xhr.responseText); - } - catch (e) { - reply = null; - } - - callback(reply); + + 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.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); - xhr.setRequestHeader("Pragma", "no-cache"); // The cache is a lie! - xhr.send(_data || null); -}; + if (method === "GET") { + uri += '?' + _params.join('&').replace(/%20/, '+'); + } else if (method === "POST") { + options.body = _params.join('&'); + } else { + return; + } + + 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); + }) + .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 8c1bf23..91ddba6 100644 --- a/js/lastfm_callback.js +++ b/js/lastfm_callback.js @@ -1,15 +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]); } -var background = chrome.extension.getBackgroundPage(); -location.href = "http://last.fm/"; +var token = _url_param('token', location.search); -background.get_lastfm_session(_url_param("token", location.search)); \ No newline at end of file +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 new file mode 100644 index 0000000..43f7d7e --- /dev/null +++ b/js/logging.js @@ -0,0 +1,5 @@ +function log(message) { + if (SETTINGS.logs_enabled) { + console.log(message); + } +} diff --git a/js/options.js b/js/options.js new file mode 100644 index 0000000..b57bc78 --- /dev/null +++ b/js/options.js @@ -0,0 +1,50 @@ +/** + * options.js — MV3 compatible + * Uses chrome.storage.local instead of localStorage + */ + +function save_options() { + var scrobble_mult = document.getElementById('scrobble_mult').checked; + var logs_enabled = document.getElementById('log_checkbox').checked; + + var updates = { logs_enabled: logs_enabled }; + if (!scrobble_mult) { + updates.max_scrobbles = 1; + } else { + // Remove the limit so background uses POSITIVE_INFINITY default + updates.max_scrobbles = null; + } + + // 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); +} + +function restore_options() { + 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 889a19d..317e6b9 100644 --- a/js/popup.js +++ b/js/popup.js @@ -1,194 +1,244 @@ /** - * 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 = chrome.extension.getBackgroundPage(); -/* Render popup when DOM is ready */ +var _state = {}; // cached background state + $(document).ready(function() { - render_scrobble_link(); - render_song(); - render_auth_link(); + // 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 (_state.session && _state.session.name && _state.session.key) { + render_scrobble_link(); + } + render_auth_link(); + }); }); -/* Render functions */ +function set_play_link() { + $('#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 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); + + 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'); + play_btn.removeClass(); + play_btn.addClass(player.is_playing ? 'pause' : 'play'); +} -/** - * Renders current song details - */ function render_song() { - if(bp.player.song) - { - $("#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" }); - - if(bp.lastfm_api.session.name && bp.lastfm_api.session.key) { - render_love_button(); + 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 { - $("#lastfm-buttons").hide(); - } - } - 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" }); - $("#lastfm-buttons").hide(); + } else { + $('#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) - { - $("#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"); - } - else { - $("#lastfm-profile").html(''); - $("#lastfm-profile a") - .attr({ - href: "#" - }) - .click(on_auth) - .text("Connect to Last.fm"); + 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/' + _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'); } } -/** - * Renders the 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 { - $("#love-button a").attr({ title: "Love this song" }) - .click(on_love) - .addClass("notloved"); - } - }); +function render_love_button(player) { + $('#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() { + 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) { + 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) { + if (!tab) return; + chrome.tabs.sendMessage(tab.id, { cmd: 'nxt' }, function(player) { + if (player) { + player.is_playing = true; + update_song_info(player); + } + }); + }); +} + +/* ---- Event handlers ---- */ -/** - * Turn on/off scrobbling link was clicked - */ 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() { - bp.lastfm_api.love_track(bp.player.song.title, bp.player.song.artist, +function on_love(player) { + $('#love-button').html(''); + chrome.runtime.sendMessage( + { type: 'love_track', track: player.song.title, artist: player.song.artist }, function(result) { - if(!result.error) { - render_love_button(); - } - else { - if(result.error == 9) { - // Session expired - bp.clear_session(); + if (!result || !result.error) { + render_love_button(player); + } 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() { - bp.lastfm_api.unlove_track(bp.player.song.title, bp.player.song.artist, +function on_unlove(player) { + $('#love-button').html(''); + chrome.runtime.sendMessage( + { type: 'unlove_track', track: player.song.title, artist: player.song.artist }, function(result) { - if(!result.error) { - render_love_button(); - } - else { - if(result.error == 9) { - // Session expired - bp.clear_session(); + if (!result || !result.error) { + render_love_button(player); + } 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(''); +function show_alert() { + $('#alert').removeClass('hidden'); + $('#extns_link').click(function() { + chrome.runtime.sendMessage({ type: 'open_extensions_page' }); + }); + $('#dismiss_alert').click(function() { + $('#alert').addClass('hidden'); + chrome.storage.local.set({ seen_alert: '1' }); + }); +} + +/* ---- 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 new file mode 100644 index 0000000..16eb96c --- /dev/null +++ b/js/settings.js @@ -0,0 +1,54 @@ +/** + * 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: 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 + max_scrobbles: Number.POSITIVE_INFINITY, + + refresh_interval: 2, + + gmusic_ads_metadata: { + title: "We'll be right back", + artist: 'Subscribe to go ad-free' + }, + + // Defaults (overwritten by loadSettings) + scrobble: true, + logs_enabled: 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 new file mode 100644 index 0000000..e83ceb8 --- /dev/null +++ b/js/util.js @@ -0,0 +1,26 @@ +/** + * 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 { + 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) { + // 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 new file mode 100644 index 0000000..d6adcd1 --- /dev/null +++ b/js/ytm_contentscript.js @@ -0,0 +1,115 @@ +/** + * ytm_contentscript.js — YouTube Music content script (MV3) + */ + +// 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.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 YtMusicParser = function() {}; + +YtMusicParser.prototype._get_has_song = function() { + return $('yt-formatted-string.title.ytmusic-player-bar').text().length > 0; +}; + +YtMusicParser.prototype._get_is_playing = function() { + var songTitle = this._get_song_title(); + return songTitle.length > 0 && window.document.title.startsWith(songTitle); +}; + +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]); + if (_time.length === 3) return parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2]); + return null; +}; + +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]); + if (_time.length === 3) return parseInt(_time[0]) * 3600 + parseInt(_time[1]) * 60 + parseInt(_time[2]); + return null; +}; + +YtMusicParser.prototype._get_song_title = function() { + return $('yt-formatted-string.title.ytmusic-player-bar').text(); +}; + +YtMusicParser.prototype._get_song_artist = function() { + return $('span.subtitle.ytmusic-player-bar>yt-formatted-string>a').first().text(); +}; + +YtMusicParser.prototype._get_album_artist = function() { + return $('span.subtitle.ytmusic-player-bar>yt-formatted-string>a').first().text(); +}; + +YtMusicParser.prototype._get_song_cover = function() { + var albumImg = $('div.thumbnail-image-wrapper.ytmusic-player-bar>img').attr('src'); + return albumImg || null; +}; + +YtMusicParser.prototype._get_song_album = function() { + return $('span.subtitle.style-scope.ytmusic-player-bar>yt-formatted-string>a').last().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 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; + } + 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; + } + 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/lastfm_callback.html b/lastfm_callback.html deleted file mode 100644 index b36954d..0000000 --- a/lastfm_callback.html +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/manifest.json b/manifest.json index 00f2ef6..161cd75 100644 --- a/manifest.json +++ b/manifest.json @@ -1,55 +1,81 @@ { - "permissions" : [ + "name" : "YouTube Music Last.fm Scrobbler", + "description" : "Scrobbles songs from YouTube Music or Google Play to Last.fm", + "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": [ + "storage" ], - "version" : "1.2.8", - "manifest_version": 2, "background": { - "page": "background.html", - "persistent": true + "service_worker": "js/background.js" }, - "name" : "Google Play Scrobbler", "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" }, - "description" : "Scrobbles songs from Google Play to Last.fm", - "browser_action" : { - "default_popup" : "popup.html", - "default_icon" : "img/main-icon.png" + "action" : { + "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*" + "*://music.youtube.com/*" ], "js" : [ - "js/inject.js" + "js/jquery-1.7.min.js", + "js/ytm_contentscript.js" ] - }, + } + ], + "options_page": "html/options.html", + "web_accessible_resources": [ { - "matches" : [ - "http://play.google.com/music/listen*", - "https://play.google.com/music/listen*" + "resources": [ + "html/lastfm_callback.html" ], - "js" : [ - "js/contentscript.js" + "matches": [ + "*://*/*" ] + } + ], + "commands": { + "toggle_play": { + "suggested_key": { + "default": "MediaPlayPause", + "linux": "Ctrl+Shift+8" + }, + "description": "Toggle play/pause", + "global": true }, - { - "run_at" : "document_start", - "matches" : [ - "http://play.google.com/music/listen*", - "https://play.google.com/music/listen*" - ], - "js" : [ - "js/jquery-1.7.min.js" - ] + "prev_song": { + "suggested_key": { + "default": "MediaPrevTrack", + "linux": "Ctrl+Shift+7" + }, + "description": "Previous song", + "global": true + }, + "next_song": { + "suggested_key": { + "default": "MediaNextTrack", + "linux": "Ctrl+Shift+9" + }, + "description": "Next song", + "global": true + }, + "goto_play_tab": { + "suggested_key": { + "default": "Ctrl+Shift+0" + }, + "description": "Go to YT Music", + "global": true } - ] + } } diff --git a/popup.html b/popup.html deleted file mode 100644 index 3818a5f..0000000 --- a/popup.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - -
-
- -
-
-
-

-

-
-
- -
-
-
-
-
-
-
-
-