├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── index.js ├── package-lock.json ├── package.json └── util ├── formatting.js └── responses.js /.gitignore: -------------------------------------------------------------------------------- 1 | # NodeJS files & folders 2 | build 3 | node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Dependency directories 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 David Hacker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default clean 2 | default: 3 | mkdir -p build 4 | npm install 5 | zip -r9 alexa-youtube-skill.zip node_modules util *.js *.json 6 | mv alexa-youtube-skill.zip build 7 | clean: 8 | rm -rf build 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alexa-youtube-skill 2 | 3 | This project contains the source code for an unpublished skill that allows users to search and play audio from YouTube 4 | as, by default, Amazon Alexa does not support playing audio from YouTube. 5 | 6 | After setting this skill up, it's easy to query YouTube for the video of your choice: 7 | 8 | > Alexa, ask YouTube to search for ... 9 | 10 | This skill performs a search, finding the most relevant video that matches the query. 11 | It then streams the video (audio only) to your Alexa device for your enjoyment. :) 12 | 13 | ## Additional Information 14 | 15 | __Setup:__ The instructions have been moved to this repository's [wiki page](https://github.com/dmhacker/alexa-youtube-skill/wiki). 16 | 17 | __Migration:__ Version 3.x.x is the latest version and fixes several critical issues present in earlier versions. 18 | To migrate to v3, please use [this guide on the wiki](https://github.com/dmhacker/alexa-youtube-skill/wiki/Migrating-to-Version-3). 19 | 20 | __Disclaimer:__ This skill is not officially supported by YouTube and will never be published on Amazon. 21 | It was originally intended as a proof-of-concept, but instructions on setting it up are provided (see above). 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Setup Python-esque formatting 4 | String.prototype.formatUnicorn = String.prototype.formatUnicorn || require("./util/formatting.js"); 5 | 6 | // Required packages 7 | let alexa = require("alexa-app"); 8 | let request = require("request"); 9 | let ssml = require("ssml-builder"); 10 | let response_messages = require("./util/responses.js"); 11 | 12 | // Create Alexa skill application 13 | let app = new alexa.app("youtube"); 14 | 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 = Math.max(1000, parseInt(process.env.CACHE_POLLING_INTERVAL || "5000", 10)); 21 | const ASK_INTERVAL = Math.max(30000, parseInt(process.env.ASK_INTERVAL || "45000", 10)); 22 | 23 | // Maps user IDs to recently searched video metadata 24 | let buffer_search = {}; 25 | 26 | // Maps user IDs to last played video metadata 27 | let last_search = {}; 28 | let last_token = {}; 29 | let last_playback = {}; 30 | 31 | // Indicates song repetition preferences for user IDs 32 | let repeat_infinitely = new Set(); 33 | let repeat_once = new Set(); 34 | 35 | // Set of users waiting for downloads to finishes 36 | let downloading_users = new Set(); 37 | 38 | /** 39 | * Generates a random UUID. Used for creating an audio stream token. 40 | * 41 | * @return {String} A random globally unique UUID 42 | */ 43 | function uuidv4() { 44 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { 45 | let r = Math.random() * 16 | 0, 46 | v = c == 'x' ? r : (r & 0x3 | 0x8); 47 | return v.toString(16); 48 | }); 49 | } 50 | 51 | /** 52 | * Returns whether a user is streaming video or not. 53 | * By default, if this is true, then the user also has_video() as well. 54 | * 55 | * @return {Boolean} The state of the user's audio stream 56 | */ 57 | function is_streaming_video(user_id) { 58 | return last_token.hasOwnProperty(user_id) && last_token[user_id] != null; 59 | } 60 | 61 | /** 62 | * Returns whether a user has downloaded a video. 63 | * Doesn't take into account if the user is currently playing it. 64 | * 65 | * @return {Boolean} The state of the user's audio reference 66 | */ 67 | function has_video(user_id) { 68 | return last_search.hasOwnProperty(user_id) && last_search[user_id] != null; 69 | } 70 | 71 | /** 72 | * Restarts the video by injecting the last search URL as a new stream. 73 | * 74 | * @param {Object} res A response that will be sent to the Alexa device 75 | * @param {Number} offset How many milliseconds from the video start to begin at 76 | */ 77 | function restart_video(req, res, offset) { 78 | let user_id = req.userId; 79 | last_token[user_id] = uuidv4(); 80 | res.audioPlayerPlayStream("REPLACE_ALL", { 81 | url: last_search[user_id], 82 | streamFormat: "AUDIO_MPEG", 83 | token: last_token[user_id], 84 | offsetInMilliseconds: offset 85 | }); 86 | if (!last_playback.hasOwnProperty(user_id)) { 87 | last_playback[user_id] = {}; 88 | } 89 | last_playback[user_id].start = new Date().getTime(); 90 | } 91 | 92 | /** 93 | * Searches for a YouTube video matching the user's query. 94 | * 95 | * @param {Object} req A request from an Alexa device 96 | * @param {Object} res A response that will be sent to the device 97 | * @param {String} lang The language of the query 98 | * @return {Promise} Execution of the request 99 | */ 100 | function search_video(req, res, lang) { 101 | let user_id = req.userId; 102 | let query = req.slot("VideoQuery"); 103 | console.log(`User ${user_id} entered search query '${query}'.`); 104 | return new Promise((resolve, reject) => { 105 | let search_url = `${HEROKU}/alexa/v3/search/${Buffer.from(query).toString("base64")}`; 106 | if (lang === "de-DE") { 107 | search_url += "?language=de"; 108 | } else if (lang === "fr-FR") { 109 | search_url += "?language=fr"; 110 | } else if (lang === "it-IT") { 111 | search_url += "?language=it"; 112 | } 113 | request(search_url, function(err, res, body) { 114 | if (err) { 115 | reject(err.message); 116 | } else { 117 | let body_json = JSON.parse(body); 118 | if (body_json.state === "error" && body_json.message === "No results found") { 119 | resolve({ 120 | message: response_messages[lang]["NO_RESULTS_FOUND"].formatUnicorn(query), 121 | metadata: null 122 | }); 123 | } else { 124 | let metadata = body_json.video; 125 | console.log(`Search result is '${metadata.title} at ${metadata.link}.`); 126 | resolve({ 127 | message: response_messages[lang]["ASK_TO_PLAY"].formatUnicorn(metadata.title), 128 | metadata: metadata 129 | }); 130 | } 131 | } 132 | }); 133 | }).then(function(content) { 134 | let speech = new ssml(); 135 | speech.say(content.message); 136 | res.say(speech.ssml(true)); 137 | if (content.metadata) { 138 | let metadata = content.metadata; 139 | res.card({ 140 | type: "Simple", 141 | title: "Search for \"" + query + "\"", 142 | content: "Alexa found \"" + metadata.title + "\" at " + metadata.link + "." 143 | }); 144 | buffer_search[user_id] = metadata; 145 | downloading_users.delete(user_id); 146 | res.reprompt().shouldEndSession(false); 147 | } 148 | res.send(); 149 | }).catch(function(reason) { 150 | res.fail(reason); 151 | }); 152 | } 153 | 154 | /** 155 | * Runs when a video download finishes. Alerts Alexa via the card system 156 | * and begins playing the audio. 157 | * 158 | * @param {Object} req A request from an Alexa device 159 | * @param {Object} res A response that will be sent to the device 160 | */ 161 | function on_download_finish(req, res) { 162 | let user_id = req.userId; 163 | let speech = new ssml(); 164 | let title = buffer_search[user_id].title; 165 | let message = response_messages[req.data.request.locale]["NOW_PLAYING"].formatUnicorn(title); 166 | speech.say(message); 167 | res.say(speech.ssml(true)); 168 | console.log(`${title} is now being played.`); 169 | restart_video(req, res, 0); 170 | } 171 | 172 | /** 173 | * Signals to the server that the video corresponding to the 174 | * given ID should be downloaded. 175 | * 176 | * @param {String} id The ID of the video 177 | * @return {Promise} Execution of the request 178 | */ 179 | function request_interactive_download(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 | let body_json = JSON.parse(body); 187 | let url = HEROKU + body_json.link; 188 | console.log(`${url} has started downloading.`); 189 | resolve(url); 190 | } 191 | }); 192 | }); 193 | } 194 | 195 | /** 196 | * Executes an interactive wait. This means that the Alexa device will 197 | * continue its normal cache polling routine but will ask the user at 198 | * a specified interval whether or not to continue the download. Fixes 199 | * issues with Alexa not being able to be interrupted for long downloads. 200 | * 201 | * @param {Object} req A request from an Alexa device 202 | * @param {Object} res A response that will be sent to the device 203 | * @return {Promise} Execution of the request 204 | */ 205 | function wait_on_interactive_download(req, res) { 206 | let user_id = req.userId; 207 | return ping_on_interactive_download(req, buffer_search[user_id].id, ASK_INTERVAL).then(() => { 208 | if (downloading_users.has(user_id)) { 209 | let message = response_messages[req.data.request.locale]["ASK_TO_CONTINUE"]; 210 | let speech = new ssml(); 211 | speech.say(message); 212 | res.say(speech.ssml(true)); 213 | res.reprompt(message).shouldEndSession(false); 214 | console.log("User has been asked if they want to continue with download."); 215 | } else { 216 | on_download_finish(req, res); 217 | } 218 | return res.send(); 219 | }).catch(reason => { 220 | console.error(reason); 221 | return res.fail(reason); 222 | }); 223 | } 224 | 225 | /** 226 | * Pings the cache at a normal polling interval until either the specified 227 | * timeout is reached or the cache finishes downloading the given video. 228 | * 229 | * SUBROUTINE for wait_on_interactive_download() method. 230 | * 231 | * @param {Object} req A request from an Alexa device 232 | * @param {String} id The ID of the video 233 | * @param {Number} timeout The remaining time to wait until the user is prompted 234 | * @return {Promise} Execution of the request 235 | */ 236 | function ping_on_interactive_download(req, id, timeout) { 237 | let user_id = req.userId; 238 | return new Promise((resolve, reject) => { 239 | request(`${HEROKU}/alexa/v3/cache/${id}`, function(err, res, body) { 240 | if (!err) { 241 | let body_json = JSON.parse(body); 242 | if (body_json.hasOwnProperty('downloaded') && body_json['downloaded'] != null) { 243 | if (body_json.downloaded) { 244 | downloading_users.delete(user_id); 245 | console.log(`${id} has finished downloading.`); 246 | resolve(); 247 | } else { 248 | downloading_users.add(user_id); 249 | if (timeout <= 0) { 250 | resolve(); 251 | return; 252 | } 253 | let interval = Math.min(CACHE_POLLING_INTERVAL, timeout); 254 | console.log(`Still downloading. Next ping occurs in ${interval} ms.`); 255 | console.log(`User will be prompted in ${timeout} ms.`); 256 | resolve(new Promise((_resolve, _reject) => { 257 | setTimeout(() => { 258 | _resolve(ping_on_interactive_download(req, id, timeout - CACHE_POLLING_INTERVAL) 259 | .catch(_reject)); 260 | }, interval); 261 | }).catch(reject)); 262 | } 263 | } else { 264 | console.error(`${id} is not being cached. Did an error occur?`); 265 | reject('Video unavailable.'); 266 | } 267 | } else { 268 | console.error(err.message); 269 | reject(err.message); 270 | } 271 | }); 272 | }); 273 | } 274 | 275 | /** 276 | * Executes a blocking download to fetch the last video the user requested. 277 | * A blocking download implies that Alexa will simply wait for the video 278 | * to download until the download either finishes or times out. 279 | * 280 | * @param {Object} req A request from an Alexa device 281 | * @param {Object} res A response that will be sent to the device 282 | * @return {Promise} Execution of the request 283 | */ 284 | function request_blocking_download(req, res) { 285 | let user_id = req.userId; 286 | let id = buffer_search[user_id].id; 287 | console.log(`${id} was requested for download.`); 288 | return new Promise((resolve, reject) => { 289 | request(`${HEROKU}/alexa/v3/download/${id}`, function(err, res, body) { 290 | if (err) { 291 | reject(err.message); 292 | } else { 293 | let body_json = JSON.parse(body); 294 | last_search[user_id] = HEROKU + body_json.link; 295 | 296 | // NOTE: hack to get Alexa to ignore a bad PlaybackNearlyFinished event 297 | repeat_once.add(user_id); 298 | repeat_infinitely.delete(user_id); 299 | 300 | console.log(`${id} has started downloading.`); 301 | ping_on_blocking_download(id, function() { 302 | console.log(`${id} has finished downloading.`); 303 | resolve(); 304 | }); 305 | } 306 | }); 307 | }).then(function() { 308 | on_download_finish(req, res); 309 | res.send(); 310 | }).catch(function(reason) { 311 | res.fail(reason); 312 | }); 313 | } 314 | 315 | /** 316 | * Blocks until the audio has been loaded on the server. 317 | * 318 | * SUBROUTINE for request_blocking_download() method. 319 | * 320 | * @param {String} id The ID of the video 321 | * @param {Function} callback The function to execute about load completion 322 | */ 323 | function ping_on_blocking_download(id, callback) { 324 | request(`${HEROKU}/alexa/v3/cache/${id}`, function(err, res, body) { 325 | if (!err) { 326 | let body_json = JSON.parse(body); 327 | if (body_json.downloaded) { 328 | callback(); 329 | } else { 330 | console.log(`Still downloading. Next ping occurs in ${CACHE_POLLING_INTERVAL} ms.`); 331 | setTimeout(ping_on_blocking_download, CACHE_POLLING_INTERVAL, id, callback); 332 | } 333 | } 334 | }); 335 | } 336 | 337 | app.pre = function(req, res, type) { 338 | if (process.env.ALEXA_APPLICATION_ID != null) { 339 | if (req.data.session !== undefined) { 340 | if (req.data.session.application.applicationId !== process.env.ALEXA_APPLICATION_ID) { 341 | res.fail("Invalid application"); 342 | } 343 | } else { 344 | if (req.applicationId !== process.env.ALEXA_APPLICATION_ID) { 345 | res.fail("Invalid application"); 346 | } 347 | } 348 | } 349 | }; 350 | 351 | app.error = function(exc, req, res) { 352 | console.error(exc); 353 | res.say("An error occured: " + exc); 354 | }; 355 | 356 | app.intent("GetVideoIntent", { 357 | "slots": { 358 | "VideoQuery": "VIDEOS" 359 | }, 360 | "utterances": [ 361 | "search for {-|VideoQuery}", 362 | "find {-|VideoQuery}", 363 | "play {-|VideoQuery}", 364 | "start playing {-|VideoQuery}", 365 | "put on {-|VideoQuery}" 366 | ] 367 | }, 368 | function(req, res) { 369 | return search_video(req, res, "en-US"); 370 | } 371 | ); 372 | 373 | app.intent("GetVideoGermanIntent", { 374 | "slots": { 375 | "VideoQuery": "VIDEOS" 376 | }, 377 | "utterances": [ 378 | "suchen nach {-|VideoQuery}", 379 | "finde {-|VideoQuery}", 380 | "spielen {-|VideoQuery}", 381 | "anfangen zu spielen {-|VideoQuery}", 382 | "anziehen {-|VideoQuery}" 383 | ] 384 | }, 385 | function(req, res) { 386 | return search_video(req, res, "de-DE"); 387 | } 388 | ); 389 | 390 | app.intent("GetVideoFrenchIntent", { 391 | "slots": { 392 | "VideoQuery": "VIDEOS" 393 | }, 394 | "utterances": [ 395 | "recherche {-|VideoQuery}", 396 | "cherche {-|VideoQuery}", 397 | "joue {-|VideoQuery}", 398 | "met {-|VideoQuery}", 399 | "lance {-|VideoQuery}", 400 | "démarre {-|VideoQuery}" 401 | ] 402 | }, 403 | function(req, res) { 404 | return search_video(req, res, "fr-FR"); 405 | } 406 | ); 407 | 408 | app.intent("GetVideoItalianIntent", { 409 | "slots": { 410 | "VideoQuery": "VIDEOS" 411 | }, 412 | "utterances": [ 413 | "trova {-|VideoQuery}", 414 | "cerca {-|VideoQuery}", 415 | "suona {-|VideoQuery}", 416 | "incomincia a suonare {-|VideoQuery}", 417 | "metti {-|VideoQuery}" 418 | ] 419 | }, 420 | function(req, res) { 421 | return search_video(req, res, "it-IT"); 422 | } 423 | ); 424 | 425 | app.intent("AMAZON.YesIntent", function(req, res) { 426 | let user_id = req.userId; 427 | if (!buffer_search.hasOwnProperty(user_id) || buffer_search[user_id] == null) { 428 | res.send(); 429 | } else if (!INTERACTIVE_WAIT) { 430 | return request_blocking_download(req, res); 431 | } else { 432 | if (downloading_users.has(user_id)) { 433 | return wait_on_interactive_download(req, res); 434 | } else { 435 | return request_interactive_download(buffer_search[user_id].id) 436 | .then(url => { 437 | downloading_users.add(user_id); 438 | last_search[user_id] = url; 439 | return wait_on_interactive_download(req, res); 440 | }) 441 | .catch(reason => { 442 | return res.fail(reason); 443 | }); 444 | } 445 | } 446 | }); 447 | 448 | app.intent("AMAZON.NoIntent", function(req, res) { 449 | let user_id = req.userId; 450 | buffer_search[user_id] = null; 451 | res.send(); 452 | }); 453 | 454 | app.audioPlayer("PlaybackFailed", function(req, res) { 455 | console.error("Playback failed."); 456 | console.error(req.data.request); 457 | console.error(req.data.request.error); 458 | }); 459 | 460 | app.audioPlayer("PlaybackNearlyFinished", function(req, res) { 461 | let user_id = req.userId; 462 | let user_wants_repeat = repeat_infinitely.has(user_id) || repeat_once.has(user_id); 463 | if (user_wants_repeat && has_video(user_id)) { 464 | let new_token = uuidv4(); 465 | res.audioPlayerPlayStream("ENQUEUE", { 466 | url: last_search[user_id], 467 | streamFormat: "AUDIO_MPEG", 468 | token: new_token, 469 | expectedPreviousToken: last_token[user_id], 470 | offsetInMilliseconds: 0 471 | }); 472 | last_token[user_id] = new_token; 473 | if (!last_playback.hasOwnProperty(user_id)) { 474 | last_playback[user_id] = {}; 475 | } 476 | last_playback[user_id].start = new Date().getTime(); 477 | repeat_once.delete(user_id); 478 | res.send(); 479 | } else { 480 | last_token[user_id] = null; 481 | } 482 | }); 483 | 484 | app.intent("AMAZON.StartOverIntent", {}, function(req, res) { 485 | let user_id = req.userId; 486 | if (has_video(user_id)) { 487 | restart_video(req, res, 0); 488 | } else { 489 | res.say(response_messages[req.data.request.locale]["NOTHING_TO_REPEAT"]); 490 | } 491 | res.send(); 492 | }); 493 | 494 | function stop_intent(req, res) { 495 | let user_id = req.userId; 496 | if (has_video(user_id)) { 497 | if (is_streaming_video(user_id)) { 498 | last_token[user_id] = null; 499 | res.audioPlayerStop(); 500 | } 501 | last_search[user_id] = null; 502 | res.audioPlayerClearQueue(); 503 | } else { 504 | res.say(response_messages[req.data.request.locale]["NOTHING_TO_REPEAT"]); 505 | } 506 | res.send(); 507 | }; 508 | 509 | app.intent("AMAZON.StopIntent", {}, stop_intent); 510 | app.intent("AMAZON.CancelIntent", {}, stop_intent); 511 | 512 | app.intent("AMAZON.ResumeIntent", {}, function(req, res) { 513 | let user_id = req.userId; 514 | if (is_streaming_video(user_id)) { 515 | restart_video(req, res, last_playback[user_id].stop - last_playback[user_id].start); 516 | } else { 517 | res.say(response_messages[req.data.request.locale]["NOTHING_TO_RESUME"]); 518 | } 519 | res.send(); 520 | }); 521 | 522 | app.intent("AMAZON.PauseIntent", {}, function(req, res) { 523 | let user_id = req.userId; 524 | if (is_streaming_video(user_id)) { 525 | if (!last_playback.hasOwnProperty(user_id)) { 526 | last_playback[user_id] = {}; 527 | } 528 | last_playback[user_id].stop = new Date().getTime(); 529 | res.audioPlayerStop(); 530 | } else { 531 | res.say(response_messages[req.data.request.locale]["NOTHING_TO_RESUME"]); 532 | } 533 | res.send(); 534 | }); 535 | 536 | app.intent("AMAZON.RepeatIntent", {}, function(req, res) { 537 | let user_id = req.userId; 538 | if (has_video(user_id) && !is_streaming_video(user_id)) { 539 | restart_video(req, res, 0); 540 | } else { 541 | repeat_once.add(user_id); 542 | } 543 | res.say( 544 | response_messages[req.data.request.locale]["REPEAT_TRIGGERED"] 545 | .formatUnicorn(has_video(user_id) ? "current" : "next") 546 | ).send(); 547 | }); 548 | 549 | app.intent("AMAZON.LoopOnIntent", {}, function(req, res) { 550 | let user_id = req.userId; 551 | repeat_infinitely.add(user_id); 552 | if (has_video(user_id) && !is_streaming_video(user_id)) { 553 | restart_video(req, res, 0); 554 | } 555 | res.say( 556 | response_messages[req.data.request.locale]["LOOP_ON_TRIGGERED"] 557 | .formatUnicorn(has_video(user_id) ? "current" : "next") 558 | ).send(); 559 | }); 560 | 561 | app.intent("AMAZON.LoopOffIntent", {}, function(req, res) { 562 | let user_id = req.userId; 563 | repeat_infinitely.delete(user_id); 564 | res.say( 565 | response_messages[req.data.request.locale]["LOOP_OFF_TRIGGERED"] 566 | .formatUnicorn(has_video(user_id) ? "current" : "next") 567 | ).send(); 568 | }); 569 | 570 | app.intent("AMAZON.HelpIntent", {}, function(req, res) { 571 | res.say(response_messages[req.data.request.locale]["HELP_TRIGGERED"]).send(); 572 | }); 573 | 574 | exports.handler = app.lambda(); 575 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexa-youtube-skill", 3 | "version": "3.0.5", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/chai": { 8 | "version": "4.2.3", 9 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.3.tgz", 10 | "integrity": "sha512-VRw2xEGbll3ZiTQ4J02/hUjNqZoue1bMhoo2dgM2LXjDdyaq4q80HgBDHwpI0/VKlo4Eg+BavyQMv/NYgTetzA==" 11 | }, 12 | "@types/chai-as-promised": { 13 | "version": "7.1.2", 14 | "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.2.tgz", 15 | "integrity": "sha512-PO2gcfR3Oxa+u0QvECLe1xKXOqYTzCmWf0FhLhjREoW3fPAVamjihL7v1MOVLJLsnAMdLcjkfrs01yvDMwVK4Q==", 16 | "requires": { 17 | "@types/chai": "*" 18 | } 19 | }, 20 | "ajv": { 21 | "version": "6.10.2", 22 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", 23 | "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", 24 | "requires": { 25 | "fast-deep-equal": "^2.0.1", 26 | "fast-json-stable-stringify": "^2.0.0", 27 | "json-schema-traverse": "^0.4.1", 28 | "uri-js": "^4.2.2" 29 | } 30 | }, 31 | "alexa-app": { 32 | "version": "4.2.3", 33 | "resolved": "https://registry.npmjs.org/alexa-app/-/alexa-app-4.2.3.tgz", 34 | "integrity": "sha512-PhUbFEqUtleN8yy0x7ltD6mAMgzUlkuW2oQKuv8WSlXEPkl9xzkAugd38XdhorkCyclCxaGgDBGAdyJk33odSQ==", 35 | "requires": { 36 | "@types/chai-as-promised": "^7.1.0", 37 | "alexa-utterances": "^0.2.0", 38 | "alexa-verifier-middleware": "^1.0.0", 39 | "bluebird": "^2.10.2", 40 | "body-parser": "^1.15.2", 41 | "lodash.defaults": "^4.2.0", 42 | "numbered": "^1.0.0" 43 | } 44 | }, 45 | "alexa-utterances": { 46 | "version": "0.2.1", 47 | "resolved": "https://registry.npmjs.org/alexa-utterances/-/alexa-utterances-0.2.1.tgz", 48 | "integrity": "sha1-8rcLav062IZxHaj5lOfklUnmC/E=", 49 | "requires": { 50 | "js-combinatorics": "^0.5.0", 51 | "numbered": "^1.0.0" 52 | } 53 | }, 54 | "alexa-verifier": { 55 | "version": "2.0.1", 56 | "resolved": "https://registry.npmjs.org/alexa-verifier/-/alexa-verifier-2.0.1.tgz", 57 | "integrity": "sha512-jRE5tRPKiR91gHi5ss4wrVilc4HK/HGgt8KQP936IJGsdjUZPyh7GoFsvMvspIJwxzTiaMxI5XUXakkzXMOL3A==", 58 | "requires": { 59 | "node-forge": "^0.7.0", 60 | "validator": "^9.0.0" 61 | } 62 | }, 63 | "alexa-verifier-middleware": { 64 | "version": "1.0.2", 65 | "resolved": "https://registry.npmjs.org/alexa-verifier-middleware/-/alexa-verifier-middleware-1.0.2.tgz", 66 | "integrity": "sha512-ytRxmTLFnVq504U00AuG0BkpHLXGTFKwau1Nvs/2/ceztouzuCi/XOAFFjOL3R53GmCtNp1vdXcOK4mIUq00eA==", 67 | "requires": { 68 | "alexa-verifier": "^2.0.1" 69 | } 70 | }, 71 | "asn1": { 72 | "version": "0.2.4", 73 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 74 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 75 | "requires": { 76 | "safer-buffer": "~2.1.0" 77 | } 78 | }, 79 | "assert-plus": { 80 | "version": "1.0.0", 81 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 82 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 83 | }, 84 | "asynckit": { 85 | "version": "0.4.0", 86 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 87 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 88 | }, 89 | "aws-sign2": { 90 | "version": "0.7.0", 91 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 92 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 93 | }, 94 | "aws4": { 95 | "version": "1.8.0", 96 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 97 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" 98 | }, 99 | "bcrypt-pbkdf": { 100 | "version": "1.0.2", 101 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 102 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 103 | "requires": { 104 | "tweetnacl": "^0.14.3" 105 | } 106 | }, 107 | "bluebird": { 108 | "version": "2.11.0", 109 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", 110 | "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=" 111 | }, 112 | "body-parser": { 113 | "version": "1.19.0", 114 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 115 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 116 | "requires": { 117 | "bytes": "3.1.0", 118 | "content-type": "~1.0.4", 119 | "debug": "2.6.9", 120 | "depd": "~1.1.2", 121 | "http-errors": "1.7.2", 122 | "iconv-lite": "0.4.24", 123 | "on-finished": "~2.3.0", 124 | "qs": "6.7.0", 125 | "raw-body": "2.4.0", 126 | "type-is": "~1.6.17" 127 | } 128 | }, 129 | "bytes": { 130 | "version": "3.1.0", 131 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 132 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 133 | }, 134 | "caseless": { 135 | "version": "0.12.0", 136 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 137 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 138 | }, 139 | "combined-stream": { 140 | "version": "1.0.8", 141 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 142 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 143 | "requires": { 144 | "delayed-stream": "~1.0.0" 145 | } 146 | }, 147 | "content-type": { 148 | "version": "1.0.4", 149 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 150 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 151 | }, 152 | "core-util-is": { 153 | "version": "1.0.2", 154 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 155 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 156 | }, 157 | "dashdash": { 158 | "version": "1.14.1", 159 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 160 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 161 | "requires": { 162 | "assert-plus": "^1.0.0" 163 | } 164 | }, 165 | "debug": { 166 | "version": "2.6.9", 167 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 168 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 169 | "requires": { 170 | "ms": "2.0.0" 171 | } 172 | }, 173 | "delayed-stream": { 174 | "version": "1.0.0", 175 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 176 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 177 | }, 178 | "depd": { 179 | "version": "1.1.2", 180 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 181 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 182 | }, 183 | "ecc-jsbn": { 184 | "version": "0.1.2", 185 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 186 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 187 | "requires": { 188 | "jsbn": "~0.1.0", 189 | "safer-buffer": "^2.1.0" 190 | } 191 | }, 192 | "ee-first": { 193 | "version": "1.1.1", 194 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 195 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 196 | }, 197 | "extend": { 198 | "version": "3.0.2", 199 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 200 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 201 | }, 202 | "extsprintf": { 203 | "version": "1.3.0", 204 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 205 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 206 | }, 207 | "fast-deep-equal": { 208 | "version": "2.0.1", 209 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 210 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" 211 | }, 212 | "fast-json-stable-stringify": { 213 | "version": "2.0.0", 214 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 215 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 216 | }, 217 | "forever-agent": { 218 | "version": "0.6.1", 219 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 220 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 221 | }, 222 | "form-data": { 223 | "version": "2.3.3", 224 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 225 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 226 | "requires": { 227 | "asynckit": "^0.4.0", 228 | "combined-stream": "^1.0.6", 229 | "mime-types": "^2.1.12" 230 | } 231 | }, 232 | "getpass": { 233 | "version": "0.1.7", 234 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 235 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 236 | "requires": { 237 | "assert-plus": "^1.0.0" 238 | } 239 | }, 240 | "har-schema": { 241 | "version": "2.0.0", 242 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 243 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 244 | }, 245 | "har-validator": { 246 | "version": "5.1.3", 247 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 248 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 249 | "requires": { 250 | "ajv": "^6.5.5", 251 | "har-schema": "^2.0.0" 252 | } 253 | }, 254 | "http-errors": { 255 | "version": "1.7.2", 256 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 257 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 258 | "requires": { 259 | "depd": "~1.1.2", 260 | "inherits": "2.0.3", 261 | "setprototypeof": "1.1.1", 262 | "statuses": ">= 1.5.0 < 2", 263 | "toidentifier": "1.0.0" 264 | } 265 | }, 266 | "http-signature": { 267 | "version": "1.2.0", 268 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 269 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 270 | "requires": { 271 | "assert-plus": "^1.0.0", 272 | "jsprim": "^1.2.2", 273 | "sshpk": "^1.7.0" 274 | } 275 | }, 276 | "iconv-lite": { 277 | "version": "0.4.24", 278 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 279 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 280 | "requires": { 281 | "safer-buffer": ">= 2.1.2 < 3" 282 | } 283 | }, 284 | "inherits": { 285 | "version": "2.0.3", 286 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 287 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 288 | }, 289 | "is-typedarray": { 290 | "version": "1.0.0", 291 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 292 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 293 | }, 294 | "isstream": { 295 | "version": "0.1.2", 296 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 297 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 298 | }, 299 | "js-combinatorics": { 300 | "version": "0.5.4", 301 | "resolved": "https://registry.npmjs.org/js-combinatorics/-/js-combinatorics-0.5.4.tgz", 302 | "integrity": "sha512-PCqUIKGqv/Kjao1G4GE/Yni6QkCP2nWW3KnxL+8IGWPlP18vQpT8ufGMf4XUAAY8JHEryUCJbf51zG8329ntMg==" 303 | }, 304 | "jsbn": { 305 | "version": "0.1.1", 306 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 307 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 308 | }, 309 | "json-schema": { 310 | "version": "0.2.3", 311 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 312 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 313 | }, 314 | "json-schema-traverse": { 315 | "version": "0.4.1", 316 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 317 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 318 | }, 319 | "json-stringify-safe": { 320 | "version": "5.0.1", 321 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 322 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 323 | }, 324 | "jsprim": { 325 | "version": "1.4.1", 326 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 327 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 328 | "requires": { 329 | "assert-plus": "1.0.0", 330 | "extsprintf": "1.3.0", 331 | "json-schema": "0.2.3", 332 | "verror": "1.10.0" 333 | } 334 | }, 335 | "lodash.defaults": { 336 | "version": "4.2.0", 337 | "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", 338 | "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" 339 | }, 340 | "media-typer": { 341 | "version": "0.3.0", 342 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 343 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 344 | }, 345 | "mime-db": { 346 | "version": "1.40.0", 347 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 348 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" 349 | }, 350 | "mime-types": { 351 | "version": "2.1.24", 352 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 353 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", 354 | "requires": { 355 | "mime-db": "1.40.0" 356 | } 357 | }, 358 | "ms": { 359 | "version": "2.0.0", 360 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 361 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 362 | }, 363 | "node-forge": { 364 | "version": "0.7.6", 365 | "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", 366 | "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==" 367 | }, 368 | "numbered": { 369 | "version": "1.1.0", 370 | "resolved": "https://registry.npmjs.org/numbered/-/numbered-1.1.0.tgz", 371 | "integrity": "sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==" 372 | }, 373 | "oauth-sign": { 374 | "version": "0.9.0", 375 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 376 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 377 | }, 378 | "on-finished": { 379 | "version": "2.3.0", 380 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 381 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 382 | "requires": { 383 | "ee-first": "1.1.1" 384 | } 385 | }, 386 | "performance-now": { 387 | "version": "2.1.0", 388 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 389 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 390 | }, 391 | "psl": { 392 | "version": "1.4.0", 393 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", 394 | "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==" 395 | }, 396 | "punycode": { 397 | "version": "2.1.1", 398 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 399 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 400 | }, 401 | "qs": { 402 | "version": "6.7.0", 403 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 404 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 405 | }, 406 | "raw-body": { 407 | "version": "2.4.0", 408 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 409 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 410 | "requires": { 411 | "bytes": "3.1.0", 412 | "http-errors": "1.7.2", 413 | "iconv-lite": "0.4.24", 414 | "unpipe": "1.0.0" 415 | } 416 | }, 417 | "request": { 418 | "version": "2.88.0", 419 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 420 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 421 | "requires": { 422 | "aws-sign2": "~0.7.0", 423 | "aws4": "^1.8.0", 424 | "caseless": "~0.12.0", 425 | "combined-stream": "~1.0.6", 426 | "extend": "~3.0.2", 427 | "forever-agent": "~0.6.1", 428 | "form-data": "~2.3.2", 429 | "har-validator": "~5.1.0", 430 | "http-signature": "~1.2.0", 431 | "is-typedarray": "~1.0.0", 432 | "isstream": "~0.1.2", 433 | "json-stringify-safe": "~5.0.1", 434 | "mime-types": "~2.1.19", 435 | "oauth-sign": "~0.9.0", 436 | "performance-now": "^2.1.0", 437 | "qs": "~6.5.2", 438 | "safe-buffer": "^5.1.2", 439 | "tough-cookie": "~2.4.3", 440 | "tunnel-agent": "^0.6.0", 441 | "uuid": "^3.3.2" 442 | }, 443 | "dependencies": { 444 | "qs": { 445 | "version": "6.5.2", 446 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 447 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 448 | } 449 | } 450 | }, 451 | "safe-buffer": { 452 | "version": "5.2.0", 453 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 454 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 455 | }, 456 | "safer-buffer": { 457 | "version": "2.1.2", 458 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 459 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 460 | }, 461 | "setprototypeof": { 462 | "version": "1.1.1", 463 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 464 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 465 | }, 466 | "sshpk": { 467 | "version": "1.16.1", 468 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 469 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 470 | "requires": { 471 | "asn1": "~0.2.3", 472 | "assert-plus": "^1.0.0", 473 | "bcrypt-pbkdf": "^1.0.0", 474 | "dashdash": "^1.12.0", 475 | "ecc-jsbn": "~0.1.1", 476 | "getpass": "^0.1.1", 477 | "jsbn": "~0.1.0", 478 | "safer-buffer": "^2.0.2", 479 | "tweetnacl": "~0.14.0" 480 | } 481 | }, 482 | "ssml-builder": { 483 | "version": "0.4.3", 484 | "resolved": "https://registry.npmjs.org/ssml-builder/-/ssml-builder-0.4.3.tgz", 485 | "integrity": "sha1-QMX3GlQViOzcGumYfEjrEhIsxj0=" 486 | }, 487 | "statuses": { 488 | "version": "1.5.0", 489 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 490 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 491 | }, 492 | "toidentifier": { 493 | "version": "1.0.0", 494 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 495 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 496 | }, 497 | "tough-cookie": { 498 | "version": "2.4.3", 499 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 500 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 501 | "requires": { 502 | "psl": "^1.1.24", 503 | "punycode": "^1.4.1" 504 | }, 505 | "dependencies": { 506 | "punycode": { 507 | "version": "1.4.1", 508 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 509 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 510 | } 511 | } 512 | }, 513 | "tunnel-agent": { 514 | "version": "0.6.0", 515 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 516 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 517 | "requires": { 518 | "safe-buffer": "^5.0.1" 519 | } 520 | }, 521 | "tweetnacl": { 522 | "version": "0.14.5", 523 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 524 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 525 | }, 526 | "type-is": { 527 | "version": "1.6.18", 528 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 529 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 530 | "requires": { 531 | "media-typer": "0.3.0", 532 | "mime-types": "~2.1.24" 533 | } 534 | }, 535 | "unpipe": { 536 | "version": "1.0.0", 537 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 538 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 539 | }, 540 | "uri-js": { 541 | "version": "4.2.2", 542 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 543 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 544 | "requires": { 545 | "punycode": "^2.1.0" 546 | } 547 | }, 548 | "uuid": { 549 | "version": "3.3.3", 550 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", 551 | "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" 552 | }, 553 | "validator": { 554 | "version": "9.4.1", 555 | "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", 556 | "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==" 557 | }, 558 | "verror": { 559 | "version": "1.10.0", 560 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 561 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 562 | "requires": { 563 | "assert-plus": "^1.0.0", 564 | "core-util-is": "1.0.2", 565 | "extsprintf": "^1.2.0" 566 | } 567 | } 568 | } 569 | } 570 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexa-youtube-skill", 3 | "version": "3.0.5", 4 | "description": "Use Alexa to search YouTube for your favorite videos", 5 | "engines": { 6 | "node": ">=10.16.3" 7 | }, 8 | "main": "index.js", 9 | "dependencies": { 10 | "alexa-app": "4.2.3", 11 | "request": "2.88.0", 12 | "ssml-builder": "0.4.3" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/dmhacker/alexa-youtube-skill" 17 | }, 18 | "keywords": [ 19 | "alexa", 20 | "youtube" 21 | ], 22 | "license": "MIT" 23 | } 24 | -------------------------------------------------------------------------------- /util/formatting.js: -------------------------------------------------------------------------------- 1 | // Enables Python-esque formatting 2 | // (e.g. "Hello {0}!".formatUnicorn("world") => "Hello world!") 3 | module.exports = function () { 4 | "use strict"; 5 | var str = this.toString(); 6 | if (arguments.length) { 7 | var t = typeof arguments[0]; 8 | var key; 9 | var args = ("string" === t || "number" === t) ? 10 | Array.prototype.slice.call(arguments) 11 | : arguments[0]; 12 | 13 | for (key in args) { 14 | str = str.replace(new RegExp("\\{" + key + "\\}", "gi"), args[key]); 15 | } 16 | } 17 | return str; 18 | }; 19 | 20 | 21 | -------------------------------------------------------------------------------- /util/responses.js: -------------------------------------------------------------------------------- 1 | let messages = { 2 | "en-US": { 3 | "NO_RESULTS_FOUND": "{0} did not return any results on YouTube.", 4 | "ASK_TO_PLAY": "I found a video called {0}. Would you like me to download it now?", 5 | "ASK_TO_CONTINUE": "Download still in progress. Would you like to keep waiting?", 6 | "NOW_PLAYING": "I am now playing {0}.", 7 | "NOTHING_TO_RESUME": "You are not playing anything currently.", 8 | "NOTHING_TO_REPEAT": "You have not selected a video to play.", 9 | "LOOP_ON_TRIGGERED": "I will repeat your {0} selection infinitely.", 10 | "LOOP_OFF_TRIGGERED": "I will no longer repeat your {0} selection.", 11 | "REPEAT_TRIGGERED": "I will repeat your {0} selection once.", 12 | "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." 13 | }, 14 | "de-DE": { 15 | "NO_RESULTS_FOUND": "Keine Ergebnisse auf Youtube gefunden.", 16 | "ASK_TO_PLAY": "Ich habe ein Video mit dem Namen {0} gefunden. Möchtest du, dass ich es spiele?", 17 | "ASK_TO_CONTINUE": "Download läuft noch. Möchtest du gerne warten?", 18 | "NOW_PLAYING": "Ich spiele jetzt {0}.", 19 | "NOTHING_TO_RESUME": "Du spielst derzeit kein Video ab.", 20 | "NOTHING_TO_REPEAT": "Du hast kein Video zum Abspielen ausgewählt.", 21 | "LOOP_ON_TRIGGERED": "Ich werde deine Auswahl unendlich wiederholen.", 22 | "LOOP_OFF_TRIGGERED": "Ich werde deine Auswahl nicht mehr wiederholen.", 23 | "REPEAT_TRIGGERED": "Ich werde deine Auswahl einmal wiederholen.", 24 | "HELP_TRIGGERED": "Um die YouTube-Fertigkeit zu verwenden, musst du die Fähigkeit angeben, nach dem gewünschten Video zu suchen. Wenn das Video abgespielt wird, können Sie Alexa außerdem mitteilen, dass es angehalten, neu gestartet oder wiederholt werden soll." 25 | }, 26 | "fr-FR": { 27 | "NO_RESULTS_FOUND": "{0} n'a pas été trouvé sur YouTube.", 28 | "ASK_TO_PLAY": "J'ai trouvé une vidéo appelée {0}. Voulez-vous que je joue?", 29 | "ASK_TO_CONTINUE": "Téléchargement toujours en cours. Voulez-vous continuer à attendre?", 30 | "NOW_PLAYING": "Je suis en train de jouer {0}.", 31 | "NOTHING_TO_RESUME": "Il n'y a aucun élément en cours de lecture.", 32 | "NOTHING_TO_REPEAT": "Vous n'avez pas sélectionné de vidéo à écouter.", 33 | "LOOP_ON_TRIGGERED": "Je vais répéter {0} à l'infini.", 34 | "LOOP_OFF_TRIGGERED": "Je ne vais plus répéter {0} .", 35 | "REPEAT_TRIGGERED": "Je ne vais répéter {0} une fois.", 36 | "HELP_TRIGGERED": "Pour utiliser cette skill, dites à Alexa de rechercher la vidéo que vous souhaitez. De plus, lorsque la lecture est en cours, vous pouvez dire à Alexa de mettre en pause, redémarrer, ou répéter." 37 | }, 38 | "it-IT": { 39 | "NO_RESULTS_FOUND": "{0} non lo trovo su YouTube.", 40 | "ASK_TO_PLAY": "Ho trovato un video chiamato {0}. Vorresti che lo suonassi?", 41 | "ASK_TO_CONTINUE": "Download ancora in corso. Ti piacerebbe continuare ad aspettare?", 42 | "NOW_PLAYING": "Sto suonando {0}.", 43 | "NOTHING_TO_RESUME": "Non stai suonando nulla ora.", 44 | "NOTHING_TO_REPEAT": "Non hai selezionato nessun video da riprodurre.", 45 | "LOOP_ON_TRIGGERED": "Ti ripeterò la tua selezione su {0} infinitamente.", 46 | "LOOP_OFF_TRIGGERED": "Non ti ripeterò più la tua selezione su {0}.", 47 | "REPEAT_TRIGGERED": "Ti suonerò la tua selezione su {0} solo una volta.", 48 | "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." 49 | } 50 | } 51 | 52 | messages['en-GB'] = messages['en-US']; 53 | 54 | module.exports = messages; 55 | --------------------------------------------------------------------------------