Skip to content
This repository was archived by the owner on Nov 3, 2020. It is now read-only.

Commit 60d09bb

Browse files
authored
Merge pull request #59 from chokkyvista/alexa_timeout
Reprompt to keep session alive for long downloads
2 parents 8db5c91 + 04521c2 commit 60d09bb

File tree

4 files changed

+245
-114
lines changed

4 files changed

+245
-114
lines changed

src/index.js

Lines changed: 155 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,29 @@ var response_messages = require("./util/responses.js");
1212
// Create Alexa skill application
1313
var app = new alexa.app("youtube");
1414

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);
1722

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 = {};
2025

2126
// Variables relating to the last video searched
2227
var last_search = {};
2328
var last_token = {};
2429
var last_playback = {};
2530

26-
// Variables for repetition of current song
31+
// Variables for repetition of current song
2732
var repeat_infinitely = {};
2833
var repeat_once = {};
2934

35+
// To track whether downloading is already in progress
36+
var downloading = false;
37+
3038
/**
3139
* Generates a random UUID. Used for creating an audio stream token.
3240
*
@@ -103,7 +111,7 @@ function search_video(req, res, lang) {
103111
return new Promise((resolve, reject) => {
104112
var search = heroku + "/alexa/v3/search/" + new Buffer(query).toString("base64");
105113

106-
// Populate URL with correct language
114+
// Populate URL with correct language
107115
if (lang === "de-DE") {
108116
search += "?language=de";
109117
} else if (lang === "fr-FR") {
@@ -120,7 +128,7 @@ function search_video(req, res, lang) {
120128
} else {
121129
// Convert body text in response to JSON object
122130
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") {
124132
// Query did not return any video
125133
resolve({
126134
message: response_messages[lang]["NO_RESULTS_FOUND"].formatUnicorn(query),
@@ -155,6 +163,7 @@ function search_video(req, res, lang) {
155163

156164
// Set most recently searched for video
157165
buffer_search[userId] = metadata;
166+
downloading = false;
158167

159168
res.reprompt().shouldEndSession(false);
160169
}
@@ -167,8 +176,110 @@ function search_video(req, res, lang) {
167176
});
168177
}
169178

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+
170281
/**
171-
* Downloads the mostly recent video the user requested.
282+
* Downloads the mostly recent video the user requested.
172283
*
173284
* @param {Object} req A request from an Alexa device
174285
* @param {Object} res A response that will be sent to the device
@@ -181,7 +292,7 @@ function download_video(req, res) {
181292
console.log("Requesting download ... " + id);
182293

183294
return new Promise((resolve, reject) => {
184-
var download = heroku + "/alexa/v3/download/" + id;
295+
var download = heroku + "/alexa/v3/download/" + id;
185296

186297
// Make download request to server
187298
request(download, function(err, res, body) {
@@ -209,12 +320,7 @@ function download_video(req, res) {
209320
});
210321
}).then(function() {
211322
// 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);
218324

219325
// Send response to Alexa device
220326
res.send();
@@ -231,19 +337,17 @@ function download_video(req, res) {
231337
* @param {Function} callback The function to execute about load completion
232338
*/
233339
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();
244345
}
245-
});
246-
}, 2000);
346+
else {
347+
setTimeout(wait_for_video, cache_polling_interval, id, callback);
348+
}
349+
}
350+
});
247351
}
248352

249353
// 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) {
260364
}
261365
};
262366

367+
app.error = function(exc, req, res) {
368+
res.say("An error occured: " + exc);
369+
};
370+
263371
// Looking up a video in English
264372
app.intent("GetVideoIntent", {
265373
"slots": {
@@ -339,9 +447,24 @@ app.intent("AMAZON.YesIntent", function(req, res) {
339447
if (!buffer_search.hasOwnProperty(userId) || buffer_search[userId] == null) {
340448
res.send();
341449
}
342-
else {
450+
else if (!interactive_wait) {
343451
return download_video(req, res);
344452
}
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+
}
345468
});
346469

347470
app.intent("AMAZON.NoIntent", function(req, res) {
@@ -362,9 +485,9 @@ app.audioPlayer("PlaybackNearlyFinished", function(req, res) {
362485
var userId = req.userId;
363486

364487
// 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])))
368491
{
369492
// Generate new token for the stream
370493
var new_token = uuidv4();
@@ -473,7 +596,7 @@ app.intent("AMAZON.PauseIntent", {}, function(req, res) {
473596
res.send();
474597
});
475598

476-
// User told Alexa to repeat audio once
599+
// User told Alexa to repeat audio once
477600
app.intent("AMAZON.RepeatIntent", {}, function(req, res) {
478601
var userId = req.userId;
479602

0 commit comments

Comments
 (0)