commit 39b8d15540a0d574c95e01ad5f7b43be5a54b6b2 Author: Joseph Schmitt Date: Sun Mar 27 16:22:50 2016 -0400 SickBeard Alexa skill 1.0 diff --git a/.env b/.env new file mode 100644 index 0000000..3d28a50 --- /dev/null +++ b/.env @@ -0,0 +1,12 @@ +AWS_ENVIRONMENT= +AWS_SESSION_TOKEN= +AWS_REGION=us-east-1 +AWS_FUNCTION_NAME=sickbeard +AWS_HANDLER=index.handler +AWS_MODE=event +AWS_MEMORY_SIZE=128 +AWS_TIMEOUT=10 +AWS_DESCRIPTION= +AWS_RUNTIME=nodejs +SB_URL=http://url-to-couch-potato-server +SB_API_KEY=apiKey diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..babf4c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +deploy.env +lambda.zip +node_modules/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf5daf1 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Couch Potato Alexa Skill + +This is a skill built for Amazon's Alexa service that tells you about your Couch Potato queue. It +allows you to ask Alexa the following: + +> Alexa, ask Couch Potato to add The Godfather + +> Alexa, ask Couch Potato if The Dark Knight is on the list + +If you're just getting started developing skills for Alexa, I'd recommend reading [Getting Started +with the Alexa Skills +Kit](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/getting-started-guide) and +[Developing an Alexa Skill as a Lambda +Function](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/developing-an-alexa-skill-as-a-lambda-function) to get familiar with the process. + +## Configuring The Skill + +To configure the skill, open up the `.env` file and fill in the correct values for `CP_URL`, which +should point to your Couch Potato server, and `CP_API_KEY` which should have your server's API key. + +## Testing The Skill Locally + +You can use [node-lambda](https://github.com/motdotla/node-lambda) to test this skill locally. In +the `test_events` directory are several event files you can use for testing, and they should map +pretty well to each Intent. To test an intent, simply copy the contents of one of the json files in +that directory and overwrite the contents of `event.json`. Then run `node-lambda run` from the +command line. + +## Setting up the Skill + +To set up the skill, head on over to [Alexa skills kit +development console](https://developer.amazon.com/edw/home.html) and add a new skill. Fill in the +basic skill information however you choose. For Endpoint, you'll need to fill in your Lambda ARN +which you'll get in the next step. Next, head on over to Interaction Model. In the Intent +Schema field, copy and paste the contents of the `interaction_model/intent_schema.json` file. Then +in the Sample Utterances field, copy and paste the contents of +`interaction_model/sample_utterances.txt`. + +## Hosting the Skill + +The skill is built to be easily hosted on Amazon's [AWS +Lambda service](https://aws.amazon.com/lambda/). Create your Lambda function (using the +alexa-skills-kit-color-expert blueprint) and make sure you choose Node.js as the runtime. After +you've created your Lambda function, look at the top right of the page to get your Lambda ARN +number and put that in the Alexa Skill Information Endpoint field. + +To deploy to Lambda, first makes sure you do an `npm install` at the root of the project. +Once all the dependencies are installed, run `npm run bundle`, which will create a lambda.zip file. +You can then upload that zip file to Lambda for use in your function and skill. + +You can also use [node-lambda](https://github.com/motdotla/node-lambda) to deploy to your Lambda +function directly from the command line. Simply add a deploy.env file with your environment +configuration (and double check the supplied .env file in this repository) and then run +`node-lambda deploy`. Please visit the [node-lambda](https://github.com/motdotla/node-lambda) +project page for more information on deploying from the command line. diff --git a/event.json b/event.json new file mode 100644 index 0000000..348c747 --- /dev/null +++ b/event.json @@ -0,0 +1,27 @@ +{ + "session": { + "sessionId": "SessionId.1234", + "application": { + "applicationId": "amzn1.echo-sdk-ams.app.1234" + }, + "user": { + "userId": "amzn1.echo-sdk-account.1234" + }, + "new": true + }, + "request": { + "type": "IntentRequest", + "requestId": "EdwRequestId.1234", + "timestamp": "2016-01-01T00:00:00Z", + "intent": { + "name": "AddShow", + "slots": { + "showName": { + "name": "showName", + "value": "silicon valley" + } + } + } + }, + "version": "1.0" +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..5581cfc --- /dev/null +++ b/index.js @@ -0,0 +1,4 @@ +'use strict'; + +var app = require('./lib'); +exports.handler = app.lambda(); diff --git a/interaction_model/intent_schema.json b/interaction_model/intent_schema.json new file mode 100644 index 0000000..021caa7 --- /dev/null +++ b/interaction_model/intent_schema.json @@ -0,0 +1,31 @@ +{ + "intents": [ + { + "intent": "AddShow", + "slots": [ + { + "name": "showName", + "type": "AMAZON.LITERAL" + } + ] + }, + { + "intent": "FindShow", + "slots": [ + { + "name": "showName", + "type": "AMAZON.LITERAL" + } + ] + }, + { + "intent": "AMAZON.YesIntent" + }, + { + "intent": "AMAZON.NoIntent" + }, + { + "intent": "AMAZON.CancelIntent" + } + ] +} diff --git a/interaction_model/sample_utterances.txt b/interaction_model/sample_utterances.txt new file mode 100644 index 0000000..b9a51d5 --- /dev/null +++ b/interaction_model/sample_utterances.txt @@ -0,0 +1,34 @@ +AddShow add {How I Met Your Mother|showName} +AddShow add {Jessica Jones|showName} +AddShow add {Seinfeld|showName} +AddShow add {Better Call Saul|showName} +AddShow add {Parks and Recreation|showName} +AddShow add {Silicon Valley|showName} + +FindShow is {How I Met Your Mother|showName} on the list +FindShow is {Jessica Jones|showName} on the list +FindShow is {Seinfeld|showName} on the list +FindShow is {Better Call Saul|showName} on the list +FindShow is {Parks and Recreation|showName} on the list +FindShow is {Silicon Valley|showName} on the list + +FindShow if {How I Met Your Mother|showName} is on the list +FindShow if {Jessica Jones|showName} is on the list +FindShow if {Seinfeld|showName} is on the list +FindShow if {Better Call Saul|showName} is on the list +FindShow if {Parks and Recreation|showName} is on the list +FindShow if {Silicon Valley|showName} is on the list + +FindShow is {How I Met Your Mother|showName} already +FindShow is {Jessica Jones|showName} already +FindShow is {Seinfeld|showName} already +FindShow is {Better Call Saul|showName} already +FindShow is {Parks and Recreation|showName} already +FindShow is {Silicon Valley|showName} already + +FindShow if {How I Met Your Mother|showName} is already +FindShow if {Jessica Jones|showName} is already +FindShow if {Seinfeld|showName} is already +FindShow if {Better Call Saul|showName} is already +FindShow if {Parks and Recreation|showName} is already +FindShow if {Silicon Valley| showName} is already diff --git a/lib/handlers.js b/lib/handlers.js new file mode 100644 index 0000000..369e12e --- /dev/null +++ b/lib/handlers.js @@ -0,0 +1,132 @@ +'use strict'; + +var _ = require('underscore'); +var SickBeard = require('node-sickbeard'); +var utils = require('./utils.js'); + +var WELCOME_DESCRIPTION = 'This skill allows you to manage your SickBeard movie list.'; +var HELP_RESPONSE = ['You can ask SickBeard about the movies in your queue or add new movies', + 'to it. Try asking "is Silicon Valley on the list?". If it\'s not and you want to add it, try', + 'saying "add Silicon Valley".'].join(' '); + +var config = require('dotenv').config(); +var sb = new SickBeard({ + url: config.SB_URL, + apikey: config.SB_API_KEY +}); + +function handleLaunchIntent(req, resp) { + resp + .say(WELCOME_DESCRIPTION) + .say(HELP_RESPONSE) + .send(); +} + +function handleFindShowIntent(req, resp) { + var showName = req.slot('showName'); + + sb.cmd('shows').then(function (searchResp) { + var shows = searchResp.data; + var result = shows && Object.keys(shows).length ? _.find(shows, function (show) { + return show.show_name.toLowerCase().indexOf(showName.toLowerCase()) >= 0; + }) : null; + + if (!result) { + resp.say('Couldn\'t find ' + showName + ' queued for download. '); + + sb.cmd('sb.searchtvdb', {name: showName}).then(function (searchResults) { + utils.sendSearchResponse(searchResults.data.results, resp); + }); + } + else { + resp + .say(['It looks like', result.show_name, 'is already on your list.'].join(' ')) + .send(); + } + }); + + //Async response + return false; +} + +function handleAddShowIntent(req, resp) { + var showName = req.slot('showName'); + + sb.cmd('sb.searchtvdb', {name: showName}).then(function (searchResults) { + utils.sendSearchResponse(searchResults.data.results, resp); + }); + + //Async response + return false; +} + +function handleYesIntent(req, resp) { + var promptData = req.session('promptData'); + var show; + + if (!promptData) { + console.log('Got a AMAZON.YesIntent but no promptData. Ending session.'); + resp.send(); + } + else if (promptData.yesAction === 'addShow') { + show = promptData.searchResults[0]; + + sb.cmd('show.addnew', { + tvdbid: show.tvdbid, + status: 'wanted' + }).then(function () { + resp + .say(promptData.yesResponse) + .send(); + }); + + //Async response + return false; + } + else { + console.log("Got an unexpected yesAction. PromptData:"); + console.log(promptData); + resp.send(); + } +} + +function handleNoIntent(req, resp) { + var promptData = req.session('promptData'); + + if (!promptData) { + console.log('Got a AMAZON.YesIntent but no promptData. Ending session.'); + resp.send(); + } + else if (promptData.noAction === 'endSession') { + resp.say(promptData.noResponse).send(); + } + else if (promptData.noAction === 'suggestNextShow') { + var shows = promptData.searchResults; + resp + .say(promptData.noResponse) + .session('promptData', utils.buildPrompt(shows.slice(1))) + .shouldEndSession(false) + .send(); + } + else { + console.log("Got an unexpected noAction. PromptData:"); + console.log(promptData); + resp.send(); + } +} + +function handleCancelIntent(req, resp) { + resp.shouldEndSession(true).send(); +} + +function handleCancelIntent(req, resp) { + resp.say(HELP_RESPONSE).send(); +} + +module.exports = { + handleFindShowIntent: handleFindShowIntent, + handleAddShowIntent: handleAddShowIntent, + handleYesIntent: handleYesIntent, + handleNoIntent: handleNoIntent, + handleCancelIntent: handleCancelIntent +}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..0fe3382 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,15 @@ +'use strict'; + +var handlers = require('./handlers.js'); + +var alexa = require('alexa-app'); +var app = new alexa.app('sickBeard'); + +app.launch(handlers.handleLaunchIntent); +app.intent('FindShow', handlers.handleFindShowIntent); +app.intent('AddShow', handlers.handleAddShowIntent); +app.intent('AMAZON.YesIntent', handlers.handleYesIntent); +app.intent('AMAZON.NoIntent', handlers.handleNoIntent); +app.intent('AMAZON.CancelIntent', handlers.handleCancelIntent); + +module.exports = app; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..6ae9ae1 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,35 @@ +function buildPrompt(shows) { + var promptData = { + searchResults: shows.slice(0, 5), + yesAction : 'addShow', + yesResponse: ['Added', shows[0].name, 'to your list of shows to download.'].join(' ') + }; + + if (shows.length > 1) { + promptData.noAction = 'suggestNextShow'; + promptData.noResponse = 'Ok, did you mean ' + shows[1].name + '?'; + } + else { + promptData.noAction = 'endSession'; + promptData.noResponse = 'Ok. I\'m out of suggestions. Sorry about that.'; + } + + return promptData; +} + +function sendSearchResponse(shows, resp) { + if(!shows || !shows.length) { + return resp.say('No show found for ' + showName).send(); + } + + resp + .say(['Add', shows[0].name, 'to your list?'].join(' ')) + .session('promptData', buildPrompt(shows.slice(0, 5))) + .shouldEndSession(false) + .send(); +} + +module.exports = { + buildPrompt: buildPrompt, + sendSearchResponse: sendSearchResponse +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..6c94b28 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "alexa-sickbeard", + "version": "1.0.0", + "description": "A skill to ask Alexa about your SickBeard queue.", + "main": "index.js", + "scripts": { + "bundle": "mkdir -p bundle && cp -r {.env,index.js,lib,node_modules} bundle/ && cd bundle && bestzip ../lambda.zip * .env && rm -rf ../bundle" + }, + "author": "Joe Schmitt", + "license": "MIT", + "dependencies": { + "alexa-app": "^2.3.2", + "dotenv": "^2.0.0", + "node-sickbeard": "0.0.1", + "underscore": "^1.8.3" + }, + "devDependencies": { + "bestzip": "^1.1.3", + "node-lambda": "^0.7.1" + } +} diff --git a/test_events/couch_potato_add_show.json b/test_events/couch_potato_add_show.json new file mode 100644 index 0000000..c419d7a --- /dev/null +++ b/test_events/couch_potato_add_show.json @@ -0,0 +1,27 @@ +{ + "session": { + "sessionId": "SessionId.1234", + "application": { + "applicationId": "amzn1.echo-sdk-ams.app.1234" + }, + "user": { + "userId": "amzn1.echo-sdk-account.1234" + }, + "new": true + }, + "request": { + "type": "IntentRequest", + "requestId": "EdwRequestId.1234", + "timestamp": "2016-01-01T00:00:00Z", + "intent": { + "name": "AddShow", + "slots": { + "showName": { + "name": "showName", + "value": "silicon" + } + } + } + }, + "version": "1.0" +} diff --git a/test_events/couch_potato_find_show.json b/test_events/couch_potato_find_show.json new file mode 100644 index 0000000..f3556c1 --- /dev/null +++ b/test_events/couch_potato_find_show.json @@ -0,0 +1,26 @@ +{ + "session": { + "sessionId": "SessionId.1234", + "application": { + "applicationId": "amzn1.echo-sdk-ams.app.1234" + }, + "user": { + "userId": "amzn1.echo-sdk-account.1234" + }, + "new": true + }, + "request": { + "type": "IntentRequest", + "requestId": "EdwRequestId.1234", + "timestamp": "2016-01-01T00:00:00Z", + "intent": { + "name": "FindShow", + "slots": { + "showName": { + "name": "showName", + "value": "silicon valley" + } + } + } + } +} diff --git a/test_events/yes_reprompt.json b/test_events/yes_reprompt.json new file mode 100644 index 0000000..12b7cc7 --- /dev/null +++ b/test_events/yes_reprompt.json @@ -0,0 +1,50 @@ +{ + "session": { + "sessionId": "SessionId.1234", + "application": { + "applicationId": "amzn1.echo-sdk-ams.app.1234" + }, + "attributes": { + "promptData": { + "searchResults": [ + { + "name": "Silicon Wadi", + "tvdbid": 268854 + }, + { + "first_aired": "2014-04-06", + "name": "Silicon Valley", + "tvdbid": 277165 + }, + { + "first_aired": "2012-11-01", + "name": "Silicon Valley Rebels", + "tvdbid": 283723 + }, + { + "first_aired": "2012-11-05", + "name": "Start-ups: Silicon Valley", + "tvdbid": 263541 + } + ], + "yesAction": "addShow", + "yesResponse": "Added Silicon Wadi to your list of shows to download.", + "noAction": "suggestNextShow", + "noResponse": "Ok, did you mean Silicon Valley?" + } + }, + "user": { + "userId": "amzn1.echo-sdk-account.1234" + }, + "new": false + }, + "request": { + "type": "IntentRequest", + "requestId": "EdwRequestId.1234", + "timestamp": "2016-01-01T00:00:00Z", + "intent": { + "name": "AMAZON.NoIntent", + "slots": {} + } + } +}