├── .github └── FUNDING.yml ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── UNLICENSE ├── bin └── google-music-electron.js ├── docs ├── mpris-screenshot.png └── screenshot.png ├── lib ├── app-menu.js ├── app-tray.js ├── assets │ ├── icon-32.png │ ├── icon-paused-32.png │ ├── icon-playing-32.png │ └── index.js ├── browser.js ├── cli-parser.js ├── config-browser.js ├── config.js ├── google-music-electron.js ├── install-mpris.js ├── logger.js ├── menus │ ├── darwin.json │ ├── linux.json │ └── win32.json ├── mpris.js ├── shortcuts.js └── views │ └── config.html ├── package.json └── resources ├── headphones-filled-with-states.svg └── headphones.svg /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://twolfson.com/support-me 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "try", 9 | "catch", 10 | "finally", 11 | "with" 12 | ], 13 | "requireSpaceAfterKeywords": true, 14 | "requireSpaceBeforeBlockStatements": true, 15 | "requireSpacesInConditionalExpression": true, 16 | "requireSpacesInFunctionExpression": { 17 | "beforeOpeningRoundBrace": true, 18 | "beforeOpeningCurlyBrace": true 19 | }, 20 | "requireSpacesInFunctionDeclaration": { 21 | "beforeOpeningCurlyBrace": true 22 | }, 23 | "disallowSpacesInFunctionDeclaration": { 24 | "beforeOpeningRoundBrace": true 25 | }, 26 | "disallowSpacesInCallExpression": true, 27 | "disallowMultipleVarDecl": true, 28 | "requireBlocksOnNewline": 1, 29 | "disallowPaddingNewlinesInBlocks": true, 30 | "disallowSpacesInsideObjectBrackets": "all", 31 | "disallowSpacesInsideArrayBrackets": "all", 32 | "disallowSpacesInsideParentheses": true, 33 | "disallowQuotedKeysInObjects": "allButReserved", 34 | "disallowSpaceAfterObjectKeys": true, 35 | "requireSpaceBeforeObjectValues": true, 36 | "requireCommaBeforeLineBreak": true, 37 | "requireOperatorBeforeLineBreak": true, 38 | "disallowSpaceAfterPrefixUnaryOperators": true, 39 | "disallowSpaceBeforePostfixUnaryOperators": true, 40 | "requireSpaceBeforeBinaryOperators": true, 41 | "requireSpaceAfterBinaryOperators": true, 42 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties", 43 | "disallowKeywords": [ 44 | "with" 45 | ], 46 | "disallowMultipleLineStrings": true, 47 | "disallowMultipleLineBreaks": true, 48 | "disallowMixedSpacesAndTabs": true, 49 | "disallowTrailingWhitespace": true, 50 | "disallowTrailingComma": true, 51 | "disallowKeywordsOnNewLine": [ 52 | "else", 53 | "catch", 54 | "finally" 55 | ], 56 | "requireLineFeedAtFileEnd": true, 57 | "maximumLineLength": { 58 | "value": 120, 59 | "allowUrlComments": true 60 | }, 61 | "requireDotNotation": true, 62 | "disallowYodaConditions": true, 63 | "requireSpaceAfterLineComment": true, 64 | "disallowNewlineBeforeBlockStatements": true, 65 | "validateLineBreaks": "LF", 66 | "validateQuoteMarks": { 67 | "mark": "'", 68 | "escape": true 69 | }, 70 | "validateIndentation": 2, 71 | "validateParameterSeparator": ", ", 72 | "safeContextKeyword": [ 73 | "that" 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "eqeqeq": true, 3 | "freeze": true, 4 | "immed": true, 5 | "latedef": true, 6 | "nonbsp": true, 7 | "undef": true, 8 | "strict": false, 9 | "node": true, 10 | "browser": true, 11 | "sub": false, 12 | "globals": { 13 | "exports": true, 14 | "describe": true, 15 | "before": true, 16 | "beforeEach": true, 17 | "after": true, 18 | "afterEach": true, 19 | "it": true 20 | }, 21 | "curly": true, 22 | "indent": 2, 23 | "newcap": true, 24 | "noarg": true, 25 | "quotmark": "single", 26 | "unused": "vars", 27 | "maxparams": 4, 28 | "maxdepth": 5 29 | } 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "7" 5 | - "6" 6 | matrix: 7 | allow_failures: 8 | - node_js: "7" 9 | 10 | before_install: 11 | - curl --location http://rawgit.com/twolfson/fix-travis-ci/master/lib/install.sh | bash -s 12 | 13 | notifications: 14 | email: false 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # google-music-electron changelog 2 | 2.20.0 - Moved to fake user agent to trick Google auth, via https://github.com/MarshallOfSound/Google-Play-Music-Desktop-Player-UNOFFICIAL-/commit/a6065183e767e48a34a3f57eea852d69d13bc007 3 | 4 | 2.19.0 - Upgraded to electron@7.1.4 to fix playback issues 5 | 6 | 2.18.0 - Upgraded to electron@1.8.4 to fix GitHub vulnerability warning 7 | 8 | 2.17.1 - Replaced Gratipay with support me page 9 | 10 | 2.17.0 - Added macOS hide controls via @RickyRomero in #46 11 | 12 | 2.16.1 - Fixed Node.js supported versions for Travis CI 13 | 14 | 2.16.0 - Repaired arrow bindings timings 15 | 16 | 2.15.0 - Upgraded to `electron@1.6.2` to attempt to finally repair 2FA 17 | 18 | 2.14.0 - Fixed missing and small arrows 19 | 20 | 2.13.0 - Fixed navigation arrow size for new Google Music UI 21 | 22 | 2.12.1 - Corrected forward/back enabled/disabled after page reload 23 | 24 | 2.12.0 - Added enabling/disabling of forward/back buttons when available/not 25 | 26 | 2.11.0 - Followed back rename of `gmusic.js` to `google-music` 27 | 28 | 2.10.1 - Repaired `google-music-electron` failing to launch due to a lack of `window-info` 29 | 30 | 2.10.0 - Added window size/location preservation via @JordanRobinson in #42 31 | 32 | 2.9.0 - Updated deprecated requires and Electron methods 33 | 34 | 2.8.0 - Added Edit menu for OS X editing support via @chushao in #36 35 | 36 | 2.7.0 - Upgraded to `electron@0.36.2` to patch OS specific crashes. Fixes #29 37 | 38 | 2.6.0 - Relocated `install-mpris` command from running inside `electron` to `node` launcher (part of #25) 39 | 40 | 2.5.1 - Followed renamed `google-music` to `gmusic.js` 41 | 42 | 2.5.0 - Upgraded to `google-music@3.3.0` to receive error noise patches 43 | 44 | 2.4.0 - Increased `min-width` of arrow container to prevent shrinking arrows. Fixes #26 45 | 46 | 2.3.0 - Added truncation to tooltip to stop Windows crashes. Fixes #24 47 | 48 | 2.2.1 - Corrected license to SPDX format by @execat in #23 49 | 50 | 2.2.0 - Added support for `paper-icon-button` navigation 51 | 52 | 2.1.1 - Upgraded `electron-rebuild` to fix `node@4.0` (in Electron) issues 53 | 54 | 2.1.0 - Upgraded to `google-music-electron@3.2.0` for cross-version selectors and added `setTimeout` loop for binding initialization 55 | 56 | 2.0.1 - Added `Node.js` version to about window 57 | 58 | 2.0.0 - Moved to using single instance by default. Fixes #19 59 | 60 | 1.23.1 - Repaired respecting CLI overrides 61 | 62 | 1.23.0 - Added CLI options to preferences 63 | 64 | 1.22.0 - Added configuration bindings for shortcuts 65 | 66 | 1.21.0 - Upgraded to `electron@0.34.0` to pick up Windows hide patches. Fixes #16 67 | 68 | 1.20.0 - Added `icon` to browser window. Fixes #17 69 | 70 | 1.19.1 - Added `foundry` for release 71 | 72 | 1.19.0 - Repaired missing forward/back buttons 73 | 74 | 1.18.1 - Added newsletter subscription to README.md 75 | 76 | 1.18.0 - Upgraded to `google-music@3.1.0` to repair duplicate playback events and detect stops 77 | 78 | 1.17.2 - Repaired lint error 79 | 80 | 1.17.1 - Updated MPRIS screenshot 81 | 82 | 1.17.0 - Added playback time tracking for MPRIS 83 | 84 | 1.16.0 - Added album art, duration, exit, and raise events/actions to MPRIS 85 | 86 | 1.15.0 - Added MPRIS support via @jck in #10 87 | 88 | 1.14.1 - Added documentation on how to upgrade via @Q11x in #9 89 | 90 | 1.14.0 - Added "Forward/Back" navigation buttons. Fixed #6 91 | 92 | 1.13.0 - Added `--minimize-to-tray` via @kempniu in #8 93 | 94 | 1.12.0 - Added `--hide-via-tray` CLI option 95 | 96 | 1.11.0 - Upgraded to `electron@0.26.1` and added tray click for minimization 97 | 98 | 1.10.1 - Added documentation for development 99 | 100 | 1.10.0 - Repaired separator menu bug for OSX via @arboleya in #5. Fixes #4 101 | 102 | 1.9.0 - Added support for Chromium flags 103 | 104 | 1.8.0 - Added debug repl option 105 | 106 | 1.7.0 - Refactored again to keep all application state/methods under one roof 107 | 108 | 1.6.0 - Repaired bug with restoring minimized window from tray 109 | 110 | 1.5.1 - Updated CLI documentation 111 | 112 | 1.5.0 - Added `winston` as our logger 113 | 114 | 1.4.0 - Repaired electron PATH issues 115 | 116 | 1.3.0 - Added `--version` and `--skip-taskbar` support 117 | 118 | 1.2.0 - Added menu item for show/hide application window 119 | 120 | 1.1.0 - Abstracted menu/tray/shortcut hooks into separate modules 121 | 122 | 1.0.1 - Added missing bin script 123 | 124 | 1.0.0 - Initial release 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # google-music-electron [![Build status](https://travis-ci.org/twolfson/google-music-electron.png?branch=master)](https://travis-ci.org/twolfson/google-music-electron) 2 | 3 | Desktop app for [Google Music][] on top of [Electron][] 4 | 5 | **Features:** 6 | 7 | - Google Music as a standalone application 8 | - Tray for quick play/pause/quit and tooltip with information 9 | - Media key shortcuts 10 | - MPRIS integration (for GNU/Linux desktop environments) 11 | 12 | ![Screenshot](docs/screenshot.png) 13 | 14 | This was written as a successsor to [google-music-webkit][]. When upgrading between versions of [nw.js][], there were regressions with taskbar and shortcut bindings. We wrote this as an alternative. 15 | 16 | [Google Music]: https://play.google.com/music/listen 17 | [Electron]: http://electron.atom.io/ 18 | [google-music-webkit]: https://github.com/twolfson/google-music-webkit 19 | [nw.js]: https://github.com/nwjs/nw.js 20 | 21 | ## Requirements 22 | - [npm][], usually installed with [node][] 23 | 24 | [npm]: http://npmjs.org/ 25 | [node]: http://nodejs.org/ 26 | 27 | ## Getting Started 28 | `google-music-electron` can be installed globally via `npm`: 29 | 30 | ```js 31 | # Install google-music-electron via npm 32 | npm install -g google-music-electron 33 | 34 | # Run google-music-electron 35 | google-music-electron 36 | ``` 37 | 38 | When the application has launched, it will appear in your taskbar and via a tray icon, ![tray icon](lib/assets/icon.png). 39 | 40 | ![Screenshot](docs/screenshot.png) 41 | 42 | ## Newsletter 43 | Interested in hearing about updates and new releases of `google-music-electron`? 44 | 45 | [Subscribe to our newsletter!](https://groups.google.com/forum/#!forum/google-music-electron) 46 | 47 | ## MPRIS integration 48 | If you are on GNU/Linux and your desktop environment supports [MPRIS][], you can install our [MPRIS][] integration via: 49 | 50 | ```bash 51 | google-music-electron install-mpris 52 | # Once this succeeds, MRPIS will be integrated on `google-music-electron` restart 53 | ``` 54 | 55 | ![MPRIS screenshot](docs/mpris-screenshot.png) 56 | 57 | [MPRIS]: http://specifications.freedesktop.org/mpris-spec/latest/ 58 | 59 | ## Updating 60 | `google-music-electron` can be updated via `npm`: 61 | 62 | ```js 63 | # Update google-music-electron to a newer version via npm 64 | npm update -g google-music-electron 65 | # Alternatively, the following can be used as well to specify a version 66 | # npm install -g google-music-electron@latest 67 | ``` 68 | 69 | ## Documentation 70 | ### CLI 71 | We have a few CLI options available for you: 72 | 73 | ``` 74 | Usage: google-music-electron [options] [command] 75 | 76 | 77 | Commands: 78 | 79 | install-mpris Install integration with MPRIS (Linux only) 80 | 81 | Options: 82 | 83 | -h, --help output usage information 84 | -V, --version output the version number 85 | -S, --skip-taskbar Skip showing the application in the taskbar 86 | --minimize-to-tray Hide window to tray instead of minimizing 87 | --hide-via-tray Hide window to tray instead of minimizing (only for tray icon) 88 | --allow-multiple-instances Allow multiple instances of `google-music-electron` to run 89 | --verbose Display verbose log output in stdout 90 | --debug-repl Starts a `replify` server as `google-music-electron` for debugging 91 | ``` 92 | 93 | ## Development 94 | ### Running locally 95 | To get a local development copy running, you will need: 96 | 97 | - [npm][], usually installed with [node][]. Same `npm` that is used during installation 98 | - [git][], version control tool 99 | 100 | [git]: http://git-scm.com/ 101 | 102 | Follow the steps below to get a development copy set up: 103 | 104 | ```bash 105 | # Clone our repository 106 | git clone https://github.com/twolfson/google-music-electron.git 107 | cd google-music-electron/ 108 | 109 | # Install our dependencies and dev dependencies 110 | npm install 111 | 112 | # Start up `google-music-electron` 113 | npm start 114 | ``` 115 | 116 | After running the above steps, a copy of `google-music-electron` should begin running. 117 | 118 | ![Screenshot](docs/screenshot.png) 119 | 120 | #### Adding local setup as a global installation 121 | After getting our local development set up, we can go one step further and get `google-music-electron` working on our CLI as if it were installed via `npm install -g`. 122 | 123 | ```bash 124 | # Link local copy as a global copy 125 | # WARNING: Make sure that `npm install` has been run before this point 126 | # or your local copy's permissions may get messed up 127 | npm link 128 | 129 | # Run `google-music-electron` for local copy 130 | google-music-electron 131 | ``` 132 | 133 | More information on `npm link` can be found in `npm's` documentation: 134 | 135 | https://docs.npmjs.com/cli/link 136 | 137 | ### Icons 138 | Source images are kept in the `resources/` folder. Icons are maintained via Inkscape and the `play/pause` buttons are isolated in layers. 139 | 140 | To generate icons: 141 | 142 | 1. Export each of the play/pause/clean variants as a `.svg` file 143 | 2. Load the icons via GIMP as a 32x32 SVG 144 | 3. Export via GIMP as a `.png` 145 | 146 | At the time of writing, Inkscape and Image Magick seemed to be generating non-transparent backgrounds upon converting SVG to PNG. 147 | 148 | ## Contributing 149 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint via `npm run lint` and test via `npm test`. 150 | 151 | ## Donating 152 | Support this project and [others by twolfson][twolfson-projects] via [donations][twolfson-support-me]. 153 | 154 | 155 | 156 | [twolfson-projects]: http://twolfson.com/projects 157 | [twolfson-support-me]: http://twolfson.com/support-me 158 | 159 | ## Attribution 160 | Headphones designed by Jake Dunham from [the Noun Project][headphones-icon] 161 | 162 | [headphones-icon]: http://thenounproject.com/term/headphones/16097/ 163 | 164 | ## Unlicense 165 | As of May 16 2015, Todd Wolfson has released this repository and its contents to the public domain. 166 | 167 | It has been released under the [UNLICENSE][]. 168 | 169 | [UNLICENSE]: UNLICENSE 170 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /bin/google-music-electron.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Load in our dependencies 3 | var path = require('path'); 4 | var spawn = require('child_process').spawn; 5 | var electronPath = require('electron'); 6 | var parseCli = require('../lib/cli-parser').parse; 7 | 8 | // Process our arguments (catches any `--help` and `install-mpris` commands) 9 | var program = parseCli(process.argv); 10 | 11 | // If didn't match a command (e.g. `install-mpris`), then launch our application 12 | if (program.args.length === 0) { 13 | // Find our application 14 | var googleMusicElectronPath = path.join(__dirname, '..'); 15 | var args = [googleMusicElectronPath]; 16 | 17 | // Append all arguments after our node invocation 18 | // e.g. `node bin/google-music-electron.js --version` -> `--version` 19 | args = args.concat(process.argv.slice(2)); 20 | 21 | // Run electron on our application and forward all stdio 22 | spawn(electronPath, args, {stdio: [0, 1, 2]}); 23 | } 24 | -------------------------------------------------------------------------------- /docs/mpris-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twolfson/google-music-electron/eec99851ef5119a947a50a5a0b99e51705e765c6/docs/mpris-screenshot.png -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twolfson/google-music-electron/eec99851ef5119a947a50a5a0b99e51705e765c6/docs/screenshot.png -------------------------------------------------------------------------------- /lib/app-menu.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var Menu = require('electron').Menu; 3 | 4 | // Load in JSON for our menus (e.g. `./menus/linux.json`) 5 | // https://github.com/atom/electron-starter/blob/96f6117b4c1f33c0881d504d655467fc049db433/src/browser/appmenu.coffee#L15 6 | var menuTemplate = require('./menus/' + process.platform + '.json'); 7 | 8 | // Define a function to set up our application menu 9 | exports.init = function (gme) { 10 | // Parse and set up our menu 11 | // https://github.com/atom/electron-starter/blob/96f6117b4c1f33c0881d504d655467fc049db433/src/browser/appmenu.coffee#L27-L41 12 | function bindMenuItems(menuItems) { 13 | menuItems.forEach(function bindMenuItemFn (menuItem) { 14 | // If there is a role, continue 15 | if (menuItem.role !== undefined) { 16 | return; 17 | } 18 | 19 | // If there is a separator, continue 20 | if (menuItem.type === 'separator') { 21 | return; 22 | } 23 | 24 | // If there is a submenu, recurse it 25 | if (menuItem.submenu) { 26 | bindMenuItems(menuItem.submenu); 27 | return; 28 | } 29 | 30 | // Otherwise, find the function for our command 31 | var cmd = menuItem.command; 32 | if (cmd === 'application:about') { 33 | menuItem.click = gme.openAboutWindow; 34 | } else if (cmd === 'application:show-settings') { 35 | menuItem.click = gme.openConfigWindow; 36 | } else if (cmd === 'application:quit') { 37 | menuItem.click = gme.quitApplication; 38 | } else if (cmd === 'window:reload') { 39 | menuItem.click = gme.reloadWindow; 40 | } else if (cmd === 'window:toggle-dev-tools') { 41 | menuItem.click = gme.toggleDevTools; 42 | } else if (cmd === 'window:toggle-full-screen') { 43 | menuItem.click = gme.toggleFullScreen; 44 | } else { 45 | throw new Error('Could not find function for menu command "' + cmd + '" ' + 46 | 'under label "' + menuItem.label + '"'); 47 | } 48 | }); 49 | } 50 | bindMenuItems(menuTemplate.menu); 51 | Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate.menu)); 52 | }; 53 | -------------------------------------------------------------------------------- /lib/app-tray.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var Tray = require('electron').Tray; 3 | var ipcMain = require('electron').ipcMain; 4 | var Menu = require('electron').Menu; 5 | var MenuItem = require('electron').MenuItem; 6 | var GoogleMusic = require('google-music'); 7 | var assets = require('./assets'); 8 | 9 | // Define a truncation utility for tooltip 10 | function truncateStr(str, len) { 11 | // If the string is over the length, then truncate it 12 | // DEV: We go 1 under length so we have room for ellipses 13 | if (str.length > len) { 14 | return str.slice(0, len - 2) + '…'; 15 | } 16 | 17 | // Otherwise, return the string 18 | return str; 19 | } 20 | 21 | // Define a function to set up our tray icon 22 | exports.init = function (gme) { 23 | // Set up our tray 24 | var trayMenu = new Menu(); 25 | trayMenu.append(new MenuItem({ 26 | label: 'Show/hide window', 27 | click: gme.onTrayClick 28 | })); 29 | trayMenu.append(new MenuItem({ 30 | type: 'separator' 31 | })); 32 | trayMenu.append(new MenuItem({ 33 | label: 'Play/Pause', 34 | click: gme.controlPlayPause 35 | })); 36 | trayMenu.append(new MenuItem({ 37 | label: 'Next', 38 | click: gme.controlNext 39 | })); 40 | trayMenu.append(new MenuItem({ 41 | label: 'Previous', 42 | click: gme.controlPrevious 43 | })); 44 | trayMenu.append(new MenuItem({ 45 | type: 'separator' 46 | })); 47 | trayMenu.append(new MenuItem({ 48 | label: 'Quit', 49 | click: gme.quitApplication 50 | })); 51 | var tray = new Tray(assets['icon-32']); 52 | tray.setContextMenu(trayMenu); 53 | 54 | // When our tray is clicked, toggle visibility of the window 55 | tray.on('click', gme.onTrayClick); 56 | 57 | // When the song changes, update our tooltip 58 | ipcMain.on('change:song', function handleSongChange (evt, songInfo) { 59 | gme.logger.debug('Song has changed. Updating tray tooltip', { 60 | songInfo: songInfo 61 | }); 62 | // We have a max length of 127 characters on Windows 63 | // so divvy up 47, 31, 47 (with 2 characters for line breaks) 64 | // https://github.com/twolfson/google-music-electron/issues/24 65 | var infoStr = [ 66 | truncateStr('Title: ' + songInfo.title, 47), 67 | truncateStr('Artist: ' + songInfo.artist, 31), 68 | truncateStr('Album: ' + songInfo.album, 47) 69 | ].join('\n'); 70 | tray.setToolTip(infoStr); 71 | }); 72 | 73 | // When the playback state changes, update the icon 74 | ipcMain.on('change:playback', function handlePlaybackChange (evt, playbackState) { 75 | // Determine which icon to display based on state 76 | // By default, render the clean icon (stopped state) 77 | gme.logger.debug('Playback state has changed. Updating tray icon', { 78 | playbackState: playbackState 79 | }); 80 | var icon = assets['icon-32']; 81 | if (playbackState === GoogleMusic.Playback.PLAYING) { 82 | icon = assets['icon-playing-32']; 83 | } else if (playbackState === GoogleMusic.Playback.PAUSED) { 84 | icon = assets['icon-paused-32']; 85 | } 86 | 87 | // Update the icon 88 | tray.setImage(icon); 89 | }); 90 | }; 91 | -------------------------------------------------------------------------------- /lib/assets/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twolfson/google-music-electron/eec99851ef5119a947a50a5a0b99e51705e765c6/lib/assets/icon-32.png -------------------------------------------------------------------------------- /lib/assets/icon-paused-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twolfson/google-music-electron/eec99851ef5119a947a50a5a0b99e51705e765c6/lib/assets/icon-paused-32.png -------------------------------------------------------------------------------- /lib/assets/icon-playing-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twolfson/google-music-electron/eec99851ef5119a947a50a5a0b99e51705e765c6/lib/assets/icon-playing-32.png -------------------------------------------------------------------------------- /lib/assets/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'icon-32': __dirname + '/icon-32.png', 3 | 'icon-paused-32': __dirname + '/icon-paused-32.png', 4 | 'icon-playing-32': __dirname + '/icon-playing-32.png' 5 | }; 6 | -------------------------------------------------------------------------------- /lib/browser.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var ipcRenderer = require('electron').ipcRenderer; 3 | var webContents = require('electron').remote.getCurrentWebContents(); 4 | var GoogleMusic = require('google-music'); 5 | 6 | // Overload `window.addEventListener` to prevent `unload` bindings 7 | var _addEventListener = window.addEventListener; 8 | window.addEventListener = function (eventName, fn, bubbles) { 9 | // If we received an unload binding, ignore it 10 | if (eventName === 'unload' || eventName === 'beforeunload') { 11 | return; 12 | } 13 | 14 | // Otherwise, run our normal addEventListener 15 | return _addEventListener.apply(window, arguments); 16 | }; 17 | 18 | // When the page loads 19 | // DEV: We originally used `DOMContentLoaded` but Google decided to stop eagerly rendering navigation 20 | window.addEventListener('load', function handleNavigationLoad () { 21 | // Find our attachment point for nav buttons 22 | var leftNavContainer = document.querySelector('#topBar #material-one-left'); 23 | var navOpenEl = leftNavContainer ? leftNavContainer.querySelector('#left-nav-open-button') : null; 24 | 25 | // If there is one 26 | if (navOpenEl) { 27 | // Generate our buttons 28 | // https://github.com/google/material-design-icons 29 | // Match aria info for existing "back" button (role/tabindex given by Chrome/Polymer) 30 | // DEV: We use `nodeName` to to guarantee `sj-icon-button` or `paper-icon-button` on their respective pages 31 | var nodeName = navOpenEl.nodeName; 32 | var backEl = document.createElement(nodeName); 33 | backEl.setAttribute('aria-label', 'Back'); 34 | backEl.setAttribute('icon', 'arrow-back'); 35 | backEl.setAttribute('id', 'gme-back-button'); 36 | var forwardEl = document.createElement(nodeName); 37 | forwardEl.setAttribute('aria-label', 'Forward'); 38 | forwardEl.setAttribute('icon', 'arrow-forward'); 39 | forwardEl.setAttribute('id', 'gme-forward-button'); 40 | 41 | // Apply one-off styles to repair positioning and padding 42 | // DEV: Taken from CSS styles on hidden "back" button 43 | var cssFixes = [ 44 | 'align-self: center;', 45 | 'min-width: 24px;' 46 | ].join(''); 47 | backEl.style.cssText = cssFixes; 48 | forwardEl.style.cssText = cssFixes; 49 | 50 | // Determine the current size of the menu button 51 | // 40px -> 40 52 | var navOpenElWidthStr = window.getComputedStyle(navOpenEl).width; 53 | var navOpenElWidthPx = parseInt(navOpenElWidthStr.replace(/px$/, ''), 10); 54 | 55 | // Increase the `min-width` for our leftNavContainer 56 | // 226px -> 226 -> 306px 57 | var leftNavContainerMinWidthStr = window.getComputedStyle(leftNavContainer).minWidth; 58 | var leftNavContainerMinWidthPx = parseInt(leftNavContainerMinWidthStr.replace(/px$/, ''), 10); 59 | leftNavContainer.style.minWidth = (leftNavContainerMinWidthPx + (navOpenElWidthPx * 2)) + 'px'; 60 | 61 | // Strip away `min-width` from breadcrumbs as it leads to issues 62 | // See: "New releases" 63 | // calc(100% - 80px) -> N/A 64 | var breadcrumbsEl = leftNavContainer.querySelector('#material-breadcrumbs'); 65 | if (breadcrumbsEl) { 66 | breadcrumbsEl.style.minWidth = '0px'; 67 | } 68 | 69 | // Attach event listeners 70 | backEl.addEventListener('click', function onBackClick () { 71 | window.history.back(); 72 | }); 73 | forwardEl.addEventListener('click', function onBackClick () { 74 | window.history.forward(); 75 | }); 76 | 77 | // When our page changes, update enabled/disabled navigation 78 | var updateNavigation = function () { 79 | if (webContents.canGoBack()) { 80 | // DEV: Google Music automatically sets `aria-disabled` as well 81 | backEl.removeAttribute('disabled'); 82 | } else { 83 | backEl.setAttribute('disabled', true); 84 | } 85 | if (webContents.canGoForward()) { 86 | forwardEl.removeAttribute('disabled'); 87 | } else { 88 | forwardEl.setAttribute('disabled', true); 89 | } 90 | }; 91 | window.addEventListener('hashchange', updateNavigation); 92 | 93 | // Update navigation immediately 94 | // DEV: On initial page load/reload, we won't have disabled our arrows otherwise 95 | // DEV: We might still show "back" as navigable on reload, it is but the page will refresh 96 | // This is caused by the page not being in push state (same behavior in Chrome) 97 | updateNavigation(); 98 | 99 | // Expose our buttons adjacent to the hidden back element 100 | navOpenEl.parentNode.insertBefore(forwardEl, navOpenEl.nextSibling); 101 | navOpenEl.parentNode.insertBefore(backEl, forwardEl); 102 | console.info('Added navigation buttons'); 103 | 104 | // Watch our music logo for hide/show events 105 | // DEV: Logo is hidden on non-root page but its container isn't (yet) 106 | // DEV: This occupies dead whitespace that shrinks our arrows otherwise 107 | // https://github.com/twolfson/google-music-electron/issues/43 108 | var musicLogoContainer = leftNavContainer.querySelector('.music-logo-link'); 109 | var musicLogoEl = musicLogoContainer.querySelector('.music-logo'); 110 | var updateLogoDisplay = function () { 111 | var displayVal = window.getComputedStyle(musicLogoEl).display; 112 | musicLogoContainer.style.display = displayVal === 'none' ? 'none' : 'initial'; 113 | }; 114 | var musicLogoObserver = new MutationObserver(function handleMutations (mutations) { 115 | mutations.forEach(function handleMutation (mutation) { 116 | var targetEl = mutation.target; 117 | if (targetEl === musicLogoEl) { 118 | updateLogoDisplay(); 119 | } 120 | }); 121 | }); 122 | musicLogoObserver.observe(musicLogoEl, { 123 | attributes: true 124 | }); 125 | 126 | // Update music logo immediately as it starts as `display: none` on page refresh 127 | updateLogoDisplay(); 128 | 129 | // Notify user of our changes 130 | console.info('Added monitor for music logo visibility'); 131 | } else { 132 | console.error('Failed to find navigation button'); 133 | } 134 | }); 135 | 136 | // When we finish loading 137 | // DEV: We must wait until the UI fully loads otherwise mutation observers won't bind 138 | // DEV: Even with the `onload` event, we still could not have JS fully loaded so use a setTimeout loop 139 | var loadAttempts = 0; 140 | function handleLoad() { 141 | // Try to bind GoogleMusic to the UI 142 | var googleMusic; 143 | try { 144 | googleMusic = new GoogleMusic(window); 145 | console.info('Successfully initialized `GoogleMusic`'); 146 | // If there was an error 147 | } catch (err) { 148 | // If this is our 60th attempt (i.e. 1 minute of failures), then throw the error 149 | if (loadAttempts > 60) { 150 | throw err; 151 | // Otherwise, try again in 1 second 152 | } else { 153 | console.info('Failed to initialize `GoogleMusic`. Trying again in 1 second'); 154 | loadAttempts += 1; 155 | return setTimeout(handleLoad, 1000); 156 | } 157 | } 158 | 159 | // Forward events over `ipc` 160 | var events = ['change:song', 'change:playback', 'change:playback-time']; 161 | events.forEach(function bindForwardEvent (event) { 162 | googleMusic.on(event, function forwardEvent (data) { 163 | // Send same event with data (e.g. `change:song` `GoogleMusic.Playback.PLAYING`) 164 | ipcRenderer.send(event, data); 165 | }); 166 | }); 167 | 168 | // When we receive requests to control playback, run them 169 | ipcRenderer.on('control:play-pause', function handlePlayPause (evt) { 170 | googleMusic.playback.playPause(); 171 | }); 172 | ipcRenderer.on('control:next', function handleNext (evt) { 173 | googleMusic.playback.forward(); 174 | }); 175 | ipcRenderer.on('control:previous', function handlePrevious (evt) { 176 | googleMusic.playback.rewind(); 177 | }); 178 | } 179 | window.addEventListener('load', handleLoad); 180 | -------------------------------------------------------------------------------- /lib/cli-parser.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var _ = require('underscore'); 3 | var program = require('commander'); 4 | var installMpris = require('./install-mpris'); 5 | 6 | // Load in package info 7 | var pkg = require('../package.json'); 8 | 9 | // Define our CLI parser 10 | exports.parse = function (argv) { 11 | // Handle CLI arguments 12 | program 13 | .version(pkg.version) 14 | .option('-S, --skip-taskbar', 'Skip showing the application in the taskbar') 15 | .option('--minimize-to-tray', 'Hide window to tray instead of minimizing') 16 | .option('--hide-via-tray', 'Hide window to tray instead of minimizing (only for tray icon)') 17 | .option('--allow-multiple-instances', 'Allow multiple instances of `google-music-electron` to run') 18 | .option('--verbose', 'Display verbose log output in stdout') 19 | .option('--debug-repl', 'Starts a `replify` server as `google-music-electron` for debugging') 20 | // Allow unknown Chromium flags 21 | // https://github.com/atom/electron/blob/v0.26.0/docs/api/chrome-command-line-switches.md 22 | .allowUnknownOption(); 23 | 24 | // Specify keys that can be used by config if CLI isn't provided 25 | var cliConfigKeys = ['skip-taskbar', 'minimize-to-tray', 'hide-via-tray', 'allow-multiple-instances']; 26 | var cliInfo = _.object(cliConfigKeys.map(function generateCliInfo (key) { 27 | return [key, _.findWhere(program.options, {long: '--' + key})]; 28 | })); 29 | 30 | // Define our commands 31 | program 32 | .command('install-mpris') 33 | .description('Install integration with MPRIS (Linux only)') 34 | .action(function handleInstallMrpis () { 35 | // If we are in Electron, then raise an error 36 | // https://github.com/twolfson/google-music-electron/issues/25#issuecomment-167368775 37 | if (process.versions.electron) { 38 | throw new Error('`install-mpris` command should be handled by `bin/google-music-electron.js`'); 39 | } 40 | 41 | // Otherwise, run our installer 42 | installMpris(); 43 | }); 44 | 45 | // Process our arguments 46 | program.parse(argv); 47 | 48 | // Amend cliConfigKeys and cliInfo as attributes 49 | program._cliConfigKeys = cliConfigKeys; 50 | program._cliInfo = cliInfo; 51 | 52 | // Return our parsed info 53 | return program; 54 | }; 55 | -------------------------------------------------------------------------------- /lib/config-browser.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var ipcRenderer = require('electron').ipcRenderer; 3 | 4 | // When the DOM loads 5 | window.addEventListener('DOMContentLoaded', function handleDOMLoad () { 6 | // Request our config 7 | var config = JSON.parse(ipcRenderer.sendSync('get-config-sync')); 8 | var configInfo = JSON.parse(ipcRenderer.sendSync('get-config-info-sync')); 9 | var configOverrides = JSON.parse(ipcRenderer.sendSync('get-config-overrides-sync')); 10 | 11 | // Find and bind all known shortcuts 12 | var $shortcutContainers = document.querySelectorAll('[data-save-shortcut]'); 13 | [].slice.call($shortcutContainers).forEach(function bindShortcut ($shortcutContainer) { 14 | // Fill in our existing value 15 | var shortcutName = $shortcutContainer.dataset.saveShortcut; 16 | var $input = $shortcutContainer.querySelector('input[type=text]'); 17 | var $output = $shortcutContainer.querySelector('.output'); 18 | $input.value = config[shortcutName]; 19 | 20 | // Add change binding for our shortcut 21 | $input.addEventListener('change', function handleShortcutChange (evt) { 22 | // Register our new handler 23 | var result = JSON.parse(ipcRenderer.sendSync('set-shortcut-sync', shortcutName, $input.value)); 24 | 25 | // Reset output state 26 | $output.classList.remove('success'); 27 | $output.classList.remove('error'); 28 | 29 | // Provide feedback to user 30 | if (result.success === false) { 31 | $output.classList.add('error'); 32 | $output.textContent = 'Failed to bind shortcut "' + result.accelerator + '". ' + 33 | 'Keeping current shortcut "' + result.previousAccelerator + '".'; 34 | } else if (result.previousAccelerator === result.accelerator) { 35 | $output.textContent = ''; 36 | } else { 37 | $output.classList.add('success'); 38 | $output.textContent = 'Successfully moved from "' + result.previousAccelerator + '" ' + 39 | 'to "' + result.accelerator + '"!'; 40 | } 41 | }); 42 | }); 43 | 44 | // Find and bind all known checkboxes 45 | var $checkboxContainers = document.querySelectorAll('[data-save-checkbox]'); 46 | [].slice.call($checkboxContainers).forEach(function bindCheckbox ($checkboxContainer) { 47 | // Fill in our existing value 48 | var configItemName = $checkboxContainer.dataset.saveCheckbox; 49 | var $input = $checkboxContainer.querySelector('input[type=checkbox]'); 50 | $input.checked = config[configItemName]; 51 | 52 | // If our config item is overridden, then disable it 53 | if (configOverrides[configItemName] !== undefined) { 54 | $checkboxContainer.classList.add('muted'); 55 | $input.disabled = true; 56 | var $overriddenSpan = $checkboxContainer.querySelector('.overridden'); 57 | $overriddenSpan.classList.remove('hidden'); 58 | } 59 | 60 | // If we have config information, fill out that content as well 61 | if (configInfo[configItemName]) { 62 | var $cliFlags = $checkboxContainer.querySelector('.cli-flags'); 63 | // e.g. Overridden by `-S, --skip-taskbar` in CLI 64 | $cliFlags.textContent = configInfo[configItemName].flags; 65 | var $description = $checkboxContainer.querySelector('.description'); 66 | // e.g. Skip showing the application in the taskbar 67 | $description.textContent += configInfo[configItemName].description; 68 | } 69 | 70 | // If the container is mutually exclusive 71 | var $unsetTarget; 72 | if ($checkboxContainer.dataset.unsetCheckbox) { 73 | $unsetTarget = document.querySelector($checkboxContainer.dataset.unsetCheckbox); 74 | } 75 | 76 | // Add change binding for our setting 77 | $input.addEventListener('change', function handleCheckboxChange (evt) { 78 | // Update our setting 79 | var result = JSON.parse(ipcRenderer.sendSync('set-config-item-sync', configItemName, $input.checked)); 80 | 81 | // If there was an error, complain about it 82 | if (result.success === false) { 83 | window.alert('Attempted to set "' + configItemName + '" to "' + $input.checked + '" but failed. ' + 84 | 'Please see console output for more info.'); 85 | } 86 | 87 | // If there is a target to unset and we are truthy, unset them and trigger a change 88 | if ($unsetTarget && $input.checked) { 89 | // http://youmightnotneedjquery.com/#trigger_native 90 | $unsetTarget.checked = false; 91 | var triggerEvt = document.createEvent('HTMLEvents'); 92 | triggerEvt.initEvent('change', true, false); 93 | $unsetTarget.dispatchEvent(triggerEvt); 94 | } 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var ipcMain = require('electron').ipcMain; 3 | var _ = require('underscore'); 4 | var Configstore = require('configstore'); 5 | var pkg = require('../package.json'); 6 | 7 | // Define config constructor 8 | function GmeConfig(cliOverrides, cliInfo) { 9 | // Create our config 10 | this.config = new Configstore(pkg.name, { 11 | 'playpause-shortcut': 'mediaplaypause', 12 | 'next-shortcut': 'medianexttrack', 13 | 'previous-shortcut': 'mediaprevioustrack' 14 | }); 15 | this.cliOverrides = cliOverrides; 16 | 17 | // Generate IPC bindings for config and its info 18 | var that = this; 19 | ipcMain.on('get-config-sync', function handleGetConfigSync (evt) { 20 | evt.returnValue = JSON.stringify(that.getAll()); 21 | }); 22 | ipcMain.on('get-config-info-sync', function handleGetConfigInfoSync (evt) { 23 | evt.returnValue = JSON.stringify(cliInfo); 24 | }); 25 | ipcMain.on('get-config-overrides-sync', function handleGetConfigInfoSync (evt) { 26 | evt.returnValue = JSON.stringify(cliOverrides); 27 | }); 28 | ipcMain.on('set-config-item-sync', function handleSetConfigItemSync (evt, key, val) { 29 | that.set(key, val); 30 | evt.returnValue = JSON.stringify({success: true}); 31 | }); 32 | } 33 | // DEV: We need to define our own `getAll` since we can't subclass `Configstore#all` 34 | // Also, since the `setAll` behavior is confusing because we don't want cliOverrides to contaminate anything 35 | // so we don't ever allow setting it =_= 36 | // https://github.com/yeoman/configstore/blob/v1.2.1/index.js 37 | GmeConfig.prototype = { 38 | getAll: function () { 39 | return _.defaults({}, this.cliOverrides, this.config.all); 40 | }, 41 | get: function (key) { 42 | var all = this.getAll(); 43 | return all[key]; 44 | }, 45 | set: function (key, val) { 46 | return this.config.set(key, val); 47 | }, 48 | del: function (key) { 49 | return this.config.del(key); 50 | }, 51 | clear: function () { 52 | return this.config.clear(); 53 | } 54 | }; 55 | 56 | // Export our constructor 57 | module.exports = GmeConfig; 58 | -------------------------------------------------------------------------------- /lib/google-music-electron.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var app = require('electron').app; 3 | var BrowserWindow = require('electron').BrowserWindow; 4 | var monogamous = require('monogamous'); 5 | var _ = require('underscore'); 6 | var replify = require('replify'); 7 | var assets = require('./assets'); 8 | var appMenu = require('./app-menu'); 9 | var appTray = require('./app-tray'); 10 | var Config = require('./config'); 11 | var getLogger = require('./logger'); 12 | var shortcuts = require('./shortcuts'); 13 | var mpris; 14 | try { 15 | mpris = require('./mpris'); 16 | } catch (err) { 17 | // Optionally allow `mpris` to be installed 18 | } 19 | 20 | // Set our User Agent to trick Google auth 21 | app.userAgentFallback = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0'; 22 | 23 | // Load in package info and process our CLI 24 | var pkg = require('../package.json'); 25 | var program = require('./cli-parser').parse(process.argv); 26 | 27 | // Generate a logger 28 | var logger = getLogger({verbose: program.verbose}); 29 | 30 | // Log our CLI arguments 31 | logger.debug('CLI arguments received', {argv: process.argv}); 32 | 33 | // When all Windows are closed 34 | app.on('window-all-closed', function handleWindowsClosed () { 35 | // If we are not on OSX, exit 36 | // DEV: OSX requires users to quit via the menu/cmd+q 37 | if (process.platform !== 'darwin') { 38 | logger.debug('All windows closed. Exiting application'); 39 | app.quit(); 40 | } else { 41 | logger.debug('All windows closed but not exiting because OSX'); 42 | } 43 | }); 44 | 45 | // Generate a config based on our CLI arguments 46 | // DEV: We need to build cliConfig off of options since we are using `camelCase` from `commander` 47 | // https://github.com/tj/commander.js/blob/v2.9.0/index.js#L1046-L1050 48 | function camelcase(flag) { 49 | return flag.split('-').reduce(function (str, word) { 50 | return str + word[0].toUpperCase() + word.slice(1); 51 | }); 52 | } 53 | var cliConfig = _.object(program._cliConfigKeys.map(function getCliValue (dashCaseKey) { 54 | var camelCaseKey = camelcase(dashCaseKey); 55 | return [dashCaseKey, program[camelCaseKey]]; 56 | })); 57 | logger.debug('CLI options overriding config', cliConfig); 58 | var config = new Config(cliConfig, program._cliInfo); 59 | logger.debug('Generated starting config options', config.getAll()); 60 | 61 | // Define helpers for controlling/sending messages to our window 62 | // https://github.com/atom/electron-starter/blob/96f6117b4c1f33c0881d504d655467fc049db433/src/browser/application.coffee#L87-L104 63 | // DEV: We are choosing to dodge classes to avoid `.bind` calls 64 | // DEV: This must be in the top level scope, otherwise our window gets GC'd 65 | var gme = { 66 | browserWindow: null, 67 | config: config, 68 | controlPlayPause: function () { 69 | if (gme.browserWindow && gme.browserWindow.webContents) { 70 | logger.debug('Sending `control:play-pause` to browser window'); 71 | gme.browserWindow.webContents.send('control:play-pause'); 72 | } else { 73 | logger.debug('`control:play-pause` requested but couldn\'t find browser window'); 74 | } 75 | }, 76 | controlNext: function () { 77 | if (gme.browserWindow && gme.browserWindow.webContents) { 78 | logger.debug('Sending `control:next` to browser window'); 79 | gme.browserWindow.webContents.send('control:next'); 80 | } else { 81 | logger.debug('`control:next` requested but couldn\'t find browser window'); 82 | } 83 | }, 84 | controlPrevious: function () { 85 | if (gme.browserWindow && gme.browserWindow.webContents) { 86 | logger.debug('Sending `control:previous` to browser window'); 87 | gme.browserWindow.webContents.send('control:previous'); 88 | } else { 89 | logger.debug('`control:previous` requested but couldn\'t find browser window'); 90 | } 91 | }, 92 | logger: logger, 93 | openAboutWindow: function () { 94 | logger.debug('Showing `about` window for `google-music-electron`'); 95 | var info = [ 96 | // https://github.com/corysimmons/typographic/blob/2.9.3/scss/typographic.scss#L34 97 | '
', 98 | '

google-music-electron

', 99 | '

', 100 | 'Version: ' + pkg.version, 101 | '
', 102 | 'Electron version: ' + process.versions.electron, 103 | '
', 104 | 'Node.js version: ' + process.versions.node, 105 | '
', 106 | 'Chromium version: ' + process.versions.chrome, 107 | '

', 108 | '
' 109 | ].join(''); 110 | // DEV: aboutWindow will be garbage collection automatically 111 | var aboutWindow = new BrowserWindow({ 112 | height: 180, 113 | icon: assets['icon-32'], 114 | width: 400 115 | }); 116 | aboutWindow.loadURL('data:text/html,' + info); 117 | }, 118 | openConfigWindow: function () { 119 | logger.debug('Showing `config` window for `google-music-electron`'); 120 | // DEV: configWindow will be garbage collection automatically 121 | var configWindow = new BrowserWindow({ 122 | height: 440, 123 | icon: assets['icon-32'], 124 | width: 620 125 | }); 126 | configWindow.loadURL('file://' + __dirname + '/views/config.html'); 127 | }, 128 | quitApplication: function () { 129 | logger.debug('Exiting `google-music-electron`'); 130 | app.quit(); 131 | }, 132 | reloadWindow: function () { 133 | logger.debug('Reloading focused browser window'); 134 | BrowserWindow.getFocusedWindow().reload(); 135 | }, 136 | showMinimizedWindow: function () { 137 | // DEV: Focus is necessary when there is no taskbar and we have lost focus for the app 138 | gme.browserWindow.restore(); 139 | gme.browserWindow.focus(); 140 | }, 141 | showInvisibleWindow: function () { 142 | gme.browserWindow.show(); 143 | }, 144 | toggleDevTools: function () { 145 | logger.debug('Toggling developer tools in focused browser window'); 146 | BrowserWindow.getFocusedWindow().toggleDevTools(); 147 | }, 148 | toggleFullScreen: function () { 149 | var focusedWindow = BrowserWindow.getFocusedWindow(); 150 | // Move to other full screen state (e.g. true -> false) 151 | var wasFullScreen = focusedWindow.isFullScreen(); 152 | var toggledFullScreen = !wasFullScreen; 153 | logger.debug('Toggling focused browser window full screen', { 154 | wasFullScreen: wasFullScreen, 155 | toggledFullScreen: toggledFullScreen 156 | }); 157 | focusedWindow.setFullScreen(toggledFullScreen); 158 | }, 159 | toggleMinimize: function () { 160 | if (gme.browserWindow) { 161 | var isMinimized = gme.browserWindow.isMinimized(); 162 | logger.debug('Toggling browser window minimization', { 163 | isMinimized: isMinimized 164 | }); 165 | if (isMinimized) { 166 | gme.showMinimizedWindow(); 167 | } else { 168 | gme.browserWindow.minimize(); 169 | } 170 | } else { 171 | logger.debug('Browser window minimization toggling requested but browser window as not found'); 172 | } 173 | }, 174 | toggleVisibility: function () { 175 | if (gme.browserWindow) { 176 | var isVisible = gme.browserWindow.isVisible(); 177 | logger.debug('Toggling browser window visibility', { 178 | isVisible: isVisible 179 | }); 180 | if (isVisible) { 181 | gme.browserWindow.hide(); 182 | } else { 183 | gme.showInvisibleWindow(); 184 | } 185 | } else { 186 | logger.debug('Browser window visibility toggling requested but browser window as not found'); 187 | } 188 | } 189 | }; 190 | 191 | // Assign tray click behavior 192 | gme.onTrayClick = (config.get('hide-via-tray') || config.get('minimize-to-tray')) ? 193 | gme.toggleVisibility : gme.toggleMinimize; 194 | gme.onRaise = (config.get('hide-via-tray') || config.get('minimize-to-tray')) ? 195 | gme.showInvisibleWindow : gme.showMinimizedWindow; 196 | 197 | // Define our launch handler 198 | function launchGme() { 199 | // Create our browser window for Google Music 200 | var windowInfo = config.get('window-info') || {}; 201 | var windowOpts = { 202 | height: windowInfo.height || 920, 203 | icon: assets['icon-32'], 204 | skipTaskbar: config.get('skip-taskbar'), 205 | // Load in our Google Music bindings on the page 206 | webPreferences: { 207 | preload: __dirname + '/browser.js' 208 | }, 209 | width: windowInfo.width || 1024, 210 | x: windowInfo.x || null, 211 | y: windowInfo.y || null 212 | }; 213 | logger.info('App ready. Opening Google Music window', { 214 | options: windowOpts, 215 | processVersions: process.versions, 216 | version: pkg.version 217 | }); 218 | gme.browserWindow = new BrowserWindow(windowOpts); 219 | gme.browserWindow.loadURL('https://play.google.com/music/listen'); 220 | 221 | // If hiding to tray was requested, trigger a visibility toggle when the window is minimized 222 | if (config.get('minimize-to-tray')) { 223 | gme.browserWindow.on('minimize', gme.toggleVisibility); 224 | } 225 | 226 | // Save the window position after moving 227 | function saveWindowInfo() { 228 | config.set('window-info', gme.browserWindow.getBounds()); 229 | } 230 | gme.browserWindow.on('move', _.debounce(function handleWindowMove () { 231 | logger.debug('Browser window moved, saving window info in config.'); 232 | saveWindowInfo(); 233 | }, 250)); 234 | 235 | // Save the window size after resizing 236 | gme.browserWindow.on('resize', _.debounce(function handleWindowResize () { 237 | logger.debug('Browser window resized, saving window info in config.'); 238 | saveWindowInfo(); 239 | }, 250)); 240 | 241 | // When our window is closed, clean up the reference to our window 242 | gme.browserWindow.on('closed', function handleWindowClose () { 243 | logger.debug('Browser window closed, garbage collecting `browserWindow`'); 244 | gme.browserWindow = null; 245 | }); 246 | 247 | // Save browser window context to replify 248 | // http://dshaw.github.io/2012-10-nodedublin/#/ 249 | if (program.debugRepl) { 250 | var replServer = replify('google-music-electron', null, {gme: gme}); 251 | replServer.on('listening', function handleReplServerListen () { 252 | var socketPath = replServer.address(); 253 | logger.info('Debug repl opened at "%s". This should be accessible via `npm run debug-repl`', socketPath); 254 | }); 255 | } 256 | 257 | // Set up our application menu, tray, and shortcuts 258 | appMenu.init(gme); 259 | appTray.init(gme); 260 | shortcuts.init(gme); 261 | if (mpris) { 262 | mpris.init(gme); 263 | } 264 | } 265 | 266 | // If we are only allowing single instances 267 | var booter; 268 | if (!config.get('allow-multiple-instances')) { 269 | // Start up/connect to a monogamous server (detects other instances) 270 | booter = monogamous({sock: pkg.name}); 271 | 272 | // If we are the first instance, start up gme 273 | booter.on('boot', launchGme); 274 | 275 | // Otherwise, focus it 276 | booter.on('reboot', gme.onRaise); 277 | 278 | // If we encounter an error, log it and start anyway 279 | booter.on('error', function handleError (err) { 280 | logger.error('Error while starting/connecting to monogamous server', err); 281 | logger.info('Ignoring monogamous error, starting google-music-electron'); 282 | launchGme(); 283 | }); 284 | } 285 | 286 | // When Electron is done loading 287 | app.on('ready', function handleReady () { 288 | // If we have a booter, invoke it 289 | if (booter) { 290 | booter.boot(); 291 | // Otherwise, launch immediately 292 | } else { 293 | launchGme(); 294 | } 295 | }); 296 | -------------------------------------------------------------------------------- /lib/install-mpris.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var assert = require('assert'); 3 | var path = require('path'); 4 | var spawn = require('child_process').spawn; 5 | 6 | // Load in package info 7 | var pkg = require('../package.json'); 8 | 9 | // Define our installer 10 | module.exports = function () { 11 | // Resolve our mpris dependencies 12 | var installArgs = Object.keys(pkg.mprisDependencies).map(function getInstallArg (dependencyName) { 13 | return dependencyName + '@' + pkg.mprisDependencies[dependencyName]; 14 | }); 15 | 16 | // Run our install command 17 | // DEV: We are inside of `io.js` of Electron which allows us to use the latest hotness 18 | var child = spawn( 19 | 'npm', 20 | // Use `--ignore-scripts` to avoid compiling against system's node 21 | // Use `--save false` prevent saving to `package.json` during development 22 | ['install', '--ignore-scripts', '--save', 'false'].concat(installArgs), 23 | {cwd: path.join(__dirname, '..'), stdio: 'inherit'}); 24 | 25 | // If there is an error, throw it 26 | child.on('error', function handleError (err) { 27 | throw err; 28 | }); 29 | 30 | // When the child exits 31 | child.on('exit', function handleExit (code, signal) { 32 | // Verify we received a zero exit code 33 | assert.strictEqual(code, 0, 'Expected "npm install" exit code to be "0" but it was "' + code + '"'); 34 | 35 | // Rebuild electron with our new `mpris-service` 36 | var electronRebuildCmd = require.resolve('electron-rebuild/lib/cli.js'); 37 | child = spawn(electronRebuildCmd, {cwd: path.join(__dirname, '..'), stdio: 'inherit'}); 38 | 39 | // If there is an error, throw it 40 | child.on('error', function handleError (err) { 41 | throw err; 42 | }); 43 | 44 | // When the child exits 45 | child.on('exit', function handleExit (code, signal) { 46 | // Verify we received a zero exit code 47 | assert.strictEqual(code, 0, 'Expected "electron-rebuild" exit code to be "0" but it was "' + code + '"'); 48 | 49 | // Log our success and exit 50 | console.log('MPRIS integration successfully installed! ' + 51 | 'Please start `google-music-electron` to see it in action!'); 52 | process.exit(); 53 | }); 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var app = require('electron').app; 3 | var path = require('path'); 4 | var winston = require('winston'); 5 | 6 | // Load in our constants 7 | // e.g. ~/.config/google-music-electron/verbose.log 8 | var logPath = path.join(app.getPath('userData'), 'verbose.log'); 9 | 10 | // Define our logger setup 11 | module.exports = function (options) { 12 | // Create our logger 13 | // https://github.com/winstonjs/winston/blob/v1.0.0/lib/winston/config/npm-config.js 14 | // https://github.com/winstonjs/winston/tree/v1.0.0#using-logging-levels 15 | var logger = new winston.Logger({ 16 | transports: [ 17 | // https://github.com/winstonjs/winston/tree/v1.0.0#console-transport 18 | new winston.transports.Console({ 19 | level: options.verbose ? 'silly' : 'info', 20 | colorize: true, 21 | timestamp: true 22 | }), 23 | // https://github.com/winstonjs/winston/tree/v1.0.0#file-transport 24 | new winston.transports.File({ 25 | level: 'silly', 26 | filename: logPath, 27 | colorize: false, 28 | timestamp: true 29 | }) 30 | ] 31 | }); 32 | 33 | // Log for sanity 34 | logger.info('Logger initialized. Writing info/warnings/errors to stdout. ' + 35 | 'Writing all logs to "%s"', logPath); 36 | 37 | // Return our logger 38 | return logger; 39 | }; 40 | -------------------------------------------------------------------------------- /lib/menus/darwin.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": [{ 3 | "label": "google-music-electron", 4 | "submenu": [{ 5 | "label": "About google-music-electron", 6 | "command": "application:about" 7 | }, { 8 | "type": "separator" 9 | }, { 10 | "label": "Preferences...", 11 | "command": "application:show-settings", 12 | "accelerator": "Command+," 13 | }, { 14 | "type": "separator" 15 | }, { 16 | "label": "Hide google-music-electron", 17 | "role": "hide", 18 | "accelerator": "Command+H" 19 | }, { 20 | "label": "Hide Others", 21 | "role": "hideothers", 22 | "accelerator": "Alt+Command+H" 23 | }, { 24 | "label": "Show All", 25 | "role": "unhide" 26 | }, { 27 | "type": "separator" 28 | }, { 29 | "label": "Quit", 30 | "command": "application:quit", 31 | "accelerator": "Command+Q" 32 | }] 33 | }, { 34 | "label": "Edit", 35 | "submenu": [{ 36 | "label": "Undo", 37 | "role": "undo", 38 | "accelerator": "Command+Z" 39 | }, { 40 | "label": "Redo", 41 | "role": "redo", 42 | "accelerator": "Shift+Command+Z" 43 | }, { 44 | "type": "separator" 45 | }, { 46 | "label": "Cut", 47 | "role": "cut", 48 | "accelerator": "Command+X" 49 | }, { 50 | "label": "Copy", 51 | "role": "copy", 52 | "accelerator": "Command+C" 53 | }, { 54 | "label": "Paste", 55 | "role": "paste", 56 | "accelerator": "Command+V" 57 | }, { 58 | "label": "Select All", 59 | "role": "selectall", 60 | "accelerator": "Command+A" 61 | }] 62 | }, { 63 | "label": "View", 64 | "submenu": [{ 65 | "label": "Reload", 66 | "command": "window:reload", 67 | "accelerator": "Command+R" 68 | }, { 69 | "label": "Toggle Full Screen", 70 | "command": "window:toggle-full-screen", 71 | "accelerator": "Command+Control+F" 72 | }, { 73 | "label": "Developer", 74 | "submenu": [{ 75 | "label": "Toggle Developer Tools", 76 | "command": "window:toggle-dev-tools", 77 | "accelerator": "Alt+Command+I" 78 | }] 79 | }] 80 | }, { 81 | "label": "Window", 82 | "submenu": [{ 83 | "label": "Minimize", 84 | "role": "minimize", 85 | "accelerator": "Command+M" 86 | }] 87 | }] 88 | } 89 | -------------------------------------------------------------------------------- /lib/menus/linux.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": [{ 3 | "label": "&File", 4 | "submenu": [{ 5 | "label": "&Preferences", 6 | "command": "application:show-settings" 7 | }, { 8 | "label": "Quit", 9 | "command": "application:quit", 10 | "accelerator": "Control+Q" 11 | }] 12 | }, { 13 | "label": "&Edit", 14 | "submenu": [{ 15 | "label": "&Undo", 16 | "role": "undo", 17 | "accelerator": "Control+Z" 18 | }, { 19 | "label": "&Redo", 20 | "role": "redo", 21 | "accelerator": "Control+Y" 22 | }, { 23 | "type": "separator" 24 | }, { 25 | "label": "&Cut", 26 | "role": "cut", 27 | "accelerator": "Control+X" 28 | }, { 29 | "label": "C&opy", 30 | "role": "copy", 31 | "accelerator": "Control+C" 32 | }, { 33 | "label": "&Paste", 34 | "role": "paste", 35 | "accelerator": "Control+V" 36 | }, { 37 | "label": "Select &All", 38 | "role": "selectall", 39 | "accelerator": "Control+A" 40 | }] 41 | }, { 42 | "label": "&View", 43 | "submenu": [{ 44 | "label": "&Reload", 45 | "command": "window:reload", 46 | "accelerator": "Control+R" 47 | }, { 48 | "label": "Toggle &Full Screen", 49 | "command": "window:toggle-full-screen", 50 | "accelerator": "Control+Shift+F" 51 | }, { 52 | "label": "Developer", 53 | "submenu": [{ 54 | "label": "Toggle Developer &Tools", 55 | "command": "window:toggle-dev-tools", 56 | "accelerator": "Control+Shift+I" 57 | }] 58 | }] 59 | }, { 60 | "label": "&Help", 61 | "submenu": [{ 62 | "label": "About google-music-electron", 63 | "command": "application:about" 64 | }] 65 | }] 66 | } 67 | -------------------------------------------------------------------------------- /lib/menus/win32.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": [{ 3 | "label": "&File", 4 | "submenu": [{ 5 | "label": "&Options", 6 | "command": "application:show-settings" 7 | }, { 8 | "type": "separator" 9 | }, { 10 | "label": "E&xit", 11 | "command": "application:quit", 12 | "accelerator": "Alt+F4" 13 | }] 14 | }, { 15 | "label": "&Edit", 16 | "submenu": [{ 17 | "label": "&Undo", 18 | "role": "undo", 19 | "accelerator": "Control+Z" 20 | }, { 21 | "label": "&Redo", 22 | "role": "redo", 23 | "accelerator": "Control+Y" 24 | }, { 25 | "type": "separator" 26 | }, { 27 | "label": "&Cut", 28 | "role": "cut", 29 | "accelerator": "Control+X" 30 | }, { 31 | "label": "C&opy", 32 | "role": "copy", 33 | "accelerator": "Control+C" 34 | }, { 35 | "label": "&Paste", 36 | "role": "paste", 37 | "accelerator": "Control+V" 38 | }, { 39 | "label": "Select &All", 40 | "role": "selectall", 41 | "accelerator": "Control+A" 42 | }] 43 | }, { 44 | "label": "&View", 45 | "submenu": [{ 46 | "label": "&Reload", 47 | "command": "window:reload", 48 | "accelerator": "Control+R" 49 | }, { 50 | "label": "Toggle &Full Screen", 51 | "command": "window:toggle-full-screen", 52 | "accelerator": "Control+Shift+F" 53 | }, { 54 | "label": "Developer", 55 | "submenu": [{ 56 | "label": "Toggle Developer &Tools", 57 | "command": "window:toggle-dev-tools", 58 | "accelerator": "Control+Shift+I" 59 | }] 60 | }] 61 | }, { 62 | "label": "&Help", 63 | "submenu": [{ 64 | "label": "About google-music-electron", 65 | "command": "application:about" 66 | }] 67 | }] 68 | } 69 | -------------------------------------------------------------------------------- /lib/mpris.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var ipcMain = require('electron').ipcMain; 3 | var _ = require('underscore'); 4 | var GoogleMusic = require('google-music'); 5 | var MprisService = require('mpris-service'); 6 | 7 | // Define a function to set up mpris 8 | exports.init = function (gme) { 9 | // https://github.com/emersion/mpris-service/tree/a245730635b55c8eb06c605f4ece61e251f04e20 10 | // https://github.com/emersion/mpris-service/blob/a245730635b55c8eb06c605f4ece61e251f04e20/index.js 11 | // http://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/ 12 | // http://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html 13 | var mpris = new MprisService({ 14 | name: 'google-music-electron' 15 | }); 16 | mpris.on('next', gme.controlNext); 17 | mpris.on('playpause', gme.controlPlayPause); 18 | mpris.on('previous', gme.controlPrevious); 19 | mpris.on('quit', gme.quitApplication); 20 | mpris.on('raise', gme.onRaise); 21 | // Currently position and seek aren't supported due to not receiving events in Cinnamon =( 22 | // DEV: Stop isn't supported in Google Music (unless it's pause + set position 0) 23 | // DEV: We choose to let the OS volume be controlled by MPRIS 24 | 25 | var songInfo = {}; 26 | ipcMain.on('change:song', function handleSongChange (evt, _songInfo) { 27 | mpris.metadata = songInfo = { 28 | 'mpris:artUrl': _songInfo.art, 29 | // Convert milliseconds to microseconds (1s = 1e3ms = 1e6µs) 30 | 'mpris:length': _songInfo.duration * 1e3, 31 | 'xesam:album': _songInfo.album, 32 | 'xesam:artist': _songInfo.artist, 33 | 'xesam:title': _songInfo.title 34 | }; 35 | }); 36 | 37 | ipcMain.on('change:playback-time', function handlePlaybackUpdate (evt, playbackInfo) { 38 | // Convert milliseconds to microseconds (1s = 1e3ms = 1e6µs) 39 | var newPosition = playbackInfo.current * 1e3; 40 | var newTotal = playbackInfo.total * 1e3; 41 | 42 | // If the total has been updated, update our songInfo cache 43 | // DEV: This is due to `google-music.js` not always having an up to date length upon song change 44 | if (songInfo['mpris:length'] !== newTotal) { 45 | mpris.metadata = _.extend(songInfo, { 46 | 'mpris:length': newTotal 47 | }); 48 | } 49 | 50 | // If our position varies by 2 seconds, consider it a seek 51 | // DEV: Seeked takes the delta (positive/negative depending on position 52 | var delta = newPosition - mpris.position; 53 | if (Math.abs(delta) > 2e6) { 54 | mpris.seeked(delta); 55 | } 56 | }); 57 | 58 | var playbackStrings = {}; 59 | playbackStrings[GoogleMusic.Playback.PLAYING] = 'Playing'; 60 | playbackStrings[GoogleMusic.Playback.PAUSED] = 'Paused'; 61 | playbackStrings[GoogleMusic.Playback.STOPPED] = 'Stopped'; 62 | ipcMain.on('change:playback', function handlePlaybackChange (evt, playbackState) { 63 | mpris.playbackStatus = playbackStrings[playbackState]; 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /lib/shortcuts.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var globalShortcut = require('electron').globalShortcut; 3 | var ipcMain = require('electron').ipcMain; 4 | 5 | // Define a function to bind shortcuts 6 | exports.init = function (gme) { 7 | // Set up media keys 8 | var shortcutCallbacks = { 9 | 'playpause-shortcut': gme.controlPlayPause, 10 | 'next-shortcut': gme.controlNext, 11 | 'previous-shortcut': gme.controlPrevious 12 | }; 13 | var playpauseShortcut = gme.config.get('playpause-shortcut'); 14 | if (playpauseShortcut && !globalShortcut.register(playpauseShortcut, shortcutCallbacks['playpause-shortcut'])) { 15 | gme.logger.warn('Failed to bind `' + playpauseShortcut + '` shortcut'); 16 | } 17 | var nextShortcut = gme.config.get('next-shortcut'); 18 | if (nextShortcut && !globalShortcut.register(nextShortcut, shortcutCallbacks['next-shortcut'])) { 19 | gme.logger.warn('Failed to bind `' + nextShortcut + '` shortcut'); 20 | } 21 | var previousShortcut = gme.config.get('previous-shortcut'); 22 | if (previousShortcut && !globalShortcut.register(previousShortcut, shortcutCallbacks['previous-shortcut'])) { 23 | gme.logger.warn('Failed to bind `' + previousShortcut + '` shortcut'); 24 | } 25 | 26 | // When a shortcut change is requested 27 | ipcMain.on('set-shortcut-sync', function handleShortcutChange (evt, shortcutName, accelerator) { 28 | // Prepare common set of results 29 | var previousAccelerator = gme.config.get(shortcutName); 30 | var retVal = { 31 | success: false, 32 | previousAccelerator: previousAccelerator, 33 | accelerator: accelerator 34 | }; 35 | 36 | // If the accelerator is the same as the current one, exit with success 37 | if (previousAccelerator === accelerator) { 38 | retVal.success = true; 39 | evt.returnValue = JSON.stringify(retVal); 40 | return; 41 | } 42 | 43 | // If the accelerator is nothing, then consider it a success 44 | if (accelerator === '') { 45 | retVal.success = true; 46 | // Otherwise, attempt to register the new shortcut 47 | } else { 48 | gme.logger.info('Attempting to register shortcut "' + shortcutName + '" under "' + accelerator + '"'); 49 | try { 50 | retVal.success = globalShortcut.register(accelerator, shortcutCallbacks[shortcutName]); 51 | gme.logger.info('Registration successful'); 52 | } catch (err) { 53 | // Catch any unrecognized accelerators 54 | } 55 | } 56 | 57 | // If we were successful, remove the last binding and update our config 58 | if (retVal.success) { 59 | if (previousAccelerator) { 60 | gme.logger.info('Unregistering shortcut "' + 61 | shortcutName + '" from "' + previousAccelerator + '"'); 62 | globalShortcut.unregister(previousAccelerator); 63 | } 64 | 65 | gme.logger.info('Updating config...'); 66 | gme.config.set(shortcutName, accelerator); 67 | // Otherwise, log failure 68 | } else { 69 | gme.logger.info('Registration failed. Couldn\'t register shortcut "' + 70 | shortcutName + '" to "' + accelerator + '"'); 71 | } 72 | 73 | // In any event, return with our success status 74 | evt.returnValue = JSON.stringify(retVal); 75 | }); 76 | }; 77 | -------------------------------------------------------------------------------- /lib/views/config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | google-music-electron config 5 | 28 | 29 | 30 | 31 |

google-music-electron config

32 |

Shortcut examples: mediaplaypause, ctrl+shift+p, alt+p

33 |

34 | 41 |

42 |

43 | 50 |

51 |

52 | 59 |

60 |

61 | 65 | Requires restart ( 66 | 67 | ) 68 |
69 | 70 |

71 |

72 | 76 | Requires restart ( 77 | 78 | ) 79 |
80 | 81 |

82 |

83 | 87 | Requires restart ( 88 | 89 | ) 90 |
91 | 92 |

93 |

94 | 98 | Requires restart ( 99 | 100 | ) 101 |
102 | 103 |

104 | 105 | 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-music-electron", 3 | "description": "Desktop app for Google Music on top of Electron", 4 | "version": "2.20.0", 5 | "homepage": "https://github.com/twolfson/google-music-electron", 6 | "author": { 7 | "name": "Todd Wolfson", 8 | "email": "todd@twolfson.com", 9 | "url": "http://twolfson.com/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/twolfson/google-music-electron.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/twolfson/google-music-electron/issues" 17 | }, 18 | "license": "Unlicense", 19 | "main": "lib/google-music-electron", 20 | "engines": { 21 | "node": ">= 6.0.0" 22 | }, 23 | "bin": { 24 | "google-music-electron": "bin/google-music-electron.js" 25 | }, 26 | "scripts": { 27 | "debug-repl": "rc /tmp/repl/google-music-electron.sock", 28 | "lint": "twolfson-style lint lib/", 29 | "start": "node bin/google-music-electron.js", 30 | "test": "npm run lint" 31 | }, 32 | "dependencies": { 33 | "commander": "~2.8.1", 34 | "configstore": "~1.2.1", 35 | "electron": "^7.1.4", 36 | "google-music": "~6.0.1", 37 | "monogamous": "~1.0.3", 38 | "replify": "~1.2.0", 39 | "underscore": "~1.8.3", 40 | "winston": "~1.0.0" 41 | }, 42 | "devDependencies": { 43 | "foundry": "~4.3.2", 44 | "foundry-release-git": "~2.0.2", 45 | "foundry-release-npm": "~2.0.2", 46 | "jscs": "~1.7.3", 47 | "jshint": "~2.5.10", 48 | "repl-client": "~0.3.0", 49 | "twolfson-style": "~1.6.0" 50 | }, 51 | "mprisDependencies": { 52 | "electron-rebuild": "~1.0.2", 53 | "mpris-service": "~1.0.1" 54 | }, 55 | "keywords": [ 56 | "google", 57 | "music", 58 | "electron", 59 | "atom-shell" 60 | ], 61 | "foundry": { 62 | "releaseCommands": [ 63 | "foundry-release-git", 64 | "foundry-release-npm" 65 | ] 66 | } 67 | } -------------------------------------------------------------------------------- /resources/headphones-filled-with-states.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 53 | 60 | 65 | 70 | 75 | 130 | -------------------------------------------------------------------------------- /resources/headphones.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------