Skip to content
This repository has been archived by the owner on Oct 22, 2021. It is now read-only.

Play and summarize Artsy podcasts. #56

Merged
merged 1 commit into from
Feb 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### 0.3.1 (Next)

* [#56](https://github.com/artsy/elderfield/pull/56): Play and summarize Artsy podcasts - [@dblock](https://github.com/dblock).
* [#54](https://github.com/artsy/elderfield/pull/54): Added test code coverage - [@dblock](https://github.com/dblock).
* Your contribution here.

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ This skill adds a card with more information.

![](images/card-edelman-arts.png)

#### Ask Artsy to play the latest podcast

* Alexa, ask Artsy to play podcast number 13.
* Alexa, ask Artsy to play the latest podcast.
* Alexa, ask Artsy for a summary of podcast 13.
* Alexa, ask Artsy for a summary of the latest podcast.

Plays and summarizes one of [Artsy podcasts](https://soundcloud.com/artsypodcast).

### Deploying to AWS Lambda

See [DEPLOYMENT](DEPLOYMENT.md).
Expand Down
110 changes: 105 additions & 5 deletions functions/artsy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var alexa = require('alexa-app');
var app = new alexa.app('artsy');
var xapp = require('./models/xapp');
var api = require('./models/api');
var podcast = require('./models/podcast');
var _ = require('underscore');
var removeMd = require('remove-markdown');
var nodeGeocoder = require('node-geocoder');
Expand Down Expand Up @@ -52,6 +53,28 @@ app.intent('AMAZON.CancelIntent', {
}
);

app.intent('AMAZON.PauseIntent', {},
function(req, res) {
console.log('app.AMAZON.PauseIntent');
res.audioPlayerStop();
res.send();
}
);

app.intent('AMAZON.ResumeIntent', {},
function(req, res) {
console.log('app.AMAZON.ResumeIntent');
if (req.context.AudioPlayer.offsetInMilliseconds > 0 && req.context.AudioPlayer.playerActivity === 'STOPPED') {
res.audioPlayerPlayStream('REPLACE_ALL', {
token: req.context.AudioPlayer.token,
url: req.context.AudioPlayer.token, // hack: use token to remember the URL of the stream
offsetInMilliseconds: req.context.AudioPlayer.offsetInMilliseconds
});
}
res.send();
}
);

app.intent('AMAZON.HelpIntent', {
"slots": {},
"utterances": [
Expand Down Expand Up @@ -150,7 +173,7 @@ app.intent('AboutIntent', {

res.send();
}).fail(function(error) {
console.log(`app.AboutIntent: couldn't find an artist ${value}.`);
console.error(`app.AboutIntent: couldn't find an artist ${value}.`);
res.say(`Sorry, I couldn't find an artist ${value}. Try again?`);
res.shouldEndSession(false);
res.send();
Expand Down Expand Up @@ -246,27 +269,27 @@ app.intent('ShowsIntent', {
res.say(messageText);
res.shouldEndSession(true);
} else {
console.log(`app.ShowsIntent: couldn't find any shows in '${city}'.`);
console.error(`app.ShowsIntent: couldn't find any shows in '${city}'.`);
res.say(`Sorry, I couldn't find any shows in ${city}. Try again?`);
res.shouldEndSession(false);
}
res.send();
}).fail(function(error) {
console.log(`app.ShowsIntent: couldn't find any shows in '${city}', ${error}.`);
console.error(`app.ShowsIntent: couldn't find any shows in '${city}', ${error}.`);
res.say(`Sorry, I couldn't find any shows in ${city}. Try again?`);
res.shouldEndSession(false);
res.send();
});
}).fail(function(error) {
console.log(`app.ShowsIntent: ${error}.`);
console.error(`app.ShowsIntent: ${error}.`);
res.say("Sorry, I couldn't connect to Artsy. Try again?");
res.shouldEndSession(false);
res.send();
});
}
})
.catch(function(error) {
console.log(`app.ShowsIntent: ${error}.`);
console.error(`app.ShowsIntent: ${error}.`);
res.say(`Sorry, I couldn't find ${city}. Try again?`);
res.shouldEndSession(false);
res.send();
Expand All @@ -276,6 +299,83 @@ app.intent('ShowsIntent', {
}
);

app.intent('PodcastIntent', {
"slots": {
"NUMBER": "AMAZON.NUMBER"
},
"utterances": [
"{|to play} {|latest} podcast {|number|episode|episode number} {-|NUMBER}"
]
},
function(req, res) {
var podcastNumber = req.slot('NUMBER');
var podcastStream = podcast.instance();
console.log(`app.PodcastIntent: ${podcastNumber || 'latest'}`);
if (podcastNumber && podcastNumber !== "") {
podcastStream = podcastStream.getEpisodeStreamById(podcastNumber);
} else {
podcastStream = podcastStream.getLatestEpisodeStream();
}

podcastStream.then(function(audioMpegEnclosure) {
var streamUrl = audioMpegEnclosure.url.replace('http://', 'https://'); // SSL required by Amazon, available on SoundCloud
var stream = {
url: streamUrl,
token: streamUrl,
offsetInMilliseconds: 0
}
res.audioPlayerPlayStream('REPLACE_ALL', stream);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

magic?! Need to read about audoPlayerPlayStream

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is a "native" function of the API of alexa-app, see https://github.com/alexa-js/alexa-app

console.log(`app.PodcastIntent: ${podcastNumber || 'latest'}, streaming ${stream.url}.`);
res.send();
}).catch(function(error) {
console.error(`app.PodcastIntent: ${podcastNumber || 'latest'}, ${error}.`);
if (podcastNumber) {
res.say(`Sorry, I couldn't find Artsy podcast number ${podcastNumber}. Try again?`);
} else {
res.say(`Sorry, I couldn't find the latest Artsy podcast. Try again?`);
}
res.shouldEndSession(false);
res.send();
});

return false;
}
);

app.intent('PodcastSummaryIntent', {
"slots": {
"NUMBER": "AMAZON.NUMBER"
},
"utterances": [
"for the summary of podcast {|number|episode|episode number} {-|NUMBER}",
"for the summary of the latest podcast"
]
},
function(req, res) {
var podcastNumber = req.slot('NUMBER');
var podcastInfo = podcast.instance();
console.log(`app.PodcastSummaryIntent: ${podcastNumber || 'latest'}`);
if (podcastNumber && podcastNumber !== "") {
podcastInfo = podcastInfo.getEpisodeById(podcastNumber);
} else {
podcastInfo = podcastInfo.getLatestEpisode();
}

podcastInfo.then(function(episode) {
res.say(`Artsy podcast episode ${episode.title.replace('No. ', '')}. ${episode.description}`);
console.log(`app.PodcastSummaryIntent: ${podcastNumber}, ${episode.title}.`);
res.send();
}).catch(function(error) {
console.error(`app.PodcastSummaryIntent: ${podcastNumber}, ${error}.`);
res.say(`Sorry, I couldn't find podcast number ${podcastNumber}. Try again?`);
res.shouldEndSession(false);
res.send();
});

return false;
}
);

if (process.env['ENV'] == 'lambda') {
console.log("Starting Artsy Alexa on AWS lambda.")
exports.handle = app.lambda();
Expand Down
88 changes: 88 additions & 0 deletions functions/artsy/models/podcast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
var Q = require('q');
var feedparser = require('feedparser-promised');
var _ = require('underscore');
_.mixin(require('underscore.string').exports());

function Podcast() {
this.rssUrl = 'https://feeds.soundcloud.com/users/soundcloud:users:211089382/sounds.rss';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to suggest make this an env but I guess there is no point if we lose env everytime we deploy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, and it's not likely to change without having to do code changes.

this.parsed = feedparser.parse(this.rssUrl);
}

Podcast.instance = function() {
return new Podcast();
}

Podcast.prototype.fetchItems = function() {
var deferred = Q.defer();
if (this.items) {
deferred.resolve(this.items);
} else {
var deferred = Q.defer();
this.parsed.then(function(items) {
this.items = items;
deferred.resolve(this.items);
});
}
return deferred.promise;
}

Podcast.prototype.getEpisodeById = function(id) {
var deferred = Q.defer();
this.fetchItems().then(function(items) {
var podcastTitlePrefix = `No. ${id}:`;
var episode = _.find(items, function(item) {
return _.startsWith(item.title, podcastTitlePrefix);
});
if (episode) {
deferred.resolve(episode);
} else {
deferred.reject(`podcast ${id} not found`);
}
}).fail(function(error) {
deferred.reject(error);
});
return deferred.promise;
}

Podcast.prototype.getLatestEpisode = function() {
var deferred = Q.defer();
this.fetchItems().then(function(items) {
if (items.length > 0) {
deferred.resolve(items[0]);
} else {
deferred.reject(`no podcast episodes found`);
}
}).fail(function(error) {
deferred.reject(error);
});
return deferred.promise;
}

Podcast.prototype.getEpisodeStream = function(getEpisodeFunction) {
var deferred = Q.defer();
getEpisodeFunction.then(function(episode) {
if (episode.enclosures) {
var audioMpegEnclosure = _.findWhere(episode.enclosures, { type: 'audio/mpeg' });
if (audioMpegEnclosure) {
deferred.resolve(audioMpegEnclosure);
} else {
deferred.reject(`podcast ${episode.guid} audio stream not found`);
}
} else {
deferred.reject(`podcast ${episode.guid} has no audio streams`);
}
}).fail(function(error) {
deferred.reject(error);
});
return deferred.promise;
}

Podcast.prototype.getEpisodeStreamById = function(id) {
return this.getEpisodeStream(this.getEpisodeById(id));
}

Podcast.prototype.getLatestEpisodeStream = function(id) {
return this.getEpisodeStream(this.getLatestEpisode());
}

module.exports = Podcast;
4 changes: 3 additions & 1 deletion functions/artsy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"alexa-app": "^3.0.0",
"superagent": "2.3.0",
"underscore": "1.8.3",
"underscore.string": "2.3.0",
"remove-markdown": "0.1.0",
"node-geocoder": "^3.15.2"
"node-geocoder": "^3.15.2",
"feedparser-promised": "1.1.1"
}
}
26 changes: 26 additions & 0 deletions functions/artsy/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
"intent": "AMAZON.CancelIntent",
"slots": []
},
{
"intent": "AMAZON.PauseIntent",
"slots": []
},
{
"intent": "AMAZON.ResumeIntent",
"slots": []
},
{
"intent": "AMAZON.HelpIntent",
"slots": []
Expand All @@ -29,6 +37,24 @@
"type": "AMAZON.US_CITY"
}
]
},
{
"intent": "PodcastIntent",
"slots": [
{
"name": "NUMBER",
"type": "AMAZON.NUMBER"
}
]
},
{
"intent": "PodcastSummaryIntent",
"slots": [
{
"name": "NUMBER",
"type": "AMAZON.NUMBER"
}
]
}
]
}
21 changes: 21 additions & 0 deletions functions/artsy/utterances.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,24 @@ ShowsIntent to recommend a show around {CITY}
ShowsIntent current show around {CITY}
ShowsIntent for current show around {CITY}
ShowsIntent to recommend current show around {CITY}
PodcastIntent podcast {NUMBER}
PodcastIntent to play podcast {NUMBER}
PodcastIntent latest podcast {NUMBER}
PodcastIntent to play latest podcast {NUMBER}
PodcastIntent podcast number {NUMBER}
PodcastIntent to play podcast number {NUMBER}
PodcastIntent latest podcast number {NUMBER}
PodcastIntent to play latest podcast number {NUMBER}
PodcastIntent podcast episode {NUMBER}
PodcastIntent to play podcast episode {NUMBER}
PodcastIntent latest podcast episode {NUMBER}
PodcastIntent to play latest podcast episode {NUMBER}
PodcastIntent podcast episode number {NUMBER}
PodcastIntent to play podcast episode number {NUMBER}
PodcastIntent latest podcast episode number {NUMBER}
PodcastIntent to play latest podcast episode number {NUMBER}
PodcastSummaryIntent for the summary of podcast {NUMBER}
PodcastSummaryIntent for the summary of podcast number {NUMBER}
PodcastSummaryIntent for the summary of podcast episode {NUMBER}
PodcastSummaryIntent for the summary of podcast episode number {NUMBER}
PodcastSummaryIntent for the summary of the latest podcast
22 changes: 22 additions & 0 deletions test/functions/artsy/fixtures/PauseIntentRequest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"version": "1.0",
"session": {
"new": false,
"sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef",
"application": {
"applicationId": "amzn1.echo-sdk-ams.app.999999-9999-9999-9999-999999999999"
},
"attributes": {},
"user": {
"userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2"
}
},
"request": {
"type": "IntentRequest",
"requestId": "amzn1.echo-api.request.6919844a-733e-4e89-893a-fdcb77e2ef0d",
"timestamp": "2015-05-13T12:34:56Z",
"intent": {
"name": "AMAZON.PauseIntent"
}
}
}
Loading