├── Icon.icns ├── Icon.png ├── .travis.yml ├── Icon@2x.png ├── app.css ├── index.html ├── lib ├── detect-country.js └── find-music.js ├── .gitignore ├── main.js ├── README.md ├── package.json ├── index.js └── app.js /Icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uiur/playing/HEAD/Icon.icns -------------------------------------------------------------------------------- /Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uiur/playing/HEAD/Icon.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.11' 4 | - '0.10' 5 | -------------------------------------------------------------------------------- /Icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uiur/playing/HEAD/Icon@2x.png -------------------------------------------------------------------------------- /app.css: -------------------------------------------------------------------------------- 1 | .description { 2 | white-space: pre-wrap; 3 | font-size: 16px; 4 | } 5 | 6 | .alert-success { 7 | margin-top: 10px; 8 | padding: 10px; 9 | } 10 | 11 | .btn-quit { 12 | position: absolute; 13 | right: 15px; 14 | bottom: 15px; 15 | } 16 | 17 | .btn-sendnow { 18 | position: absolute; 19 | left: 15px; 20 | bottom: 15px; 21 | } 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/detect-country.js: -------------------------------------------------------------------------------- 1 | var once = require('once') 2 | var fetch = require('isomorphic-fetch') 3 | 4 | module.exports = once(function () { 5 | return ipInfo().then(function (data) { 6 | return data.country 7 | }) 8 | }) 9 | 10 | function ipInfo () { 11 | return fetch('http://ipinfo.io/json').then(function (res) { 12 | return res.json() 13 | }).catch(function (err) { 14 | console.error(err.stack) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /lib/find-music.js: -------------------------------------------------------------------------------- 1 | var fetch = require('isomorphic-fetch') 2 | 3 | module.exports = function findMusic (keywords, options) { 4 | options = options || { country: 'US' } 5 | 6 | var query = keywords.map(function (keyword) { 7 | return encodeURIComponent(keyword) 8 | }).join('+') 9 | 10 | return fetch('https://itunes.apple.com/search?term=' + query + '&media=music&limit=1&country=' + options.country).then(function (res) { 11 | if (res.status >= 400) { 12 | throw new Error(res.statusText) 13 | } 14 | 15 | return res.json() 16 | }).then(function (data) { 17 | return data.results[0] 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | build 27 | 28 | # Dependency directory 29 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 30 | node_modules 31 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | var menubar = require('menubar') 4 | 5 | var ipcMain = require('electron').ipcMain 6 | var itunes = require('./index.js') 7 | 8 | var mb = menubar({ preloadWindow: true, icon: path.join(__dirname, '/Icon.png') }) 9 | 10 | mb.on('ready', function ready () { 11 | ipcMain.on('data', function (event, data) { 12 | update(data) 13 | }) 14 | 15 | ipcMain.once('data', itunes.listen) 16 | }) 17 | 18 | ipcMain.on('terminate', function terminate () { 19 | mb.app.quit() 20 | }) 21 | 22 | ipcMain.on('sendnow', function sendnow () { 23 | itunes.sendNow() 24 | }) 25 | 26 | function update (state) { 27 | process.env.SLACK_WEBHOOK_URL = state.slackWebhookUrl 28 | process.env.LISTENER_NAME = state.listenerName 29 | process.env.AUTO_SEND = state.autoSend 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # playing 2 | [![Build Status](https://travis-ci.org/uiureo/playing.svg)](https://travis-ci.org/uiureo/playing) 3 | 4 | :musical_note: Send what you're playing on iTunes to Slack. 5 | 6 | ![](https://i.gyazo.com/1fb3fdb923d244ed86557f8b4f1066ba.png) 7 | 8 | Currently works only in OSX. 9 | 10 | ## Installation 11 | Download the latest version of zip from here and launch it. 12 | 13 | https://github.com/uiureo/playing/releases 14 | 15 | ## Usage 16 | Add an Incoming Webhook and get a Webhook URL. 17 | 18 | https://slack.com/services/new/incoming-webhook 19 | 20 | Launch settings menu from the menubar and paste it to the form. 21 | 22 | ![](https://i.gyazo.com/3213dad4d3a0663b1a9f60dc50781462.png) 23 | 24 | Play your favorite music on iTunes! 25 | 26 | ## Release 27 | 28 | ```sh 29 | npm install 30 | npm run build 31 | ``` 32 | 33 | The build will be in `build/` dir. 34 | 35 | ## Attribution 36 | The icon is created by Melvin Salas from Noun Project. 37 | Licensed under Creative Commons. 38 | 39 | https://thenounproject.com/term/funk/87940/ 40 | 41 | ## License 42 | ISC 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playing", 3 | "version": "1.1.1", 4 | "description": "Send what you're playing on iTunes to Slack", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "standard", 8 | "start": "electron ./", 9 | "build": "electron-packager ./ playing --out build/ --platform=darwin --arch=x64 --version=1.2.6 --icon=./Icon.icns --overwrite --prune --app-version \"$(node -pe \"require('./package.json').version\")\"" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/uiureo/playing.git" 14 | }, 15 | "author": "Kazato Sugimoto", 16 | "contributors": [{ 17 | "name": "Till Kothe" 18 | }], 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/uiureo/playing/issues" 22 | }, 23 | "homepage": "https://github.com/uiureo/playing#readme", 24 | "dependencies": { 25 | "es6-promise": "^3.0.2", 26 | "hipchatter": "^0.2.0", 27 | "iconv": "^2.1.10", 28 | "isomorphic-fetch": "^2.1.1", 29 | "menubar": "^4.1.2", 30 | "once": "^1.3.2", 31 | "playback": "^0.2.0", 32 | "react": "^0.14.8", 33 | "react-dom": "^0.14.7" 34 | }, 35 | "devDependencies": { 36 | "electron-packager": "^7.2.0", 37 | "standard": "^7.1.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('es6-promise').polyfill() 2 | 3 | var itunes = require('playback') 4 | var Hipchat = require('hipchatter') 5 | 6 | var findMusic = require('./lib/find-music.js') 7 | var detectCountry = require('./lib/detect-country.js') 8 | 9 | var fetch = require('isomorphic-fetch') 10 | 11 | module.exports = { 12 | listen: function run () { 13 | var prevTrack = {} 14 | itunes.on('playing', function (track) { 15 | if (isEqualTrack(track, prevTrack)) return 16 | 17 | if (process.env.AUTO_SEND === 'false') { 18 | return 19 | } 20 | 21 | findTrackAndNotify(track) 22 | prevTrack = track 23 | }) }, 24 | 25 | sendNow: function sendNow () { 26 | itunes.currentTrack(function (track) { 27 | findTrackAndNotify(track) 28 | }) 29 | } 30 | } 31 | 32 | function findTrackAndNotify (track) { 33 | detectCountry().then(function (country) { 34 | findMusic([ track.name, track.artist, track.album ], { 35 | country: country 36 | }).then(function (music) { 37 | if (music) return music 38 | 39 | return findMusic([track.name, track.artist], { country: country }) 40 | }).then(function (music) { 41 | notify(track, music) 42 | }).catch(function (err) { 43 | console.error(err.stack) 44 | }) 45 | }) 46 | } 47 | 48 | function isEqualTrack (a, b) { 49 | if (!(typeof a === 'object' && typeof b === 'object')) return 50 | 51 | return a.name === b.name && a.artist === b.artist 52 | } 53 | 54 | function trackToString (track) { 55 | return track.name + ' - ' + track.artist 56 | } 57 | 58 | function messageForSlack (track, music) { 59 | function link (text, url) { 60 | return '<' + url + '|' + text + '>' 61 | } 62 | 63 | var trackStr = track.name 64 | 65 | var url = music && music.trackViewUrl 66 | var message = url ? link(trackStr, url) : trackStr 67 | 68 | return '🎵 ' + message 69 | } 70 | 71 | function messageForHipchat (track, music) { 72 | function link (text, url) { 73 | return '' + text + '' 74 | } 75 | 76 | var trackStr = trackToString(track) 77 | var url = music && music.trackViewUrl 78 | 79 | var message = url ? link(trackStr, url) : trackStr 80 | return '🎵 ' + message 81 | } 82 | 83 | function notify (track, music) { 84 | console.log('🎵 ' + trackToString(track)) 85 | if (process.env.HIPCHAT_TOKEN) { 86 | postToHipchat(messageForHipchat(track, music)) 87 | } 88 | 89 | if (process.env.SLACK_WEBHOOK_URL) { 90 | postToSlack(messageForSlack(track, music), music, track) 91 | } 92 | } 93 | 94 | function postToSlack (message, music, track) { 95 | music = music || {} 96 | var webhookUrl = process.env.SLACK_WEBHOOK_URL 97 | var listenerName = process.env.LISTENER_NAME || 'somebody' 98 | var username = listenerName + ' playing ' + track.artist 99 | 100 | return fetch(webhookUrl, { 101 | method: 'post', 102 | body: JSON.stringify({ 103 | text: message, 104 | username: username, 105 | icon_url: music.artworkUrl60 106 | }) 107 | }) 108 | } 109 | 110 | function postToHipchat (message) { 111 | var hipchat = new Hipchat(process.env.HIPCHAT_TOKEN) 112 | 113 | hipchat.notify(process.env.HIPCHAT_ROOM, { 114 | message: message, 115 | notify: true 116 | }, function (err) { if (err) console.error(err) }) 117 | } 118 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const React = require('react') 4 | const ReactDOM = require('react-dom') 5 | const {ipcRenderer} = require('electron') 6 | const {shell} = require('electron') 7 | const url = require('url') 8 | 9 | const App = React.createClass({ 10 | 11 | getInitialState () { 12 | return { 13 | slackWebhookUrl: window.localStorage.getItem('slackWebhookUrl'), 14 | listenerName: window.localStorage.getItem('listenerName'), 15 | autoSend: (window.localStorage.getItem('autoSend') === null || window.localStorage.getItem('autoSend') === 'true') // localstorage only knows strings 16 | } 17 | }, 18 | 19 | componentDidMount () { 20 | ipcRenderer.send('data', this.state) 21 | }, 22 | 23 | componentDidUpdate () { 24 | ipcRenderer.send('data', this.state) 25 | }, 26 | 27 | inputIsValid () { 28 | return url.parse(this.state.slackWebhookUrl || '').host === 'hooks.slack.com' 29 | }, 30 | 31 | render () { 32 | const self = this 33 | 34 | const onlyIfChecked = !this.state.autoSend 35 | ? React.DOM.button({ 36 | className: 'btn btn-default btn-sendnow', 37 | onClick: function () { 38 | ipcRenderer.send('sendnow') 39 | } 40 | }, 'Send Now') 41 | : null 42 | 43 | return ( 44 | React.DOM.main({ 45 | className: 'container' 46 | }, [ 47 | React.DOM.div({}, [ 48 | React.DOM.h2({}, 'playing'), 49 | React.DOM.p({ className: 'description' }, [ 50 | React.DOM.a({ 51 | href: 'https://slack.com/services/new/incoming-webhook', 52 | onClick: function (e) { 53 | e.preventDefault() 54 | shell.openExternal(e.target.href) 55 | } 56 | }, 'Add a new Incoming Webhook in Slack'), 57 | '\nand paste the webhook URL to this form.' 58 | ]), 59 | 60 | React.DOM.label({}, 'Slack Webhook URL'), 61 | React.DOM.input({ 62 | className: 'form-control', 63 | type: 'text', 64 | placeholder: 'https://hooks.slack.com/...', 65 | value: this.state.slackWebhookUrl, 66 | onChange: function (e) { 67 | self.setState({ slackWebhookUrl: e.target.value }) 68 | window.localStorage.setItem('slackWebhookUrl', e.target.value) 69 | } 70 | }), 71 | React.DOM.label({}, 'Your name'), 72 | React.DOM.input({ 73 | className: 'form-control', 74 | type: 'text', 75 | placeholder: 'your name', 76 | value: this.state.listenerName, 77 | onChange: function (e) { 78 | self.setState({ listenerName: e.target.value }) 79 | window.localStorage.setItem('listenerName', e.target.value) 80 | } 81 | }), 82 | React.DOM.input({ 83 | className: 'checkbox', 84 | id: 'autosend-checkbox', 85 | type: 'checkbox', 86 | defaultChecked: self.state.autoSend, 87 | onChange: function (e) { 88 | self.setState({ autoSend: e.target.checked }) 89 | window.localStorage.setItem('autoSend', e.target.checked) 90 | } 91 | }), 92 | React.DOM.label({ htmlFor: 'autosend-checkbox' }, 'Send automatically'), 93 | this.inputIsValid() && React.DOM.div({ className: 'alert alert-success' }, '✔ Play music on iTunes!') 94 | ]), 95 | onlyIfChecked, 96 | React.DOM.button({ 97 | className: 'btn btn-default btn-quit', 98 | onClick: function () { 99 | ipcRenderer.send('terminate') 100 | } 101 | }, 'Quit') 102 | ]) 103 | ) 104 | } 105 | }) 106 | 107 | ReactDOM.render(React.createFactory(App)(), document.getElementById('content')) 108 | 109 | const {remote} = require('electron') 110 | const {Menu, MenuItem} = remote 111 | 112 | var menu = new Menu() 113 | menu.append(new MenuItem({ 114 | label: 'Copy', 115 | selector: 'copy:' 116 | })) 117 | menu.append(new MenuItem({ 118 | label: 'Paste', 119 | selector: 'paste:' 120 | })) 121 | 122 | window.addEventListener('contextmenu', function (e) { 123 | e.preventDefault() 124 | menu.popup(remote.getCurrentWindow()) 125 | }, false) 126 | --------------------------------------------------------------------------------