├── 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 | [](https://travis-ci.org/uiureo/playing)
3 |
4 | :musical_note: Send what you're playing on iTunes to Slack.
5 |
6 | 
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 | 
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 |
--------------------------------------------------------------------------------