Skip to content

Commit

Permalink
Merge pull request #1 from chrisdevwords/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
chrisdevwords committed Aug 14, 2017
2 parents c579bb3 + 355dec7 commit 5f434fe
Show file tree
Hide file tree
Showing 14 changed files with 742 additions and 23 deletions.
72 changes: 65 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,72 @@
const { response } = require('./util/lambda')
const { parseFormString } = require('./util/parse');
const slack = require('./slack');
const {
convertLinkToUri,
extractFromUri
} = require('./spotify');
const radio = require('./spotify/radio');
const track = require('./spotify/track');


function handler(event, context, callback) {

const { body } = event;
const { SPOTIFY_CLIENT_ID, SPOTIFY_SECRET } = process.env;
const { body = '' } = event;
const {
SPOTIFY_USER_ACCESS_TOKEN,
SPOTIFY_LOCAL_URL,
SLACK_TOKEN,
SPOTIFY_RADIO_PLAYLIST
} = process.env;

const {
text,
token,
response_url
} = parseFormString(body);

if (token !== SLACK_TOKEN) {
callback(null,
slack.slackResp(
slack.INVALID_TOKEN,
401,
slack.TYPE_PRIVATE
)
);
} else {

const trackUri = convertLinkToUri(text);
track
.getTrackInfo(
extractFromUri(trackUri, 'track'),
SPOTIFY_USER_ACCESS_TOKEN
)
.catch((error) => {
callback(null,
slack.slackResp(error.message)
);
})
.then((trackInfo) => {
callback(null,
slack.slackResp(radio.SLACK_PENDING_MESSAGE(trackInfo))
);
radio
.playBasedOnTrack(
SPOTIFY_RADIO_PLAYLIST,
trackInfo,
SPOTIFY_USER_ACCESS_TOKEN,
SPOTIFY_LOCAL_URL
)
.then((msg) => {
console.log('notify', response_url, msg);

slack.notify(response_url, msg);
})
.catch(({ message }) => {
slack.notify(response_url, `Error creating playlist: ${message}`)
});
});

callback(null, response({
message: 'It works!',
body: body
}));
}
}

module.exports = {
Expand Down
37 changes: 37 additions & 0 deletions src/slack/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const request = require('request-promise-native');
const { response } = require('../util/lambda')

// response types
const TYPE_PRIVATE = 'ephemeral';
const TYPE_PUBLIC = 'in_channel';

// error messages
const INVALID_TOKEN = 'Slack token is invalid.';

function slackResp(text, code = 200, type = TYPE_PUBLIC) {
return response({
// eslint-disable-next-line camelcase
response_type: type,
text
}, code);
}

module.exports = {
TYPE_PUBLIC,
TYPE_PRIVATE,
INVALID_TOKEN,
slackResp,
notify(uri, text) {
return request
.post({
uri,
headers: {
'content-type': 'application/json'
},
body: {
text
},
json: true
})
}
};
2 changes: 0 additions & 2 deletions src/spotify/auth.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
const request = require('request-promise-native');
const { API_BASE } = require('../spotify');


const TOKEN_ERROR = 'Error getting Spotify Token';
const TOKEN_ENDPOINT = 'https://accounts.spotify.com/api/token';
Expand Down
16 changes: 14 additions & 2 deletions src/spotify/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
const API_BASE = 'https://api.spotify.com/';
const API_BASE = 'https://api.spotify.com/v1';
const ERROR_EXPIRED_TOKEN = 'Spotify User Access token ' +
'is expired or invalid. ' +
'Please check the Spotify host machine.';

const ERROR_INVALID_TRACK_URI = 'Please provide a uri ' +
'for a valid Spotify track.';

module.exports = {
API_BASE,

ERROR_EXPIRED_TOKEN,
ERROR_INVALID_TRACK_URI,
convertLinkToUri(link) {
return link
.replace('https://open.spotify.com', 'spotify')
.split('/').join(':');
},
extractFromUri(uri, property) {
const arr = uri.split(':');
const propIndex = arr.indexOf(property);
Expand Down
53 changes: 53 additions & 0 deletions src/spotify/playlist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const request = require('request-promise-native');
const {
API_BASE,
extractFromUri
} = require('../spotify');

const PLAYLIST_ENDPOINT = (user, id) =>
`${API_BASE}/users/${user}/playlists/${id}`;

const PLAYLIST_TRACKS_ENDPOINT = (user, id) =>
`${PLAYLIST_ENDPOINT(user, id)}/tracks`;


module.exports = {

renamePlaylist(playlist, name, accessToken) {

const userId = extractFromUri(playlist, 'user');
const playlistId = extractFromUri(playlist, 'playlist');

return request
.put({
json: true,
uri: PLAYLIST_ENDPOINT(userId, playlistId),
headers: {
Authorization: `Bearer ${accessToken}`,
'content-type': 'application/json'
},
body: {
name
}
});
},

populatePlaylist(playlist, uris, accessToken) {

const userId = extractFromUri(playlist, 'user');
const playlistId = extractFromUri(playlist, 'playlist');

return request
.put({
json: true,
uri: PLAYLIST_TRACKS_ENDPOINT(userId, playlistId),
headers: {
Authorization: `Bearer ${accessToken}`,
'content-type': 'application/json'
},
body: {
uris
}
});
}
};
104 changes: 104 additions & 0 deletions src/spotify/radio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
const request = require('request-promise-native');
const {
API_BASE
} = require('../spotify');
const playlist = require('../spotify/playlist');
const track = require('../spotify/track');


// Response templates
const PLAYLIST_NAME = basedOn =>
`Radio: based on ${basedOn}`;
const TRACK_NAME = (artist, songName) =>
`"${songName}" by ${artist}`;

const RECCOMENDED_TRACKS_ENDPOINT = `${API_BASE}/recommendations`;
const SLACK_SUCCESS_MESSAGE = playlistName =>
`Playlist changed to ${playlistName}`;

const SLACK_PENDING_MESSAGE = ({ name, artist }) =>
`Finding tracks based on: ${TRACK_NAME(artist, name)}...`;

module.exports = {

PLAYLIST_NAME,
TRACK_NAME,
SLACK_SUCCESS_MESSAGE,
SLACK_PENDING_MESSAGE,

getRecommendationsFromTrack(trackId, trackInfo, accessToken) {

const { energy, valence, popularity } = trackInfo;

return request
.get({
uri: RECCOMENDED_TRACKS_ENDPOINT,
headers: {
Authorization: `Bearer ${accessToken}`
},
json: true,
qs: {
limit: 100,
seed_tracks: trackId,
seed_artists: trackInfo.artistIds.join(','),
max_popularity: popularity,
target_energy: energy,
target_valence: valence,
market:'US'
}
})
.then(({ tracks }) => tracks.map(({ uri }) => uri));
},

createStation(playlistUri, trackInfo, accessToken) {

const trackId = trackInfo.id;
const playlistName = PLAYLIST_NAME(
TRACK_NAME(trackInfo.artist, trackInfo.name)
);

return track
.getTrackFeatures(trackId, accessToken)
.then(features =>
this.getRecommendationsFromTrack(
trackId,
Object.assign(features, trackInfo),
accessToken
)
)
.then(uris =>
playlist.populatePlaylist(playlistUri, uris, accessToken)
)
.then(() =>
playlist.renamePlaylist(playlistUri, playlistName, accessToken)
)
.then(() => playlistName)
.catch((err) => {
if (err.error) {
const error = Error(err.error.error.message);
error.statusCode = err.statusCode;
throw error;
}
return Promise.reject(err)
});
},

playBasedOnTrack(playlistUri, trackUri, accessToken, spotifyLocalUrl) {

return this
.createStation(playlistUri, trackUri, accessToken)
.then(() =>
request
.post({
uri: `${spotifyLocalUrl}/api/spotify/playlist`,
body: {
playlist: playlistUri
},
json: true
})
)
.then(resp =>
SLACK_SUCCESS_MESSAGE(resp.playlist.title)
);
}
};
73 changes: 73 additions & 0 deletions src/spotify/track.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const request = require('request-promise-native');
const {
API_BASE,
ERROR_EXPIRED_TOKEN,
ERROR_INVALID_TRACK_URI,
extractFromUri
} = require('../spotify');

// Endpoint templates
const TRACK_ENDPOINT = id =>
`${API_BASE}/tracks/${id}`;

const TRACK_FEATURES_ENDPOINT = trackId =>
`${API_BASE}/audio-features/${trackId}`;

module.exports = {

handleStatusCodeError(err) {
if (err.error) {
let message;
switch (err.statusCode) {
case 404:
case 400:
message = ERROR_INVALID_TRACK_URI;
break;
case 401:
message = ERROR_EXPIRED_TOKEN;
break;
default:
message = err.error.error.message;
}
const error = Error(message);
error.statusCode = err.statusCode;
throw error;
}
return Promise.reject(err)
},

getTrackInfo(trackId, accessToken) {
return request
.get({
uri: TRACK_ENDPOINT(trackId),
headers: {
Authorization: `Bearer ${accessToken}`
},
json: true
})
.then(({ artists, name, popularity }) => ({
name,
artist: artists
.map(a => a.name)
.join(', '),
artistIds: artists
.map(a => extractFromUri(a.uri, 'artist')),
popularity,
id: trackId
}))
.catch(this.handleStatusCodeError);

},

getTrackFeatures(trackId, accessToken) {
return request
.get({
uri: TRACK_FEATURES_ENDPOINT(trackId),
headers: {
Authorization: `Bearer ${accessToken}`
},
json: true
})
.catch(this.handleStatusCodeError);
},
};
5 changes: 5 additions & 0 deletions test/.test-env
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
SLACK_TOKEN=foo_bar_baz
EXPIRED_SPOTIFY_ACCESS_TOKEN=BQANyQM3QT080jey_TnbnVSvF1Xr9lKrEc31YkiOlENYwKDuv-3261TtzAWoB0v80IXIPuscAOz7lfTXyfr6WHqCMbAa4DUhRJicDzW1kZ7UV6ofNGWXwoB8raB8ut7q9STN0yIrkvlLvgEAGd6Wsu6Ibfo1olJUDJ0Q2VCAIaNrVtG852AhMp_Wpjzs-K74CYpxO2AtYYNcXo3s0WA7gao84eh1qTnqhprnSbGIjLShqVsmZTo
SPOTIFY_LOCAL_URL=http://localhost:5000
SPOTIFY_RADIO_PLAYLIST=spotify:user:awpoops:playlist:5PP1I2m0uxEBb3VKLhI7bP
SPOTIFY_USER_ACCESS_TOKEN=BQDeNBJSIWXe3Ezx3IYKf95NJIWeA82cIOFgnDgcc0IwbblLBpi3qEAXz-0R59yAXZ4s4SYz1phS6fWHSWVkDxo08KfSZCUpA-0xEM9FUom7QII2_DL3Ti293Zrhiz1jL4fC-dGLiRxtr7NjImZntCUcss4fBxgrVjuhwCPP4T3AMOljBkE4vY5VNyWq_Pf4aRF1JshyfNg6KLJiL-xClp_AUMglH60IgsjerXWYFq3pt-b8H_U
Loading

0 comments on commit 5f434fe

Please sign in to comment.