@@ -12,21 +12,29 @@ var response_messages = require("./util/responses.js");
12
12
// Create Alexa skill application
13
13
var app = new alexa.app("youtube");
14
14
15
- // Set Heroku URL
16
- var heroku = process.env.HEROKU_APP_URL || "https://dmhacker-youtube.herokuapp.com";
15
+ // Process environment variables
16
+ const heroku = process.env.HEROKU_APP_URL || "https://dmhacker-youtube.herokuapp.com";
17
+ const interactive_wait = !(process.env.DISABLE_INTERACTIVE_WAIT === "true" ||
18
+ process.env.DISABLE_INTERACTIVE_WAIT === true ||
19
+ process.env.DISABLE_INTERACTIVE_WAIT === 1);
20
+ const cache_polling_interval = parseInt(process.env.CACHE_POLLING_INTERVAL || "5000", 10);
21
+ const ask_interval = parseInt(process.env.ASK_INTERVAL || "45000", 10);
17
22
18
- // Variables relating to videos waiting for user input
19
- var buffer_search = {};
23
+ // Variables relating to videos waiting for user input
24
+ var buffer_search = {};
20
25
21
26
// Variables relating to the last video searched
22
27
var last_search = {};
23
28
var last_token = {};
24
29
var last_playback = {};
25
30
26
- // Variables for repetition of current song
31
+ // Variables for repetition of current song
27
32
var repeat_infinitely = {};
28
33
var repeat_once = {};
29
34
35
+ // To track whether downloading is already in progress
36
+ var downloading = false;
37
+
30
38
/**
31
39
* Generates a random UUID. Used for creating an audio stream token.
32
40
*
@@ -103,7 +111,7 @@ function search_video(req, res, lang) {
103
111
return new Promise((resolve, reject) => {
104
112
var search = heroku + "/alexa/v3/search/" + new Buffer(query).toString("base64");
105
113
106
- // Populate URL with correct language
114
+ // Populate URL with correct language
107
115
if (lang === "de-DE") {
108
116
search += "?language=de";
109
117
} else if (lang === "fr-FR") {
@@ -120,7 +128,7 @@ function search_video(req, res, lang) {
120
128
} else {
121
129
// Convert body text in response to JSON object
122
130
var body_json = JSON.parse(body);
123
- if (body_json.status === "error" && body_json.message === "No results found") {
131
+ if (body_json.state === "error" && body_json.message === "No results found") {
124
132
// Query did not return any video
125
133
resolve({
126
134
message: response_messages[lang]["NO_RESULTS_FOUND"].formatUnicorn(query),
@@ -155,6 +163,7 @@ function search_video(req, res, lang) {
155
163
156
164
// Set most recently searched for video
157
165
buffer_search[userId] = metadata;
166
+ downloading = false;
158
167
159
168
res.reprompt().shouldEndSession(false);
160
169
}
@@ -167,8 +176,110 @@ function search_video(req, res, lang) {
167
176
});
168
177
}
169
178
179
+ function make_download_video_request(id) {
180
+ return new Promise((resolve, reject) => {
181
+ request(heroku + "/alexa/v3/download/" + id, function(err, res, body) {
182
+ if (err) {
183
+ console.error(err.message);
184
+ reject(err.message);
185
+ } else {
186
+ var body_json = JSON.parse(body);
187
+ var url = heroku + body_json.link;
188
+ console.log("Requested download for ... " + url);
189
+ resolve(url);
190
+ }
191
+ });
192
+ });
193
+ }
194
+
195
+ function check_cache_ready(id, timeout) {
196
+ return new Promise((resolve, reject) => {
197
+ request(heroku + "/alexa/v3/cache/" + id, function(err, res, body) {
198
+ if (!err) {
199
+ var body_json = JSON.parse(body);
200
+ if (body_json.hasOwnProperty('downloaded') && body_json['downloaded'] != null) {
201
+ if (body_json.downloaded) {
202
+ downloading = false;
203
+
204
+ console.log(id + " has been cached. Ready to play!");
205
+
206
+ resolve();
207
+ }
208
+ else {
209
+ downloading = true;
210
+
211
+ console.log(id + " is being cached.");
212
+ if (timeout <= 0) {
213
+ resolve();
214
+ return;
215
+ }
216
+
217
+ var interval = Math.min(cache_polling_interval, timeout);
218
+ console.log("Checking again in " + interval + "ms (delay: " + timeout + "ms).");
219
+
220
+ resolve(new Promise((_resolve, _reject) => {
221
+ setTimeout(() => {
222
+ _resolve(check_cache_ready(id, timeout - cache_polling_interval).catch(_reject));
223
+ }, interval);
224
+ }).catch(reject));
225
+ }
226
+ }
227
+ else {
228
+ console.log(id + " will not be cached.");
229
+ reject("Video unavailable.");
230
+ }
231
+ }
232
+ else {
233
+ console.error(err.message);
234
+ reject(err.message);
235
+ }
236
+ });
237
+ });
238
+ }
239
+
240
+ function respond_play(req, res) {
241
+ var userId = req.userId;
242
+
243
+ // Final response to the user, indicating that their video will be playing
244
+ var speech = new ssml();
245
+ var title = buffer_search[userId].title;
246
+ var message = response_messages[req.data.request.locale]["NOW_PLAYING"].formatUnicorn(title);
247
+ speech.say(message);
248
+ res.say(speech.ssml(true));
249
+
250
+ console.log("Starting to play ... " + title);
251
+
252
+ // Start playing the video!
253
+ restart_video(req, res, 0);
254
+ }
255
+
256
+ function interactively_wait_for_video(req, res) {
257
+ var userId = req.userId;
258
+ var id = buffer_search[userId].id;
259
+ return check_cache_ready(id, ask_interval).then(() => {
260
+ if (!downloading) {
261
+ // Download finished ... notify user
262
+ respond_play(req, res);
263
+ }
264
+ else {
265
+ console.log("Asking whether to continue waiting." );
266
+
267
+ // Download still in progress ... ask if the user wants to keep tracking
268
+ var message = response_messages[req.data.request.locale]["ASK_TO_CONTINUE"];
269
+ var speech = new ssml();
270
+ speech.say(message);
271
+ res.say(speech.ssml(true));
272
+ res.reprompt(message).shouldEndSession(false);
273
+ }
274
+ return res.send();
275
+ }).catch(reason => {
276
+ console.error(reason);
277
+ return res.fail(reason);
278
+ });
279
+ }
280
+
170
281
/**
171
- * Downloads the mostly recent video the user requested.
282
+ * Downloads the mostly recent video the user requested.
172
283
*
173
284
* @param {Object} req A request from an Alexa device
174
285
* @param {Object} res A response that will be sent to the device
@@ -181,7 +292,7 @@ function download_video(req, res) {
181
292
console.log("Requesting download ... " + id);
182
293
183
294
return new Promise((resolve, reject) => {
184
- var download = heroku + "/alexa/v3/download/" + id;
295
+ var download = heroku + "/alexa/v3/download/" + id;
185
296
186
297
// Make download request to server
187
298
request(download, function(err, res, body) {
@@ -209,12 +320,7 @@ function download_video(req, res) {
209
320
});
210
321
}).then(function() {
211
322
// Have Alexa tell the user that the video is finished downloading
212
- var speech = new ssml();
213
- speech.say(response_messages[req.data.request.locale]["NOW_PLAYING"].formatUnicorn(buffer_search[userId].title));
214
- res.say(speech.ssml(true));
215
-
216
- // Start playing the video!
217
- restart_video(req, res, 0);
323
+ respond_play(req, res);
218
324
219
325
// Send response to Alexa device
220
326
res.send();
@@ -231,19 +337,17 @@ function download_video(req, res) {
231
337
* @param {Function} callback The function to execute about load completion
232
338
*/
233
339
function wait_for_video(id, callback) {
234
- setTimeout(function() {
235
- request(heroku + "/alexa/v3/cache/" + id, function(err, res, body) {
236
- if (!err) {
237
- var body_json = JSON.parse(body);
238
- if (body_json.downloaded) {
239
- callback();
240
- }
241
- else {
242
- wait_for_video(id, callback);
243
- }
340
+ request(heroku + "/alexa/v3/cache/" + id, function(err, res, body) {
341
+ if (!err) {
342
+ var body_json = JSON.parse(body);
343
+ if (body_json.downloaded) {
344
+ callback();
244
345
}
245
- });
246
- }, 2000);
346
+ else {
347
+ setTimeout(wait_for_video, cache_polling_interval, id, callback);
348
+ }
349
+ }
350
+ });
247
351
}
248
352
249
353
// Filter out bad requests (the client's ID is not the same as the server's)
@@ -260,6 +364,10 @@ app.pre = function(req, res, type) {
260
364
}
261
365
};
262
366
367
+ app.error = function(exc, req, res) {
368
+ res.say("An error occured: " + exc);
369
+ };
370
+
263
371
// Looking up a video in English
264
372
app.intent("GetVideoIntent", {
265
373
"slots": {
@@ -339,9 +447,24 @@ app.intent("AMAZON.YesIntent", function(req, res) {
339
447
if (!buffer_search.hasOwnProperty(userId) || buffer_search[userId] == null) {
340
448
res.send();
341
449
}
342
- else {
450
+ else if (!interactive_wait) {
343
451
return download_video(req, res);
344
452
}
453
+ else {
454
+ var id = buffer_search[userId].id;
455
+ if (!downloading) {
456
+ return make_download_video_request(id)
457
+ .then(url => {
458
+ downloading = true;
459
+ last_search[userId] = url;
460
+ return interactively_wait_for_video(req, res);
461
+ })
462
+ .catch(reason => {
463
+ return res.fail(reason);
464
+ });
465
+ }
466
+ return interactively_wait_for_video(req, res);
467
+ }
345
468
});
346
469
347
470
app.intent("AMAZON.NoIntent", function(req, res) {
@@ -362,9 +485,9 @@ app.audioPlayer("PlaybackNearlyFinished", function(req, res) {
362
485
var userId = req.userId;
363
486
364
487
// Repeat is enabled, so begin next playback
365
- if (has_video(userId) &&
366
- ((repeat_infinitely.hasOwnProperty(userId) && repeat_infinitely[userId]) ||
367
- (repeat_once.hasOwnProperty(userId) && repeat_once[userId])))
488
+ if (has_video(userId) &&
489
+ ((repeat_infinitely.hasOwnProperty(userId) && repeat_infinitely[userId]) ||
490
+ (repeat_once.hasOwnProperty(userId) && repeat_once[userId])))
368
491
{
369
492
// Generate new token for the stream
370
493
var new_token = uuidv4();
@@ -473,7 +596,7 @@ app.intent("AMAZON.PauseIntent", {}, function(req, res) {
473
596
res.send();
474
597
});
475
598
476
- // User told Alexa to repeat audio once
599
+ // User told Alexa to repeat audio once
477
600
app.intent("AMAZON.RepeatIntent", {}, function(req, res) {
478
601
var userId = req.userId;
479
602
0 commit comments