Improved plex connection finder

This commit is contained in:
Travis Shivers 2020-06-05 03:05:05 -05:00
parent 19b47eaa7c
commit d96d151ad0
7 changed files with 181 additions and 149 deletions

26
package-lock.json generated
View File

@ -1552,6 +1552,14 @@
"lodash": "^4.17.4",
"mkdirp": "^0.5.1",
"source-map-support": "^0.4.15"
},
"dependencies": {
"core-js": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==",
"dev": true
}
}
},
"babel-runtime": {
@ -1561,6 +1569,13 @@
"requires": {
"core-js": "^2.4.0",
"regenerator-runtime": "^0.11.0"
},
"dependencies": {
"core-js": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
}
}
},
"babel-template": {
@ -2895,11 +2910,6 @@
}
}
},
"core-js": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.5.tgz",
"integrity": "sha1-sU3ek2xkDAV5prUMq8wTLdYSfjs="
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@ -4346,6 +4356,12 @@
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
},
"fast-xml-parser": {
"version": "3.17.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.17.3.tgz",
"integrity": "sha512-g3OSnHBWq5hrpS4LUgFWOS87F7B6UDklkI6v2VLC/89Ech00fabXleKEHTVXQBkKyVsfOxD+1QY6fkoIZMIO/Q==",
"dev": true
},
"fastparse": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz",

View File

@ -58,6 +58,7 @@
"eslint-plugin-vue": "^4.3.0",
"eventsource-polyfill": "^0.9.6",
"extract-text-webpack-plugin": "^2.0.0",
"fast-xml-parser": "^3.17.3",
"file-loader": "^0.11.1",
"friendly-errors-webpack-plugin": "^1.1.3",
"html-webpack-plugin": "^2.28.0",

View File

@ -1,4 +1,6 @@
import axios from 'axios';
const request = require('request');
import xmlutils from '@/utils/xmlutils';
const parseXMLString = require('xml2js').parseString;
const _PlexAuth = require('./helpers/PlexAuth.js');
@ -58,9 +60,9 @@ export default {
});
}),
PLEX_GET_DEVICES: ({ state, commit, dispatch }, dontDelete) => new Promise((resolve, reject) => {
PLEX_GET_DEVICES: async ({ state, commit, dispatch }, dontDelete) => {
if (!state.user) {
return reject(new Error('Sign in before getting devices'));
throw new Error('Sign in before getting devices');
}
if (!dontDelete) {
@ -68,112 +70,101 @@ export default {
commit('PLEX_SET_VALUE', ['servers', {}]);
commit('PLEX_SET_VALUE', ['clients', {}]);
}
const options = PlexAuth.getApiOptions('https://plex.tv/api/resources?includeHttps=1', state.user.authToken, 5000, 'GET');
request(options, (error, response, body) => {
if (!error && response.statusCode === 200) {
// Valid response
parseXMLString(body, async (err, result) => {
if (err) {
return reject(err);
}
for (const index in result.MediaContainer.Device) {
// Handle the individual device
const device = result.MediaContainer.Device[index].$;
// Each device can have multiple network connections
// Any of them can be viable routes to interacting with the device
const connections = result.MediaContainer.Device[index].Connection;
const tempConnectionsArray = [];
// Create a temporary array of object:PlexConnection
for (const i in connections) {
const connection = connections[i].$;
// Exclude local IPs starting with 169.254
if (!connection.address.startsWith('169.254')) {
const tempConnection = new PlexConnection();
for (const key in connection) {
tempConnection[key] = connection[key];
}
tempConnectionsArray.push(tempConnection);
if (connection.local === '1' && connection.uri.indexOf('plex') > -1) {
const rawConnection = new PlexConnection();
Object.assign(rawConnection, connection);
rawConnection.uri = `${connection.protocol}://${connection.address}:${connection.port}`;
rawConnection.isManual = true;
tempConnectionsArray.push(rawConnection);
}
}
}
// If device is a player
if (device.provides.indexOf('player') !== -1) {
// If device is not Plex Web
if(device.product.indexOf('Plex Web') === -1) {
// This is a Client
// Create a new PlexClient object
const tempClient = new PlexClient();
for (const key in device) {
tempClient[key] = device[key];
}
tempClient.accessToken = state.user.authToken;
tempClient.plexConnections = tempConnectionsArray;
dispatch('PLEX_ADD_CLIENT', tempClient);
}
// If device is a server
} else if (device.provides.indexOf('server') !== -1) {
// This is a Server
// Create a new PlexServer object
const tempServer = new PlexServer();
for (const key in device) {
tempServer[key] = device[key];
}
// Push a manual connection string for when DNS rebind doesnt work
tempServer.plexConnections = tempConnectionsArray;
if (tempServer.accessToken == null) {
tempServer.accessToken = state.user.authToken;
}
try {
const { data } = await axios.get('https://plex.tv/api/resources?includeHttps=1',
PlexAuth.getRequestConfig(state.user.authToken, 5000));
const result = xmlutils.parseXML(data);
dispatch('PLEX_ADD_SERVER', tempServer);
result.MediaContainer[0].Device.forEach((device) => {
// Create a temporary array of object:PlexConnection
// Exclude local IPs starting with 169.254
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);
rawConnection.uri = `${connection.protocol}://${connection.address}:${connection.port}`;
rawConnection.isManual = true;
// Return both
return [tempConnection, rawConnection];
}
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
if (device.product.indexOf('Plex Web') === -1) {
// This is a Client
// Create a new PlexClient object
const tempClient = new PlexClient();
Object.assign(tempClient, device);
tempClient.accessToken = state.user.authToken;
tempClient.plexConnections = tempConnectionsArray;
dispatch('PLEX_ADD_CLIENT', tempClient);
}
// Setup our slPlayer
const ptplayer = new PlexClient();
ptplayer.provides = 'player';
ptplayer.clientIdentifier = 'PTPLAYER9PLUS10';
ptplayer.platform = 'Web';
ptplayer.device = 'Web';
ptplayer.product = 'SyncLounge';
ptplayer.name = 'SyncLounge Player';
ptplayer.labels = [
['Recommended', 'green'],
];
ptplayer.lastSeenAt = Math.round((new Date()).getTime() / 1000);
for (const i in state.clients) {
const client = state.clients[i];
for (const j in client.plexConnections) {
const clientConnection = client.plexConnections[j];
// Check if this URL matches any server connections
for (const x in state.servers) {
const server = state.servers[x];
for (const y in server.plexConnections) {
const serverConnection = server.plexConnections[y];
if (serverConnection.uri === clientConnection.uri) {
client.accessToken = server.accessToken;
}
}
}
}
} else if (device.provides.indexOf('server') !== -1) {
// This is a Server
// Create a new PlexServer object
const tempServer = new PlexServer();
Object.assign(tempServer, device);
// Push a manual connection string for when DNS rebind doesnt work
tempServer.plexConnections = tempConnectionsArray;
if (tempServer.accessToken == null) {
tempServer.accessToken = state.user.authToken;
}
dispatch('PLEX_ADD_CLIENT', ptplayer);
commit('PLEX_SET_VALUE', ['gotDevices', true]);
dispatch('PLEX_REFRESH_SERVER_CONNECTIONS');
return resolve(true);
dispatch('PLEX_ADD_SERVER', tempServer);
}
});
// Setup our slPlayer
const ptplayer = new PlexClient();
ptplayer.provides = 'player';
ptplayer.clientIdentifier = 'PTPLAYER9PLUS10';
ptplayer.platform = 'Web';
ptplayer.device = 'Web';
ptplayer.product = 'SyncLounge';
ptplayer.name = 'SyncLounge Player';
ptplayer.labels = [['Recommended', 'green']];
ptplayer.lastSeenAt = Math.round(new Date().getTime() / 1000);
// Get an array of {accessToken, uri} objects
const serverConnectionTokens = Object.entries(state.servers)
.flatMap(([, server]) => server.plexConnections
.map((serverConnection) => ({
accessToken: serverConnection.accessToken,
uri: serverConnection.uri,
})));
Object.entries(state.clients).forEach(([, client]) => {
client.plexConnections.forEach((clientConnection) => {
const match = serverConnectionTokens
.find((serverCon) => serverCon.uri === clientConnection.uri);
if (match !== undefined) {
// Yeah it would be better if I didn't have to mutate client but oh well
// eslint-disable-next-line no-param-reassign
client.accessToken = match.accessToken;
}
});
} else {
// Invalid response
commit('PLEX_SET_VALUE', ['gotDevices', true]);
return reject(new Error('Invalid Response'));
}
});
}),
});
dispatch('PLEX_ADD_CLIENT', ptplayer);
commit('PLEX_SET_VALUE', ['gotDevices', true]);
dispatch('PLEX_REFRESH_SERVER_CONNECTIONS');
} catch (e) {
// Invalid response
commit('PLEX_SET_VALUE', ['gotDevices', true]);
throw e;
}
},
PLEX_REFRESH_SERVER_CONNECTIONS: ({ state, dispatch }) => {
for (const id in state.servers) {

View File

@ -21,6 +21,17 @@ module.exports = function PlexAuth() {
};
};
this.getRequestConfig = function (accessToken, timeout) {
return {
headers: {
'X-Plex-Client-Identifier': 'SyncLounge',
Accept: 'application/json',
'X-Plex-Token': accessToken,
},
timeout,
};
};
/**
*
* @param url

View File

@ -1,6 +1,7 @@
const request = require('request');
const safeParse = require('safe-json-parse/callback');
const _PlexAuth = require('./PlexAuth.js');
import promiseutils from '@/utils/promiseutils';
const PlexAuth = new _PlexAuth();
@ -64,56 +65,45 @@ module.exports = function PlexServer() {
}
});
};
this.hitApiTestConnection = async function (command, connection) {
return new Promise(async (resolve, reject) => {
const _url = connection.uri + command;
const options = PlexAuth.getApiOptions(_url, this.accessToken, 7500, 'GET');
request(options, (error, response, body) => {
if (!error) {
safeParse(body, (err, json) => {
if (err) {
return reject(err);
}
return resolve(json);
});
} else {
return reject(error);
}
});
});
this.hitApiTestConnection = function (command, connection) {
const url = `${connection.uri}${command}`;
const config = PlexAuth.getRequestConfig(this.accessToken, 7500);
return axios.get(url, config);
};
this.setChosenConnection = function (con) {
this.chosenConnection = con;
};
this.findConnection = function () {
this.findConnection = async function () {
// 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 */
));
if (!resolved) {
reject(new Error('Unable to find a connection'));
}
});
// 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))
);
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

12
src/utils/promiseutils.js Normal file
View File

@ -0,0 +1,12 @@
// 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) => {
return Promise.all(promises.map(promise =>
promise.then((val) => {
throw val;
}, reason => reason))).then((reasons) => {
throw reasons;
}, firstResolved => firstResolved);
},
};

11
src/utils/xmlutils.js Normal file
View File

@ -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),
};