import axios, { AxiosInstance } from 'axios'; import xml2js from 'xml2js'; import { PlexDevice } from '../interfaces/api/plexInterfaces'; import { getSettings } from '../lib/settings'; import logger from '../logger'; interface PlexAccountResponse { user: PlexUser; } interface PlexUser { id: number; uuid: string; email: string; joined_at: string; username: string; title: string; thumb: string; hasPassword: boolean; authToken: string; subscription: { active: boolean; status: string; plan: string; features: string[]; }; roles: { roles: string[]; }; 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; serverId: string; machineIdentifier: string; name: string; lastSeenAt: string; numLibraries: string; owned: string; }; } interface FriendResponse { MediaContainer: { User: { $: { id: string; title: string; username: string; email: string; thumb: string; }; Server?: ServerResponse[]; }[]; }; } interface UsersResponse { MediaContainer: { User: { $: { id: string; title: string; username: string; email: string; thumb: string; }; Server: ServerResponse[]; }[]; }; } class PlexTvAPI { private authToken: string; private axios: AxiosInstance; constructor(authToken: string) { this.authToken = authToken; this.axios = axios.create({ baseURL: 'https://plex.tv', headers: { 'X-Plex-Token': this.authToken, 'Content-Type': 'application/json', Accept: 'application/json', }, }); } public async getDevices(): Promise { 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 { try { const account = await this.axios.get( '/users/account.json' ); return account.data.user; } catch (e) { logger.error( `Something went wrong while getting the account from plex.tv: ${e.message}`, { label: 'Plex.tv API' } ); throw new Error('Invalid auth token'); } } public async getFriends(): Promise { const response = await this.axios.get('/pms/friends/all', { transformResponse: [], responseType: 'text', }); const parsedXml = (await xml2js.parseStringPromise( response.data )) as FriendResponse; return parsedXml; } public async checkUserAccess(userId: number): Promise { const settings = getSettings(); try { if (!settings.plex.machineId) { throw new Error('Plex is not configured!'); } const friends = await this.getFriends(); const users = friends.MediaContainer.User; const user = users.find((u) => parseInt(u.$.id) === userId); if (!user) { throw new Error( "This user does not exist on the main Plex account's shared list" ); } return !!user.Server?.find( (server) => server.$.machineIdentifier === settings.plex.machineId ); } catch (e) { logger.error(`Error checking user access: ${e.message}`); return false; } } public async getUsers(): Promise { const response = await this.axios.get('/api/users', { transformResponse: [], responseType: 'text', }); const parsedXml = (await xml2js.parseStringPromise( response.data )) as UsersResponse; return parsedXml; } } export default PlexTvAPI;