├── .gitignore ├── htmls ├── images │ ├── Play.jpg │ ├── next.jpg │ ├── pause.jpg │ ├── previous.jpg │ ├── roonIcon.png │ └── defaultZone.jpg ├── player.html ├── browser.css ├── browser.html ├── timers.css ├── timers.html ├── player.js ├── browser.js └── timers.js ├── server.js ├── package.json ├── routes.js ├── README.md └── controllers └── roonAPI.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | config.json 3 | -------------------------------------------------------------------------------- /htmls/images/Play.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st0g1e/roon-extension-http-api/HEAD/htmls/images/Play.jpg -------------------------------------------------------------------------------- /htmls/images/next.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st0g1e/roon-extension-http-api/HEAD/htmls/images/next.jpg -------------------------------------------------------------------------------- /htmls/images/pause.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st0g1e/roon-extension-http-api/HEAD/htmls/images/pause.jpg -------------------------------------------------------------------------------- /htmls/images/previous.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st0g1e/roon-extension-http-api/HEAD/htmls/images/previous.jpg -------------------------------------------------------------------------------- /htmls/images/roonIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st0g1e/roon-extension-http-api/HEAD/htmls/images/roonIcon.png -------------------------------------------------------------------------------- /htmls/images/defaultZone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/st0g1e/roon-extension-http-api/HEAD/htmls/images/defaultZone.jpg -------------------------------------------------------------------------------- /htmls/player.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

roon webplayer

4 |
5 | Zones: 7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /htmls/browser.css: -------------------------------------------------------------------------------- 1 | div.gallery { 2 | margin: 5px; 3 | border: 1px solid #ccc; 4 | float: left; 5 | width: 150px; 6 | height: 200px; 7 | overflow: hidden; 8 | o-text-overflow: ellipsis; 9 | text-overflow: ellipsis; 10 | } 11 | 12 | div.gallery:hover { 13 | border: 1px solid #777; 14 | background-color: lightblue; 15 | } 16 | 17 | div.gallery img { 18 | width: 150px; 19 | height: 150px; 20 | } 21 | 22 | div.desc { 23 | padding: 15px; 24 | text-align: center; 25 | } 26 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | 4 | var app = express(); 5 | const bodyParser = require('body-parser'); 6 | 7 | app.use(bodyParser.urlencoded({ extended: true })); 8 | app.use(bodyParser.json()); 9 | 10 | const PORT=3001; 11 | 12 | app.use(express.static(path.join(__dirname, 'htmls'))); 13 | 14 | app.use(function(req, res, next) { 15 | res.header("Access-Control-Allow-Origin", "*"); 16 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 17 | next(); 18 | }); 19 | 20 | 21 | require('./routes')(app); 22 | 23 | app.listen(PORT); 24 | 25 | console.log('Listening on port: ' + PORT); 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "roon-http-api", 3 | "description" : "st0g1e.roon-http-api", 4 | "version" : "1.0.4", 5 | "main" : "server.js", 6 | "private" : true, 7 | "dependencies": { 8 | "node-roon-api" : "github:roonlabs/node-roon-api", 9 | "node-roon-api-image" : "github:roonlabs/node-roon-api-image", 10 | "node-roon-api-status" : "github:roonlabs/node-roon-api-status", 11 | "node-roon-api-transport" : "github:roonlabs/node-roon-api-transport", 12 | "node-roon-api-browse" : "github:roonlabs/node-roon-api-browse", 13 | "express" : "4.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /htmls/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | roon browser 8 |
9 | Zones: 10 |
11 |
12 | 13 | 14 |
15 |

16 |

17 |
18 |

19 | Home | Up 20 |

21 |

22 | 23 |
24 |
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /htmls/timers.css: -------------------------------------------------------------------------------- 1 | form { 2 | /* Just to center the form on the page */ 3 | // margin: 0 auto; 4 | width: 400px; 5 | /* To see the outline of the form */ 6 | padding: 1em; 7 | border: 1px solid #CCC; 8 | border-radius: 1em; 9 | } 10 | 11 | 12 | form div + div { 13 | margin-top: 1em; 14 | } 15 | 16 | label { 17 | /* To make sure that all labels have the same size and are properly aligned */ 18 | display: inline-block; 19 | width: 90px; 20 | text-align: left; 21 | } 22 | 23 | input { 24 | /* To make sure that all text fields have the same font settings 25 | By default, textareas have a monospace font */ 26 | font: 1em sans-serif; 27 | 28 | /* To give the same size to all text fields */ 29 | width: 300px; 30 | box-sizing: border-box; 31 | 32 | /* To harmonize the look & feel of text field border */ 33 | border: 1px solid #999; 34 | text-align: center; 35 | } 36 | 37 | input:focus { 38 | /* To give a little highlight on active elements */ 39 | border-color: #000; 40 | } 41 | 42 | .button { 43 | /* To position the buttons to the same position of the text fields */ 44 | padding-left: 90px; /* same size as the label elements */ 45 | } 46 | 47 | button { 48 | /* This extra margin represent roughly the same space as the space 49 | between the labels and their text fields */ 50 | margin-left: .5em; 51 | } 52 | 53 | table { 54 | border-collapse: collapse; 55 | width: 100%; 56 | } 57 | 58 | th, td { 59 | text-align: center; 60 | padding: 8px; 61 | } 62 | 63 | tr:nth-child(even){background-color: #f2f2f2} 64 | -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | var apis = require('./controllers/roonAPI'); 3 | 4 | app.get('/roonAPI/getCore', apis.getCore); 5 | app.get('/roonAPI/listZones', apis.listZones); 6 | app.get('/roonAPI/listOutputs', apis.listOutputs); 7 | app.get('/roonAPI/getZone', apis.getZone); 8 | app.get('/roonAPI/play_pause', apis.play_pause); 9 | app.get('/roonAPI/stop', apis.stop); 10 | app.get('/roonAPI/previous', apis.previous); 11 | app.get('/roonAPI/next', apis.next); 12 | app.get('/roonAPI/change_volume', apis.change_volume); 13 | app.get('/roonAPI/change_volume_relative', apis.change_volume_relative); 14 | app.get('/roonAPI/getImage', apis.getImage); 15 | app.get('/roonAPI/play', apis.play); 16 | app.get('/roonAPI/pause', apis.pause); 17 | app.get('/roonAPI/listByItemKey', apis.listByItemKey); 18 | app.get('/roonAPI/listSearch', apis.listSearch); 19 | app.get('/roonAPI/goUp', apis.goUp); 20 | app.get('/roonAPI/goHome', apis.goHome); 21 | app.get('/roonAPI/listGoPage', apis.listGoPage); 22 | app.get('/roonAPI/listRefresh', apis.listRefresh); 23 | app.get('/roonAPI/getMediumImage', apis.getMediumImage); 24 | app.get('/roonAPI/getIcon', apis.getIcon); 25 | app.get('/roonAPI/getOriginalImage', apis.getOriginalImage); 26 | app.get('/roonAPI/getTimers', apis.getTimers); 27 | app.get('/roonAPI/addTimer', apis.addTimer); 28 | app.get('/roonAPI/removeTimer', apis.removeTimer); 29 | app.post('/roonAPI/group', apis.group); 30 | app.post('/roonAPI/ungroup', apis.ungroup); 31 | app.get('/roonAPI/transferZone', apis.transferZone); 32 | }; 33 | -------------------------------------------------------------------------------- /htmls/timers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | roon timer 8 |
9 |

10 |

11 | Add By Timer: 12 |

13 |

14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 36 |
37 |
38 | 39 |
40 |
41 |

42 |

43 | Add By Date/Time 44 |

45 |

46 | 47 | 48 |
49 |
50 | 51 | 52 |
53 |
54 | 55 | 56 |
57 |
58 | 59 | 60 |
61 |
62 | 63 | 64 |
65 |
66 | 67 | 68 |
69 |
70 | 71 | 75 |
76 |
77 | 78 |
79 |
80 |
81 |
82 |
83 | 84 | 85 | -------------------------------------------------------------------------------- /htmls/player.js: -------------------------------------------------------------------------------- 1 | var topUrl = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port; 2 | var callInterval; 3 | var timeInterval = 2000; 4 | 5 | function ajax_get(url, callback) { 6 | xmlhttp = new XMLHttpRequest(); 7 | xmlhttp.onreadystatechange = function() { 8 | if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { 9 | try { 10 | var data = JSON.parse(xmlhttp.responseText); 11 | } catch(err) { 12 | return; 13 | } 14 | callback(data); 15 | } 16 | }; 17 | 18 | xmlhttp.open("GET", url, true); 19 | xmlhttp.send(); 20 | } 21 | 22 | ajax_get(topUrl + '/roonAPI/listZones', function(data) { 23 | var html = "

Zone List

"; 24 | html += ""; 29 | document.getElementById("zoneList").innerHTML = html; 30 | 31 | startInterval(); 32 | updateSelected(); 33 | }); 34 | 35 | function startInterval() { 36 | callInterval = setInterval(function() { 37 | var zone_id = document.getElementById("zoneList").options[document.getElementById("zoneList").selectedIndex].value; 38 | 39 | ajax_get(topUrl + '/roonAPI/getZone?zoneId=' + zone_id, function(data) { 40 | updateSelected(); 41 | }); 42 | 43 | }, 5000); 44 | } 45 | 46 | function stopInterval() { 47 | clearInterval( callInterval ); 48 | } 49 | 50 | 51 | function updateSelected() { 52 | var zone_id = document.getElementById("zoneList").options[document.getElementById("zoneList").selectedIndex].value; 53 | 54 | ajax_get(topUrl + '/roonAPI/getZone?zoneId=' + zone_id, function(data) { 55 | var html = ""; 56 | 57 | if ( data["zone"].state == "playing" || data["zone"].state == "paused" ) { 58 | html += show_zone(data["zone"]); 59 | } 60 | document.getElementById("selectedZone").innerHTML = html; 61 | }); 62 | } 63 | 64 | function show_zone(zone) { 65 | var html = ""; 66 | 67 | html += "

\n"; 68 | 69 | html += "Artist: " + zone.now_playing.three_line.line2 + "
\n"; 70 | html += "Album: " + zone.now_playing.three_line.line3 + "
\n"; 71 | html += "Title: " + zone.now_playing.three_line.line1 + "
\n"; 72 | 73 | html += "

\n"; 74 | 75 | html += "

\n"; 76 | 77 | // Volumes 78 | 79 | html += "

\n"; 80 | for ( var i in zone.outputs ) { 81 | if ( zone.outputs[i].zone_id == zone.zone_id && zone.outputs[i].volume != null ) { 82 | html += "

\n"; 83 | html += "\n"; 89 | } 90 | } 91 | 92 | // Navigation Buttons 93 | 94 | html += "

\n"; 95 | html += "

\n"; 96 | html += "
\n"; 97 | html += "\n"; 98 | html += "\n"; 99 | html += "\n"; 100 | html += "
\n"; 101 | html += "
\n"; 102 | 103 | return html; 104 | } 105 | 106 | function changeVolume(volume, outputId) { 107 | ajax_get(topUrl + '/roonAPI/change_volume?volume=' + volume + '&outputId=' + outputId, function(data) { 108 | }); 109 | } 110 | 111 | function goPrev(zone_id) { 112 | ajax_get(topUrl + '/roonAPI/previous?zoneId=' + zone_id, function(data) { 113 | updateSelected(); 114 | }); 115 | } 116 | 117 | function goNext(zone_id) { 118 | ajax_get(topUrl + '/roonAPI/next?zoneId=' + zone_id, function(data) { 119 | updateSelected(); 120 | }); 121 | } 122 | 123 | function goPlayPause(zone_id) { 124 | ajax_get(topUrl + '/roonAPI/play_pause?zoneId=' + zone_id, function(data) { 125 | updateSelected(); 126 | }); 127 | } 128 | -------------------------------------------------------------------------------- /htmls/browser.js: -------------------------------------------------------------------------------- 1 | var topUrl = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port; 2 | 3 | function ajax_get(url, callback) { 4 | xmlhttp = new XMLHttpRequest(); 5 | xmlhttp.onreadystatechange = function() { 6 | if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { 7 | try { 8 | var data = JSON.parse(xmlhttp.responseText); 9 | } catch(err) { 10 | return; 11 | } 12 | callback(data); 13 | } 14 | }; 15 | 16 | xmlhttp.open("GET", url, true); 17 | xmlhttp.send(); 18 | } 19 | 20 | ajax_get(topUrl + '/roonAPI/listZones', function(data) { 21 | var html = "

Zone List

"; 22 | html += ""; 29 | document.getElementById("zoneList").innerHTML = html; 30 | 31 | goHome(); 32 | }); 33 | 34 | function goHome() { 35 | var zone_id = document.getElementById("zoneList").options[document.getElementById("zoneList").selectedIndex].value; 36 | 37 | ajax_get(topUrl + '/roonAPI/goHome?zoneId=' + zone_id + '&list_size=20', function(data) { 38 | show_data(data, zone_id, 1); 39 | }); 40 | } 41 | 42 | function goUp() { 43 | var zone_id = document.getElementById("zoneList").options[document.getElementById("zoneList").selectedIndex].value; 44 | 45 | ajax_get(topUrl + '/roonAPI/goUp?zoneId=' + zone_id + '&list_size=20', function(data) { 46 | show_data(data, zone_id, 1); 47 | }); 48 | } 49 | 50 | function show_data( data, zone_id, page ) { 51 | var html = ""; 52 | 53 | var pageHtml = ""; 54 | if ( page > 1 ) { 55 | pageHtml += "prev\n"; 56 | } else { 57 | pageHtml += "prev"; 58 | } 59 | 60 | pageHtml += " | Page: " + page + " | "; 61 | 62 | if ( data.list.length > 99 ) { 63 | pageHtml += "next\n"; 64 | } else { 65 | pageHtml += "next"; 66 | } 67 | 68 | document.getElementById("PageNumber").innerHTML = pageHtml; 69 | 70 | for ( var i in data['list'] ) { 71 | html += "
\n"; 72 | html += ""; 73 | 74 | if ( data.list[i].image_key == null ) { 75 | html += "\n"; 76 | } else { 77 | html += "\n"; 78 | } 79 | 80 | html += "\n"; 81 | html += "
" + data.list[i].title + "
\n"; 82 | html += "
\n"; 83 | } 84 | 85 | document.getElementById("browseTable").innerHTML = html; 86 | } 87 | 88 | function showList(item_key, zone_id, page, listSize) { 89 | ajax_get(topUrl + '/roonAPI/listByItemKey?zoneId=' + zone_id + "&item_key=" + item_key + "&page=" + page + "&list_size=" + listSize, function(data) { 90 | show_data(data, zone_id, page); 91 | }); 92 | } 93 | 94 | function goPage(zone_id, page, size) { 95 | ajax_get(topUrl + '/roonAPI/listGoPage?page=' + page + "&list_size=" + size, function(data) { 96 | show_data(data, zone_id, page); 97 | }); 98 | } 99 | 100 | function search(form) { 101 | var zone_id = document.getElementById("zoneList").options[document.getElementById("zoneList").selectedIndex].value; 102 | var toSearch = form.toSearch.value; 103 | 104 | //go home first 105 | ajax_get(topUrl + '/roonAPI/goHome?zoneId=' + zone_id + '&list_size=20', function(data) { 106 | var library_item_key = data.list[0].item_key; 107 | 108 | ajax_get(topUrl + '/roonAPI/listByItemKey?zoneId=' + zone_id + "&item_key=" + library_item_key + "&page=1&list_size=20", function(data) { 109 | var search_item_key = data.list[0].item_key; 110 | 111 | ajax_get(topUrl + '/roonAPI/listSearch?zoneId=' + zone_id + "&item_key=" + search_item_key + "&toSearch=" + toSearch + "&page=1&list_size=20", function(data) { 112 | show_data(data, zone_id, 1); 113 | }); 114 | }); 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /htmls/timers.js: -------------------------------------------------------------------------------- 1 | var topUrl = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port; 2 | 3 | var months = ["Jan", "Feb", "Mar", "Apr", "May", "June", "July", "Aug", "Sep", "Oct", "Nov", "Dec"]; 4 | var zone_list = {}; 5 | 6 | function ajax_get(url, callback) { 7 | xmlhttp = new XMLHttpRequest(); 8 | xmlhttp.onreadystatechange = function() { 9 | if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { 10 | try { 11 | var data = JSON.parse(xmlhttp.responseText); 12 | } catch(err) { 13 | return; 14 | } 15 | callback(data); 16 | } 17 | }; 18 | 19 | xmlhttp.open("GET", url, true); 20 | xmlhttp.send(); 21 | } 22 | 23 | ajax_get(topUrl + '/roonAPI/listZones', function(data) { 24 | for (var i in data["zones"]) { 25 | zone_list[data["zones"][i].zone_id] = data["zones"][i].display_name; 26 | } 27 | 28 | show_data(); 29 | }); 30 | 31 | function show_data() { 32 | var optHtml = ""; 33 | 34 | // CREATE ZONE LIST 35 | for (var i in zone_list) { 36 | optHtml += "\n"; 37 | } 38 | 39 | document.getElementById("zoneListByTimer").innerHTML = optHtml; 40 | document.getElementById("zoneListByDate").innerHTML = optHtml; 41 | 42 | 43 | // CREATE MONTH LIST 44 | monthHtml = ""; 45 | for ( var i in months ) { 46 | monthHtml += "\n"; 47 | } 48 | 49 | document.getElementById("monthList").innerHTML = monthHtml; 50 | 51 | // CREATE TIMER LIST 52 | ajax_get(topUrl + '/roonAPI/getTimers', function(data) { 53 | var html = ""; 54 | var curDate = new Date(); 55 | var timerDate; 56 | 57 | html += "\n"; 58 | html += "\n"; 59 | html += "\n"; 60 | html += "\n"; 61 | html += "\n"; 62 | html += "\n"; 63 | html += "\n"; 64 | html += "\n"; 65 | 66 | if (data != null ) { 67 | for ( var i in data.timers ) { 68 | 69 | html += "\n"; 70 | html += "\n"; 71 | html += "\n"; 72 | html += "\n"; 73 | html += "\n"; 74 | html += "\n"; 81 | html += "\n"; 83 | html += "\n"; 84 | } 85 | } 86 | 87 | html += "\n"; 88 | 89 | document.getElementById("timerList").innerHTML = html; 90 | }); 91 | } 92 | 93 | function durationFromDate(timerDate) { 94 | var duration = ""; 95 | 96 | var date = new Date(parseInt(timerDate)); 97 | var curDate = new Date(); 98 | 99 | var lapse = date - curDate; 100 | lapse = lapse / 1000; 101 | 102 | if ( lapse < 0 ) { 103 | duration = "has passed"; 104 | } else { 105 | var day = Math.floor(lapse / 60 / 60 / 24); 106 | lapse = lapse - ( day * 60 * 60 * 24 ); 107 | 108 | if ( day > 0 ) { 109 | duration += day + "d "; 110 | } 111 | 112 | var hour = Math.floor(lapse / 60 / 60); 113 | lapse = lapse - ( hour * 60 * 60); 114 | 115 | if ( hour > 0 ) { 116 | duration += hour + "h "; 117 | } 118 | 119 | var min = Math.floor(lapse / 60); 120 | lapse = Math.floor(lapse - (min * 60)); 121 | 122 | if ( min > 0 ) { 123 | duration += min + "m "; 124 | } 125 | 126 | duration += lapse + "s"; 127 | } 128 | 129 | return duration; 130 | } 131 | 132 | 133 | function dateTimeFromDate(timerDate) { 134 | var date = new Date(parseInt(timerDate)); 135 | 136 | var dateTime = ""; 137 | 138 | var mth = months[date.getMonth()]; 139 | var day = date.getDate(); 140 | var year = date.getFullYear(); 141 | var hour = date.getHours(); 142 | var min = date.getMinutes(); 143 | var sec = date.getSeconds(); 144 | 145 | dateTime = day + " " + mth + " " + year + ", " + hour + ":" + min + ":" + sec; 146 | return dateTime; 147 | } 148 | 149 | 150 | function removeTimer(zoneId, time, command, isRepeat) { 151 | ajax_get(topUrl + '/roonAPI/removeTimer?zoneId=' + zoneId + "&time=" + time + "&command=" + command + "&isRepeat=" + isRepeat, function(data) { 152 | show_data(); 153 | }); 154 | } 155 | 156 | function addByTimer(form) { 157 | var zone_id = form.zoneListByTimer.value; 158 | var second = form.second.value 159 | var minute = form.minute.value; 160 | var hour = form.hour.value; 161 | var command = form.command.value; 162 | var milliseconds = 0; 163 | var dateNow = new Date(); 164 | 165 | if ( second == null ) { 166 | second = 0; 167 | } 168 | 169 | if ( minute == null ) { 170 | minute = 0; 171 | } 172 | 173 | if ( hour == null ) { 174 | hour = 0; 175 | } 176 | 177 | milliseconds = ( hour * 60 * 60 * 1000 ) + (minute * 60 * 1000) + (second * 1000); 178 | 179 | var timerDate = dateNow.getTime() + milliseconds; 180 | 181 | addTimer(zone_id, timerDate, command, 0); 182 | } 183 | 184 | function addByDateTime(form) { 185 | var zone_id = form.zoneListByDate.value; 186 | var day = form.day.value; 187 | var month = form.monthList.value; 188 | var year = form.year.value 189 | var second = 0; 190 | var minute = form.minute.value; 191 | var hour = form.hour.value; 192 | var command = form.command.value; 193 | var milliseconds = 0; 194 | var dateNow = new Date(); 195 | 196 | if ( day == null ) { 197 | day = "01"; 198 | } else if ( day < 10 ) { 199 | day = "0" + day; 200 | } 201 | 202 | if ( minute == null ) { 203 | minute = "00"; 204 | } else if ( minute < 10 ) { 205 | minute = "0" + minute; 206 | } 207 | 208 | if ( hour == null ) { 209 | hour = "00"; 210 | } else if ( hour < 10 ) { 211 | hour = "0" + hour; 212 | } 213 | 214 | var timerRun = new Date( months[month] + " " + day + ", " + year + " " + hour + ":" + minute + ":00"); 215 | var timerDate = timerRun.getTime(); 216 | 217 | addTimer(zone_id, timerDate, command, 0); 218 | } 219 | 220 | function addTimer(zone_id, time, command, isRepeat) { 221 | ajax_get(topUrl + '/roonAPI/addTimer?zoneId=' + zone_id + "&time=" + time + "&command=" + command + "&isRepeat=" + isRepeat, function(data) { 222 | show_data(); 223 | }); 224 | } 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP Calls for Roon APIs 2 | --------------------------- 3 | 4 | These APIs are run by http calls. 5 | There is a list below with examples that calls these APIs. 6 | 7 | Have tried it with iOs swift and iOS' Workflow app to create widgets. 8 | 9 | ## Prerequisite 10 | 11 | These apis are running on Node.js. Below are the steps to install it. 12 | 13 | * On Windows, install from the above link. 14 | * On Mac OS, you can use [homebrew](http://brew.sh) to install Node.js. 15 | * On Linux, you can use your distribution's package manager, but make sure it installs a recent Node.js. Otherwise just install from the above link. 16 | 17 | Make sure you are running node 5.x or higher. 18 | ```sh 19 | node -v 20 | ``` 21 | 22 | For example: 23 | 24 | ```sh 25 | $ node -v 26 | v5.10.1 27 | ``` 28 | 29 | ## Installing roon-extension-http-api 30 | 31 | 1. Download the repository. 32 | * Go to [http-extension-http-api](https://github.com/st0g1e/roon-extension-http-api) page 33 | * Click on "Clone or Download" 34 | * Click "Download Zip" 35 | 36 | 2. Install 37 | * Copy the downloaded zip to the desired folder 38 | * unzip 39 | * open terminal/command line and change directory to the folder 40 | ``` 41 | cd [PATH] 42 | ``` 43 | * Install Dependencies 44 | ``` 45 | npm install 46 | ``` 47 | * (Optional) To remove the running log 48 | Comment console.log lines at 49 | - node_modules/node-roon-api/lib.js 50 | - node_modules/node-roon-api/moo.js ( REQUEST) 51 | - node_modules/node-roon-api/moomsj.js (CONTINUE and COMPLETE) 52 | 53 | 3. Running 54 | ``` 55 | node . 56 | ``` 57 | 58 | 4. Enable the extension 59 | In Roon, go to Settings -> Extensions and click on the "enable" button next to the roon-extension-http-api extension details. 60 | 61 | ** Testing in Browser 62 | 63 | You should now the IP address where the extension is (for the same computer, you can use localhost. the default port is 3001. 64 | This can be changed by changing the PORT value in server.js 65 | 66 | Open a browser and go to the following link: 67 | ``` 68 | http://localhost:3001/roonAPI/listZones 69 | ``` 70 | 71 | ## Available APIs 72 | The full list of APIs can be seen on routes.js 73 | 74 | The format to call these APIs are: 75 | ``` 76 | http://[IPAddress]:[Port]/roonAPI/[APIName] 77 | ``` 78 | 79 | The APIs are: 80 | * Transport APIs 81 | - getCore 82 | ``` 83 | http://localhost:3001/roonAPI/getCore 84 | ``` 85 | - listZone 86 | ``` 87 | http://localhost:3001/roonAPI/listZones 88 | ``` 89 | - getZone 90 | ``` 91 | http://localhost:3001/roonAPI/getZone?zoneId=[zoneId as found from listZones] 92 | ``` 93 | - play_pause 94 | ``` 95 | http://localhost:3001/roonAPI/play_pause?zoneId=[zoneId as found from listZones] 96 | ``` 97 | - play 98 | ``` 99 | http://localhost:3001/roonAPI/play?zoneId=[zoneId as found from listZones] 100 | ``` 101 | - pause 102 | ``` 103 | http://localhost:3001/roonAPI/pause?zoneId=[zoneId as found from listZones] 104 | ``` 105 | - stop 106 | ``` 107 | http://localhost:3001/roonAPI/stop?zoneId=[zoneId as found from listZones] 108 | ``` 109 | - previous 110 | ``` 111 | http://localhost:3001/roonAPI/previous?zoneId=[zoneId as found from listZones] 112 | ``` 113 | - next 114 | ``` 115 | http://localhost:3001/roonAPI/next?zoneId=[zoneId as found from listZones] 116 | ``` 117 | - change_volume 118 | ``` 119 | http://localhost:3001/roonAPI/change_volume?volume=[Volume % from 0 to 100]&outputId=[outputId as found from listZones] 120 | ``` 121 | - change_volume_relative 122 | ``` 123 | http://localhost:3001/roonAPI/change_volume_relative?volume=[volume change from current]&outputId=[outputId as found from listZones] 124 | ``` 125 | - transfer zone 126 | ``` 127 | http://localhost:3001/roonAPI/transferZone?fromZoneId=[source zoneId]&toZoneId=[destination zoneId] 128 | ``` 129 | 130 | * Image APIs 131 | - getImage 132 | ``` 133 | http://localhost:3001/roonAPI/getImage?image_key=[image_key as found from the browser APIs] 134 | ``` 135 | - getMediumImage 136 | ``` 137 | http://localhost:3001/roonAPI/getMediumImage?image_key=[image_key as found from the browser APIs] 138 | ``` 139 | - getIcon 140 | ``` 141 | http://localhost:3001/roonAPI/getIcon?image_key=[image_key as found from the browser APIs] 142 | ``` 143 | 144 | * Browser APIs 145 | - listByItemKey (list_size always returns 100) 146 | ``` 147 | http://localhost:3001/roonAPI/listByItemKey?zoneId=[zoneId]&item_key=[item_key from Browser APIs]&page=[page number]&list_size=[number of return per page] 148 | ``` 149 | - listSearch (list_size always returns 100) 150 | ``` 151 | http://localhost:3001/roonAPI/listSearch?zoneId=[zoneId]&toSearch=[search string]&list_size=[hits per page] 152 | ``` 153 | - goUp (list_size always returns 100) 154 | ``` 155 | http://localhost:3001/roonAPI/goUp?zoneId=[zoneId]&list_size=[hits per page] 156 | ``` 157 | - goHome (list_size always returns 100) 158 | ``` 159 | http://localhost:3001/roonAPI/goHome?zoneId=[zoneId]&list_size=[hits per page] 160 | ``` 161 | - listGoPage (list_size always returns 100) 162 | ``` 163 | http://localhost:3001/roonAPI/listGoPage?page=[page number]&list_size=[hits per page] 164 | ``` 165 | 166 | * Group / Ungroup APIs (Input Type: POST) 167 | - group, post parameter is JSON array name "output" 168 | ``` 169 | http://localhost:3001/roonAPI/group 170 | 171 | Parameter: 172 | 173 | { 174 | "output": [ 175 | "1701004f66ca89348d7baf26a8ca037bc5b1", 176 | "17016922fd031e4440ed273671f3fee578a6" 177 | ] 178 | } 179 | ``` 180 | 181 | - ungroup, post parameter is JSON array name "output" 182 | ``` 183 | http://localhost:3001/roonAPI/ungroup 184 | 185 | Parameter: 186 | 187 | { 188 | "output": [ 189 | "1701004f66ca89348d7baf26a8ca037bc5b1", 190 | "17016922fd031e4440ed273671f3fee578a6" 191 | ] 192 | } 193 | ``` 194 | 195 | * Timers 196 | - getTimers 197 | ``` 198 | http://localhost:3001/roonAPI/getTimers 199 | ``` 200 | - addTimer 201 | ``` 202 | http://localhost:3001/roonAPI/addTimer?zoneId=[zoneId]&time=[unix time in millisecond]&command=[play|[pause]&isRepeat=[0|1] 203 | ``` 204 | - removeTimer 205 | ``` 206 | http://localhost:3001/roonAPI/removeTimer?zoneId=[zoneId]&time=[unix time in milliseconds]&command=[play|pause]&isRepeat=[0|1] 207 | ``` 208 | 209 | ## Examples 210 | There are several examples that calls the APIs above under the htmls directory. 211 | 212 | URL: http://localhost:3001/player.html 213 | 214 | These are: 215 | - player.html (simple player with play/pause, next, previous and volume slider where available) 216 | - browser.html (simple viewer list, can play the songs) 217 | - timers.html (simple timers to play/pause songs) 218 | -------------------------------------------------------------------------------- /controllers/roonAPI.js: -------------------------------------------------------------------------------- 1 | var RoonApi = require("node-roon-api"); 2 | var RoonApiTransport = require("node-roon-api-transport"); 3 | var RoonApiStatus = require("node-roon-api-status"); 4 | var RoonApiImage = require("node-roon-api-image"); 5 | var RoonApiBrowse = require("node-roon-api-browse"); 6 | 7 | var path = require('path'); 8 | 9 | 10 | var core; 11 | var timeout; 12 | 13 | var roon = new RoonApi({ 14 | extension_id : "st0g1e.roon-http-api", 15 | display_name : "roon-http-api", 16 | display_version : "1.0.4", 17 | publisher : "bastian ramelan", 18 | email : "st0g1e@yahoo.com", 19 | log_level : "none", 20 | 21 | core_paired: function(core_) { 22 | core = core_; 23 | core.services.RoonApiTransport.subscribe_zones((response, msg) => { 24 | }); 25 | }, 26 | 27 | core_unpaired: function(core_) { 28 | 29 | } 30 | }); 31 | 32 | var svc_status = new RoonApiStatus(roon); 33 | 34 | roon.init_services({ 35 | required_services: [ RoonApiTransport, RoonApiBrowse, RoonApiImage ], 36 | provided_services: [ svc_status ], 37 | }); 38 | 39 | svc_status.set_status("Extension enabled", false); 40 | roon.start_discovery(); 41 | 42 | 43 | // --------------- APIs ------------------ 44 | 45 | exports.getCore = function(req, res){ 46 | res.send({ 47 | "id": core.core_id, 48 | "display_name": core.display_name, 49 | "display_version": core.display_version 50 | }); 51 | }; 52 | 53 | exports.listZones = function(req, res) { 54 | core.services.RoonApiTransport.get_zones((iserror, body) => { 55 | if (!iserror) { 56 | res.send({ 57 | "zones": body.zones 58 | }) 59 | } 60 | }) 61 | }; 62 | 63 | exports.listOutputs = function(req, res) { 64 | core.services.RoonApiTransport.get_outputs((iserror, body) => { 65 | if (!iserror) { 66 | res.send({ 67 | "outputs": body.outputs 68 | }) 69 | } 70 | }) 71 | }; 72 | exports.getZone = function(req, res) { 73 | res.send({ 74 | "zone": core.services.RoonApiTransport.zone_by_zone_id(req.query['zoneId']) 75 | }) 76 | }; 77 | 78 | exports.play_pause = function(req, res) { 79 | core.services.RoonApiTransport.control(req.query['zoneId'], 'playpause'); 80 | 81 | res.send({ 82 | "status": "success" 83 | }) 84 | }; 85 | 86 | exports.stop = function(req, res) { 87 | core.services.RoonApiTransport.control(req.query['zoneId'], 'stop'); 88 | 89 | res.send({ 90 | "status": "success" 91 | }) 92 | }; 93 | 94 | exports.play = function(req, res) { 95 | core.services.RoonApiTransport.control(req.query['zoneId'], 'play'); 96 | 97 | res.send({ 98 | "zone": "Success" 99 | }) 100 | }; 101 | 102 | exports.pause = function(req, res) { 103 | core.services.RoonApiTransport.control(req.query['zoneId'], 'pause'); 104 | 105 | res.send({ 106 | "status": "success" 107 | }) 108 | }; 109 | 110 | 111 | exports.previous = function(req, res) { 112 | core.services.RoonApiTransport.control(req.query['zoneId'], 'previous'); 113 | 114 | // setTimeout(function(){ 115 | res.send({ 116 | "zone": req.headers.referer 117 | }) 118 | // }, 2000); 119 | }; 120 | 121 | exports.next = function(req, res) { 122 | core.services.RoonApiTransport.control(req.query['zoneId'], 'next'); 123 | 124 | // setTimeout(function(){ 125 | res.send({ 126 | "zone": core.services.RoonApiTransport.zone_by_zone_id(req.query['zoneId']) 127 | }) 128 | // }, 2000); 129 | }; 130 | 131 | exports.change_volume = function(req, res) { 132 | core.services.RoonApiTransport.change_volume(req.query['outputId'], "absolute", req.query['volume']); 133 | 134 | 135 | res.send({ 136 | "status": "success" 137 | }) 138 | }; 139 | 140 | exports.change_volume_relative = function(req, res) { 141 | core.services.RoonApiTransport.change_volume(req.query['outputId'], "relative", req.query['volume']); 142 | 143 | 144 | res.send({ 145 | "status": "success" 146 | }) 147 | }; 148 | 149 | exports.getMediumImage = function( req, res ) { 150 | get_image( req.query['image_key'], "fit", 300, 200, "image/jpeg", res); 151 | }; 152 | 153 | exports.getIcon = function( req, res ) { 154 | get_image( req.query['image_key'], "fit", 100, 100, "image/jpeg", res); 155 | }; 156 | 157 | exports.getImage = function(req, res) { 158 | get_image( req.query['image_key'], "fit", 300, 200, "image/jpeg", res); 159 | }; 160 | 161 | exports.getOriginalImage = function(req, res) { 162 | core.services.RoonApiImage.get_image(req.query['image_key'], function(cb, contentType, body) { 163 | 164 | res.contentType = contentType; 165 | 166 | res.writeHead(200, {'Content-Type': 'image/jpeg' }); 167 | res.end(body, 'binary'); 168 | }); 169 | }; 170 | 171 | function get_image(image_key, scale, width, height, format, res) { 172 | core.services.RoonApiImage.get_image(image_key, {scale, width, height, format}, function(cb, contentType, body) { 173 | 174 | res.contentType = contentType; 175 | 176 | res.writeHead(200, {'Content-Type': 'image/jpeg' }); 177 | res.end(body, 'binary'); 178 | }); 179 | }; 180 | 181 | exports.listByItemKey = function(req, res) { 182 | refresh_browse( req.query['zoneId'], { item_key: req.query['item_key'] }, req.query['page'], req.query['list_size'], function(myList) { 183 | 184 | res.send({ 185 | "list": myList 186 | }) 187 | }); 188 | }; 189 | 190 | exports.listSearch = function(req, res) { 191 | refresh_browse( req.query['zoneId'], { item_key: req.query['item_key'], input: req.query['toSearch'] }, req.query['page'], req.query['list_size'], function(myList) { 192 | res.send({ 193 | "list": myList 194 | }) 195 | }); 196 | }; 197 | 198 | exports.goUp = function(req, res) { 199 | refresh_browse( req.query['zoneId'], { pop_levels: 1 }, 1, req.query['list_size'], function(myList) { 200 | 201 | res.send({ 202 | "list": myList 203 | }) 204 | }); 205 | 206 | }; 207 | 208 | exports.goHome = function(req, res) { 209 | refresh_browse( req.query['zoneId'], { pop_all: true }, 1, req.query['list_size'], function(myList) { 210 | 211 | res.send({ 212 | "list": myList 213 | }) 214 | }); 215 | }; 216 | 217 | exports.listGoPage = function(req, res) { 218 | load_browse( req.query['page'], req.query['list_size'], function(myList) { 219 | 220 | res.send({ 221 | "list": myList 222 | }) 223 | }); 224 | 225 | }; 226 | 227 | exports.listRefresh = function(req, res) { 228 | refresh_browse( req.query['zoneId'], { refresh_list: true }, 0, 0, function(myList) { 229 | 230 | res.send({ 231 | "list": myList 232 | }) 233 | }); 234 | }; 235 | 236 | 237 | // GROUP - UNGROUP 238 | 239 | exports.group = function(req, res) { 240 | core.services.RoonApiTransport.group_outputs(req.body.output); 241 | 242 | res.send({ 243 | "status": "success" 244 | }) 245 | }; 246 | 247 | exports.ungroup = function(req, res) { 248 | core.services.RoonApiTransport.ungroup_outputs(req.body.output); 249 | 250 | res.send({ 251 | "status": "success" 252 | }) 253 | }; 254 | 255 | 256 | // TRANSFERS 257 | 258 | exports.transferZone = function(req, res) { 259 | core.services.RoonApiTransport.transfer_zone(req.query['fromZoneId'], req.query['toZoneId']); 260 | 261 | res.send({ 262 | "status": "success" 263 | }) 264 | }; 265 | 266 | 267 | // Timers 268 | 269 | exports.addTimer = function(req, res) { 270 | save_timer(req.query['zoneId'], req.query['time'], req.query['command'], req.query['isRepeat']); 271 | 272 | run_later(); 273 | var timers = get_timers(); 274 | 275 | res.send({ 276 | "timers": timers 277 | }) 278 | }; 279 | 280 | exports.getTimers = function(req, res) { 281 | var timers = get_timers(); 282 | 283 | res.send({ 284 | "timers": timers 285 | }) 286 | }; 287 | 288 | exports.removeTimer = function(req, res) { 289 | var timers = get_timers(); 290 | var zoneToRemove = req.query['zoneId']; 291 | var timeToRemove = req.query['time']; 292 | var commandToRemove = req.query['command']; 293 | var isRepeatToRemove = req.query['isRepeat']; 294 | 295 | for ( var i in timers ) { 296 | if ( timers[i].zoneId == zoneToRemove && timers[i].time == timeToRemove && 297 | timers[i].command == commandToRemove && timers[i].isRepeat == isRepeatToRemove ) { 298 | timers.splice(i, 1); 299 | break; 300 | } 301 | } 302 | 303 | roon.save_config("my_timers", timers); 304 | 305 | run_later(); 306 | var timers = get_timers(); 307 | 308 | res.send({ 309 | "timers": timers 310 | }) 311 | }; 312 | 313 | 314 | function refresh_browse(zone_id, opts, page, listPerPage, cb) { 315 | var items = []; 316 | opts = Object.assign({ 317 | hierarchy: "browse", 318 | zone_or_output_id: zone_id, 319 | }, opts); 320 | 321 | 322 | core.services.RoonApiBrowse.browse(opts, (err, r) => { 323 | if (err) { console.log(err, r); return; } 324 | 325 | if (r.action == 'list') { 326 | page = ( page - 1 ) * listPerPage; 327 | 328 | core.services.RoonApiBrowse.load({ 329 | hierarchy: "browse", 330 | offset: page, 331 | set_display_offset: listPerPage, 332 | }, (err, r) => { 333 | items = r.items; 334 | 335 | cb(r.items); 336 | }); 337 | } 338 | }); 339 | } 340 | 341 | function load_browse(page, listPerPage, cb) { 342 | page = ( page - 1 ) * listPerPage; 343 | 344 | core.services.RoonApiBrowse.load({ 345 | hierarchy: "browse", 346 | offset: page, 347 | set_display_offset: page, 348 | }, (err, r) => { 349 | cb(r.items); 350 | }); 351 | } 352 | 353 | function runCommand(command, zone_id) { 354 | if ( command == "play" ) { 355 | core.services.RoonApiTransport.control(req.query['zoneId'], 'play'); 356 | } else if ( command == "pause" ) { 357 | core.services.RoonApiTransport.control(req.query['zoneId'], 'pause'); 358 | } 359 | } 360 | 361 | function get_timers() { 362 | var run_laters = roon.load_config("my_timers"); 363 | 364 | return run_laters; 365 | } 366 | 367 | function save_timer(zoneId, time, command, isRepeat) { 368 | var timers = get_timers(); 369 | 370 | if ( timers == null ) { 371 | timers = []; 372 | } 373 | 374 | var toAdd = {} 375 | toAdd.zoneId = zoneId; 376 | toAdd.time = time; 377 | toAdd.command = command; 378 | toAdd.isRepeat = isRepeat; 379 | 380 | timers.push(toAdd); 381 | 382 | roon.save_config("my_timers", timers); 383 | refresh_timer(); 384 | } 385 | 386 | function refresh_timer() { 387 | var timers = get_timers(); 388 | var dateNow = new Date(); 389 | 390 | var newTimers = []; 391 | var isFirst = true; 392 | 393 | for ( var i in timers ) { 394 | if ( timers[i].time >= dateNow.getTime() ) { 395 | newTimers.push( timers[i] ); 396 | } 397 | } 398 | newTimers.sort(compare); 399 | roon.save_config("my_timers", newTimers); 400 | 401 | } 402 | 403 | function compare(a, b) { 404 | if ( a.time < b.time ) { return -1; } 405 | if ( a.time > b.time ) { return 1; } 406 | return 0; 407 | } 408 | 409 | 410 | function run_later() { 411 | clearTimeout(timeout); 412 | 413 | var timers = get_timers(); 414 | var timer; 415 | 416 | if ( timers != null && timers.length > 0 ) { 417 | timer = timers[0]; 418 | 419 | var date = new Date(parseInt(timer.time)); 420 | var curDate = new Date(); 421 | 422 | var lapse = date - curDate; 423 | 424 | if ( timer.command == "play" ) { 425 | timeout = setTimeout( function () { 426 | playZone(timer.zoneId); 427 | run_later(); 428 | }, lapse); 429 | } else if ( timer.command == "pause" ) { 430 | timeout = setTimeout( function() { 431 | pauseZone(timer.zoneId); 432 | run_later(); 433 | }, lapse); 434 | } 435 | } 436 | } 437 | 438 | function playZone(zoneId) { 439 | refresh_timer(); 440 | core.services.RoonApiTransport.control(zoneId, 'play'); 441 | } 442 | 443 | function pauseZone(zoneId) { 444 | refresh_timer(); 445 | core.services.RoonApiTransport.control(zoneId, 'pause'); 446 | } 447 | --------------------------------------------------------------------------------
ZoneCommandDate/TimeDurationIs RepeatRemove
" + zone_list[data.timers[i].zoneId] + "" + data.timers[i].command + "" + dateTimeFromDate(data.timers[i].time) + "" + durationFromDate(data.timers[i].time) + ""; 75 | if ( data.timers[i].isRepeat == "1" ) { 76 | html += "yes"; 77 | } else { 78 | html += "no"; 79 | } 80 | html += "remove