Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/resume playback for alexa #216

Merged
merged 14 commits into from
Apr 14, 2018
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ const {debug} = require('../../../utils/logger')('ia:actions:middlewares:song-da
*
* @param mediaResponseOnly {boolean} we should return media response only
*/
module.exports = ({mediaResponseOnly = false} = {}) => (context) => {
module.exports = ({mediaResponseOnly = false, offset = 0} = {}) => (context) => {
debug('start');
const {app} = context;
dialog.playSong(app, Object.assign(
{}, context.slots, {
mediaResponseOnly,
offset,
speech: context.speech.join(' '),
description: context.description,
}
Expand Down
7 changes: 5 additions & 2 deletions functions/src/actions/playback-stopped.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
const {debug} = require('../utils/logger')('ia:actions:playback-stopped');

const playback = require('../state/playback');

function handler (app) {
// TODO: log
debug('token', app.params.getByName('token'));
const offset = app.getOffset();
debug('offset', offset);
playback.setOffset(app, offset);
}

/**
Expand Down
51 changes: 51 additions & 0 deletions functions/src/actions/resume-intent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const dialog = require('../dialog');
const dialogState = require('../state/dialog');
const playlist = require('../state/playlist');
const playback = require('../state/playback');
const query = require('../state/query');
const strings = require('../strings');
const {debug} = require('../utils/logger')('ia:actions:resume-intent');

const feederFromPlaylist = require('./high-order-handlers/middlewares/feeder-from-playlist');
const fulfilResolvers = require('./high-order-handlers/middlewares/fulfil-resolvers');
const playSong = require('./high-order-handlers/middlewares/play-song');
const parepareSongData = require('./high-order-handlers/middlewares/song-data');
const renderSpeech = require('./high-order-handlers/middlewares/render-speech');

/**
* handle ALEXA.ResumeIntent
* TODO: but maybe it would be useful for Actions of Google
* in case of new session for returned user
*
* @param app
*/
function handler (app) {
return feederFromPlaylist()({
app,
playlist,
query,
slots: {platform: app.platform}
})
.then(ctx => {
return parepareSongData()(ctx)
.then(fulfilResolvers())
.then(renderSpeech())
.then(playSong({offset: playback.getOffset(app)}))
.catch(context => {
debug('It could be an error:', context);
return dialog.ask(app, dialog.merge(
strings.intents.resume.fail,
dialogState.getReprompt(app)
));
});
}, () => {
dialog.ask(app, dialog.merge(
strings.intents.resume.empty,
dialogState.getReprompt(app)
));
});
}

module.exports = {
handler,
};
9 changes: 9 additions & 0 deletions functions/src/actions/welcome.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const _ = require('lodash');

const dialog = require('../dialog');
const playlist = require('../state/playlist');
const query = require('../state/query');
const welcomeStrings = require('../strings').intents.welcome;

Expand All @@ -19,7 +20,15 @@ function handler (app) {
speech = _.sample(welcomeStrings.acknowledges) + ' ' + welcomeStrings.speech;
}

// TODO: it would be great to implement some sophisticated
// behaviour but for the moment we just clean state of the user's session
// when we return to welcome action

// so "Resume" intent won't work after that
// we clean all that information
playlist.create(app, []);
query.resetSlots(app);

dialog.ask(app, Object.assign({}, welcomeStrings, {speech, reprompt}));
}

Expand Down
1 change: 1 addition & 0 deletions functions/src/dialog/audio.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ function playSong (app, options) {
description,
contentURL: options.audioURL,
imageURL: options.imageURL || config.media.DEFAULT_SONG_IMAGE,
offset: options.offset,

// if previous track was define we try to stitch to it
// for the moment it only works for Alexa
Expand Down
1 change: 1 addition & 0 deletions functions/src/dialog/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ const audio = require('./audio');
module.exports = {
ask: require('./ask'),
playSong: audio.playSong,
merge: require('./merge'),
tell: require('./tell'),
};
27 changes: 27 additions & 0 deletions functions/src/dialog/merge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const _ = require('lodash');

/**
* Merge speech instances
*
* @param args
*/
module.exports = (...args) => args.reduce(
(acc, item) => {
if ('speech' in item) {
acc.speech = []
.concat(acc.speech, item.speech)
.filter(i => i);
}

if ('suggestions' in item) {
acc.suggestions = _.union(acc.suggestions, item.suggestions);
}

if ('reprompt' in item) {
acc.reprompt = item.reprompt;
}

return acc;
},
{}
);
9 changes: 9 additions & 0 deletions functions/src/platform/alexa/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ class App {
isFirstTry () {
return true;
}

/**
* Current track offset
*
* @returns {Number}
*/
getOffset () {
return this.ctx.event.request.offsetInMilliseconds;
}
}

module.exports = {
Expand Down
3 changes: 2 additions & 1 deletion functions/src/platform/alexa/response/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ module.exports = (alexa) =>
m.contentURL,
// expectedPreviousToken
previousToken,
0
// offsetInMilliseconds
m.offset
);
});
} else {
Expand Down
12 changes: 12 additions & 0 deletions functions/src/platform/assistant/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ class App {
isFirstTry () {
return this.ctx.getLastSeen();
}

/**
* Current track offset
*
* for the moment Action of Google doesn't support offset
* so it's always zero
*
* @returns {Number}
*/
getOffset () {
return 0;
}
}

module.exports = {
Expand Down
21 changes: 17 additions & 4 deletions functions/src/state/dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function savePhrase (app, phrase) {
}

function getLastSpeech (app) {
return _.at(getData(app), 'lastPhrase.speech')[0];
return _.get(getData(app), 'lastPhrase.speech');
}

/**
Expand All @@ -25,7 +25,7 @@ function getLastSpeech (app) {
* @returns {undefined|string}
*/
function getLastPhrase (app) {
return _.at(getData(app), 'lastPhrase')[0];
return _.get(getData(app), 'lastPhrase');
}

/**
Expand All @@ -35,7 +35,7 @@ function getLastPhrase (app) {
* @returns {undefined|string}
*/
function getLastReprompt (app) {
return _.at(getData(app), 'lastPhrase.reprompt')[0];
return _.get(getData(app), 'lastPhrase.reprompt');
}

/**
Expand All @@ -45,13 +45,26 @@ function getLastReprompt (app) {
* @returns {undefined|string}
*/
function getLastSuggestions (app) {
return _.at(getData(app), 'lastPhrase.suggestions')[0];
return _.get(getData(app), 'lastPhrase.suggestions');
}

/**
* get reprompt for speech
*
* @param app
* @returns {{reprompt: (undefined|string), speech: Array, suggestions: (undefined|string)}}
*/
const getReprompt = (app) => ({
reprompt: getLastReprompt(app),
speech: getLastReprompt(app),
suggestions: getLastSuggestions(app),
});

module.exports = {
getLastPhrase,
getLastReprompt,
getLastSpeech,
getLastSuggestions,
getReprompt,
savePhrase,
};
2 changes: 1 addition & 1 deletion functions/src/state/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ module.exports = {
*/
setData: (app, value) => {
debug(`set user's state ${name} to ${JSON.stringify(value)}`);
if (typeof app === 'string') {
if (typeof app === 'string' || !app) {
throw new Error(`Argument 'app' should be DialogflowApp object but we get ${app}`);
}

Expand Down
21 changes: 21 additions & 0 deletions functions/src/state/playback.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,28 @@ const setMuteSpeechBeforePlayback = (app, muteSpeech) =>
muteSpeech,
}));

/**
* Get current played track offest
*
* @param app
*/
const getOffset = (app) => getData(app).offset || 0;

/**
* Set current played track offset
*
* @param app
* @param offset
*/
const setOffset = (app, offset) =>
setData(app, Object.assign({}, getData(app), {
offset,
}));

module.exports = {
isMuteSpeechBeforePlayback,
setMuteSpeechBeforePlayback,

getOffset,
setOffset,
};
10 changes: 10 additions & 0 deletions functions/src/strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,16 @@ module.exports = {
speech: "I'm sorry I'm having trouble here. Maybe we should try this again later.",
}],

resume: {
fail: {
speech: 'Fail to resume.',
},

empty: {
speech: 'Nothing to resume.',
},
},

titleOption: {
false: {
speech: `Ok, muting song titles.`,
Expand Down
6 changes: 6 additions & 0 deletions functions/tests/_utils/mocking/dialog/ask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const sinon = require('sinon');
const dialog = require('../../../../src/dialog');

module.exports = () => Object.assign({}, dialog, {
ask: sinon.spy(),
});
5 changes: 4 additions & 1 deletion functions/tests/_utils/mocking/platforms/app.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
const sinon = require('sinon');

module.exports = ({getByName = {}, getData = {}}) => ({
module.exports = ({getByName = {}, getData = {}, offset = 0} = {}) => ({
getOffset: sinon.stub().returns(offset),

params: {getByName: sinon.stub().callsFake(name => getByName[name])},

persist: {
getData: sinon.stub().callsFake(name => getData[name]),
setData: sinon.spy(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ describe('actions', () => {
const slots = {
id: '123456',
};
return middleware()({app, description, speech, slots})
return middleware({offset: 1234})({app, description, speech, slots})
.then(context => {
expect(dialog.playSong).to.have.been.called;
expect(dialog.playSong.args[0][0]).to.be.equal(app);
expect(dialog.playSong.args[0][1]).to.be.deep.equal({
description,
id: '123456',
mediaResponseOnly: false,
offset: 1234,
speech: speech[0],
});
});
Expand Down
22 changes: 22 additions & 0 deletions functions/tests/actions/playback-stopped.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const {expect} = require('chai');

const mockApp = require('../_utils/mocking/platforms/app');
const playbackStopped = require('../../src/actions/playback-stopped');

describe('actions', () => {
describe('playback stopped', () => {
let app;

beforeEach(() => {
app = mockApp({
offset: 12345,
});
});

it('should store offset', () => {
playbackStopped.handler(app);
expect(app.persist.setData).to.have.been.called;
expect(app.persist.setData.args[0][1]).to.have.property('offset', 12345);
});
});
});
Loading