├── .gitignore ├── README.md ├── app.js ├── icons ├── music-note.icns └── music-note.svg ├── itunes.js ├── package.json ├── preferences.html └── tinystore.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | Kyoku* 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Kyoku 2 | === 3 | 4 | ![Screenshot](http://i.imgur.com/nhRHJw6.png) 5 | 6 | A tiny app that shows iTunes current playing song on Mac menu bar. 7 | 8 | To download the latest version, visit the [releases page](https://github.com/cheeaun/kyoku/releases). 9 | 10 | Development 11 | --- 12 | 13 | 1. `npm install` 14 | 2. `npm start` to launch the app 15 | 3. `npm run package` to package the app 16 | 4. `npm run zip` to zip the app package 17 | 18 | TODO 19 | --- 20 | 21 | - [x] Show Album and Artist info in the menu 22 | - [x] Have a preference for setting custom maximum number of characters for song title 23 | - [x] Have a nice icon 24 | - [ ] Windows version? 25 | 26 | Icon 27 | --- 28 | 29 | The music note icon is made by [Freepik](http://www.flaticon.com/authors/freepik) from [www.flaticon.com](http://www.flaticon.com) and licensed by [CC BY 3.0](http://creativecommons.org/licenses/by/3.0/). 30 | 31 | License 32 | --- 33 | 34 | [MIT](http://cheeaun.mit-license.org/). 35 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const app = global.app = electron.app; 3 | const Menu = electron.Menu; 4 | const Tray = electron.Tray; 5 | const BrowserWindow = electron.BrowserWindow; 6 | 7 | const userHome = require('user-home'); 8 | const TinyStore = require(__dirname + '/tinystore'); 9 | const store = global.store = new TinyStore(userHome + '/.kyoku'); 10 | 11 | const itunes = require(__dirname + '/itunes'); 12 | 13 | // Don't quit app when closing any spawned windows 14 | app.on('window-all-closed', function(e){ 15 | e.preventDefault(); 16 | }); 17 | 18 | var defaultTitle = '♫ Kyoku'; 19 | 20 | var menuTemplate = [ 21 | { label: 'Name', visible: false, enabled: false }, 22 | { label: 'Artist', visible: false, enabled: false }, 23 | { label: 'Album', visible: false, enabled: false }, 24 | { label: 'Preferences…', click: showOptions }, 25 | { label: 'Quit', click: app.quit } 26 | ]; 27 | 28 | var appTray, contextMenu; 29 | app.on('ready', function(){ 30 | appTray = new Tray(null); 31 | contextMenu = Menu.buildFromTemplate(menuTemplate); 32 | appTray.setTitle(defaultTitle); 33 | appTray.setContextMenu(contextMenu); 34 | }); 35 | 36 | function showOptions(){ 37 | var optionsWindow = new BrowserWindow({ 38 | width: 400, 39 | height: 200, 40 | show: false, 41 | center: true, 42 | resizable: false, 43 | fullscreen: false, 44 | 'always-on-top': true, 45 | title: 'Preferences' 46 | }); 47 | optionsWindow.loadURL('file://' + __dirname + '/preferences.html'); 48 | optionsWindow.webContents.on('did-finish-load', function(){ 49 | optionsWindow.show(); 50 | }); 51 | }; 52 | 53 | function truncateName(name, charsLimit){ 54 | if (!charsLimit) charsLimit = store.get('charsLimit') || 10; 55 | if (!charsLimit || charsLimit < 5) return name; 56 | if (name.length <= charsLimit) return name; 57 | return name.slice(0, charsLimit) + '…'; 58 | }; 59 | 60 | var currentName = '', currentState = 'paused'; 61 | 62 | itunes.on('playing', function(data){ 63 | currentState = 'playing'; 64 | currentName = data.name; 65 | appTray.setTitle('▶ ' + truncateName(currentName) + ' '); 66 | 67 | menuTemplate[0].label = 'Name: ' + data.name; 68 | menuTemplate[1].label = (data.artist) ? 'Artist: ' + data.artist : ''; 69 | menuTemplate[2].label = (data.album) ? 'Album: ' + data.album : ''; 70 | 71 | menuTemplate[0].visible = true; 72 | menuTemplate[1].visible = data.artist.length > 0; 73 | menuTemplate[2].visible = data.album.length > 0; 74 | 75 | contextMenu = Menu.buildFromTemplate(menuTemplate); 76 | 77 | appTray.setContextMenu(contextMenu); 78 | }); 79 | 80 | itunes.on('paused', function(data){ 81 | currentState = 'paused'; 82 | 83 | menuTemplate[0].visible = menuTemplate[1].visible = menuTemplate[2].visible = false; 84 | 85 | contextMenu = Menu.buildFromTemplate(menuTemplate); 86 | 87 | appTray.setTitle(defaultTitle); 88 | appTray.setContextMenu(contextMenu); 89 | }); 90 | 91 | store.on('change', function(key, value){ 92 | if (key == 'charsLimit' && currentState == 'playing'){ 93 | appTray.setTitle('▶ ' + truncateName(currentName, value) + ' '); 94 | } 95 | }); 96 | 97 | app.dock.hide(); 98 | -------------------------------------------------------------------------------- /icons/music-note.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheeaun/kyoku/9c973ebf95c63e399df73a7287160c85f45fa262/icons/music-note.icns -------------------------------------------------------------------------------- /icons/music-note.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /itunes.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | var events = require('events'); 3 | var util = require('util'); 4 | 5 | var Itunes = function(){ 6 | var self = this; 7 | events.EventEmitter.call(this); 8 | 9 | self.track = {}; 10 | 11 | var cmd = 'osascript -l JavaScript -e \'' 12 | + 'var itunes = Application("iTunes");' 13 | + 'var currentTrack = itunes.currentTrack();' 14 | + 'JSON.stringify({' 15 | + 'name: itunes.currentStreamTitle() || currentTrack.name(),' 16 | + 'artist: currentTrack.artist(),' 17 | + 'album: currentTrack.album(),' 18 | + 'playing: itunes.playerState() == "playing"' 19 | + '})\''; 20 | 21 | var fetchData = function(){ 22 | exec(cmd, function(e, stdout, stderr){ 23 | var data = {}; 24 | try { data = JSON.parse(stdout); } catch (e) {} 25 | if (!data.playing){ 26 | if (data.playing != self.track.playing){ 27 | self.track = {playing: false}; 28 | self.emit('paused'); 29 | } 30 | return; 31 | } 32 | if (data.playing != self.track.playing){ 33 | self.track = data; 34 | self.emit('playing', data); 35 | return; 36 | } 37 | if (data.name != self.track.name || data.album != self.track.album || data.artist != self.track.artist){ 38 | self.track = data; 39 | self.emit('playing', data); 40 | } 41 | }); 42 | }; 43 | 44 | fetchData(); 45 | setInterval(fetchData, 1000); 46 | } 47 | 48 | util.inherits(Itunes, events.EventEmitter); 49 | 50 | module.exports = new Itunes(); 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kyoku", 3 | "version": "0.0.6", 4 | "description": "A tiny app that shows iTunes current playing song on Mac menu bar", 5 | "main": "app.js", 6 | "keywords": [ 7 | "electron-app", 8 | "itunes", 9 | "mac", 10 | "menu", 11 | "menubar", 12 | "song" 13 | ], 14 | "author": "Lim Chee Aun ", 15 | "license": "MIT", 16 | "dependencies": { 17 | "user-home": "~2.0.0" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/cheeaun/kyoku" 22 | }, 23 | "devDependencies": { 24 | "electron-packager": "~7.0.1", 25 | "electron-prebuilt": "~1.0.1" 26 | }, 27 | "scripts": { 28 | "start": "electron .", 29 | "package": "electron-packager . Kyoku --overwrite --prune --platform=darwin --arch=x64 --version=0.34.0 --ignore=.DS_Store --ignore=icons --ignore=Kyoku.app --ignore=node_modules/.bin --app-version=$npm_package_version --icon=icons/music-note.icns", 30 | "zip": "cd Kyoku-darwin-x64 && zip -ryXq Kyoku.app.zip Kyoku.app" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /preferences.html: -------------------------------------------------------------------------------- 1 | 2 | 30 |
31 |
32 | 33 |
34 | Set this to limit the number of characters of the current song title displayed. Set to no value for no limit. 35 |
36 |
37 |
38 | 41 | 66 | -------------------------------------------------------------------------------- /tinystore.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var util = require('util'); 3 | var fs = require('fs'); 4 | 5 | var TinyStore = function(path){ 6 | this.path = path; 7 | }; 8 | 9 | util.inherits(TinyStore, EventEmitter); 10 | 11 | TinyStore.prototype.set = function(key, value, callback){ 12 | var json; 13 | try { 14 | var content = fs.readFileSync(this.path, { encoding: 'utf-8' }); 15 | json = JSON.parse(content); 16 | } catch (e){ 17 | json = {}; 18 | } 19 | if (value !== json[key]){ 20 | json[key] = value; 21 | this.emit('change', key, value); 22 | } 23 | fs.writeFile(this.path, JSON.stringify(json), callback); 24 | }; 25 | 26 | TinyStore.prototype.get = function(key, callback){ 27 | var content; 28 | try { 29 | content = fs.readFileSync(this.path, { encoding: 'utf-8' }); 30 | } catch (e){ 31 | return null; 32 | } 33 | var json = JSON.parse(content); 34 | return json[key]; 35 | }; 36 | 37 | module.exports = TinyStore; 38 | --------------------------------------------------------------------------------