SickBeard Alexa skill 1.0
This commit is contained in:
commit
39b8d15540
12
.env
Normal file
12
.env
Normal file
@ -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
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
deploy.env
|
||||
lambda.zip
|
||||
node_modules/
|
55
README.md
Normal file
55
README.md
Normal file
@ -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.
|
27
event.json
Normal file
27
event.json
Normal file
@ -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"
|
||||
}
|
4
index.js
Normal file
4
index.js
Normal file
@ -0,0 +1,4 @@
|
||||
'use strict';
|
||||
|
||||
var app = require('./lib');
|
||||
exports.handler = app.lambda();
|
31
interaction_model/intent_schema.json
Normal file
31
interaction_model/intent_schema.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
34
interaction_model/sample_utterances.txt
Normal file
34
interaction_model/sample_utterances.txt
Normal file
@ -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
|
132
lib/handlers.js
Normal file
132
lib/handlers.js
Normal file
@ -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
|
||||
};
|
15
lib/index.js
Normal file
15
lib/index.js
Normal file
@ -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;
|
35
lib/utils.js
Normal file
35
lib/utils.js
Normal file
@ -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
|
||||
};
|
21
package.json
Normal file
21
package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
27
test_events/couch_potato_add_show.json
Normal file
27
test_events/couch_potato_add_show.json
Normal file
@ -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"
|
||||
}
|
26
test_events/couch_potato_find_show.json
Normal file
26
test_events/couch_potato_find_show.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
50
test_events/yes_reprompt.json
Normal file
50
test_events/yes_reprompt.json
Normal file
@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user