forked from lordpengwin/muzak
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmuzak.js
More file actions
622 lines (486 loc) · 21.1 KB
/
muzak.js
File metadata and controls
622 lines (486 loc) · 21.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
/**
* Alexa Skills Kit program to expose the SqueezeServer to Alexa
*
*/
// Integration with the squeeze server
var SqueezeServer = require('squeezenode-lordpengwin');
var repromptText = "What do you want me to do";
// Configuration
var config = require('./config');
/**
* Route the incoming request based on type (LaunchRequest, IntentRequest,
* etc.) The JSON body of the request is provided in the event parameter.
*
* @param event
* @param context
*/
exports.handler = function (event, context) {
try {
console.log("event.session.application.applicationId=" + event.session.application.applicationId);
console.log("Event is %j", event);
// Limit access to only the configured application ID
if (event.session.application.applicationId !== config.alexaAppID) {
context.fail("Invalid Application ID");
}
if (event.session.new) {
onSessionStarted({requestId: event.request.requestId}, event.session);
}
if (event.request.type === "LaunchRequest") {
onLaunch(event.request,
event.session,
function callback(sessionAttributes, speechletResponse) {
context.succeed(buildResponse(sessionAttributes, speechletResponse));
});
} else if (event.request.type === "IntentRequest") {
onIntent(event.request,
event.session,
function callback(sessionAttributes, speechletResponse) {
context.succeed(buildResponse(sessionAttributes, speechletResponse));
});
} else if (event.request.type === "SessionEndedRequest") {
onSessionEnded(event.request, event.session);
context.succeed();
}
} catch (e) {
console.log("Caught exception %j", e);
context.fail("Exception: " + e);
}
};
/**
* Called when the session starts.
*/
function onSessionStarted(sessionStartedRequest, session) {
console.log("onSessionStarted requestId=" + sessionStartedRequest.requestId + ", sessionId=" + session.sessionId);
}
/**
* Called when the user launches the skill without specifying what they want. When this happens we go into a mode where
* they can issue multiple requests
*
* @param launchRequest The request
* @param session The current session
* @param callback A callback used to return the result
*/
function onLaunch(launchRequest, session, callback) {
console.log("onLaunch requestId=" + launchRequest.requestId + ", sessionId=" + session.sessionId);
// Connect to the squeeze server and wait for it to finish its registration. We do this to make sure that it is online
var squeezeserver = new SqueezeServer(config.squeezeserverURL, config.squeezeserverPort, config.squeezeServerUsername, config.squeezeServerPassword);
squeezeserver.on('register', function() {
startInteractiveSession(callback)
});
}
/**
* Called when the user specifies an intent for this skill.
*
* @param intentRequest The full request
* @param session The current session
* @param callback A callback used to return results
*/
function onIntent(intentRequest, session, callback) {
console.log("onIntent requestId=" + intentRequest.requestId + ", sessionId=" + session.sessionId);
// Check for a Close intent
if (intentRequest.intent.intentName == "Close") {
closeInteractiveSession(callback);
return;
}
// Connect to the squeeze server and wait for it to finish its registration
var squeezeserver = new SqueezeServer(config.squeezeserverURL, config.squeezeserverPort, config.squeezeServerUsername, config.squeezeServerPassword);
squeezeserver.on('register', function() {
// Get the list of players as any request will require them
squeezeserver.getPlayers(function(reply) {
if (reply.ok) {
console.log("getPlayers: %j", reply);
dispatchIntent(squeezeserver, reply.result, intentRequest.intent, session, callback);
} else
callback(session.attributes, buildSpeechletResponse("Get Players", "Failed to get list of players", null, true));
})
});
}
/**
* Identify the intent and dispatch it to the target function
*
* @param squeezeserver The handler to the SqueezeServer
* @param players A list of players on the server
* @param intent The target intent
* @param session The current session
* @param callback The callback to use to return the result
*/
function dispatchIntent(squeezeserver, players, intent, session, callback) {
var intentName = intent.name;
console.log("Got intent: %j", intent);
console.log("Session is %j", session);
if ("SyncPlayers" == intentName) {
syncPlayers(squeezeserver, players, intent, session, callback);
} else {
// Try to find the target player
var player = findPlayerObject(squeezeserver, players, ((typeof intent.slots.Player.value !== 'undefined') && (intent.slots.Player.value != null) ?
intent.slots.Player.value :
(typeof session.attributes !== 'undefined' ? session.attributes.player : "")));
if (player == null) {
// Couldn't find the player, return an error response
console.log("Player not found: " + intent.slots.Player.value);
callback(session.attributes, buildSpeechletResponse(intentName, "Player not found", null, session.new));
} else {
console.log("Player is " + player);
session.attributes = {player: player.name.toLowerCase()};
// Call the target intent
if ("StartPlayer" == intentName) {
startPlayer(player, session, callback);
} else if ("StopPlayer" == intentName) {
stopPlayer(player, session, callback);
} else if ("UnsyncPlayer" == intentName) {
unsyncPlayer(player, session, callback);
} else if ("SetVolume" == intentName) {
setPlayerVolume(player, Number(intent.slots.Volume.value), session, callback);
} else if ("IncreaseVolume" == intentName) {
getPlayerVolume(player, session, callback, 10);
} else if ("DecreaseVolume" == intentName) {
getPlayerVolume(player, session, callback, -10);
} else if ("WhatsPlaying" == intentName) {
whatsPlaying(player, session, callback);
} else if ("SelectPlayer" == intentName) {
selectPlayer(player, session, callback);
} else {
callback(session.attributes, buildSpeechletResponse("Muzak", intentName + " is not a valid request", repromptText, session.new));
throw " intent";
}
}
}
}
/**
* Called when the user ends the session.
* Is not called when the skill returns shouldEndSession=true.
*/
function onSessionEnded(sessionEndedRequest, session) {
console.log("onSessionEnded requestId=" + sessionEndedRequest.requestId + ", sessionId=" + session.sessionId);
}
/**
* This is called when the user activates the service without arguments
*
* @param callback A callback to execute to return the response
*/
function startInteractiveSession(callback) {
// If we wanted to initialize the session to have some attributes we could add those here.
var sessionAttributes = {};
var cardTitle = "Muzak Started";
var speechOutput = "Muzak Online";
var shouldEndSession = false;
// Format the default response
callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
/**
* Called to close an insteractive session
*
* @param callback A callback to execute to return the response
*/
function closeInteractiveSession(callback) {
var sessionAttributes = {};
var cardTitle = "Muzak Closed";
var speechOutput = "Muzak Offline";
var shouldEndSession = true;
// Format the default response
callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}
/**
* Start a player to play random tracks
*
* @param player The player to start
* @param session The current session
* @param callback The callback to use to return the result
*/
function startPlayer(player, session, callback) {
console.log("In startPlayer with player %s", player.name);
try {
// Start the player
player.randomPlay("tracks", function(reply) {
if (reply.ok)
callback(session.attributes, buildSpeechletResponse("Start Player", "Playing " + player.name, null, session.new));
else
callback(session.attributes, buildSpeechletResponse("Start Player", "Failed to start player " + player.name, null, true));
});
} catch (ex) {
console.log("Caught exception in startPlayer %j", ex);
callback(session.attributes, buildSpeechletResponse("Start Player", "Caught Exception", null, true));
}
}
/**
* Select the given player for an interactive session.
*
* @param player The player to select
* @param session The current session
* @param callback The callback to use to return the result
*/
function selectPlayer(player, session, callback) {
// The player is already selected
callback(session.attributes, buildSpeechletResponse("Select Player", "Selected player " + player.name, null, false));
}
/**
* Stop a player
*
* @param player The player to stop
* @param session The current session
* @param callback The callback to use to return the result
*/
function stopPlayer(player, session, callback) {
try {
console.log("In stopPlayer with player %s", player.name);
// Stop the player
player.power(0, function(reply) {
if (reply.ok)
callback(session.attributes, buildSpeechletResponse("Stop Player", "Stopped " + player.name, null, session.new));
else {
console.log("Reply %j", reply);
callback(session.attributes, buildSpeechletResponse("Stop Player", "Failed to stop player " + player.name, null, true));
}
});
} catch (ex) {
console.log("Caught exception in stopPlayer %j", ex);
callback(session.attributes, buildSpeechletResponse("Stop Player", "Caught Exception", null, true));
}
}
/**
* Sync one player to another
*
* @param squeezeserver The handler to the SqueezeServer
* @param players A list of players on the server
* @param intent The target intent
* @param session The current session
* @param callback The callback to use to return the result
*/
function syncPlayers(squeezeserver, players, intent, session, callback) {
//// TODO: Need to make sure that both players are turned on.
var player1 = null;
var player2 = null;
try {
console.log("In syncPlayers with intent %j", intent);
// Try to find the target players. We need the sqeezeserver player object for the first, but only the player info
// object for the second.
player1 = findPlayerObject(squeezeserver, players, ((typeof intent.slots.FirstPlayer.value !== 'undefined') && (intent.slots.FirstPlayer.value != null) ? intent.slots.FirstPlayer.value : session.attributes.player));
if (player1 == null) {
// Couldn't find the player, return an error response
console.log("Player not found: " + intent.slots.FirstPlayer.value);
callback(session.attributes, buildSpeechletResponse(intentName, "Player not found", null, session.new));
}
session.attributes = {player: player1.name.toLowerCase()};
player2 = null;
for (var pl in players) {
if (players[pl].name.toLowerCase() === normalizePlayer(intent.slots.SecondPlayer.value))
player2 = players[pl];
}
// If we found the target players, sync them
if (player1 && player2) {
console.log("Found players: %j and player2", player1, player2);
player1.sync(player2.playerindex, function(reply) {
if (reply.ok)
callback(session.attributes, buildSpeechletResponse("Sync Players", "Synced " + player1.name + " to " + player2.name, null, session.new));
else {
console.log("Failed to sync %j", reply);
callback(session.attributes, buildSpeechletResponse("Sync Players", "Failed to sync players " + player1.name + " and " + player2.name, null, true));
}
});
} else {
console.log("Player not found: ");
callback(session.attributes, buildSpeechletResponse("Sync Players", "Player not found", null, session.new));
}
} catch (ex) {
console.log("Caught exception in syncPlayers %j for " + player1 + " and " + player2, ex);
callback(session.attributes, buildSpeechletResponse("Sync Players", "Caught Exception", null, true));
}
}
/**
* Get the current volume of a player and then perform a change function on it
*
* @param player The player to get the volume for
* @param session The current session
* @param callback The callback to use to return the result
* @param delta The amount to change the player volume
*/
function getPlayerVolume(player, session, callback, delta) {
console.log("In getPlayerVolume with player %s", player.name);
try {
// Get the volume of the player
player.getVolume(function(reply) {
if (reply.ok) {
var volume = Number(reply.result);
setPlayerVolume(player, volume + delta, session, callback);
} else
callback(session.attributes, buildSpeechletResponse("Get Player Volume", "Failed to get volume for player " + player.name, null, true));
});
} catch (ex) {
console.log("Caught exception in stopPlayer %j", ex);
callback(session.attributes, buildSpeechletResponse("Get Player Volume", "Caught Exception", null, true));
}
}
/**
* Set the volume of a player.
*
* @param player The target player
* @param volume The level to set the volume to
* @param session The current session
* @param callback The callback to use to return the result
*/
function setPlayerVolume(player, volume, session, callback) {
// Make sure the volume is in the range 0 - 100
if (volume > 100)
volume = 100;
else if (volume < 0)
volume = 0;
try {
console.log("In setPlayerVolume with volume:" + volume);
// Set the volume on the player
player.setVolume(volume, function(reply) {
if (reply.ok)
callback(session.attributes, buildSpeechletResponse("Set Player Volume", "Player " + player.name + " set to volume " + volume, null, session.new));
else {
console.log("Failed to set volume %j", reply);
callback(session.attributes, buildSpeechletResponse("Set Player Volume", "Failed to set player volume", null, true));
}
});
} catch (ex) {
console.log("Caught exception in setPlayerVolume %j", ex);
callback(session.attributes, buildSpeechletResponse("Set Player", "Caught Exception", null, true));
}
}
/**
* Unsync a player
*
* @param player The player to unsync
* @param session The current session
* @param callback The callback to use to return the result
*/
function unsyncPlayer(player, session, callback) {
console.log("In unsyncPlayer with player %s", player.name);
try {
// Unsynchronize the player
player.unSync(function(reply) {
if (reply.ok)
callback(session.attributes, buildSpeechletResponse("Unsync Player", "Player " + player.name + " unsynced", null, session.new));
else {
console.log("Failed to sync %j", reply);
callback(session.attributes, buildSpeechletResponse("Unsync Player", "Failed to unsync player " + player.name, null, true));
}
});
} catch (ex) {
console.log("Caught exception in unsyncPlayer %j", ex);
callback(session.attributes, buildSpeechletResponse("Unsync Player", "Caught Exception", null, true));
}
}
/**
* Find out what is playing on a player.
*
* @param squeezeserver The handler to the SqueezeServer
* @param player The player to get the information for
* @param session The current session
* @param callback The callback to use to return the result
*/
function whatsPlaying(player, session, callback) {
console.log("In whatIsPlaying with player %s", player.name);
try {
// Ask the player it what it is playing. This is a series of requests for the song, artist and album
player.getCurrentTitle(function(reply) {
if (reply.ok) {
// We got the title now get the artist
var title = reply.result;
player.getArtist(function(reply) {
if (reply.ok) {
var artist = reply.result;
player.getAlbum(function(reply) {
if (reply.ok) {
var album = reply.result;
callback(session.attributes, buildSpeechletResponse("Whats Playing", "Player " + player.name + " is playing " + title + " by " + artist + " from " + album, null, session.new));
} else {
console.log("Failed to get album");
callback(session.attributes, buildSpeechletResponse("Whats Playing", "Player " + player.name + " is playing " + title + " by " + artist, null, session.new));
}
});
} else {
console.log("Failed to get current artist");
callback(session.attributes, buildSpeechletResponse("Whats Playing", "Player " + player.name + " is playing " + title, null, session.new));
}
});
} else {
console.log("Failed to getCurrentTitle %j", reply);
callback(session.attributes, buildSpeechletResponse("Whats Player", "Failed to get current song for " + player.name, null, true));
}
});
} catch (ex) {
console.log("Caught exception in whatIsplaying %j", ex);
callback(session.attributes, buildSpeechletResponse("Whats Playing", "Caught Exception", null, true));
}
}
/**
* Find a player object given its name. Player objects can be used to interact with the player
*
* @param squeezeserver The SqueezeServer to get the Player object from
* @param players A list of players to search
* @param name The name of the player to find
* @returns The target player or null if it is not found
*/
function findPlayerObject(squeezeserver, players, name) {
name = normalizePlayer(name);
console.log("In findPlayerObject with " + name);
// Look for the player in the players list that matches the given name. Then return the corresponding player object
// from the squeezeserver stored by the player's id
// NOTE: For some reason squeezeserver.players[] is empty but you can still reference values in it. I think it
// is a weird javascript timing thing
for (var pl in players) {
if (players[pl].name.toLowerCase() === name)
return squeezeserver.players[players[pl].playerid];
}
console.log("Player %s not found", name);
}
/**
* Do any necessary clean up of player names
*
* @param playerName The name of the player to clean up
* @returns The normalized player name
*/
function normalizePlayer(playerName) {
// After the switch to custom slots multi name players like living room became living-room. Revert the string back to what it was
playerName = playerName.replace("-", " ");
if (playerName.toLowerCase() == "livingroom")
playerName = "living room";
return playerName;
}
/**
* Format a response to send to the Echo
*
* @param title The title for the UI Card
* @param output The speech output
* @param repromptText The prompt for more information
* @param shouldEndSession A flag to end the session
* @returns A formatted JSON object containing the response
*/
function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
return {
outputSpeech: {
type: "PlainText",
text: output
},
card: {
type: "Simple",
title: "SessionSpeechlet - " + title,
content: "SessionSpeechlet - " + output
},
reprompt: {
outputSpeech: {
type: "PlainText",
text: repromptText
}
},
shouldEndSession: shouldEndSession
}
}
/**
* Return the response
*
* @param sessionAttributes The attributes for the current session
* @param speechletResponse The response object
* @returns A formatted object for the response
*/
function buildResponse(sessionAttributes, speechletResponse) {
return {
version: "1.0",
sessionAttributes: sessionAttributes,
response: speechletResponse
}
}