diff --git a/.browserslistrc b/.browserslistrc index 214388fe..6d96c07b 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,3 +1,4 @@ > 1% last 2 versions not dead +not ie <= 8 \ No newline at end of file diff --git a/src/main.js b/src/main.js index 24af4897..f7653adf 100644 --- a/src/main.js +++ b/src/main.js @@ -32,7 +32,7 @@ window.EventBus = new Vue(); window.EventBus.$on('command', (data) => { if (router.app.route.fullPath.indexOf('/player') === -1) { if (data.command === '/player/timeline/poll') { - return data.callback({ + data.callback({ key: null, ratingKey: null, time: 0, @@ -41,7 +41,7 @@ window.EventBus.$on('command', (data) => { duration: 0, state: 'stopped', }); - } if (data.command === '/player/playback/playMedia') { + } else if (data.command === '/player/playback/playMedia') { router.push({ path: '/player', query: { @@ -51,7 +51,8 @@ window.EventBus.$on('command', (data) => { offset: data.params.offset, }, }); - return data.callback(true); + + data.callback(true); } } }); diff --git a/src/store/modules/plex/actions.js b/src/store/modules/plex/actions.js index 17a15ce7..ec63fd74 100644 --- a/src/store/modules/plex/actions.js +++ b/src/store/modules/plex/actions.js @@ -1,16 +1,16 @@ import axios from 'axios'; -import parser from 'fast-xml-parser'; +import xmlutils from '@/utils/xmlutils'; + +import plexauth from './helpers/PlexAuth'; +import PlexServer from './helpers/PlexServer'; +import PlexClient from './helpers/PlexClient'; -const PlexAuthMaker = require('./helpers/PlexAuth.js'); const PlexConnection = require('./helpers/PlexConnection.js'); -const PlexServer = require('./helpers/PlexServer.js'); -const PlexClient = require('./helpers/PlexClient.js'); -const PlexAuth = new PlexAuthMaker(); export default { PLEX_LOGIN_TOKEN: async ({ commit, dispatch, rootGetters }, token) => { - const config = PlexAuth.getRequestConfig(token, 5000); + const config = plexauth.getRequestConfig(token, 5000); config.headers['X-Plex-Client-Identifier'] = rootGetters['settings/GET_CLIENTIDENTIFIER']; try { @@ -40,20 +40,18 @@ export default { } try { const { data } = await axios.get('https://plex.tv/api/resources?includeHttps=1', { - ...PlexAuth.getRequestConfig(state.user.authToken, 5000), - transformResponse: parser.parse, + ...plexauth.getRequestConfig(state.user.authToken, 5000), + transformResponse: xmlutils.parseXML, }); - console.log(data); - data.MediaContainer.Device.forEach(({ $: device, Connection: connections }) => { + data.MediaContainer[0].Device.forEach((device) => { // Create a temporary array of object:PlexConnection // Exclude local IPs starting with 169.254 - const tempConnectionsArray = connections - .filter(({ $: connection }) => !connection.address.startsWith('169.254')) - .flatMap(({ $: connection }) => { + const tempConnectionsArray = device.Connection + .filter((connection) => !connection.address.startsWith('169.254')) + .flatMap((connection) => { const tempConnection = new PlexConnection(); Object.assign(tempConnection, connection); - if (connection.local === '1' && connection.uri.indexOf('plex') > -1) { const rawConnection = new PlexConnection(); Object.assign(rawConnection, connection); @@ -66,6 +64,10 @@ export default { return [tempConnection]; }); + tempConnectionsArray.sort( + (con1, con2) => parseInt(con1.port, 10) - parseInt(con2.port, 10), + ); + // If device is a player if (device.provides.indexOf('player') !== -1) { // If device is not Plex Web @@ -189,10 +191,12 @@ export default { throw new Error('Sign in before getting devices'); } - const { data } = await axios.get('https://plex.tv/pms/servers.xml', - PlexAuth.getRequestConfig(token, 5000)); - const result = await parser.parse(data); - state.user.servers = result.MediaContainer.Server; + const { data } = await axios.get('https://plex.tv/pms/servers.xml', { + ...plexauth.getRequestConfig(token, 5000), + transformResponse: xmlutils.parseXML, + }); + + state.user.servers = data.MediaContainer.Server; }, PLEX_CHECK_AUTH: async ({ state, dispatch, getters }, authToken) => { diff --git a/src/store/modules/plex/helpers/PlexAuth.js b/src/store/modules/plex/helpers/PlexAuth.js index 0ad914bf..bdb0a665 100644 --- a/src/store/modules/plex/helpers/PlexAuth.js +++ b/src/store/modules/plex/helpers/PlexAuth.js @@ -6,8 +6,8 @@ * @param method * @returns {{url: *, time: boolean, headers: {X-Plex-Client-Identifier: string, Accept: string, X-Plex-Token: *}, timeout: *, method: *}} */ -module.exports = function PlexAuth() { - this.getApiOptions = function (url, accessToken, timeout, method) { +export default { + getApiOptions(url, accessToken, timeout, method) { return { url, time: true, @@ -19,9 +19,9 @@ module.exports = function PlexAuth() { timeout, method, }; - }; + }, - this.getRequestConfig = function (accessToken, timeout) { + getRequestConfig(accessToken, timeout) { return { headers: { 'X-Plex-Client-Identifier': 'SyncLounge', @@ -30,7 +30,7 @@ module.exports = function PlexAuth() { }, timeout, }; - }; + }, /** * @@ -40,7 +40,7 @@ module.exports = function PlexAuth() { * @param timeout * @returns {{url: *, time: boolean, headers: {X-Plex-Device-Name: string, X-Plex-Client-Identifier: string, X-Plex-Provides: string, X-Plex-Target-Client-Identifier: *}, timeout: *, method: string}} */ - this.getClientApiOptions = function (clientIdentifier, timeout, token) { + getClientApiOptions(clientIdentifier, timeout, token) { let sBrowser; const sUsrAg = navigator.userAgent; if (sUsrAg.indexOf('Chrome') > -1) { @@ -74,5 +74,5 @@ module.exports = function PlexAuth() { }, timeout, }; - }; + }, }; diff --git a/src/store/modules/plex/helpers/PlexClient.js b/src/store/modules/plex/helpers/PlexClient.js index 9682baf9..5dbc48a5 100644 --- a/src/store/modules/plex/helpers/PlexClient.js +++ b/src/store/modules/plex/helpers/PlexClient.js @@ -1,10 +1,9 @@ import axios from 'axios'; import parser from 'fast-xml-parser'; +import plexauth from './PlexAuth'; const EventEmitter = require('events'); -const _PlexAuth = require('./PlexAuth.js'); -const PlexAuth = new _PlexAuth(); const stringSimilarity = require('string-similarity'); class PlexClient { @@ -48,18 +47,18 @@ class PlexClient { this.commit = null; this.dispatch = null; - let previousTimeline = {}; - const differenceCache = []; + this.previousTimeline = {}; + this.differenceCache = []; this.uuid = this.generateGuid(); } - setValue (key, value) { + setValue(key, value) { this[key] = value; this.commit('PLEX_CLIENT_SET_VALUE', [this, key, value]); - }; + } - generateGuid () { + generateGuid() { function s4() { return Math.floor((1 + Math.random()) * 0x10000) .toString(16) @@ -67,9 +66,9 @@ class PlexClient { } return `${s4() + s4()}-${s4()}`; - }; + } - async hitApi (command, params, connection, needResponse, dontSub) { + async hitApi(command, params, connection, needResponse, dontSub) { if (this.clientIdentifier === 'PTPLAYER9PLUS10') { return new Promise(async (resolve, reject) => { // We are using the SyncLounge Player @@ -105,15 +104,15 @@ class PlexClient { } const _url = `${connection.uri + command}?${query}`; this.setValue('commandId', this.commandId + 1); - const options = PlexAuth.getClientApiOptions(this.clientIdentifier, 5000, this.accessToken); + const options = plexauth.getClientApiOptions(this.clientIdentifier, 5000, this.accessToken); const { data } = await axios.get(_url, options); if (needResponse) { return parser.parse(data); } return true; - }; + } - getTimeline () { + getTimeline() { return new Promise(async (resolve, reject) => { let data; try { @@ -127,9 +126,9 @@ class PlexClient { } }); // Get the timeline object from the client - }; + } - updateTimelineObject (result) { + updateTimelineObject(result) { // Check if we are the SLPlayer if (this.clientIdentifier === 'PTPLAYER9PLUS10') { // SLPLAYER @@ -139,10 +138,10 @@ class PlexClient { }, }; result = tempObj; - if (!previousTimeline.MediaContainer || result.MediaContainer.Timeline[0].ratingKey !== previousTimeline.MediaContainer.Timeline[0].ratingKey) { + if (!this.previousTimeline.MediaContainer || result.MediaContainer.Timeline[0].ratingKey !== this.previousTimeline.MediaContainer.Timeline[0].ratingKey) { window.EventBus.$emit('PLAYBACK_CHANGE', [this, result.MediaContainer.Timeline[0].ratingKey, result.MediaContainer.Timeline[0]]); } - previousTimeline = tempObj; + this.previousTimeline = tempObj; this.lastTimelineObject = result.MediaContainer.Timeline[0]; this.lastTimelineObject.recievedAt = new Date().getTime(); window.EventBus.$emit('NEW_TIMELINE', result.MediaContainer.Timeline[0]); @@ -151,44 +150,44 @@ class PlexClient { // Standard player const timelines = result.MediaContainer.Timeline; let videoTimeline = {}; - for (let i = 0; i < timelines.length; i++) { + for (let i = 0; i < timelines.length; i += 1) { const _timeline = timelines[i].$; if (_timeline.type === 'video') { videoTimeline = _timeline; - if (videoTimeline.ratingKey !== previousTimeline.ratingKey) { + if (videoTimeline.ratingKey !== this.previousTimeline.ratingKey) { window.EventBus.$emit('PLAYBACK_CHANGE', [this, videoTimeline.ratingKey, videoTimeline]); } } } window.EventBus.$emit('NEW_TIMELINE', videoTimeline); - previousTimeline = videoTimeline; + this.previousTimeline = videoTimeline; this.lastTimelineObject = videoTimeline; this.lastTimelineObject.recievedAt = new Date().getTime(); // this.setValue('lastTimelineObject', videoTimeline) return videoTimeline; - }; + } - pressPlay () { + pressPlay() { // Press play on the client return this.hitApi('/player/playback/play', { wait: 0 }); - }; + } - pressPause () { + pressPause() { // Press pause on the client return this.hitApi('/player/playback/pause', { wait: 0 }); - }; + } - pressStop () { + pressStop() { // Press pause on the client return this.hitApi('/player/playback/stop', { wait: 0 }); - }; + } - seekTo (time, params) { + seekTo(time, params) { // Seek to a time (in ms) return this.hitApi('/player/playback/seekTo', { wait: 0, offset: Math.round(time), ...params }); - }; + } - waitForMovement (startTime) { + waitForMovement(startTime) { return new Promise((resolve, reject) => { let time = 500; if (this.clientIdentifier === 'PTPLAYER9PLUS10') { @@ -203,9 +202,9 @@ class PlexClient { } }, time); }); - }; + } - skipAhead (current, duration) { + skipAhead(current, duration) { return new Promise(async (resolve, reject) => { const startedAt = new Date().getTime(); const now = this.lastTimelineObject.time; @@ -219,16 +218,16 @@ class PlexClient { await this.pressPlay(); resolve(); }); - }; + } - cleanSeek (time, isSoft) { + cleanSeek(time, isSoft) { if (isSoft && this.clientIdentifier === 'PTPLAYER9PLUS10') { return this.seekTo(time, { softSeek: true }); } return this.seekTo(time); - }; + } - sync (hostTimeline, SYNCFLEXIBILITY, SYNCMODE, POLLINTERVAL) { + sync(hostTimeline, SYNCFLEXIBILITY, SYNCMODE, POLLINTERVAL) { return new Promise(async (resolve, reject) => { if (this.clientIdentifier === 'PTPLAYER9PLUS10') { await this.getTimeline(); @@ -247,7 +246,7 @@ class PlexClient { // console.log('Difference with host is', difference); const bothPaused = hostTimeline.playerState === 'paused' && this.lastTimelineObject.state === 'paused'; - if (parseInt(difference) > parseInt(SYNCFLEXIBILITY) || (bothPaused && difference > 10)) { + if (parseInt(difference, 10) > parseInt(SYNCFLEXIBILITY, 10) || (bothPaused && difference > 10)) { // We need to seek! this.lastSyncCommand = new Date().getTime(); // Decide what seeking method we want to use @@ -262,24 +261,24 @@ class PlexClient { } // Calc the average delay of the last 10 host timeline updates // We do this to avoid any issues with random lag spikes - differenceCache.unshift(difference); - if (differenceCache.length > 5) { - differenceCache.pop(); + this.differenceCache.unshift(difference); + if (this.differenceCache.length > 5) { + this.differenceCache.pop(); } let total = 0; - for (let i = 0; i < differenceCache.length; i++) { - total += differenceCache[i]; + for (let i = 0; i < this.differenceCache.length; i += 1) { + total += this.differenceCache[i]; } - const avg = total / differenceCache.length; + const avg = total / this.differenceCache.length; if (this.clientIdentifier === 'PTPLAYER9PLUS10' && avg > 1500) { console.log('Soft syncing because difference is', difference); return resolve(await this.cleanSeek(hostTimeline.time, true)); } return resolve('No sync needed'); }); - }; + } - async playMedia (data) { + async playMedia(data) { // Play a media item given a mediaId key and a server to play from // We need the following variables to build our paramaters: // MediaId Key, Offset, server MachineId, @@ -318,9 +317,9 @@ class PlexClient { await this.waitForMovement(); resolve(true); }); - }; + } - playContentAutomatically (client, hostData, servers, offset) { + playContentAutomatically(client, hostData, servers, offset) { // Automatically play content on the client searching all servers based on the title return new Promise(async (resolve, reject) => { // First lets find all of our playable items @@ -397,5 +396,7 @@ class PlexClient { return false; } }); - }; -}; + } +} + +export default PlexClient; diff --git a/src/store/modules/plex/helpers/PlexServer.js b/src/store/modules/plex/helpers/PlexServer.js index 9c2f6187..40e0e841 100644 --- a/src/store/modules/plex/helpers/PlexServer.js +++ b/src/store/modules/plex/helpers/PlexServer.js @@ -1,8 +1,6 @@ import axios from 'axios'; - -const _PlexAuth = require('./PlexAuth.js'); - -const PlexAuth = new _PlexAuth(); +import promiseutils from '@/utils/promiseutils'; +import plexauth from './PlexAuth'; class PlexServer { @@ -47,7 +45,7 @@ class PlexServer { return reject(new Error('Failed to find a connection')); } } - const options = PlexAuth.getApiOptions('', this.accessToken, 15000, 'GET'); + const options = plexauth.getApiOptions('', this.accessToken, 15000, 'GET'); axios .get(this.chosenConnection.uri + command, { params, @@ -66,11 +64,10 @@ class PlexServer { }); } - async hitApiTestConnection(command, connection) { - const _url = connection.uri + command; - const config = PlexAuth.getRequestConfig(this.accessToken, 7500); - const { data } = await axios.get(_url, config); - return data; + hitApiTestConnection(command, connection) { + const url = `${connection.uri}${command}`; + const config = plexauth.getRequestConfig(this.accessToken, 7500); + return axios.get(url, config); } setChosenConnection(con) { @@ -81,34 +78,30 @@ class PlexServer { // This function iterates through all available connections and // if any of them return a valid response we'll set that connection // as the chosen connection for future use. - let resolved = false; - return new Promise(async (resolve, reject) => { - await Promise.all( - this.plexConnections.map( - async (connection, index) => - /*eslint-disable */ - new Promise(async (_resolve, _reject) => { - try { - let result = await this.hitApiTestConnection('', connection); - if (result) { - resolved = true; - // console.log('Succesfully connected to', server, 'via', connection) - this.setValue('chosenConnection', connection); - resolve(true); - } - _resolve(false); - } catch (e) { - _resolve(false); - } - }), - /* eslint-enable */ - ), + // Prefer secure connections first. + const secureConnections = this.plexConnections.filter((connection) => connection.protocol === 'https'); + + try { + const secureConnection = promiseutils.any( + secureConnections.map((connection) => this.hitApiTestConnection('', connection).then(() => connection)), ); - if (!resolved) { - reject(new Error('Unable to find a connection')); - } - }); + this.setValue('chosenConnection', secureConnection); + return true; + } catch (e) { + console.log('No secure connections found'); + } + + // If we are using synclounge over https, we can't access connections over http because + // most modern web browsers block mixed content + if (window.location.protocol === 'http:') { + const insecureConnections = this.plexConnections.filter((connection) => connection.protocol === 'http'); + const insecureConnection = promiseutils.any(insecureConnections.map((connection) => this.hitApiTestConnection('', connection).then(() => connection))); + this.setValue('chosenConnection', insecureConnection); + return true; + } + + throw new Error('Unable to find a connection'); } // Functions for dealing with media diff --git a/src/utils/promiseutils.js b/src/utils/promiseutils.js new file mode 100644 index 00000000..cc78ea11 --- /dev/null +++ b/src/utils/promiseutils.js @@ -0,0 +1,9 @@ +// There doesn't seem to be a valid Promise.any polyfill in the current build config so I found one myself + +export default { + any: (promises) => Promise.all(promises.map((promise) => promise.then((val) => { + throw val; + }, (reason) => reason))).then((reasons) => { + throw reasons; + }, (firstResolved) => firstResolved), +}; diff --git a/src/utils/xmlutils.js b/src/utils/xmlutils.js new file mode 100644 index 00000000..80904b8c --- /dev/null +++ b/src/utils/xmlutils.js @@ -0,0 +1,11 @@ +import parser from 'fast-xml-parser'; + +const options = { + attributeNamePrefix: '', + ignoreAttributes: false, + arrayMode: true, +}; + +export default { + parseXML: (xml) => parser.parse(xml, options), +};