From a0815eb934f413ef43560c6942672741ee765626 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Sat, 9 Jan 2016 21:27:23 -0800 Subject: [PATCH] Few big changes Moved all globals to an "App" instance - now each execution of index.handler is entirely contained Removed API cals for getting server machine identifier and player IP since we now cache all of that in the DB Changed all references to "library 1" to a db-stored library URI - for now it just grabs the first tv library and uses that --- index.js | 28 +----- lib/app.js | 57 ++++++++++-- lib/db.js | 35 +++++++ lib/plex.js | 12 ++- lib/plexutils.js | 167 +++++++++++++++++++--------------- lib/statemachine.js | 11 +-- lib/states/authed.js | 30 ++++-- lib/states/not-authed.js | 21 +++-- lib/user.js | 91 ++++++++++++++++-- lib/utils.js | 2 +- test/common.js | 1 + test/helpers.test.js | 2 - test/mockdbdata.json | 108 +++++++++++++++++++++- test/plex-api-stubs.helper.js | 11 +-- test/requests.test.js | 25 +---- 15 files changed, 426 insertions(+), 175 deletions(-) diff --git a/index.js b/index.js index 3c2a12a5..dca994c9 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,8 @@ /** * @module */ - require('dotenv').load(); -var app = require('./lib/app'); -var db = require('./lib/db'); -var stateMachine = require('./lib/statemachine'); -var User = require('./lib/user'); -var Alexa = require('alexa-app'); -var Plex = require('./lib/plex'); +var App = require('./lib/app').App; /** * The main AWS Lambda handler. @@ -24,22 +18,6 @@ exports.handler = function(event, context) { } } - app.skill = new Alexa.app('plex'); - app.plex = new Plex.Plex(); - - db.initializeUserRecord(event.session.user.userId).then(function(dbuser) { - app.user = new User(dbuser); - app.plex.pinAuth.token = app.user.authtoken; - - if(!app.user.authtoken) { - return stateMachine.initApp('not-authed'); - } else { - return stateMachine.initApp('authed'); - } - }).then(function() { - app.skill.lambda()(event, context); - }).catch(function(err) { - console.error(err); - console.error(err.stack); - }); + var app = new App(); + app.execute(event, context); }; diff --git a/lib/app.js b/lib/app.js index 5c39a688..dc61b2f9 100644 --- a/lib/app.js +++ b/lib/app.js @@ -1,28 +1,67 @@ /** - * @module app + * @module App */ +//var Plex = require('./plex').Plex; +var AlexaSkill = require('alexa-app').app; +var User = require('./user').User; + +var db = require('./db'); +var stateMachine = require('./statemachine'); + /** - * @name module:app + * Creates a new App object, which holds all of the various stateful objects necessary on each request + * @constructor App + * @classdesc Holds all of the various stateful objects necessary on each request */ -app = module.exports = { +var App = function() { + var Plex = require('./plex').Plex; + /** @type {module:Plex~Plex} */ - plex: null, + this.plex = new Plex(this); + /** @type {AlexaSkill} */ + this.skill = new AlexaSkill('plex'); - // TODO figure out how to properly jsdoc this so that it is recognized as an alexa-app.app object - /** @type {Object} */ - skill: null, + /** @type {module:User~User} */ + this.user = null; /** * How confident we need to be when trying to figure out which show a user is talkijng about * @const */ - CONFIDICE_CONFIRM_THRESHOLD: 0.4, + this.CONFIDICE_CONFIRM_THRESHOLD = 0.4; /** * The invocation name used for this app. Used in many responses so put here in case it changes. * @const * @type {string} */ - INVOCATION_NAME: "the home theater" + this.INVOCATION_NAME = "the home theater"; +}; + +App.prototype.execute = function(event, callbacks) { + var context = this; + db.initializeUserRecord(event.session.user.userId).then(function(dbuser) { + context.user = new User(context, dbuser); + context.plex.pinAuth.token = context.user.authtoken; + + if(!context.user.authtoken) { + return stateMachine.initSkillState(context, 'not-authed'); + } else { + return stateMachine.initSkillState(context, 'authed'); + } + }).then(function() { + // Pass off the rest of the execution to the alexa-plex module which handles running intents + // TODO: we're doing so much of our own app framework now that it might make sense to just take over the last few things that alexa-app is handling? + // HUGE HACK to make this App object available to the Intent handlers running inside the alexa-app module. Another reason why the above note makes sense + event._plex_app = context; + context.skill.lambda()(event, callbacks); + }).catch(function(err) { + console.error(err); + console.error(err.stack); + }); +}; + +module.exports = { + App: App }; \ No newline at end of file diff --git a/lib/db.js b/lib/db.js index f64b1765..04544f23 100644 --- a/lib/db.js +++ b/lib/db.js @@ -180,8 +180,43 @@ function updateUserPlayer(user, player) { return deferred.promise; } +function updateUserLibraries(user, libraries) { + var dynamodbDoc = new AWS.DynamoDB.DocumentClient(); + var deferred = Q.defer(); + + var userid = user; + if(typeof user === 'object') { + userid = user.dbobject.userid; + } + + var updateParams = { + TableName: TABLE_NAME, + Key: { "userid": userid }, + UpdateExpression: "set libraries = :l", + ExpressionAttributeValues:{ + ":l":libraries + }, + ReturnValues:"ALL_NEW" + }; + + dynamodbDoc.update(updateParams, function(err, data) { + if (err) { + return deferred.reject(err); + } + + if(typeof user === 'object') { + user.dbobject = data.Attributes; + } + + deferred.fulfill(data); + }); + + return deferred.promise; +} + module.exports.initializeUserRecord = initializeUserRecord; module.exports.updatePin = updatePin; module.exports.updateAuthToken = updateAuthToken; module.exports.updateUserServer = updateUserServer; module.exports.updateUserPlayer = updateUserPlayer; +module.exports.updateUserLibraries = updateUserLibraries; diff --git a/lib/plex.js b/lib/plex.js index 40f47b63..c1ea7c71 100644 --- a/lib/plex.js +++ b/lib/plex.js @@ -5,7 +5,6 @@ "use strict"; var PlexAPI = require('plex-api'); var PlexPinAuth = require('plex-api-pinauth'); -var app = require('./app'); require('dotenv').load('../'); @@ -27,9 +26,16 @@ var PLEX_APP_CONFIG = { * @constructor Plex * @classdesc A container for the multiple stateful plex-api objects needed for the app */ -var Plex = function() { +var Plex = function(app) { var context = this; + /** + * The App object for this specific request + * @private + * @member {module:App~App} + */ + this._app = app; + /** * The stateful PlexPinAuth object that provides handy PIN auth functions * @member {PlexPinAuth} @@ -103,6 +109,8 @@ Plex.prototype.initializeWebApi = function() { * @returns {Boolean} true if creation of the API object was successful */ Plex.prototype.initializeServerApi = function() { + var app = this._app; + if(!app.user.server) { return false; } diff --git a/lib/plexutils.js b/lib/plexutils.js index 4623c28b..2e03e3e3 100644 --- a/lib/plexutils.js +++ b/lib/plexutils.js @@ -1,14 +1,46 @@ var Q = require('q'); -var app = require('./app.js'); var utils = require('./utils.js'); var util = require('util'); + +function query(api, options) { + console.time("query: " + options); + return api.query(options).then(function(response) { + console.timeEnd("query: " + options); + return response; + }) +} + +function postQuery(api, options) { + console.time("postQuery: " + options); + return api.postQuery(options).then(function(response) { + console.timeEnd("postQuery: " + options); + return response; + }) +} + +function perform(api, options) { + console.time("perform: " + options); + return api.perform(options).then(function(response) { + console.timeEnd("perform: " + options); + return response; + }) +} + +function find(api, options) { + console.time("find: " + options); + return api.find(options).then(function(response) { + console.timeEnd("find: " + options); + return response; + }) +} + /** * Get the list of online servers * @returns {Promise.array.object} An array of online servers, as returned by the plex API */ -var getServers = function() { - return app.plex.web.query('/api/resources?includeHttps=1').then(function(resources) { +var getServers = function(app) { + return query(app.plex.web, '/api/resources?includeHttps=1').then(function(resources) { return resources.MediaContainer.Device.filter(function(resource){ return (resource.attributes.provides.search(/server/i) != -1 && resource.attributes.presence == '1'); @@ -20,8 +52,8 @@ var getServers = function() { * Get the list of players available to a Plex Media Server for playback * @returns {Promise.array.object} An array of online clients capable of playback, as returned by the plex API */ -var getPlayers = function() { - return app.plex.pms.query('/clients').then(function(resources) { +var getPlayers = function(app) { + return query(app.plex.pms, '/clients').then(function(resources) { return resources._children.filter(function(resource){ return (resource.protocolCapabilities.search(/playback/i) != -1); }); @@ -30,13 +62,31 @@ var getPlayers = function() { /** * Get a list of items that are "On Deck" + * @param {module:App~App} app + * @param {?Object} library - Optional library object to ask for only items in that library * @returns {Object} JSON object of shows/movies/etc on desk */ -var getOnDeck = function() { - return app.plex.pms.query('/library/onDeck'); +var getOnDeck = function(app, library) { + if(library) { + return query(app.plex.pms, library.uri + '/onDeck'); + } else { + return query(app.plex.pms, '/library/onDeck'); + } }; -var startShow = function(options, response) { +/** + * @typedef {Object} PlexAPI.Directory + */ + +/** + * Get a list of available libraries + * @returns {PlexAPI.Directory} JSON object of "Directories" + */ +var getLibrarySections = function(app) { + return query(app.plex.pms, '/library/sections'); +}; + +var startShow = function(app, options, response) { if(!options.spokenShowName) { return Q.reject(new Error('startShow must be provided with a spokenShowName option')); } @@ -54,7 +104,7 @@ var startShow = function(options, response) { var responseSpeech; var matchConfidence; - return getListOfTVShows().then(function(listOfTVShows) { + return getListOfTVShows(app, app.user.TVLibrary).then(function(listOfTVShows) { var bestShowMatch = getShowFromSpokenName(spokenShowName, listOfTVShows._children); var show = bestShowMatch.bestMatch; matchConfidence = bestShowMatch.confidence; @@ -66,7 +116,7 @@ var startShow = function(options, response) { return Q.resolve(); } - return getAllEpisodesOfShow(show).then(function (allEpisodes) { + return getAllEpisodesOfShow(app, show).then(function (allEpisodes) { var episode; var viewOffset = 0; @@ -140,8 +190,8 @@ var startShow = function(options, response) { if (matchConfidence >= app.CONFIDICE_CONFIRM_THRESHOLD) { response.say(responseSpeech); - return playMedia({ - playerName: playerName, + return playMedia(app, { + playerName: playerName, // TODO! this is no longer respected and should be removed. Eventually player selection on each request will be supported mediaKey: episode.key, offset: viewOffset }); @@ -167,78 +217,48 @@ var startShow = function(options, response) { }); }; -var getListOfTVShows = function() { - return app.plex.pms.query('/library/sections/1/all'); +/** + * Gets a list of all top-level items in a given library. For TV shows this will be the shows, not episodes + * @param {App~App} app + * @param {Object} library + */ +var getListOfTVShows = function(app, library) { + return query(app.plex.pms, library.uri + '/all'); }; -var getAllEpisodesOfShow = function(show) { +var getAllEpisodesOfShow = function(app, show) { if(typeof show === 'object') { show = show.ratingKey; } - return app.plex.pms.query('/library/metadata/' + show + '/allLeaves'); + return query(app.plex.pms, '/library/metadata/' + show + '/allLeaves'); }; -var playMedia = function(parameters) { +var playMedia = function(app, parameters) { var mediaKey = parameters.mediaKey; - var playerName = parameters.playerName; var offset = parameters.offset || 0; - // We need the server machineIdentifier for the final playMedia request - return getMachineIdentifier().then(function(serverMachineIdentifier) { - // Get the Client's IP, which can also be provided as an env var to skip an API call - return getClientIP(playerName).then(function(clientIP) { - var keyURI = encodeURIComponent(mediaKey); - // Yes, there is a double-nested URI encode here. Wacky Plex API! - var libraryURI = encodeURIComponent('library://' + app.plex.pms.getIdentifier() + '/item/' + keyURI); - - // To play something on a client, we need to add it to a new "Play Queue" - return app.plex.pms.postQuery('/playQueues?type=video&includechapters=1&uri='+libraryURI+'&shuffle=0&continuous=1&repeat=0').then(function(result) { - var playQueueID = result.playQueueID; - var containerKeyURI=encodeURIComponent('/playQueues/' + playQueueID + '?own=1&window=200'); - - var playMediaURI = '/system/players/'+clientIP+'/playback/playMedia' + - '?key=' + keyURI + - '&offset=' + offset + - '&machineIdentifier=' + serverMachineIdentifier + - //'&address=' + process.env.PMS_HOSTNAME + // Address and port aren't needed. Leaving here in case that changes... - //'&port=' + process.env.PMS_PORT + - '&protocol=http' + - '&containerKey=' + containerKeyURI + - '&commandID=2' + // TODO this is supposed to be an incrementing number on each request - ''; - console.log(playMediaURI); - - return app.plex.pms.perform(playMediaURI); - }); - }); - }); -}; + var serverIdentifier = getServerIdentifier(app); + var playerURI = getPlayerURI(app); + var keyURI = encodeURIComponent(mediaKey); -var getMachineIdentifier = function() { - if (process.env.PMS_IDENTIFIER) { - return Q.resolve(process.env.PMS_IDENTIFIER); - } else { - return app.plex.pms.query('/').then(function (res) { - process.env.PMS_IDENTIFIER = res.machineIdentifier; - return Q.resolve(process.env.PMS_IDENTIFIER); - }); - } -}; + var playMediaURI = ''+playerURI+'/playback/playMedia' + + '?key=' + keyURI + + '&offset=' + offset + + '&machineIdentifier=' + serverIdentifier + + '&protocol=http' + + '&containerKey=' + keyURI + + '&commandID=2' + // TODO this is supposed to be an incrementing number on each request + ''; -var getClientIP = function(clientname) { - if(process.env.PLEXPLAYER_IP) { - return Q.resolve(process.env.PLEXPLAYER_IP); - } else { - return app.plex.pms.find("/clients", {name: clientname}).then(function (clients) { - var clientMatch; + return perform(app.plex.pms, playMediaURI); +}; - if (Array.isArray(clients)) { - clientMatch = clients[0]; - } +var getPlayerURI = function(app) { + return app.user.playerURI; +}; - return Q.resolve(clientMatch.address); - }); - } +var getServerIdentifier = function(app) { + return app.user.serverIdentifier; }; var filterEpisodesByExists = function(episodes) { @@ -292,7 +312,7 @@ var getRandomEpisode = function(episodes, onlyTopRated) { var getFirstUnwatched = function(episodes) { var firstepisode; - for (i = 0; i < episodes.length; i++) { + for (var i = 0; i < episodes.length; i++) { if ('viewCount' in episodes[i]) { continue; } @@ -344,13 +364,12 @@ module.exports = { getListOfTVShows: getListOfTVShows, getAllEpisodesOfShow: getAllEpisodesOfShow, playMedia: playMedia, - getMachineIdentifier: getMachineIdentifier, - getClientIP: getClientIP, filterEpisodesByExists: filterEpisodesByExists, filterEpisodesByBestRated: filterEpisodesByBestRated, findEpisodeWithOffset: findEpisodeWithOffset, getRandomEpisode: getRandomEpisode, getFirstUnwatched: getFirstUnwatched, getShowNamesFromList: getShowNamesFromList, - getShowFromSpokenName: getShowFromSpokenName + getShowFromSpokenName: getShowFromSpokenName, + getLibrarySections: getLibrarySections }; \ No newline at end of file diff --git a/lib/statemachine.js b/lib/statemachine.js index a9777bec..0d80a50a 100644 --- a/lib/statemachine.js +++ b/lib/statemachine.js @@ -1,12 +1,11 @@ var Q = require('q'); -var app = require('./app.js'); var states = { 'authed': require('./states/authed'), 'not-authed': require('./states/not-authed') }; -var initAppState = function(state) { +var initSkillState = function(app, state) { if (!states[state]) { return Q.reject(new RangeError("No state function found: " + state)); } @@ -27,12 +26,12 @@ var initAppState = function(state) { app.skill.launch(states[state]['launch']); } - return runSetup(state); + return runSetup(app, state); }; -function runSetup(state) { +function runSetup(app, state) { if(typeof states[state]['setup'] === 'function') { - return states[state].setup(); + return states[state].setup(app); } else { return Q.resolve(); } @@ -56,4 +55,4 @@ function appError(error, request, response) { response.send(); } -module.exports.initApp = initAppState; \ No newline at end of file +module.exports.initSkillState = initSkillState; \ No newline at end of file diff --git a/lib/states/authed.js b/lib/states/authed.js index ab8467ed..b945ece2 100644 --- a/lib/states/authed.js +++ b/lib/states/authed.js @@ -2,11 +2,10 @@ * @module states/not-authed */ -var app = require('../app'); var utils = require('../utils'); var plexutils = require('../plexutils'); -var setup = function() { +var setup = function(app) { // Ensure that a server and player are on the object with setupDefaults return app.user.setupDefaults(); }; @@ -17,10 +16,12 @@ var launch = function(request,response) { }; var defaultIntent = function(request, response) { + console.warn("Got an intent in the authed state that was not handled!"); response.say("Sorry, I am not sure what to do with that request."); }; var setupIntent = function(request, response) { + var app = request.data._plex_app; app.user.setupDefaults(true).then(function(changed) { if(changed) { response.say("I'm sorry, right now the only configuration possible is to have me reset your server and player selections, which I " + @@ -38,7 +39,8 @@ var setupIntent = function(request, response) { }; var onDeckIntent = function(request, response) { - plexutils.getOnDeck() + var app = request.data._plex_app; + plexutils.getOnDeck(app, app.user.TVLibrary) .then(plexutils.getShowNamesFromList) .then(function(showList) { if(showList.length === 0) { @@ -58,6 +60,7 @@ var onDeckIntent = function(request, response) { }; var startShowIntent = function(request,response) { + var app = request.data._plex_app; var showName = request.slot('showName', null); if(!showName) { @@ -66,7 +69,7 @@ var startShowIntent = function(request,response) { return response.send(); } - plexutils.startShow({ + plexutils.startShow(app, { playerName: app.user.playerName, spokenShowName: showName }, response).then(function() { @@ -79,6 +82,7 @@ var startShowIntent = function(request,response) { }; var startRandomShowIntent = function(request,response) { + var app = request.data._plex_app; var showName = request.slot('showName', null); if(!showName) { @@ -87,7 +91,7 @@ var startRandomShowIntent = function(request,response) { return response.send(); } - plexutils.startShow({ + plexutils.startShow(app, { playerName: app.user.playerName, spokenShowName: showName, forceRandom: true @@ -101,6 +105,8 @@ var startRandomShowIntent = function(request,response) { }; var startSpecificEpisodeIntent = function(request,response) { + var app = request.data._plex_app; + var showName = request.slot('showName', null); var episodeNumber = request.slot('episodeNumber', null); var seasonNumber = request.slot('seasonNumber', null); @@ -111,7 +117,7 @@ var startSpecificEpisodeIntent = function(request,response) { return response.send(); } - plexutils.startShow({ + plexutils.startShow(app, { playerName: app.user.playerName, spokenShowName: showName, episodeNumber: episodeNumber, @@ -126,6 +132,8 @@ var startSpecificEpisodeIntent = function(request,response) { }; var startHighRatedEpisodeIntent = function(request,response) { + var app = request.data._plex_app; + var showName = request.slot('showName', null); if(!showName) { @@ -134,7 +142,7 @@ var startHighRatedEpisodeIntent = function(request,response) { return response.send(); } - plexutils.startShow({ + plexutils.startShow(app, { playerName: app.user.playerName, spokenShowName: showName, forceRandom: true, @@ -149,6 +157,7 @@ var startHighRatedEpisodeIntent = function(request,response) { }; var yesIntent = function(request,response) { + var app = request.data._plex_app; var promptData = request.session('promptData'); if(!promptData) { @@ -157,7 +166,7 @@ var yesIntent = function(request,response) { } if(promptData.yesAction === 'startEpisode') { - plexutils.playMedia({ + plexutils.playMedia(app, { playerName : promptData.playerName, mediaKey: promptData.mediaKey, offset: promptData.mediaOffset || 0 @@ -178,6 +187,7 @@ var yesIntent = function(request,response) { }; var noIntent = function(request,response) { + var app = request.data._plex_app; var promptData = request.session('promptData'); if(!promptData) { @@ -188,7 +198,7 @@ var noIntent = function(request,response) { if(promptData.noAction === 'endSession') { return response.say(promptData.noResponse).send(); } else if(promptData.noAction === 'startEpisode') { - plexutils.playMedia({ + plexutils.playMedia(app, { playerName : promptData.playerName, mediaKey: promptData.noMediaKey, offset: promptData.noMediaOffset || 0 @@ -221,7 +231,7 @@ module.exports = { // TODO currently all point to a stub "not implemented" intent 'SetupIntent': setupIntent, 'ContinueSetupIntent': setupIntent, - 'StartSetupIntent': setupIntent, + 'BeginSetupIntent': setupIntent, 'AuthorizeMeIntent': setupIntent, 'ChangeSettingsIntent': setupIntent, diff --git a/lib/states/not-authed.js b/lib/states/not-authed.js index 0aaada66..928db07a 100644 --- a/lib/states/not-authed.js +++ b/lib/states/not-authed.js @@ -2,7 +2,6 @@ * @module states/not-authed */ -var app = require('../app'); var db = require('../db'); var plexutils = require('../plexutils'); var Q = require('q'); @@ -28,6 +27,10 @@ var setup = function() { }; var introIntent = function(request, response) { + var app = request.data._plex_app; + + console.dir(app); + if (app.user.pin) { // If they already have a PIN, we should just push them to the next step, otherwise it can be confusing. setupIntent(request, response); @@ -40,7 +43,7 @@ var introIntent = function(request, response) { return false; }; -var needsNewPin = function(response) { +var needsNewPin = function(app, response) { app.plex.pinAuth.getNewPin().then(function(pin) { db.updatePin(app.user, pin).then(function() { var spokenPin = generateSpokenPin(pin); @@ -59,7 +62,7 @@ var needsNewPin = function(response) { }); }; -var pinExpired = function(response) { +var pinExpired = function(app, response) { app.plex.pinAuth.getNewPin().then(function(pin) { db.updatePin(app.user, pin).then(function() { var spokenPin = generateSpokenPin(pin); @@ -76,7 +79,7 @@ var pinExpired = function(response) { }); }; -var promptPinAgain = function(response) { +var promptPinAgain = function(app, response) { var pin = app.user.pin; var spokenPin = generateSpokenPin(pin); @@ -89,6 +92,8 @@ var promptPinAgain = function(response) { }; var setupIntent = function(request, response) { + var app = request.data._plex_app; + if (app.user.pin) { app.plex.pinAuth.checkPinForAuth(app.user.pin, function(err, result) { if(err) { @@ -108,13 +113,13 @@ var setupIntent = function(request, response) { app.skill.error(err, request, response); }); } else if (result === 'waiting') { - promptPinAgain(response); + promptPinAgain(app, response); } else if (result === 'invalid') { - pinExpired(response); + pinExpired(app, response); } }) } else { - needsNewPin(response); + needsNewPin(app, response); } return false; @@ -125,7 +130,7 @@ module.exports = { '_default': introIntent, 'SetupIntent': setupIntent, 'ContinueSetupIntent': setupIntent, - 'StartSetupIntent': setupIntent, + 'BeginSetupIntent': setupIntent, 'AuthorizeMeIntent': setupIntent, 'ChangeSettingsIntent': setupIntent, diff --git a/lib/user.js b/lib/user.js index f0b01da7..1c08932e 100644 --- a/lib/user.js +++ b/lib/user.js @@ -1,12 +1,26 @@ -var app = require('./app'); +/** + * @module User + */ + var plexutils = require('./plexutils'); var Q = require('q'); var url = require('url'); var db = require('./db'); -var User = function(dbobject) { +/** + * @constructor User + * @param {Object} dbobject - an object representing exactly what is in the database for this user + */ +var User = function(app, dbobject) { this.dbobject = dbobject; + /** + * The App object for this specific request + * @private + * @member {module:App~App} + */ + this._app = app; + var context = this; Object.defineProperty(this, 'authtoken', { get: function() { @@ -17,6 +31,31 @@ var User = function(dbobject) { } }); + Object.defineProperty(this, 'libraries', { + get: function() { + return context.dbobject.libraries; + } + }); + + Object.defineProperty(this, 'TVLibrary', { + get: function() { + if (!context.dbobject.libraries) { + throw new Error("Trying to get TVLirary with no libraries on the user record"); + } + + var libraries = context.dbobject.libraries.filter(function(library) { + return library.type == "show"; + }); + + // For now we're going to sort by key, which more or less gives us a sort by creation date. + libraries.sort(function(a, b) { + return Number(a.key) - Number(b.key); + }); + + return libraries[0]; + } + }); + Object.defineProperty(this, 'server', { get: function() { return context.dbobject.server; @@ -78,6 +117,12 @@ var User = function(dbobject) { } }); + Object.defineProperty(this, 'serverIdentifier', { + get: function() { + return context.dbobject.server.attributes.clientIdentifier; + } + }); + Object.defineProperty(this, 'player', { get: function() { return context.dbobject.player; @@ -90,6 +135,12 @@ var User = function(dbobject) { } }); + Object.defineProperty(this, 'playerURI', { + get: function() { + return context.dbobject.player.uri; + } + }); + Object.defineProperty(this, 'pin', { get: function() { return context.dbobject.pin; @@ -105,7 +156,7 @@ var User = function(dbobject) { User.prototype.setDefaultServer = function(forceReset) { var context = this; if (!this.server || forceReset) { - return plexutils.getServers() + return plexutils.getServers(context._app) .then(function(servers) { return db.updateUserServer(context, servers[0]) .then(function() { @@ -117,6 +168,27 @@ User.prototype.setDefaultServer = function(forceReset) { return Q.resolve(false); }; +/** + * Retrieves the list of "Directories" on the server and caches them in the database + * so we can save ourselves an API call on every future request. + * @param {boolean} forceReset - forces the update even if we already had this in the database + * @returns {Promise.boolean} Resolves with bool representing whether or not an update was made + */ +User.prototype.cacheServerLibraries = function(forceReset) { + var context = this; + if (!this.libraries || forceReset) { + return plexutils.getLibrarySections(context._app) + .then(function(directories) { + return db.updateUserLibraries(context, directories._children) + .then(function() { + return true; + }) + }) + } + + return Q.resolve(false); +}; + /** * Sets the user's player to the first one in their account if they didn't have a player already set * @param {boolean} forceReset - forces the player to be reset even if they already had one @@ -125,7 +197,7 @@ User.prototype.setDefaultServer = function(forceReset) { User.prototype.setDefaultPlayer = function(forceReset) { var context = this; if (!this.player || forceReset) { - return plexutils.getPlayers() + return plexutils.getPlayers(context._app) .then(function(players) { return db.updateUserPlayer(context, players[0]) .then(function() { @@ -140,15 +212,20 @@ User.prototype.setDefaultPlayer = function(forceReset) { /** * Sets the user's server and player if none are already set * @param {boolean} [forceReset=false] - forces a refresh from the account - * @returns {Promise.boolean} Resolves when all API and DB calls are done + * @returns {Promise.boolean} Resolves when all API and DB calls are done with whether or not any updates were made */ User.prototype.setupDefaults = function(forceReset) { var context = this; return this.setDefaultServer(forceReset) .then(function(serverUpdated) { // We want to force update the player if the server changed - return context.setDefaultPlayer(forceReset || serverUpdated); + return context.setDefaultPlayer(forceReset || serverUpdated) + .then(function() { + return context.cacheServerLibraries(forceReset || serverUpdated); + }); }); }; -module.exports = User; \ No newline at end of file +module.exports = { + User: User +}; \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js index ff363b65..08d6fca7 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,7 +4,7 @@ module.exports.findBestMatch = function(phrase, items, mapfunc) { var MINIMUM = 0.2; var bestmatch = {index: -1, score: -1}; - for(i=0; i