├── version ├── Icon.icns ├── Icon.ico ├── icon.png ├── icon@2x.png ├── main.css ├── LICENSE ├── package.json ├── index.html ├── readme.md ├── inject.js ├── app.js └── main.js /version: -------------------------------------------------------------------------------- 1 | 0.0.5 2 | -------------------------------------------------------------------------------- /Icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevchentw/CI/HEAD/Icon.icns -------------------------------------------------------------------------------- /Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevchentw/CI/HEAD/Icon.ico -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevchentw/CI/HEAD/icon.png -------------------------------------------------------------------------------- /icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevchentw/CI/HEAD/icon@2x.png -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | .button-icon { 2 | display: inline-block; 3 | font-size: 1.2rem; 4 | font-weight: 200; 5 | border-radius: 3px; 6 | margin-left: .5em; 7 | opacity: .8; 8 | white-space: nowrap; 9 | overflow: hidden; 10 | text-overflow: ellipsis; 11 | background: transparent; 12 | border-color: transparent; 13 | flex: 1; 14 | } 15 | 16 | .button-icon:hover { 17 | background: #f0f0f0; 18 | } 19 | 20 | .icon { 21 | color: #999 !important; 22 | font-size: 1.5em; 23 | position: relative; 24 | top: 0; 25 | } 26 | 27 | .buttons-bar { 28 | display: flex; 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present C. T. Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SpotifyLyrics", 3 | "version": "0.0.4", 4 | "description": "An small app to show lyrics from spotify", 5 | "dependencies": { 6 | "menubar": "*", 7 | "electron-rpc": "2.0.1", 8 | "superagent": "2.1.0", 9 | "@jonny/spotify-web-helper": "1.0.4", 10 | "remove-accents": "0.3.0", 11 | "node-version-compare": "1.0.1" 12 | }, 13 | "devDependencies": { 14 | "electron-packager": "^5.1.0", 15 | "electron-prebuilt": "^0.36.0" 16 | }, 17 | "main": "main.js", 18 | "scripts": { 19 | "build": "electron-packager . Example --platform=darwin --arch=x64 --version=0.36.12 --icon=Icon.icns", 20 | "start": "electron .", 21 | "build_mac": "electron-packager . SpotifyLyrics --platform=darwin --arch=x64 --version=0.36.12 --icon=Icon.icns --overwrite", 22 | "build_win32": "electron-packager . SpotifyLyrics --platform=win32 --arch=x64 --version=0.36.12 --icon=Icon.icns --asar=true --overwrite --ignore=node_modules/electron-* ", 23 | "build_linux": "electron-packager . SpotifyLyrics --platform=linux --arch=x64 --version=0.36.12 --icon=Icon.icns --asar=true --overwrite --ignore=node_modules/electron-* " 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SpotifyLyrics 5 | 6 | 7 | 8 | 9 | 11 | 12 |
13 | 16 | 19 | 22 | 25 | 28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # CI 2 | 3 | CI shows lyrics that you are currently playing on Spotify. 4 | 5 | Ci(詞) is a Chinese Character, which means Classical Chinese lyric poetry. 6 | 7 | 8 | 9 | ## Screenshot 10 | 11 | 12 | 13 | ## Usage 14 | 15 | CI stays at your menu bar, click the icon then CI will show the lyrics intanstly! 16 | 17 | There are four buttons on it, which are Leave, Play/Pause, Reload Spotify, Pinned. 18 | 19 | If the lyrics didn't show up, or it didn't refresh to the new song's lyrics, press Reload to reconnect Spotify. 20 | 21 | Jumping circles means that the lyrics is not found. 22 | 23 | 24 | 25 | ## Platform 26 | 27 | It supports Mac, Windows, and Linux. (Thanks to Electron!) 28 | 29 | ## Download 30 | 31 | Checkout our [Releases Page](https://github.com/kevchentw/CI/releases) 32 | 33 | ## Bug Report 34 | Open a issue on this repo. 35 | Or send me [Facebook message](https://www.facebook.com/kevchentw) 36 | 37 | ## Dev Instructions 38 | 39 | - run `npm install` 40 | - run `npm run build` to makeapp 41 | - run `npm start` to run app from CLI without building app 42 | - run `electron-packager . --all --overwrite --icon=Icon` to build Linux, Mac, Windows App 43 | 44 | ## License 45 | MIT © [Kevin Chen](https://github.com/kevchentw) 46 | -------------------------------------------------------------------------------- /inject.js: -------------------------------------------------------------------------------- 1 | const ipc = require('electron').ipcRenderer 2 | 3 | function get_page_type() { 4 | var url = window.location.href; 5 | var type_lyrics = url.match("https:\/\/www.musixmatch.com\/lyrics\/(.*)\/embed") 6 | var type_black = url.match("about:blank") 7 | if (type_lyrics) { 8 | return { 9 | 'type': 'lyrics', 10 | 'url': url 11 | } 12 | } else if (type_black) { 13 | return { 14 | 'type': 'blank', 15 | 'url': url 16 | } 17 | } else { 18 | return { 19 | 'type': 'unknown', 20 | 'url': url 21 | } 22 | } 23 | } 24 | 25 | var page_type = get_page_type(); 26 | 27 | function ready(fn) { 28 | if (document.readyState != 'loading') { 29 | fn(); 30 | } else { 31 | document.addEventListener('DOMContentLoaded', fn); 32 | } 33 | } 34 | 35 | ready(init) 36 | 37 | function init() { 38 | var lyrics_exist = check_lyrics_exist() 39 | if (page_type['type'] == 'lyrics') { 40 | if(lyrics_exist){ 41 | ipc.sendToHost('lyrics_exist', {'exist': true}) 42 | } 43 | else{ 44 | ipc.sendToHost('lyrics_exist', {'exist': false}) 45 | } 46 | } 47 | console.log(lyrics_exist); 48 | } 49 | 50 | function check_lyrics_exist() { 51 | if (page_type['type'] == 'lyrics') { 52 | var exist = document.getElementsByClassName('track-widget-header') 53 | if (exist.length) { 54 | return true; 55 | } 56 | } 57 | return false 58 | } 59 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var Client = require('electron-rpc/client') 2 | var client = new Client() 3 | 4 | const webview = document.getElementById('view'); 5 | 6 | webview.addEventListener('ipc-message', (event, args) => { 7 | console.log(event.channel) 8 | console.log(event) 9 | if(event.channel=='lyrics_exist'){ 10 | console.log('exist') 11 | if(!event.args[0].exist){ 12 | client.request('get_lyrics_from_best_result'); 13 | } 14 | } 15 | }) 16 | 17 | client.on('new_track', function(err, data) { 18 | if (webview){ 19 | view.src=encodeURI(data.url); 20 | } 21 | console.log(encodeURI(data.url)); 22 | }) 23 | 24 | client.on('player_status_change', function (err, data) { 25 | var icon = document.getElementById('iconPlay'); 26 | if(data.status){ 27 | icon.innerHTML = 'pause'; 28 | } 29 | else{ 30 | icon.innerHTML = 'play_arrow'; 31 | } 32 | console.log("player_status_change") 33 | }) 34 | 35 | function quit() { 36 | client.request('terminate'); 37 | } 38 | 39 | function dev() { 40 | client.request('dev'); 41 | } 42 | 43 | function refresh_spotify() { 44 | client.request('refresh_spotify'); 45 | } 46 | 47 | function pinned() { 48 | client.request('pinned', {}, function (err, data){ 49 | if (err) return; 50 | var icon = document.getElementById('iconPinned'); 51 | if(data.new_pinned_status){ 52 | icon.innerHTML = 'lock'; 53 | } 54 | else{ 55 | icon.innerHTML = 'lock_open'; 56 | } 57 | }); 58 | } 59 | 60 | function playPause() { 61 | client.request('play_pause'); 62 | } 63 | 64 | webview.addEventListener('dom-ready', () => { 65 | console.log('dom ready'); 66 | // webview.openDevTools() 67 | }) 68 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var menubar = require('menubar') 2 | var accents = require('remove-accents') 3 | var dialog = require('dialog'); 4 | const shell = require('electron').shell; 5 | 6 | var mb = menubar({ 7 | width: 415, 8 | height: 550, 9 | resizable: false, 10 | icon: __dirname + '/icon.png' 11 | }) 12 | var SpotifyWebHelper = require('@jonny/spotify-web-helper') 13 | var helper = SpotifyWebHelper() 14 | var compare = require('node-version-compare'); 15 | var VERSION_URL = "https://raw.githubusercontent.com/kevchentw/CI/master/version" 16 | var RELEASE_URL = "https://github.com/kevchentw/CI/releases" 17 | var request = require('superagent'); 18 | var Server = require('electron-rpc/server') 19 | var app = new Server() 20 | var mb_ready = false; 21 | var spotify_ready = false; 22 | 23 | function check_new_version() { 24 | request 25 | .get(VERSION_URL) 26 | .end(function(err, res) { 27 | var now_version = mb.app.getVersion() 28 | var lastest_version = res.text 29 | var result = compare(lastest_version, now_version) 30 | if (result > 0) { 31 | var index = dialog.showMessageBox(mb.window, { 32 | type: 'info', 33 | buttons: ['Download', 'Cancel'], 34 | title: "New Version Available", 35 | message: 'New Version Available', 36 | detail: `You are currently on v${now_version}, update to v${lastest_version} to try out new features!` 37 | }); 38 | if (index == 1) { 39 | return 40 | } else if (index == 0) { 41 | shell.openExternal(RELEASE_URL); 42 | } 43 | } 44 | }); 45 | } 46 | 47 | function get_lyrics_from_best_result() { 48 | var track = helper.status.track 49 | request 50 | .get(`https://www.musixmatch.com/search/${encodeURI(track.artist_resource.name)}%20${encodeURI(track.track_resource.name)}`) 51 | .end(function(err, res) { 52 | var url = res.text.match(' 0) { 54 | success = true; 55 | var data = { 56 | 'url': `https://www.musixmatch.com${url[1]}/embed`, 57 | 'track': track 58 | }; 59 | if (mb_ready) { 60 | app.send('new_track', data); 61 | } 62 | } else { 63 | request 64 | .get(`https://www.musixmatch.com/search/${encodeURI(parseString(track.artist_resource.name))}%20${encodeURI(parseString(track.track_resource.name))}`) 65 | .end(function(err, res) { 66 | var url = res.text.match(' 0) { 68 | success = true; 69 | var data = { 70 | 'url': `https://www.musixmatch.com${url[1]}/embed`, 71 | 'track': track 72 | }; 73 | if (mb_ready) { 74 | app.send('new_track', data); 75 | } 76 | } 77 | }); 78 | } 79 | }); 80 | } 81 | 82 | function replaceDot(s) { 83 | return s.replace(".", "-2").trim(); 84 | } 85 | 86 | function removeBrackets(s) { 87 | return s.replace(/ *\([^)]*\) */g, "").trim(); 88 | } 89 | 90 | function slugify(s) { 91 | return s.split(/'| |,|\.|-|&|!/).filter(Boolean).join("-").trim(); 92 | } 93 | 94 | function removeDash(s) { 95 | return s.split('-')[0].trim(); 96 | } 97 | 98 | function parseString(s) { 99 | s = removeDash(s); 100 | s = removeBrackets(s); 101 | s = replaceDot(s); 102 | s = accents(s); 103 | s = slugify(s) 104 | s = s.trim(); 105 | return s; 106 | } 107 | 108 | function generate_musixmatch_url(track) { 109 | return `https://www.musixmatch.com/lyrics/${parseString(track.artist_resource.name)}/${parseString(track.track_resource.name)}/embed` 110 | } 111 | 112 | function initSpotify() { 113 | helper.player.on('ready', function() { 114 | spotify_ready = true; 115 | send_new_track(helper.status.track); 116 | send_player_status_change(helper.status.playing); 117 | helper.player.on('play', function() { 118 | send_player_status_change(true); 119 | }) 120 | 121 | helper.player.on('pause', function() { 122 | send_player_status_change(false); 123 | }) 124 | 125 | helper.player.on('end', function() { 126 | send_player_status_change(helper.status.playing); 127 | }) 128 | 129 | helper.player.on('track-change', function(track) { 130 | send_player_status_change(helper.status.playing); 131 | send_new_track(track); 132 | }) 133 | 134 | helper.player.on('error', function(err) {}) 135 | }); 136 | } 137 | 138 | function send_pinned_status() { 139 | var pinned_status = { 140 | 'status': mb.getOption('alwaysOnTop') 141 | } 142 | if (mb_ready) { 143 | app.send('pinned_status', pinned_status); 144 | } 145 | } 146 | 147 | function send_player_status_change(s) { 148 | var player_status = { 149 | 'status': s 150 | } 151 | if (mb_ready) { 152 | app.send('player_status_change', player_status); 153 | } 154 | } 155 | 156 | function send_new_track(track) { 157 | var data = { 158 | 'url': generate_musixmatch_url(track), 159 | 'track': track 160 | }; 161 | if (mb_ready) { 162 | app.send('new_track', data); 163 | } 164 | } 165 | 166 | 167 | // menubar event listener 168 | mb.on('ready', function ready() {}) 169 | 170 | mb.on('show', function ready() { 171 | send_pinned_status(); 172 | }) 173 | 174 | mb.on('after-create-window', function show() { 175 | app.configure(mb.window.webContents); 176 | check_new_version() 177 | mb_ready = true; 178 | initSpotify(); 179 | // mb.window.openDevTools(); 180 | }) 181 | 182 | 183 | // rpc 184 | app.on('terminate', function(ev) { 185 | mb.app.quit(); 186 | }) 187 | 188 | app.on('dev', function(ev) { 189 | mb.window.openDevTools(); 190 | }) 191 | 192 | app.on('refresh_lyrics', function(ev) { 193 | if (spotify_ready) { 194 | refresh_lyrics(helper.status.track); 195 | } 196 | }) 197 | 198 | app.on('refresh_spotify', function(ev) { 199 | helper = SpotifyWebHelper(); 200 | spotify_ready = false; 201 | initSpotify(); 202 | }) 203 | 204 | app.on('pinned', function(req, next) { 205 | var next_pinned_status = !mb.getOption('alwaysOnTop'); 206 | mb.setOption('alwaysOnTop', next_pinned_status); 207 | next(null, { 208 | 'new_pinned_status': next_pinned_status 209 | }); 210 | mb.window.setAlwaysOnTop(next_pinned_status) 211 | }) 212 | 213 | app.on('play_pause', function(ev) { 214 | if (spotify_ready) { 215 | if (helper.status.playing) { 216 | helper.player.pause(); 217 | } else { 218 | helper.player.pause(true); 219 | } 220 | } 221 | }) 222 | 223 | app.on('get_lyrics_from_best_result', function(ev) { 224 | get_lyrics_from_best_result(); 225 | }) 226 | --------------------------------------------------------------------------------