From 4c2430a9913c10f49cb7babc6f1b0a80b8fdf7f5 Mon Sep 17 00:00:00 2001 From: Minjie Xu Date: Sun, 30 Dec 2018 17:07:21 +0000 Subject: [PATCH 1/9] Reprompt to keep session alive for long videos --- src/index.js | 171 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 139 insertions(+), 32 deletions(-) diff --git a/src/index.js b/src/index.js index dd5b8e8..b287743 100644 --- a/src/index.js +++ b/src/index.js @@ -13,20 +13,27 @@ var response_messages = require("./util/responses.js"); var app = new alexa.app("youtube"); // Set Heroku URL -var heroku = process.env.HEROKU_APP_URL || "https://dmhacker-youtube.herokuapp.com"; +const heroku = process.env.HEROKU_APP_URL || "https://dmhacker-youtube.herokuapp.com"; -// Variables relating to videos waiting for user input -var buffer_search = {}; +const interactive_wait = process.env.INTERACTIVE_WAIT == "true"; +const cache_polling_interval = parseInt(process.env.CACHE_POLLING_INTERVAL || "10000", 10); +const alexa_request_timeout = parseInt(process.env.ALEXA_REQUEST_TIMEOUT || "45000", 10); + +// Variables relating to videos waiting for user input +var buffer_search = {}; // Variables relating to the last video searched var last_search = {}; var last_token = {}; var last_playback = {}; -// Variables for repetition of current song +// Variables for repetition of current song var repeat_infinitely = {}; var repeat_once = {}; +// To track whether downloading is already in progress +var downloading = false; + /** * Generates a random UUID. Used for creating an audio stream token. * @@ -103,7 +110,7 @@ function search_video(req, res, lang) { return new Promise((resolve, reject) => { var search = heroku + "/alexa/v3/search/" + new Buffer(query).toString("base64"); - // Populate URL with correct language + // Populate URL with correct language if (lang === "de-DE") { search += "?language=de"; } else if (lang === "fr-FR") { @@ -155,6 +162,7 @@ function search_video(req, res, lang) { // Set most recently searched for video buffer_search[userId] = metadata; + downloading = false; res.reprompt().shouldEndSession(false); } @@ -167,8 +175,77 @@ function search_video(req, res, lang) { }); } +function make_download_video_request(id) { + return new Promise((resolve, reject) => { + request(heroku + "/alexa/v3/download/" + id, function(err, res, body) { + if (err) { + console.error(err.message); + reject(err.message); + } else { + var body_json = JSON.parse(body); + var url = heroku + body_json.link; + console.log("Requested downloading for ... " + url); + resolve(url); + } + }); + }); +} + +function check_cache_ready(id, timeout) { + return new Promise((resolve, reject) => { + request(heroku + "/alexa/v3/cache/" + id, function(err, res, body) { + if (!err) { + var body_json = JSON.parse(body); + if (body_json.hasOwnProperty('downloaded') && body_json['downloaded'] != null) { + if (body_json.downloaded) { + downloading = false; + console.log(id + " ready in the cache!"); + resolve(); + } + else { + downloading = true; + console.log(id + " being cached atm."); + if (timeout <= 0) { + resolve(); + return; + } + var interval = Math.min(cache_polling_interval, timeout); + console.log("Will check again in " + interval + "ms (timeout: " + timeout + "ms)."); + resolve(new Promise((_resolve) => { + setTimeout(() => { + _resolve(check_cache_ready(id, timeout - cache_polling_interval)); + }, interval); + })); + } + } + else { + console.log(id + " never cached yet."); + reject("Video unavailable"); + } + } + else { + console.error(err.message); + reject(err.message); + } + }); + }); +} + +function respond_play(req, res) { + var userId = req.userId; + var speech = new ssml(); + var title = buffer_search[userId].title; + var message = response_messages[req.data.request.locale]["NOW_PLAYING"].formatUnicorn(title); + speech.say(message); + res.say(speech.ssml(true)); + + console.log("Start playing ... " + title); + // Start playing the video! + restart_video(req, res, 0); +} + /** - * Downloads the mostly recent video the user requested. + * Downloads the mostly recent video the user requested. * * @param {Object} req A request from an Alexa device * @param {Object} res A response that will be sent to the device @@ -181,7 +258,7 @@ function download_video(req, res) { console.log("Requesting download ... " + id); return new Promise((resolve, reject) => { - var download = heroku + "/alexa/v3/download/" + id; + var download = heroku + "/alexa/v3/download/" + id; // Make download request to server request(download, function(err, res, body) { @@ -195,9 +272,10 @@ function download_video(req, res) { // Set last search & token to equal the current video's parameters last_search[userId] = heroku + body_json.link; + // MX: Not convinced this should be necessary ... // NOTE: this is somewhat of hack to get Alexa to ignore an errant PlaybackNearlyFinished event - repeat_once[userId] = true; - repeat_infinitely[userId] = false; + // repeat_once[userId] = true; + // repeat_infinitely[userId] = false; // Wait until video is downloaded by repeatedly pinging cache console.log("Waiting for ... " + last_search[userId]); @@ -209,12 +287,7 @@ function download_video(req, res) { }); }).then(function() { // Have Alexa tell the user that the video is finished downloading - var speech = new ssml(); - speech.say(response_messages[req.data.request.locale]["NOW_PLAYING"].formatUnicorn(buffer_search[userId].title)); - res.say(speech.ssml(true)); - - // Start playing the video! - restart_video(req, res, 0); + respond_play(req, res); // Send response to Alexa device res.send(); @@ -231,19 +304,17 @@ function download_video(req, res) { * @param {Function} callback The function to execute about load completion */ function wait_for_video(id, callback) { - setTimeout(function() { - request(heroku + "/alexa/v3/cache/" + id, function(err, res, body) { - if (!err) { - var body_json = JSON.parse(body); - if (body_json.downloaded) { - callback(); - } - else { - wait_for_video(id, callback); - } + request(heroku + "/alexa/v3/cache/" + id, function(err, res, body) { + if (!err) { + var body_json = JSON.parse(body); + if (body_json.downloaded) { + callback(); } - }); - }, 2000); + else { + setTimeout(wait_for_video, cache_polling_interval, id, callback); + } + } + }); } // Filter out bad requests (the client's ID is not the same as the server's) @@ -333,15 +404,51 @@ app.intent("GetVideoItalianIntent", { } ); +function interactively_wait_for_video(req, res) { + var userId = req.userId; + var id = buffer_search[userId].id; + return check_cache_ready(id, alexa_request_timeout).then(() => { + if (!downloading) { + respond_play(req, res); + } + else { + console.log("Asking whether to continue waiting ..."); + var message = "Download still in progress. Would you like to keep waiting?"; + var speech = new ssml(); + speech.say(message); + res.say(speech.ssml(true)); + res.reprompt(message).shouldEndSession(false); + } + res.send(); + }).catch(reason => { + res.fail(reason); + }); +} + app.intent("AMAZON.YesIntent", function(req, res) { var userId = req.userId; if (!buffer_search.hasOwnProperty(userId) || buffer_search[userId] == null) { res.send(); } - else { + else if (!interactive_wait) { return download_video(req, res); } + else { + var id = buffer_search[userId].id; + if (!downloading) { + return make_download_video_request(id) + .then(url => { + downloading = true; + last_search[userId] = url; + return interactively_wait_for_video(req, res); + }) + .catch(reason => { + res.fail(reason); + }); + } + return interactively_wait_for_video(req, res); + } }); app.intent("AMAZON.NoIntent", function(req, res) { @@ -362,9 +469,9 @@ app.audioPlayer("PlaybackNearlyFinished", function(req, res) { var userId = req.userId; // Repeat is enabled, so begin next playback - if (has_video(userId) && - ((repeat_infinitely.hasOwnProperty(userId) && repeat_infinitely[userId]) || - (repeat_once.hasOwnProperty(userId) && repeat_once[userId]))) + if (has_video(userId) && + ((repeat_infinitely.hasOwnProperty(userId) && repeat_infinitely[userId]) || + (repeat_once.hasOwnProperty(userId) && repeat_once[userId]))) { // Generate new token for the stream var new_token = uuidv4(); @@ -473,7 +580,7 @@ app.intent("AMAZON.PauseIntent", {}, function(req, res) { res.send(); }); -// User told Alexa to repeat audio once +// User told Alexa to repeat audio once app.intent("AMAZON.RepeatIntent", {}, function(req, res) { var userId = req.userId; From 5728ddb8afbea265c7d6aa6ae87813aa3a85f40a Mon Sep 17 00:00:00 2001 From: Minjie Xu Date: Sun, 30 Dec 2018 17:08:22 +0000 Subject: [PATCH 2/9] Enable UK locale --- src/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.js b/src/index.js index b287743..8eb985b 100644 --- a/src/index.js +++ b/src/index.js @@ -8,6 +8,7 @@ var alexa = require("alexa-app"); var request = require("request"); var ssml = require("ssml-builder"); var response_messages = require("./util/responses.js"); +response_messages['en-GB'] = response_messages['en-US']; // Create Alexa skill application var app = new alexa.app("youtube"); From d203be8960ab35963667b480314614b7d573dbae Mon Sep 17 00:00:00 2001 From: Minjie Xu Date: Sun, 30 Dec 2018 17:09:17 +0000 Subject: [PATCH 3/9] Minor fixes --- src/index.js | 2 +- src/util/responses.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 8eb985b..f8baa3b 100644 --- a/src/index.js +++ b/src/index.js @@ -128,7 +128,7 @@ function search_video(req, res, lang) { } else { // Convert body text in response to JSON object var body_json = JSON.parse(body); - if (body_json.status === "error" && body_json.message === "No results found") { + if (body_json.state === "error" && body_json.message === "No results found") { // Query did not return any video resolve({ message: response_messages[lang]["NO_RESULTS_FOUND"].formatUnicorn(query), diff --git a/src/util/responses.js b/src/util/responses.js index 40a21e7..d0b86b9 100644 --- a/src/util/responses.js +++ b/src/util/responses.js @@ -1,14 +1,14 @@ module.exports = { "en-US": { "NO_RESULTS_FOUND": "{0} did not return any results on YouTube.", - "ASK_TO_PLAY": "I found a video called {0}. Would you like me to play it?", + "ASK_TO_PLAY": "I found a video called {0}. Would you like me to download it now?", "NOW_PLAYING": "I am now playing {0}.", "NOTHING_TO_RESUME": "You are not playing anything currently.", "NOTHING_TO_REPEAT": "You have not selected a video to play.", "LOOP_ON_TRIGGERED": "I will repeat your {0} selection infinitely.", "LOOP_OFF_TRIGGERED": "I will no longer repeat your {0} selection.", "REPEAT_TRIGGERED": "I will repeat your {0} selection once.", - "HELP_TRIGGERED": "To use the YouTube skill, tell the skill to search for the video you want. Additionally, once the video is playing, you can tell Alexa to pause, restart, or loop it." + "HELP_TRIGGERED": "To use the YouTube skill, tell the skill to search for the video you want. Additionally, once the video is playing, you can tell Alexa to pause, restart, or loop it." }, "de-DE": { "NO_RESULTS_FOUND": "Keine Ergebnisse auf Youtube gefunden.", From 22d89a9f54e4e17a5071a0cb561d4d3f05819826 Mon Sep 17 00:00:00 2001 From: Minjie Xu Date: Sun, 30 Dec 2018 18:08:52 +0000 Subject: [PATCH 4/9] Better error handling --- src/index.js | 51 ++++++++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/src/index.js b/src/index.js index f8baa3b..7d24702 100644 --- a/src/index.js +++ b/src/index.js @@ -169,10 +169,10 @@ function search_video(req, res, lang) { } // Send response to Alexa device - res.send(); + return res.send(); }).catch(function(reason) { // Error occurred in the promise - res.fail(reason); + return res.fail(reason); }); } @@ -212,11 +212,11 @@ function check_cache_ready(id, timeout) { } var interval = Math.min(cache_polling_interval, timeout); console.log("Will check again in " + interval + "ms (timeout: " + timeout + "ms)."); - resolve(new Promise((_resolve) => { + resolve(new Promise((_resolve, _reject) => { setTimeout(() => { - _resolve(check_cache_ready(id, timeout - cache_polling_interval)); + _resolve(check_cache_ready(id, timeout - cache_polling_interval).catch(_reject)); }, interval); - })); + }).catch(reject)); } } else { @@ -291,10 +291,10 @@ function download_video(req, res) { respond_play(req, res); // Send response to Alexa device - res.send(); + return res.send(); }).catch(function(reason) { // Error occurred in the promise - res.fail(reason); + return res.fail(reason); }); } @@ -322,16 +322,20 @@ function wait_for_video(id, callback) { app.pre = function(req, res, type) { if (req.data.session !== undefined) { if (req.data.session.application.applicationId !== process.env.ALEXA_APPLICATION_ID) { - res.fail("Invalid application"); + return res.fail("Invalid application"); } } else { if (req.applicationId !== process.env.ALEXA_APPLICATION_ID) { - res.fail("Invalid application"); + return res.fail("Invalid application"); } } }; +app.error = function(exc, req, res) { + res.say("An error occured: " + exc); +}; + // Looking up a video in English app.intent("GetVideoIntent", { "slots": { @@ -420,9 +424,10 @@ function interactively_wait_for_video(req, res) { res.say(speech.ssml(true)); res.reprompt(message).shouldEndSession(false); } - res.send(); + return res.send(); }).catch(reason => { - res.fail(reason); + console.error(reason); + return res.fail(reason); }); } @@ -430,7 +435,7 @@ app.intent("AMAZON.YesIntent", function(req, res) { var userId = req.userId; if (!buffer_search.hasOwnProperty(userId) || buffer_search[userId] == null) { - res.send(); + return res.send(); } else if (!interactive_wait) { return download_video(req, res); @@ -445,7 +450,7 @@ app.intent("AMAZON.YesIntent", function(req, res) { return interactively_wait_for_video(req, res); }) .catch(reason => { - res.fail(reason); + return res.fail(reason); }); } return interactively_wait_for_video(req, res); @@ -455,7 +460,7 @@ app.intent("AMAZON.YesIntent", function(req, res) { app.intent("AMAZON.NoIntent", function(req, res) { var userId = req.userId; buffer_search[userId] = null; - res.send(); + return res.send(); }); // Log playback failed events @@ -499,7 +504,7 @@ app.audioPlayer("PlaybackNearlyFinished", function(req, res) { repeat_once[userId] = false; // Send response to Alexa device - res.send(); + return res.send(); } else { // Token is set to null because playback is done @@ -519,7 +524,7 @@ app.intent("AMAZON.StartOverIntent", {}, function(req, res) { res.say(response_messages[req.data.request.locale]["NOTHING_TO_REPEAT"]); } - res.send(); + return res.send(); }); var stop_intent = function(req, res) { @@ -540,7 +545,7 @@ var stop_intent = function(req, res) { res.say(response_messages[req.data.request.locale]["NOTHING_TO_REPEAT"]); } - res.send(); + return res.send(); }; // User told Alexa to stop playing audio @@ -559,7 +564,7 @@ app.intent("AMAZON.ResumeIntent", {}, function(req, res) { res.say(response_messages[req.data.request.locale]["NOTHING_TO_RESUME"]); } - res.send(); + return res.send(); }); // User told Alexa to pause the audio @@ -578,7 +583,7 @@ app.intent("AMAZON.PauseIntent", {}, function(req, res) { res.say(response_messages[req.data.request.locale]["NOTHING_TO_RESUME"]); } - res.send(); + return res.send(); }); // User told Alexa to repeat audio once @@ -593,7 +598,7 @@ app.intent("AMAZON.RepeatIntent", {}, function(req, res) { repeat_once[userId] = true; } - res.say( + return res.say( response_messages[req.data.request.locale]["REPEAT_TRIGGERED"] .formatUnicorn(has_video(userId) ? "current" : "next") ).send(); @@ -610,7 +615,7 @@ app.intent("AMAZON.LoopOnIntent", {}, function(req, res) { restart_video(req, res, 0); } - res.say( + return res.say( response_messages[req.data.request.locale]["LOOP_ON_TRIGGERED"] .formatUnicorn(has_video(userId) ? "current" : "next") ).send(); @@ -622,7 +627,7 @@ app.intent("AMAZON.LoopOffIntent", {}, function(req, res) { repeat_infinitely[userId] = false; - res.say( + return res.say( response_messages[req.data.request.locale]["LOOP_OFF_TRIGGERED"] .formatUnicorn(has_video(userId) ? "current" : "next") ).send(); @@ -630,7 +635,7 @@ app.intent("AMAZON.LoopOffIntent", {}, function(req, res) { // User asked Alexa for help app.intent("AMAZON.HelpIntent", {}, function(req, res) { - res.say(response_messages[req.data.request.locale]["HELP_TRIGGERED"]).send(); + return res.say(response_messages[req.data.request.locale]["HELP_TRIGGERED"]).send(); }); exports.handler = app.lambda(); From 81c97cbc774548b4de305139bc313ce12dab14b6 Mon Sep 17 00:00:00 2001 From: David Hacker Date: Thu, 7 Feb 2019 22:09:58 -0800 Subject: [PATCH 5/9] Change package lock --- src/package-lock.json | 154 +++++++++++++++++++++--------------------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/src/package-lock.json b/src/package-lock.json index a85d807..44cfa76 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -14,7 +14,7 @@ "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.0.tgz", "integrity": "sha512-MFiW54UOSt+f2bRw8J7LgQeIvE/9b4oGvwU7XW30S9QGAiHGnU/fmiOprsyMkdmH2rl8xSPc0/yrQw8juXU6bQ==", "requires": { - "@types/chai": "4.1.7" + "@types/chai": "*" } }, "ajv": { @@ -22,10 +22,10 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", "requires": { - "fast-deep-equal": "2.0.1", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.4.1", - "uri-js": "4.2.2" + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, "alexa-app": { @@ -33,13 +33,13 @@ "resolved": "https://registry.npmjs.org/alexa-app/-/alexa-app-4.2.3.tgz", "integrity": "sha512-PhUbFEqUtleN8yy0x7ltD6mAMgzUlkuW2oQKuv8WSlXEPkl9xzkAugd38XdhorkCyclCxaGgDBGAdyJk33odSQ==", "requires": { - "@types/chai-as-promised": "7.1.0", - "alexa-utterances": "0.2.1", - "alexa-verifier-middleware": "1.0.1", - "bluebird": "2.11.0", - "body-parser": "1.18.3", - "lodash.defaults": "4.2.0", - "numbered": "1.1.0" + "@types/chai-as-promised": "^7.1.0", + "alexa-utterances": "^0.2.0", + "alexa-verifier-middleware": "^1.0.0", + "bluebird": "^2.10.2", + "body-parser": "^1.15.2", + "lodash.defaults": "^4.2.0", + "numbered": "^1.0.0" } }, "alexa-utterances": { @@ -47,8 +47,8 @@ "resolved": "https://registry.npmjs.org/alexa-utterances/-/alexa-utterances-0.2.1.tgz", "integrity": "sha1-8rcLav062IZxHaj5lOfklUnmC/E=", "requires": { - "js-combinatorics": "0.5.4", - "numbered": "1.1.0" + "js-combinatorics": "^0.5.0", + "numbered": "^1.0.0" } }, "alexa-verifier": { @@ -56,8 +56,8 @@ "resolved": "https://registry.npmjs.org/alexa-verifier/-/alexa-verifier-1.0.0.tgz", "integrity": "sha512-1XE/40ajf4sESuvAdacxVrxy06tkewC2sJ8qC5T/zQtGiYRsuoQjj6UkSpw7WTqG8wkmESANz/w5NnfIruyCjQ==", "requires": { - "node-forge": "0.7.6", - "validator": "9.4.1" + "node-forge": "^0.7.0", + "validator": "^9.0.0" } }, "alexa-verifier-middleware": { @@ -65,7 +65,7 @@ "resolved": "https://registry.npmjs.org/alexa-verifier-middleware/-/alexa-verifier-middleware-1.0.1.tgz", "integrity": "sha512-dBWmXmtK4HHdibu18csh7f9uOzMvsu55X7PoROfRntLRRSJI5faPnNp/K5oAKnA9A8SzOpZDTQ92FSXDX0RZQQ==", "requires": { - "alexa-verifier": "1.0.0" + "alexa-verifier": "^1.0.0" } }, "asn1": { @@ -73,7 +73,7 @@ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", "requires": { - "safer-buffer": "2.1.2" + "safer-buffer": "~2.1.0" } }, "assert-plus": { @@ -101,7 +101,7 @@ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "requires": { - "tweetnacl": "0.14.5" + "tweetnacl": "^0.14.3" } }, "bluebird": { @@ -115,15 +115,15 @@ "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", "requires": { "bytes": "3.0.0", - "content-type": "1.0.4", + "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "1.1.2", - "http-errors": "1.6.3", + "depd": "~1.1.2", + "http-errors": "~1.6.3", "iconv-lite": "0.4.23", - "on-finished": "2.3.0", + "on-finished": "~2.3.0", "qs": "6.5.2", "raw-body": "2.3.3", - "type-is": "1.6.16" + "type-is": "~1.6.16" } }, "bytes": { @@ -141,7 +141,7 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", "requires": { - "delayed-stream": "1.0.0" + "delayed-stream": "~1.0.0" } }, "content-type": { @@ -159,7 +159,7 @@ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "debug": { @@ -185,8 +185,8 @@ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", "requires": { - "jsbn": "0.1.1", - "safer-buffer": "2.1.2" + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" } }, "ee-first": { @@ -224,9 +224,9 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.7", - "mime-types": "2.1.21" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" } }, "getpass": { @@ -234,7 +234,7 @@ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "har-schema": { @@ -247,8 +247,8 @@ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "requires": { - "ajv": "6.6.1", - "har-schema": "2.0.0" + "ajv": "^6.5.5", + "har-schema": "^2.0.0" } }, "http-errors": { @@ -256,10 +256,10 @@ "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "requires": { - "depd": "1.1.2", + "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", - "statuses": "1.5.0" + "statuses": ">= 1.4.0 < 2" } }, "http-signature": { @@ -267,9 +267,9 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.15.2" + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" } }, "iconv-lite": { @@ -277,7 +277,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", "requires": { - "safer-buffer": "2.1.2" + "safer-buffer": ">= 2.1.2 < 3" } }, "inherits": { @@ -351,7 +351,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", "requires": { - "mime-db": "1.37.0" + "mime-db": "~1.37.0" } }, "ms": { @@ -418,26 +418,26 @@ "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.8.0", - "caseless": "0.12.0", - "combined-stream": "1.0.7", - "extend": "3.0.2", - "forever-agent": "0.6.1", - "form-data": "2.3.3", - "har-validator": "5.1.3", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.21", - "oauth-sign": "0.9.0", - "performance-now": "2.1.0", - "qs": "6.5.2", - "safe-buffer": "5.1.2", - "tough-cookie": "2.4.3", - "tunnel-agent": "0.6.0", - "uuid": "3.3.2" + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" } }, "safe-buffer": { @@ -460,15 +460,15 @@ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", "requires": { - "asn1": "0.2.4", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.2", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.2", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "safer-buffer": "2.1.2", - "tweetnacl": "0.14.5" + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" } }, "ssml-builder": { @@ -486,8 +486,8 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "requires": { - "psl": "1.1.29", - "punycode": "1.4.1" + "psl": "^1.1.24", + "punycode": "^1.4.1" }, "dependencies": { "punycode": { @@ -502,7 +502,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "^5.0.1" } }, "tweetnacl": { @@ -516,7 +516,7 @@ "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", "requires": { "media-typer": "0.3.0", - "mime-types": "2.1.21" + "mime-types": "~2.1.18" } }, "unpipe": { @@ -529,7 +529,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "requires": { - "punycode": "2.1.1" + "punycode": "^2.1.0" } }, "uuid": { @@ -547,9 +547,9 @@ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { - "assert-plus": "1.0.0", + "assert-plus": "^1.0.0", "core-util-is": "1.0.2", - "extsprintf": "1.3.0" + "extsprintf": "^1.2.0" } } } From d6619370023dcc33f36c2a11a9fb96b6114e4415 Mon Sep 17 00:00:00 2001 From: David Hacker Date: Fri, 8 Feb 2019 21:51:21 -0800 Subject: [PATCH 6/9] Revert return logic, add ASK_TO_CONTINUE locale-based message --- src/index.js | 120 +++++++++++++++++++++++------------------- src/util/responses.js | 10 +++- 2 files changed, 74 insertions(+), 56 deletions(-) diff --git a/src/index.js b/src/index.js index 7d24702..acc7a9f 100644 --- a/src/index.js +++ b/src/index.js @@ -8,17 +8,17 @@ var alexa = require("alexa-app"); var request = require("request"); var ssml = require("ssml-builder"); var response_messages = require("./util/responses.js"); -response_messages['en-GB'] = response_messages['en-US']; // Create Alexa skill application var app = new alexa.app("youtube"); -// Set Heroku URL +// Process environment variables const heroku = process.env.HEROKU_APP_URL || "https://dmhacker-youtube.herokuapp.com"; - -const interactive_wait = process.env.INTERACTIVE_WAIT == "true"; -const cache_polling_interval = parseInt(process.env.CACHE_POLLING_INTERVAL || "10000", 10); -const alexa_request_timeout = parseInt(process.env.ALEXA_REQUEST_TIMEOUT || "45000", 10); +const interactive_wait = !(process.env.DISABLE_INTERACTIVE_WAIT === "true" || + process.env.DISABLE_INTERACTIVE_WAIT === true || + process.env.DISABLE_INTERACTIVE_WAIT === 1); +const cache_polling_interval = parseInt(process.env.CACHE_POLLING_INTERVAL || "5000", 10); +const ask_interval = parseInt(process.env.ASK_INTERVAL || "60000", 10); // Variables relating to videos waiting for user input var buffer_search = {}; @@ -169,10 +169,10 @@ function search_video(req, res, lang) { } // Send response to Alexa device - return res.send(); + res.send(); }).catch(function(reason) { // Error occurred in the promise - return res.fail(reason); + res.fail(reason); }); } @@ -185,7 +185,7 @@ function make_download_video_request(id) { } else { var body_json = JSON.parse(body); var url = heroku + body_json.link; - console.log("Requested downloading for ... " + url); + console.log("Requested download for ... " + url); resolve(url); } }); @@ -200,18 +200,23 @@ function check_cache_ready(id, timeout) { if (body_json.hasOwnProperty('downloaded') && body_json['downloaded'] != null) { if (body_json.downloaded) { downloading = false; - console.log(id + " ready in the cache!"); + + console.log(id + " has been cached. Ready to play!"); + resolve(); } else { downloading = true; - console.log(id + " being cached atm."); + + console.log(id + " is being cached."); if (timeout <= 0) { resolve(); return; } + var interval = Math.min(cache_polling_interval, timeout); - console.log("Will check again in " + interval + "ms (timeout: " + timeout + "ms)."); + console.log("Checking again in " + interval + "ms (delay: " + timeout + "ms)."); + resolve(new Promise((_resolve, _reject) => { setTimeout(() => { _resolve(check_cache_ready(id, timeout - cache_polling_interval).catch(_reject)); @@ -220,8 +225,8 @@ function check_cache_ready(id, timeout) { } } else { - console.log(id + " never cached yet."); - reject("Video unavailable"); + console.log(id + " will not be cached."); + reject("Video unavailable."); } } else { @@ -234,17 +239,45 @@ function check_cache_ready(id, timeout) { function respond_play(req, res) { var userId = req.userId; + + // Final response to the user, indicating that their video will be playing var speech = new ssml(); var title = buffer_search[userId].title; var message = response_messages[req.data.request.locale]["NOW_PLAYING"].formatUnicorn(title); speech.say(message); res.say(speech.ssml(true)); - console.log("Start playing ... " + title); + console.log("Starting to play ... " + title); + // Start playing the video! restart_video(req, res, 0); } +function interactively_wait_for_video(req, res) { + var userId = req.userId; + var id = buffer_search[userId].id; + return check_cache_ready(id, ask_interval).then(() => { + if (!downloading) { + // Download finished ... notify user + respond_play(req, res); + } + else { + console.log("Asking whether to continue waiting." ); + + // Download still in progress ... ask if the user wants to keep tracking + var message = response_messages[req.data.request.locale]["ASK_TO_CONTINUE"]; + var speech = new ssml(); + speech.say(message); + res.say(speech.ssml(true)); + res.reprompt(message).shouldEndSession(false); + } + return res.send(); + }).catch(reason => { + console.error(reason); + return res.fail(reason); + }); +} + /** * Downloads the mostly recent video the user requested. * @@ -273,10 +306,9 @@ function download_video(req, res) { // Set last search & token to equal the current video's parameters last_search[userId] = heroku + body_json.link; - // MX: Not convinced this should be necessary ... // NOTE: this is somewhat of hack to get Alexa to ignore an errant PlaybackNearlyFinished event - // repeat_once[userId] = true; - // repeat_infinitely[userId] = false; + repeat_once[userId] = true; + repeat_infinitely[userId] = false; // Wait until video is downloaded by repeatedly pinging cache console.log("Waiting for ... " + last_search[userId]); @@ -291,10 +323,10 @@ function download_video(req, res) { respond_play(req, res); // Send response to Alexa device - return res.send(); + res.send(); }).catch(function(reason) { // Error occurred in the promise - return res.fail(reason); + res.fail(reason); }); } @@ -322,12 +354,12 @@ function wait_for_video(id, callback) { app.pre = function(req, res, type) { if (req.data.session !== undefined) { if (req.data.session.application.applicationId !== process.env.ALEXA_APPLICATION_ID) { - return res.fail("Invalid application"); + res.fail("Invalid application"); } } else { if (req.applicationId !== process.env.ALEXA_APPLICATION_ID) { - return res.fail("Invalid application"); + res.fail("Invalid application"); } } }; @@ -409,33 +441,11 @@ app.intent("GetVideoItalianIntent", { } ); -function interactively_wait_for_video(req, res) { - var userId = req.userId; - var id = buffer_search[userId].id; - return check_cache_ready(id, alexa_request_timeout).then(() => { - if (!downloading) { - respond_play(req, res); - } - else { - console.log("Asking whether to continue waiting ..."); - var message = "Download still in progress. Would you like to keep waiting?"; - var speech = new ssml(); - speech.say(message); - res.say(speech.ssml(true)); - res.reprompt(message).shouldEndSession(false); - } - return res.send(); - }).catch(reason => { - console.error(reason); - return res.fail(reason); - }); -} - app.intent("AMAZON.YesIntent", function(req, res) { var userId = req.userId; if (!buffer_search.hasOwnProperty(userId) || buffer_search[userId] == null) { - return res.send(); + res.send(); } else if (!interactive_wait) { return download_video(req, res); @@ -460,7 +470,7 @@ app.intent("AMAZON.YesIntent", function(req, res) { app.intent("AMAZON.NoIntent", function(req, res) { var userId = req.userId; buffer_search[userId] = null; - return res.send(); + res.send(); }); // Log playback failed events @@ -504,7 +514,7 @@ app.audioPlayer("PlaybackNearlyFinished", function(req, res) { repeat_once[userId] = false; // Send response to Alexa device - return res.send(); + res.send(); } else { // Token is set to null because playback is done @@ -524,7 +534,7 @@ app.intent("AMAZON.StartOverIntent", {}, function(req, res) { res.say(response_messages[req.data.request.locale]["NOTHING_TO_REPEAT"]); } - return res.send(); + res.send(); }); var stop_intent = function(req, res) { @@ -545,7 +555,7 @@ var stop_intent = function(req, res) { res.say(response_messages[req.data.request.locale]["NOTHING_TO_REPEAT"]); } - return res.send(); + res.send(); }; // User told Alexa to stop playing audio @@ -564,7 +574,7 @@ app.intent("AMAZON.ResumeIntent", {}, function(req, res) { res.say(response_messages[req.data.request.locale]["NOTHING_TO_RESUME"]); } - return res.send(); + res.send(); }); // User told Alexa to pause the audio @@ -583,7 +593,7 @@ app.intent("AMAZON.PauseIntent", {}, function(req, res) { res.say(response_messages[req.data.request.locale]["NOTHING_TO_RESUME"]); } - return res.send(); + res.send(); }); // User told Alexa to repeat audio once @@ -598,7 +608,7 @@ app.intent("AMAZON.RepeatIntent", {}, function(req, res) { repeat_once[userId] = true; } - return res.say( + res.say( response_messages[req.data.request.locale]["REPEAT_TRIGGERED"] .formatUnicorn(has_video(userId) ? "current" : "next") ).send(); @@ -615,7 +625,7 @@ app.intent("AMAZON.LoopOnIntent", {}, function(req, res) { restart_video(req, res, 0); } - return res.say( + res.say( response_messages[req.data.request.locale]["LOOP_ON_TRIGGERED"] .formatUnicorn(has_video(userId) ? "current" : "next") ).send(); @@ -627,7 +637,7 @@ app.intent("AMAZON.LoopOffIntent", {}, function(req, res) { repeat_infinitely[userId] = false; - return res.say( + res.say( response_messages[req.data.request.locale]["LOOP_OFF_TRIGGERED"] .formatUnicorn(has_video(userId) ? "current" : "next") ).send(); @@ -635,7 +645,7 @@ app.intent("AMAZON.LoopOffIntent", {}, function(req, res) { // User asked Alexa for help app.intent("AMAZON.HelpIntent", {}, function(req, res) { - return res.say(response_messages[req.data.request.locale]["HELP_TRIGGERED"]).send(); + res.say(response_messages[req.data.request.locale]["HELP_TRIGGERED"]).send(); }); exports.handler = app.lambda(); diff --git a/src/util/responses.js b/src/util/responses.js index d0b86b9..2fe2b74 100644 --- a/src/util/responses.js +++ b/src/util/responses.js @@ -1,7 +1,8 @@ -module.exports = { +let messages = { "en-US": { "NO_RESULTS_FOUND": "{0} did not return any results on YouTube.", "ASK_TO_PLAY": "I found a video called {0}. Would you like me to download it now?", + "ASK_TO_CONTINUE": "Download still in progress. Would you like to keep waiting?", "NOW_PLAYING": "I am now playing {0}.", "NOTHING_TO_RESUME": "You are not playing anything currently.", "NOTHING_TO_REPEAT": "You have not selected a video to play.", @@ -13,6 +14,7 @@ module.exports = { "de-DE": { "NO_RESULTS_FOUND": "Keine Ergebnisse auf Youtube gefunden.", "ASK_TO_PLAY": "Ich habe ein Video mit dem Namen {0} gefunden. Möchten Sie, dass ich es spiele?", + "ASK_TO_CONTINUE": "Download läuft noch. Möchten Sie gerne warten?", "NOW_PLAYING": "Ich spiele jetzt {0}.", "NOTHING_TO_RESUME": "Sie spielen derzeit nichts.", "NOTHING_TO_REPEAT": "Sie haben kein Video zum Abspielen ausgewählt.", @@ -24,6 +26,7 @@ module.exports = { "fr-FR": { "NO_RESULTS_FOUND": "{0} n'a pas été trouvé sur YouTube.", "ASK_TO_PLAY": "J'ai trouvé une vidéo appelée {0}. Voulez-vous que je joue?", + "ASK_TO_CONTINUE": "Téléchargement toujours en cours. Voulez-vous continuer à attendre?", "NOW_PLAYING": "Je suis en train de jouer {0}.", "NOTHING_TO_RESUME": "Il n'y a aucun élément en cours de lecture.", "NOTHING_TO_REPEAT": "Vous n'avez pas sélectionné de vidéo à écouter.", @@ -35,6 +38,7 @@ module.exports = { "it-IT": { "NO_RESULTS_FOUND": "{0} non lo trovo su YouTube.", "ASK_TO_PLAY": "Ho trovato un video chiamato {0}. Vorresti che lo suonassi?", + "ASK_TO_CONTINUE": "Download ancora in corso. Ti piacerebbe continuare ad aspettare?", "NOW_PLAYING": "Sto suonando {0}.", "NOTHING_TO_RESUME": "Non stai suonando nulla ora.", "NOTHING_TO_REPEAT": "Non hai selezionato nessun video da riprodurre.", @@ -44,3 +48,7 @@ module.exports = { "HELP_TRIGGERED": "Per usare la skill di YouTube, chiedi alla skill di cercare il video che vuoi. In aggiunta, una volta che il video è in riproduzione, puoi chiedere ad Alexa di metterlo in pausa, riavviarlo, o di metterlo in loop." } } + +messages['en-GB'] = messages['en-US']; + +module.exports = messages; From a8106eb2fcc2d109281c7f3f6b89192fc4e93d5d Mon Sep 17 00:00:00 2001 From: David Hacker Date: Fri, 8 Feb 2019 21:53:48 -0800 Subject: [PATCH 7/9] Update version to 3.0.1 --- src/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/package.json b/src/package.json index f707164..c95e50a 100644 --- a/src/package.json +++ b/src/package.json @@ -1,6 +1,6 @@ { "name": "alexa-youtube-skill", - "version": "3.0.0", + "version": "3.0.1", "description": "Use Alexa to search YouTube for your favorite videos", "engines": { "node": "6.1.0" From d96a5f7a653ef9b044500ab53746cf26edbe9397 Mon Sep 17 00:00:00 2001 From: David Hacker Date: Fri, 8 Feb 2019 21:58:59 -0800 Subject: [PATCH 8/9] Add package-lock --- src/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/package-lock.json b/src/package-lock.json index 44cfa76..f7f1dd5 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,6 +1,6 @@ { "name": "alexa-youtube-skill", - "version": "3.0.0", + "version": "3.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { From 04521c2b029cd7f25c2ec83b4fac93b075a05603 Mon Sep 17 00:00:00 2001 From: David Hacker Date: Fri, 8 Feb 2019 22:01:12 -0800 Subject: [PATCH 9/9] Decrease default ask interval --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index acc7a9f..13de279 100644 --- a/src/index.js +++ b/src/index.js @@ -18,7 +18,7 @@ const interactive_wait = !(process.env.DISABLE_INTERACTIVE_WAIT === "true" || process.env.DISABLE_INTERACTIVE_WAIT === true || process.env.DISABLE_INTERACTIVE_WAIT === 1); const cache_polling_interval = parseInt(process.env.CACHE_POLLING_INTERVAL || "5000", 10); -const ask_interval = parseInt(process.env.ASK_INTERVAL || "60000", 10); +const ask_interval = parseInt(process.env.ASK_INTERVAL || "45000", 10); // Variables relating to videos waiting for user input var buffer_search = {};