Skip to content

Commit de3b154

Browse files
committed
✨ talk with sonos initial commit
0 parents  commit de3b154

File tree

100 files changed

+18989
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

100 files changed

+18989
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
.env

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
var audioAesKeyBuffer = new Buffer(audioAesKey, 'binary');
2+
var decipher = crypto.createDecipheriv('aes-128-cbc', audioAesKeyBuffer, audioAesIv); decipher.setAutoPadding(false);

main.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const electron = require('electron')
2+
// Module to control application life.
3+
const app = electron.app
4+
// Module to create native browser window.
5+
const BrowserWindow = electron.BrowserWindow
6+
7+
const path = require('path')
8+
const url = require('url')
9+
const sonos = require('./src/sonos/server.js')
10+
11+
// Keep a global reference of the window object, if you don't, the window will
12+
// be closed automatically when the JavaScript object is garbage collected.
13+
let mainWindow
14+
15+
function createWindow () {
16+
// Create the browser window.
17+
mainWindow = new BrowserWindow({width: 800, height: 600})
18+
19+
// and load the index.html of the app.
20+
mainWindow.loadURL(url.format({
21+
pathname: path.join(__dirname, './src/index.html'),
22+
protocol: 'file:',
23+
slashes: true
24+
}))
25+
26+
// Open the DevTools.
27+
mainWindow.webContents.openDevTools()
28+
29+
// Emitted when the window is closed.
30+
mainWindow.on('closed', function () {
31+
// Dereference the window object, usually you would store windows
32+
// in an array if your app supports multi windows, this is the time
33+
// when you should delete the corresponding element.
34+
mainWindow = null
35+
})
36+
37+
sonos;
38+
}
39+
40+
// This method will be called when Electron has finished
41+
// initialization and is ready to create browser windows.
42+
// Some APIs can only be used after this event occurs.
43+
app.on('ready', createWindow)
44+
45+
// Quit when all windows are closed.
46+
app.on('window-all-closed', function () {
47+
// On OS X it is common for applications and their menu bar
48+
// to stay active until the user quits explicitly with Cmd + Q
49+
if (process.platform !== 'darwin') {
50+
app.quit()
51+
}
52+
})
53+
54+
app.on('activate', function () {
55+
// On OS X it's common to re-create a window in the app when the
56+
// dock icon is clicked and there are no other windows open.
57+
if (mainWindow === null) {
58+
createWindow()
59+
}
60+
})
61+
62+
// In this file you can include the rest of your app's specific main process
63+
// code. You can also put them in separate files and require them here.

package.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"scripts": {
3+
"start": "electron ./main.js",
4+
"install": "electron-rebuild",
5+
"build-mac": "electron-packager . --overwrite --platform=darwin --arch=x64 --icon=release/icons/icon.icns --out=release/build/"
6+
},
7+
"name": "SonosSays",
8+
"version": "1.0.0",
9+
"main": "main.js",
10+
"author": "Gert-Jan Wille",
11+
"license": "MIT",
12+
"dependencies": {
13+
"electron": "^1.6.6",
14+
"anesidora": "^1.2.0",
15+
"aws-sdk": "^2.12.0",
16+
"basic-auth": "~1.1.0",
17+
"fuse.js": "^2.5.0",
18+
"json5": "^0.5.1",
19+
"node-static": "~0.7.0",
20+
"request-promise": "~1.0.2",
21+
"sonos-discovery": "https://github.com/jishi/node-sonos-discovery/archive/v1.4.1.tar.gz"
22+
},
23+
"devDependencies": {
24+
"electron-packager": "^8.6.0",
25+
"electron-rebuild": "^1.5.7"
26+
}
27+
}

src/css/style.css

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
* {
2+
margin:0;
3+
padding: 0;
4+
}
5+
6+
body, html {
7+
width: 100vw;
8+
height: 100vh;
9+
overflow: hidden;
10+
}
11+
12+
main {
13+
overflow: hidden;
14+
display: flex;
15+
align-items: center;
16+
justify-content: center;
17+
width: 100vw;
18+
height: 100vh;
19+
}
20+
21+
form {
22+
z-index: 0;
23+
display: flex;
24+
align-items: center;
25+
justify-content: space-between;
26+
position: absolute;
27+
width: 100%;
28+
height: 100%;
29+
padding: 5rem;
30+
}
31+
32+
textarea, .settings {
33+
width: 50%;
34+
height: 100%;
35+
padding: 5rem;
36+
border: 1px solid black;
37+
padding-top: 10rem;
38+
}
39+
40+
textarea {
41+
appearance: none;
42+
outline: none;
43+
padding-top: 10rem;
44+
font-size: 1.2em;
45+
}
46+
47+
.settings {
48+
display: flex;
49+
flex-direction: column;
50+
justify-content: space-around;
51+
}
52+
53+
.item {
54+
display: flex;
55+
flex-direction: column;
56+
width: 90%;
57+
font-family: sans-serif;
58+
font-size: .8em;
59+
}
60+
61+
select {
62+
appearance: none;
63+
border-radius: none;
64+
border: 1px solid black;
65+
height: 2rem;
66+
}
67+
68+
input[type=submit] {
69+
border: 1px solid black;
70+
background: none;
71+
height: 2rem;
72+
border-radius: 100px;
73+
font-size: 1em;
74+
font-family: sans-serif;
75+
text-transform: uppercase;
76+
transition: all .3s ease;
77+
}
78+
79+
input[type=submit]:hover {
80+
background: grey;
81+
}
82+
83+
.succes {
84+
visibility: hidden;
85+
z-index: 1000;
86+
position: absolute;
87+
bottom: 0;
88+
background: green;
89+
width: 100vw;
90+
height: 10vh;
91+
display: flex;
92+
align-items: center;
93+
justify-content: center;
94+
font-family: sans-serif;
95+
color: white;
96+
}
97+
98+
.visible {
99+
visibility: visible;
100+
}

src/index.html

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Sonos Says</title>
6+
<link rel="stylesheet" href="./css/style.css">
7+
</head>
8+
<body>
9+
<main class="container">
10+
<p class="succes">It has been said</p>
11+
12+
<form action="#">
13+
<textarea class="txtcon" rows="8" cols="80" placeholder="Type the text you wanna say"></textarea>
14+
15+
<div class="settings">
16+
<div class="item">
17+
<label for="range">Sound</label>
18+
<input id="range" type="range" min="0" max="100" value="40" class="range">
19+
</div>
20+
21+
<div class="item">
22+
<label for="lang">Language</label>
23+
<select id="lang" class="language">
24+
<option value="nl-nl">Select a Language</option>
25+
<option value="nl-nl">Nederlands</option>
26+
<option value="fr-fr">Frans</option>
27+
<option value="en-us">Engels USA</option>
28+
<option value="en-gb">Engels Brits</option>
29+
</select>
30+
</div>
31+
32+
33+
<div class="item">
34+
<label for="room">Select Room</label>
35+
<select id="room" class="room">
36+
<option value="no">Select a room</option>
37+
</select>
38+
</div>
39+
40+
<input type="submit" class="submit" value="Say">
41+
</div>
42+
43+
</form>
44+
</main>
45+
<!-- <script src="./js/server.js" charset="utf-8"></script> -->
46+
<script src="./js/script.js" charset="utf-8"></script>
47+
</body>
48+
</html>

src/js/script.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// require("./sonos/server.js");
2+
3+
const init = data => {
4+
5+
getRooms(data);
6+
7+
const input = document.querySelector('.txtcon');
8+
const sound = document.querySelector('.range');
9+
const form = document.querySelector('form');
10+
const select = document.querySelector('.room');
11+
const language = document.querySelector('.language');
12+
13+
form.addEventListener('submit', e => {
14+
e.preventDefault();
15+
if (input.value !== '') SayIt({text: input.value, room: select.value, sound: sound.value, lang: language.value});
16+
input.value = '';
17+
});
18+
}
19+
20+
getRooms = data => {
21+
data.forEach(d => {
22+
const item = d.members[0].roomName;
23+
24+
let $option = html(`
25+
<option value="${item}">${item}</option>`);
26+
document.querySelector('.room').appendChild($option);
27+
})
28+
}
29+
30+
31+
const SayIt = string => {
32+
33+
fetch(`http://localhost:5005/${string.room}/say/${string.text.replace(/ /g,"%20")}/${string.lang}/${string.sound}`)
34+
.then(d => {
35+
console.log(d);
36+
if (d.status === 200) {
37+
document.querySelector('.succes').innerHTML = "It has been said!";
38+
}else {
39+
document.querySelector('.succes').innerHTML = "something went wrong";
40+
}
41+
document.querySelector('.succes').classList.add('visible');
42+
})
43+
}
44+
45+
const html = (strings, ...values) => {
46+
47+
let str = '';
48+
49+
if(Array.isArray(strings)){
50+
for(let i = 0; i < strings.length; i++){
51+
if(strings[i]) str += strings[i];
52+
if(values[i]) str += values[i];
53+
}
54+
}else{
55+
str = strings;
56+
}
57+
58+
let doc = new DOMParser().parseFromString(str.trim(), 'text/html');
59+
60+
return doc.body.firstChild;
61+
62+
};
63+
64+
fetchData = () => {
65+
fetch("http://localhost:5005/zones")
66+
.then(r => r.json())
67+
.then(data => init(data))
68+
.catch(err => {
69+
setTimeout(fetchData, 1000)
70+
});
71+
}
72+
73+
fetchData();
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict';
2+
function getMetadata(id, parentUri, type, title) {
3+
return `<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
4+
xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
5+
<item id="${id}" parentID="${parentUri}" restricted="true"><dc:title>"${title}"</dc:title><upnp:class>object.item.audioItem.${type}</upnp:class>
6+
<desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">SA_RINCON52231_X_#Svc52231-0-Token</desc></item></DIDL-Lite>`;
7+
}
8+
9+
function getSongUri(id) {
10+
return `x-sonos-http:${id}.mp4?sid=204&flags=8224&sn=4`;
11+
}
12+
13+
function getAlbumUri(id) {
14+
return `x-rincon-cpcontainer:0004206c${id}`;
15+
}
16+
17+
const uriTemplates = {
18+
song: getSongUri,
19+
album: getAlbumUri
20+
};
21+
22+
const CLASSES = {
23+
song: 'musicTrack',
24+
album: 'musicAlbum'
25+
};
26+
27+
const METADATA_URI_STARTERS = {
28+
song: '00032020',
29+
album: '0004206c'
30+
};
31+
32+
const PARENTS = {
33+
song: '0004206calbum%3a',
34+
album: '00020000album%3a'
35+
};
36+
37+
function appleMusic(player, values) {
38+
const action = values[0];
39+
const trackID = values[1];
40+
const type = trackID.split(':')[0];
41+
var nextTrackNo = 0;
42+
43+
const metadataID = METADATA_URI_STARTERS[type] + encodeURIComponent(trackID);
44+
const metadata = getMetadata(metadataID, PARENTS[type], CLASSES[type], '');
45+
const uri = uriTemplates[type](encodeURIComponent(trackID));
46+
47+
if (action == 'queue') {
48+
return player.coordinator.addURIToQueue(uri, metadata);
49+
} else if (action == 'now') {
50+
nextTrackNo = player.coordinator.state.trackNo + 1;
51+
return player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo)
52+
.then(() => player.coordinator.nextTrack())
53+
.then(() => player.coordinator.play());
54+
} else if (action == 'next') {
55+
nextTrackNo = player.coordinator.state.trackNo + 1;
56+
return player.coordinator.addURIToQueue(uri, metadata, true, nextTrackNo);
57+
}
58+
}
59+
60+
module.exports = function (api) {
61+
api.registerAction('applemusic', appleMusic);
62+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
3+
function clearqueue(player) {
4+
return player.coordinator.clearQueue();
5+
}
6+
7+
module.exports = function (api) {
8+
api.registerAction('clearqueue', clearqueue);
9+
};

0 commit comments

Comments
 (0)