feat: pre-populate server info from plex.tv API (#563)

Co-authored-by: sct <sctsnipe@gmail.com>
This commit is contained in:
Chris Pritchard
2021-01-25 07:34:21 +00:00
committed by GitHub
parent b3313a7f4d
commit 82ac76b054
11 changed files with 741 additions and 76 deletions

View File

@@ -1,5 +1,5 @@
import NodePlexAPI from 'plex-api';
import { getSettings } from '../lib/settings';
import { getSettings, PlexSettings } from '../lib/settings';
export interface PlexLibraryItem {
ratingKey: string;
@@ -80,13 +80,26 @@ interface PlexMetadataResponse {
class PlexAPI {
private plexClient: NodePlexAPI;
constructor({ plexToken }: { plexToken?: string }) {
constructor({
plexToken,
plexSettings,
timeout,
}: {
plexToken?: string;
plexSettings?: PlexSettings;
timeout?: number;
}) {
const settings = getSettings();
let settingsPlex: PlexSettings | undefined;
plexSettings
? (settingsPlex = plexSettings)
: (settingsPlex = getSettings().plex);
this.plexClient = new NodePlexAPI({
hostname: settings.plex.ip,
port: settings.plex.port,
https: settings.plex.useSsl,
hostname: settingsPlex.ip,
port: settingsPlex.port,
https: settingsPlex.useSsl,
timeout: timeout,
token: plexToken,
authenticator: {
authenticate: (
@@ -111,6 +124,7 @@ class PlexAPI {
});
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public async getStatus() {
return await this.plexClient.query('/');
}

View File

@@ -1,5 +1,6 @@
import axios, { AxiosInstance } from 'axios';
import xml2js from 'xml2js';
import { PlexDevice } from '../interfaces/api/plexInterfaces';
import { getSettings } from '../lib/settings';
import logger from '../logger';
@@ -29,6 +30,45 @@ interface PlexUser {
entitlements: string[];
}
interface ConnectionResponse {
$: {
protocol: string;
address: string;
port: string;
uri: string;
local: string;
};
}
interface DeviceResponse {
$: {
name: string;
product: string;
productVersion: string;
platform: string;
platformVersion: string;
device: string;
clientIdentifier: string;
createdAt: string;
lastSeenAt: string;
provides: string;
owned: string;
accessToken?: string;
publicAddress?: string;
httpsRequired?: string;
synced?: string;
relay?: string;
dnsRebindingProtection?: string;
natLoopbackSupported?: string;
publicAddressMatches?: string;
presence?: string;
ownerID?: string;
home?: string;
sourceTitle?: string;
};
Connection: ConnectionResponse[];
}
interface ServerResponse {
$: {
id: string;
@@ -87,6 +127,62 @@ class PlexTvAPI {
});
}
public async getDevices(): Promise<PlexDevice[]> {
try {
const devicesResp = await this.axios.get(
'/api/resources?includeHttps=1',
{
transformResponse: [],
responseType: 'text',
}
);
const parsedXml = await xml2js.parseStringPromise(
devicesResp.data as DeviceResponse
);
return parsedXml?.MediaContainer?.Device?.map((pxml: DeviceResponse) => ({
name: pxml.$.name,
product: pxml.$.product,
productVersion: pxml.$.productVersion,
platform: pxml.$?.platform,
platformVersion: pxml.$?.platformVersion,
device: pxml.$?.device,
clientIdentifier: pxml.$.clientIdentifier,
createdAt: new Date(parseInt(pxml.$?.createdAt, 10) * 1000),
lastSeenAt: new Date(parseInt(pxml.$?.lastSeenAt, 10) * 1000),
provides: pxml.$.provides.split(','),
owned: pxml.$.owned == '1' ? true : false,
accessToken: pxml.$?.accessToken,
publicAddress: pxml.$?.publicAddress,
publicAddressMatches:
pxml.$?.publicAddressMatches == '1' ? true : false,
httpsRequired: pxml.$?.httpsRequired == '1' ? true : false,
synced: pxml.$?.synced == '1' ? true : false,
relay: pxml.$?.relay == '1' ? true : false,
dnsRebindingProtection:
pxml.$?.dnsRebindingProtection == '1' ? true : false,
natLoopbackSupported:
pxml.$?.natLoopbackSupported == '1' ? true : false,
presence: pxml.$?.presence == '1' ? true : false,
ownerID: pxml.$?.ownerID,
home: pxml.$?.home == '1' ? true : false,
sourceTitle: pxml.$?.sourceTitle,
connection: pxml?.Connection?.map((conn: ConnectionResponse) => ({
protocol: conn.$.protocol,
address: conn.$.address,
port: parseInt(conn.$.port, 10),
uri: conn.$.uri,
local: conn.$.local == '1' ? true : false,
})),
}));
} catch (e) {
logger.error('Something went wrong getting the devices from plex.tv', {
label: 'Plex.tv API',
errorMessage: e.message,
});
throw new Error('Invalid auth token');
}
}
public async getUser(): Promise<PlexUser> {
try {
const account = await this.axios.get<PlexAccountResponse>(

View File

@@ -0,0 +1,45 @@
import { PlexSettings } from '../../lib/settings';
export interface PlexStatus {
settings: PlexSettings;
status: number;
message: string;
}
export interface PlexConnection {
protocol: string;
address: string;
port: number;
uri: string;
local: boolean;
status?: number;
message?: string;
host?: string;
}
export interface PlexDevice {
name: string;
product: string;
productVersion: string;
platform: string;
platformVersion: string;
device: string;
clientIdentifier: string;
createdAt: Date;
lastSeenAt: Date;
provides: string[];
owned: boolean;
accessToken?: string;
publicAddress?: string;
httpsRequired?: boolean;
synced?: boolean;
relay?: boolean;
dnsRebindingProtection?: boolean;
natLoopbackSupported?: boolean;
publicAddressMatches?: boolean;
presence?: boolean;
ownerID?: string;
home?: boolean;
sourceTitle?: string;
connection: PlexConnection[];
}

View File

@@ -9,6 +9,7 @@ import {
import { getRepository } from 'typeorm';
import { User } from '../../entity/User';
import PlexAPI from '../../api/plexapi';
import PlexTvAPI from '../../api/plextv';
import { jobPlexFullSync } from '../../job/plexsync';
import SonarrAPI from '../../api/sonarr';
import RadarrAPI from '../../api/radarr';
@@ -106,6 +107,69 @@ settingsRoutes.post('/plex', async (req, res, next) => {
return res.status(200).json(settings.plex);
});
settingsRoutes.get('/plex/devices/servers', async (req, res, next) => {
const userRepository = getRepository(User);
const regexp = /(http(s?):\/\/)(.*)(:[0-9]*)/;
try {
const admin = await userRepository.findOneOrFail({
select: ['id', 'plexToken'],
order: { id: 'ASC' },
});
const plexTvClient = admin.plexToken
? new PlexTvAPI(admin.plexToken)
: null;
const devices = (await plexTvClient?.getDevices())?.filter((device) => {
return device.provides.includes('server') && device.owned;
});
const settings = getSettings();
if (devices) {
await Promise.all(
devices.map(async (device) => {
await Promise.all(
device.connection.map(async (connection) => {
connection.host = connection.uri.replace(regexp, '$3');
let msg:
| { status: number; message: string }
| undefined = undefined;
const plexDeviceSettings = {
...settings.plex,
ip: connection.host,
port: connection.port,
useSsl: connection.protocol === 'https' ? true : false,
};
const plexClient = new PlexAPI({
plexToken: admin.plexToken,
plexSettings: plexDeviceSettings,
timeout: 5000,
});
try {
await plexClient.getStatus();
msg = {
status: 200,
message: 'OK',
};
} catch (e) {
msg = {
status: 500,
message: e.message,
};
}
connection.status = msg?.status;
connection.message = msg?.message;
})
);
})
);
}
return res.status(200).json(devices);
} catch (e) {
return next({
status: 500,
message: `Failed to connect to Plex: ${e.message}`,
});
}
});
settingsRoutes.get('/plex/library', async (req, res) => {
const settings = getSettings();
@@ -156,7 +220,6 @@ settingsRoutes.get('/plex/sync', (req, res) => {
} else if (req.query.start) {
jobPlexFullSync.run();
}
return res.status(200).json(jobPlexFullSync.status());
});

View File

@@ -5,6 +5,7 @@ declare module 'plex-api' {
port: number;
token?: string;
https?: boolean;
timeout?: number;
authenticator: {
authenticate: (
_plexApi: PlexAPI,
@@ -19,7 +20,7 @@ declare module 'plex-api' {
};
requestOptions?: Record<string, string | number>;
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
query: <T extends Record<string, any>>(endpoint: string) => Promise<T>;
}
}