This repository has been archived by the owner on Nov 3, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 109
Reprompt to keep session alive for long downloads #59
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
4c2430a
Reprompt to keep session alive for long videos
chokkyvista 5728ddb
Enable UK locale
chokkyvista d203be8
Minor fixes
chokkyvista 22d89a9
Better error handling
chokkyvista 81c97cb
Change package lock
dmhacker d661937
Revert return logic, add ASK_TO_CONTINUE locale-based message
dmhacker a8106eb
Update version to 3.0.1
dmhacker d96a5f7
Add package-lock
dmhacker 04521c2
Decrease default ask interval
dmhacker File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,21 +12,29 @@ var response_messages = require("./util/responses.js"); | |
// Create Alexa skill application | ||
var app = new alexa.app("youtube"); | ||
|
||
// Set Heroku URL | ||
var heroku = process.env.HEROKU_APP_URL || "https://dmhacker-youtube.herokuapp.com"; | ||
// Process environment variables | ||
const heroku = process.env.HEROKU_APP_URL || "https://dmhacker-youtube.herokuapp.com"; | ||
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 || "45000", 10); | ||
|
||
// Variables relating to videos waiting for user input | ||
var buffer_search = {}; | ||
// 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 +111,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") { | ||
|
@@ -120,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), | ||
|
@@ -155,6 +163,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 +176,110 @@ 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 download 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 + " has been cached. Ready to play!"); | ||
|
||
resolve(); | ||
} | ||
else { | ||
downloading = true; | ||
|
||
console.log(id + " is being cached."); | ||
if (timeout <= 0) { | ||
resolve(); | ||
return; | ||
} | ||
|
||
var interval = Math.min(cache_polling_interval, timeout); | ||
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)); | ||
}, interval); | ||
}).catch(reject)); | ||
} | ||
} | ||
else { | ||
console.log(id + " will not be cached."); | ||
reject("Video unavailable."); | ||
} | ||
} | ||
else { | ||
console.error(err.message); | ||
reject(err.message); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
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("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. | ||
* 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 +292,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) { | ||
|
@@ -209,12 +320,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 +337,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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've changed it from "wait and check" to "check and wait" to save the few seconds in case the cache should be already ready (e.g. from previous searches). |
||
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) | ||
|
@@ -260,6 +364,10 @@ app.pre = function(req, res, type) { | |
} | ||
}; | ||
|
||
app.error = function(exc, req, res) { | ||
res.say("An error occured: " + exc); | ||
}; | ||
|
||
// Looking up a video in English | ||
app.intent("GetVideoIntent", { | ||
"slots": { | ||
|
@@ -339,9 +447,24 @@ app.intent("AMAZON.YesIntent", function(req, res) { | |
if (!buffer_search.hasOwnProperty(userId) || buffer_search[userId] == null) { | ||
res.send(); | ||
} | ||
else { | ||
else if (!interactive_wait) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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 => { | ||
return res.fail(reason); | ||
}); | ||
} | ||
return interactively_wait_for_video(req, res); | ||
} | ||
}); | ||
|
||
app.intent("AMAZON.NoIntent", function(req, res) { | ||
|
@@ -362,9 +485,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 +596,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; | ||
|
||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will periodically (per
CACHE_POLLING_INTERVAL
) check if the cache is ready up untiltimeout
, when Alexa has to reprompt the user to avoid being timed out.