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