diff --git a/MMM-Spotify.css b/MMM-Spotify.css index 19ee9107c0..fb66dada72 100644 --- a/MMM-Spotify.css +++ b/MMM-Spotify.css @@ -1,14 +1,17 @@ - + #SPOTIFY { position: relative; width:360px; border-radius:10px; height:480px; + box-sizing:border-box; } + + #SPOTIFY_BACKGROUND { background-size: cover; - background-position: center top; + background-position: center center; filter: blur(32px) opacity(80%) grayscale(30%); position:absolute; top:0; @@ -26,6 +29,8 @@ width:100%; height:100%; z-index:2; + display:flex; + flex-direction:column; } #SPOTIFY_INFO { @@ -45,6 +50,15 @@ color:#FFF; font-weight:bold; font-size:22px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#SPOTIFY_ARTIST { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } #SPOTIFY_COVER { @@ -62,3 +76,77 @@ #SPOTIFY.pausing #SPOTIFY_FOREGROUND { filter:brightness(50%); } + +#SPOTIFY_PROGRESS_TIME { + margin:0 20px 0 20px; + display:flex; + flex-direction:row; + justify-content:space-between; +} + +#SPOTIFY_PROGRESS_END { + +} + +#SPOTIFY_PROGRESS_CURRENT { + +} + + + +#SPOTIFY_PROGRESS_BAR { + margin: 0 20px 10px 20px; + background-color:#666; + height:10px; + position:relative; +} + + +#SPOTIFY_PROGRESS_BAR_NOW { + background-color:#FFF; + position:absolute; + top:0; + left:0; + height:10px; +} + + +/* style "mini" */ +#SPOTIFY.mini { + width:360px; + height:120px; +} + +#SPOTIFY.mini #SPOTIFY_FOREGROUND { + flex-direction:row; +} + +#SPOTIFY.mini #SPOTIFY_COVER { + min-width:120px; + width:120px; + height:120px; + padding:0; +} + +#SPOTIFY.mini #SPOTIFY_COVER_IMAGE { + height:120px; +} + +#SPOTIFY.mini #SPOTIFY_INFO { + width:240px; + padding-left:10px; +} + +#SPOTIFY.mini #SPOTIFY_PROGRESS_BAR { + width:240px; + margin:0; + margin-bottom:10px; +} + +#SPOTIFY.mini #SPOTIFY_PROGRESS_TIME { + margin:0; +} + +#SPOTIFY.mini #SPOTIFY_INFO .fas { + margin:0; +} diff --git a/MMM-Spotify.js b/MMM-Spotify.js index 17a4bdf9c2..7d385c82b5 100644 --- a/MMM-Spotify.js +++ b/MMM-Spotify.js @@ -3,9 +3,21 @@ // Module.register("MMM-Spotify", { default: { - defaultPlayer: "RASPOTIFY", - - updateInterval: 2000, + style: "default", // "default", "mini" available. + updateInterval: 1000, + onStart: null, + //If you want to play something on start; set like this. + /* + onStart: { + deviceName: "Web Player (Chrome)", //if null, current(last) activated device will be. + spotifyUri : "spotify:playlist:37i9dQZF1DX9EM98aZosoy", //when search is set, sportifyUri will be ignored. + search: { + type: "artist, track", // `artist`, track`, `album`, `playlist` available + keyword: "michael+jackson", + random:true, + } + } + */ }, getStyles: function() { @@ -19,24 +31,24 @@ Module.register("MMM-Spotify", { notificationReceived: function(noti, payload, sender) { if (noti == "DOM_OBJECTS_CREATED") { this.sendSocketNotification("INIT", this.config) + this.onStart() } switch(noti) { case "SPOTIFY_SEARCH": var pl = { query: { - q:"michael+jackson", - type:"artist", + q: payload.query, + type: payload.type, }, condition: { - random:false, + random:payload.random, autoplay:true, } } this.sendSocketNotification("SEARCH_AND_PLAY", pl) break case "SPOTIFY_PLAY": - var pl = {context_uri:"spotify:playlist:37i9dQZF1DX9EM98aZosoy"} - this.sendSocketNotification("PLAY", pl) + this.sendSocketNotification("PLAY", payload) break case "SPOTIFY_PAUSE": this.sendSocketNotification("PAUSE") @@ -48,9 +60,10 @@ Module.register("MMM-Spotify", { this.sendSocketNotification("PREVIOUS") break case "SPOTIFY_VOLUME": - var pl = 50 - this.sendSocketNotification("VOLUME", pl) + this.sendSocketNotification("VOLUME", payload) break + case "SPOTIFY_TRANSFER": + this.sendSocketNotification("TRANSFER", payload) } }, @@ -64,6 +77,40 @@ Module.register("MMM-Spotify", { } }, + onStart: function() { + if (!this.config.onStart) return + this.sendSocketNotification("ONSTART", this.config.onStart) + }, + + updateProgress: function( + current, + end = document.getElementById("SPOTIFY_PROGRESS_END"), + curbar = document.getElementById("SPOTIFY_PROGRESS_CURRENT"), + now = document.getElementById("SPOTIFY_PROGRESS_BAR_NOW") + ) { + var msToTime = (duration) => { + var ret = "" + var milliseconds = parseInt((duration%1000)/100) + , seconds = parseInt((duration/1000)%60) + , minutes = parseInt((duration/(1000*60))%60) + , hours = parseInt((duration/(1000*60*60))%24) + if (hours > 0) { + hours = (hours < 10) ? "0" + hours : hours + ret = ret + hours + ":" + } + minutes = (minutes < 10) ? "0" + minutes : minutes + seconds = (seconds < 10) ? "0" + seconds : seconds + return ret + minutes + ":" + seconds + } + var songDur = current.item.duration_ms + var cur = current.progress_ms + var pros = (cur / songDur) * 100 + + end.innerHTML = msToTime(songDur) + curbar.innerHTML = msToTime(cur) + now.style.width = pros + "%" + }, + updateCurrentPlayback: function(current) { if (!current) return var isChanged = false @@ -76,7 +123,10 @@ Module.register("MMM-Spotify", { } else if (this.currentPlayback.device.id !== current.device.id) { isChanged = true } else if (this.currentPlayback.progress_ms !== current.progress_ms) { - isChanged = true + //isChanged = true //It would make too many updateDom. + //It's better to manipulate Dom directly + this.currentPlayback = current + this.updateProgress(current) } if (isChanged) { @@ -88,12 +138,8 @@ Module.register("MMM-Spotify", { getDom: function(){ var m = document.createElement("div") m.id = "SPOTIFY" - if (this.currentPlayback) { - if (this.currentPlayback.is_playing) { - m.className = "playing" - } else { - m.className = "pausing" - } + if (this.config.style !== "default") { + m.classList.add(this.config.style) } var back = document.createElement("div") @@ -108,44 +154,77 @@ Module.register("MMM-Spotify", { var cover_img = document.createElement("img") cover_img.id = "SPOTIFY_COVER_IMAGE" - if (this.currentPlayback) { - cover_img.src = this.currentPlayback.item.album.images[0].url - back.style.backgroundImage = `url(${this.currentPlayback.item.album.images[0].url})` - } else { - cover_img.src = "./modules/MMM-Spotify/spotify-xxl.png" - } + cover_img.src = "./modules/MMM-Spotify/resources/spotify-xxl.png" cover.appendChild(cover_img) fore.appendChild(cover) - var info = document.createElement("div") - info.id = "SPOTIFY_INFO" - - var title = document.createElement("div") - title.id = "SPOTIFY_TITLE" - - var artist = document.createElement("div") - artist.id = "SPOTIFY_ARTIST" - - var device = document.createElement("div") - device.id = "SPOTIFY_DEVICE" - - var progress = document.createElement("div") - progress_ms = "PROGRESS_BAR" - -// var time_ms = this.currentPlayback.progress_ms - - if (this.currentPlayback) { - title.innerHTML = `` + " " + this.currentPlayback.item.name - artist.innerHTML = `` + " " + this.currentPlayback.item.artists[0].name + var info = document.createElement("div") + info.id = "SPOTIFY_INFO" + + var title = document.createElement("div") + title.id = "SPOTIFY_TITLE" + + var artist = document.createElement("div") + artist.id = "SPOTIFY_ARTIST" + + var device = document.createElement("div") + device.id = "SPOTIFY_DEVICE" + + var progress = document.createElement("div") + progress.id = "SPOTIFY_PROGRESS" + var currentTime = document.createElement("div") + currentTime.id = "SPOTIFY_PROGRESS_CURRENT" + currentTime.innerHTML = "--:--" + var songTime = document.createElement("div") + songTime.id = "SPOTIFY_PROGRESS_END" + songTime.innerHTML = "--:--" + var time = document.createElement("div") + time.id = "SPOTIFY_PROGRESS_TIME" + time.appendChild(currentTime) + time.appendChild(songTime) + progress.appendChild(time) + var bar = document.createElement("div") + bar.id = "SPOTIFY_PROGRESS_BAR" + var barNow = document.createElement("div") + barNow.id = "SPOTIFY_PROGRESS_BAR_NOW" + bar.appendChild(barNow) + progress.appendChild(bar) + + this.updateProgress(this.currentPlayback, songTime, currentTime, barNow) + + if (this.currentPlayback.is_playing) { + m.classList.add("playing") + m.classList.remove("pausing") + } else { + m.classList.add("pausing") + m.classList.remove("playing") + } + if (this.currentPlayback.item) { + cover_img.src = this.currentPlayback.item.album.images[0].url + back.style.backgroundImage = `url(${this.currentPlayback.item.album.images[0].url})` + //progress.innerHTML = `` + " " + Math.floor(this.currentPlayback.progress_ms / 60000) + ":" + (((this.currentPlayback.progress_ms % 60000) / 1000).toFixed(0)-1) + " / " + Math.floor(this.currentPlayback.item.duration_ms / 60000) + ":" + (((this.currentPlayback.item.duration_ms % 60000) / 1000).toFixed(0)-1) } + title.innerHTML = `` + " " + this.currentPlayback.item.name + var artists = this.currentPlayback.item.artists + var artistName = "" + for (var x = 0; x < artists.length; x++) { + if (!artistName) { + artistName = artists[x].name + } else { + artistName += ", " + artists[x].name + } + } + artist.innerHTML = `` + " " + artistName + } device.innerHTML = `` + " " + this.currentPlayback.device.name - progress.innerHTML = `` + " " + Math.floor(this.currentPlayback.progress_ms / 60000) + ":" + (((this.currentPlayback.progress_ms % 60000) / 1000).toFixed(0)-1) + " / " + Math.floor(this.currentPlayback.item.duration_ms / 60000) + ":" + (((this.currentPlayback.item.duration_ms % 60000) / 1000).toFixed(0)-1) } - info.appendChild(title) - info.appendChild(artist) - info.appendChild(device) - info.appendChild(progress) - fore.appendChild(info) + info.appendChild(progress) + info.appendChild(title) + info.appendChild(artist) + info.appendChild(device) + fore.appendChild(info) + } + m.appendChild(fore) return m }, diff --git a/README.md b/README.md index b87e01bd85..386c9c58fd 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,137 @@ # MMM-Spotify +Spotify controller for MagicMirror -## TODO -- Spotify Connect. (Impossible without Raspotify on Raspberry Pi at this moment. Damn Widevine missing!!!) -- Change connected devices by condition. (Like, when you are coming home, automatically changing device to Raspotify) -- Control current spotify player with notification -- Display Track info -- Play spotify_uri by notification. (Your playlist, 90's hit..., specific song...) -- Search and play. +## Screenshot +- ![default](screenshots/spotify_default.png) +- ![mini](screenshots/spotify_mini.png) + +## Main Features +- Showing Current playback +- Playing Controllable by Notification (Play, pause, next, previous, volume) +- Spotify Controllable by Notification (change device, search and play) + +## Install +### 1. module install +```sh +cd ~/MagicMirror/modules +git clone https://github.com/eouia/MMM-Spotify +cd MMM-Spotify +npm install +``` + +### 2. Setup Spotify +- You should be a premium member of Spotify +1. Go to https://developer.spotify.com +2. Navigate to **DASHBOARD** > **Create an app** (fill information as your thought) +3. Setup the app created, (**EDIT SETTINGS**) + - Redirect URIs. : `http://localhost:8888/callback` + - That's all you need. Just save it. +4. Now copy your **Client ID** and **Client Secret** to any memo + +### 3. Setup your module. +```sh +cd ~/MagicMirror/modules/MMM-Spotify +cp spotify.config.json.example spotify.config.json +nano spotify.config.json +``` +Or any editor as your wish be ok. Open the `spotify.config.json` then modify it. You need to just fill `CLIENT_ID` and `CLIENT_SECRET`. Then, save it. +```json +{ + "CLIENT_ID" : "YOUR_CLIENT_ID", + "CLIENT_SECRET" : "YOUR_CLIENT_SECRET", + "AUTH_DOMAIN" : "http://localhost", + "AUTH_PATH" : "/callback", + "AUTH_PORT" : "8888", + "SCOPE" : "user-read-private playlist-read-private streaming user-read-playback-state user-modify-playback-state", + "TOKEN" : "./token.json" +} +``` + +### 4. Get Auth +```sh +cd ~/MagicMirror/modules/MMM-Spotify +node first_auth.js +``` +Then, Allowance dialog popup will be opened. Log in(if it is needed) and allow it. +That's all. `token.json` will be created, if success. + + +## Configuration +### Simple +```js +{ + module: "MMM-Spotify", + position: "bottom_left", + config: { + + } +} +``` + +### Detail & Default +```js +{ + module: "MMM-Spotify", + position: "bottom_left", + config: { + style: "default", // "default" or "mini" available + updateInterval: 1000, + onStart: null, // disable onStart feature with `null` + } +} +``` + +### `onStart` feature +You can control Spotify on start of MagicMirror (By example; Autoplay specific playlist when MM starts) +```js +onStart { + onStart: { + deviceName: "RASPOTIFY", //if null, current(last) activated device will be. + spotifyUri: "spotify:track:3ENXjRhFPkH8YSH3qBXTfQ" + //when search is set, sportifyUri will be ignored. + search: { + type: "playlist", // `artist`, track`, `album`, `playlist` and its combination(`artist,playlist,album`) be available + keyword: "death metal", + random:true, + } + } +} +``` +When `search` field exists, `spotifyUri` will be ignored. + + +## Control with notification +- `SPOTIFY_SEARCH` : search items with query and play it. `type`, `query`, `random` be payloads +``` + this.sendNotification("SPOTIFY_SEARCH", {type:"artist,playlist", query:"michael+jackson", random:false}) +``` +- `SPOTIFY_PLAY` : playing specific SpotifyUri. +``` + this.sendNotification("SPOTIFY_PLAY", "spotify:track:3ENXjRhFPkH8YSH3qBXTfQ") +``` +This notification also be used as `resume` reature of stopped player without payloads +- `SPOTIFY_PAUSE` : pausing current playback. +``` + this.sendNotification("SPOTIFY_PAUSE") +``` +- `SPOTIFY_PAUSE` : pausing current playback. +``` + this.sendNotification("SPOTIFY_PAUSE") +``` +- `SPOTIFY_NEXT` : next track of current playback. +``` + this.sendNotification("SPOTIFY_NEXT") +``` +- `SPOTIFY_PREVIOUS` : previous track of current playback. +``` + this.sendNotification("SPOTIFY_PREVIOUS") +``` +- `SPOTIFY_VOLUME` : setting volume of current playback. payload will be volume (0 - 100) +``` + this.sendNotification("SPOTIFY_VOLUME", 50) +``` + +- `SPOTIFY_TRANSFER` : change device of playing with device name (e.g: RASPOTIFY) +``` + this.sendNotification("SPOTIFY_TRANSFER", "RASPOTIFY") +``` diff --git a/Spotify.js b/Spotify.js index bc75cdad4d..b9dc567444 100644 --- a/Spotify.js +++ b/Spotify.js @@ -180,6 +180,10 @@ class Spotify { if (error) { console.log(`[SPOTIFY] API Request fail on :`, api) console.log(error, body) + } else { + if (api !== "/v1/me/player" && type !== "GET") { + console.log(`[SPOTIFY] API Requested:`, api) + } } if (cb) { cb(response.statusCode, error, body) @@ -220,24 +224,42 @@ class Spotify { this.doRequest("/v1/me/player/seek", "PUT", null, {position_ms:0}, cb) } - search(obj, cb) { - var param = obj.query + search(param, cb) { param.limit = 50 this.doRequest("/v1/search", "GET", param, null, cb) } - // Not yet implemented transfer(req, cb) { + if (req.device_ids.length > 1) { + req.device_ids = [req.device_ids[0]] + } + this.doRequest("/v1/me/player", "PUT", null, req, cb) + } + transferByName(device_name, cb) { + this.getDevices((code, error, result)=>{ + if (code == 200) { + var devices = result.devices + for (var i = 0; i < devices.length; i++) { + if (devices[i].name == device_name) { + this.transfer({device_ids:[devices[i].id]}, cb) + return + } + } + } else { + cb(code, error, result) + } + }) } - // Not yet implemented volume(param, cb) { - + if (param.volume_percent) { + if (param.volume_percent > 100 || param.volume_percent < 0) { + param.volume_percent = 50 + } + } + this.doRequest("/v1/me/player/volume", "PUT", param, null, cb) } } - - - module.exports = Spotify diff --git a/node_helper.js b/node_helper.js index f71f490919..55a6feb0e7 100644 --- a/node_helper.js +++ b/node_helper.js @@ -46,6 +46,27 @@ module.exports = NodeHelper.create({ this.sendSocketNotification("INITIALIZED") } + if (noti == "ONSTART") { + payload.position_ms = 0 + if (payload.search) { + var param = { + q: payload.search.keyword, + type: payload.search.type, + } + var condition = { + random: payload.search.random, + autoplay: true, + } + this.searchAndPlay(param, condition) + + } else if (payload.spotifyUri.match("track")) { + this.spotify.play({uris:[payload.spotifyUri]}) + } else if (payload.spotifyUri) { + this.spotify.play({context_uri:payload.spotifyUri}) + } + if (payload.deviceName) this.spotify.transferByName(payload.deviceName) + } + if (noti == "GET_DEVICES") { this.spotify.getDevices((code, error, result)=>{ this.sendSocketNotification("LIST_DEVICES", result) @@ -81,49 +102,59 @@ module.exports = NodeHelper.create({ } if (noti == "SEARCH_AND_PLAY") { - var pickup = (items, random, retType)=>{ - var ret = {} - var r = null - r = (random) ? items[Math.floor(Math.random() * items.length)] : items[0] - ret[retType] = (retType == "uris") ? [r.uri] : r.uri - return ret - } - this.spotify.search(payload, (code, error, result)=>{ - var foundForPlay = null - if (code == 200) { //When success - const map = { - "tracks" : "uris", - "artists" : "context_uri", - "albums" : "context_uri", - "playlists" : "context_uri" - } + this.searchAndPlay(payload.query, payload.condition) + } - for (var section in map) { - if (map.hasOwnProperty(section) && !foundForPlay) { - var retType = map[section] - if (result[section]) { - foundForPlay = pickup(result[section].items, payload.condition.random, retType) - } - } - } - console.log("FP", foundForPlay) - if (foundForPlay && payload.condition.autoplay) { - this.spotify.play(foundForPlay, (code, error, result)=>{ - console.log("@", code, result) - if (code !== 204) { - console.log("!", error) - return - } - this.sendSocketNotification("DONE_SEARCH_AUTOPLAY", result) - }) - } else { - // nothing found - this.sendSocketNotification("DONE_SEARCH_NOTHING") - } - } else { //when fail - this.sendSocketNotification("DONE_SEARCH_ERROR") - } + if (noti == "TRANSFER") { + this.spotify.transferByName(payload, (code, error, result)=>{ + this.sendSocketNotification("DONE_TRANSFER", result) }) } }, + + searchAndPlay: function(param, condition) { + var pickup = (items, random, retType)=>{ + var ret = {} + var r = null + r = (random) ? items[Math.floor(Math.random() * items.length)] : items[0] + ret[retType] = (retType == "uris") ? [r.uri] : r.uri + return ret + } + this.spotify.search(param, (code, error, result)=>{ + //console.log(code, error, result) + var foundForPlay = null + if (code == 200) { //When success + const map = { + "tracks" : "uris", + "artists" : "context_uri", + "albums" : "context_uri", + "playlists" : "context_uri" + } + //console.log(result) + for (var section in map) { + if (map.hasOwnProperty(section) && !foundForPlay) { + var retType = map[section] + if (result[section]) { + foundForPlay = pickup(result[section].items, condition.random, retType) + } + } + } + //console.log(foundForPlay) + if (foundForPlay && condition.autoplay) { + this.spotify.play(foundForPlay, (code, error, result)=>{ + if (code !== 204) { + return + } + this.sendSocketNotification("DONE_SEARCH_AUTOPLAY", result) + }) + } else { + // nothing found + this.sendSocketNotification("DONE_SEARCH_NOTHING") + } + } else { //when fail + console.log(code, error, result) + this.sendSocketNotification("DONE_SEARCH_ERROR") + } + }) + } }) diff --git a/spotify-xxl.png b/resources/spotify-xxl.png similarity index 100% rename from spotify-xxl.png rename to resources/spotify-xxl.png diff --git a/screenshots/spotify_default.png b/screenshots/spotify_default.png new file mode 100644 index 0000000000..88d106c6ee Binary files /dev/null and b/screenshots/spotify_default.png differ diff --git a/screenshots/spotify_mini.png b/screenshots/spotify_mini.png new file mode 100644 index 0000000000..7b623410ff Binary files /dev/null and b/screenshots/spotify_mini.png differ