Skip to content

Commit

Permalink
feat(settings): Added the ability to find and load available Plex ser…
Browse files Browse the repository at this point in the history
…vers
  • Loading branch information
jorenn92 committed Jan 29, 2024
1 parent 5fae885 commit 8be95d1
Show file tree
Hide file tree
Showing 8 changed files with 429 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class ExternalApiService {
this.axios = axios.create({
baseURL: baseUrl,
params,
timeout: 5000, // timeout after 5s
timeout: 10000, // timeout after 10s
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Expand Down
12 changes: 12 additions & 0 deletions server/src/modules/api/lib/plexApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,18 @@ class PlexApi extends NodePlexAPI {
}
return obj;
}

public async getStatus(): Promise<boolean> {
try {
const status: { MediaContainer: any } = await this.query(
{ uri: `/` },
false,
);
return status?.MediaContainer ? true : false;
} catch (err) {
return false;
}
}
}

export default PlexApi;
55 changes: 55 additions & 0 deletions server/src/modules/api/lib/plextvApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Logger } from '@nestjs/common';
import { ExternalApiService } from '../external-api/external-api.service';
import cacheManager from './cache';
import { parseStringPromise } from 'xml2js';
import { PlexDevice } from '../../api/plex-api/interfaces/server.interface';
import xml2js from 'xml2js';

interface PlexAccountResponse {
user: PlexUser;
Expand Down Expand Up @@ -245,6 +247,59 @@ export class PlexTvApi extends ExternalApiService {
};
}
}

public async getDevices(): Promise<PlexDevice[]> {
try {
const devicesResp = await this.get('/api/resources?includeHttps=1', {
transformResponse: [],
responseType: 'text',
});
const parsedXml = await xml2js.parseStringPromise(
devicesResp 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) {
this.logger.error(
'Something went wrong getting the devices from plex.tv',
);
this.logger.debug(e);
return [];
}
}
}

export default PlexTvApi;
37 changes: 37 additions & 0 deletions server/src/modules/api/plex-api/interfaces/server.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,40 @@ export interface PlexStatusResponse {
export interface PlexAccountsResponse {
MediaContainer: { Account: PlexUserAccount[] };
}

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[];
}

export interface PlexConnection {
protocol: string;
address: string;
port: number;
uri: string;
local: boolean;
status?: number;
message?: string;
}
86 changes: 80 additions & 6 deletions server/src/modules/api/plex-api/plex-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ import {
} from './interfaces/media.interface';
import {
PlexAccountsResponse,
PlexDevice,
PlexStatusResponse,
} from './interfaces/server.interface';
import { EPlexDataType } from './enums/plex-data-type-enum';
import axios from 'axios';
import PlexApi from '../lib/plexApi';
import PlexTvApi from '../lib/plextvApi';
import cacheManager from '../../api/lib/cache';
import { Settings } from '../../settings/entities/settings.entities';

@Injectable()
export class PlexApiService {
Expand All @@ -45,6 +47,13 @@ export class PlexApiService {
this.initialize({});
}

maintainerrClientOptions = {
identifier: '695b47f5-3c61-4cbd-8eb3-bcc3d6d06ac5',
product: 'Maintainerr',
deviceName: 'Maintainerr',
platform: 'Maintainerr',
};

private async getDbSettings(): Promise<PlexSettings> {
return {
name: this.settings.plex_name,
Expand Down Expand Up @@ -89,12 +98,7 @@ export class PlexApiService {
// requestOptions: {
// includeChildren: 1,
// },
options: {
identifier: '695b47f5-3c61-4cbd-8eb3-bcc3d6d06ac5', // this.settings.clientId
product: 'Maintainerr', // this.settings.applicationTitle
deviceName: 'Maintainerr', // this.settings.applicationTitle
platform: 'Maintainerr', // this.settings.applicationTitle
},
options: this.maintainerrClientOptions,
});

this.plexTvClient = new PlexTvApi(plexToken);
Expand Down Expand Up @@ -604,6 +608,76 @@ export class PlexApiService {
}
}

public async getAvailableServers(): Promise<PlexDevice[]> {
try {
// reload requirements, auth token might have changed
const settings = (await this.settings.getSettings()) as Settings;
this.plexTvClient = new PlexTvApi(settings.plex_auth_token);

const devices = (await this.plexTvClient?.getDevices())?.filter(
(device) => {
return device.provides.includes('server') && device.owned;
},
);

if (devices) {
await Promise.all(
devices.map(async (device) => {
device.connection.map((connection) => {
const url = new URL(connection.uri);
if (url.hostname !== connection.address) {
const plexDirectConnection = {
...connection,
address: url.hostname,
};
device.connection.push(plexDirectConnection);
connection.protocol = 'http';
}
});

const filteredConnectionPromises = device.connection.map(
async (connection) => {
const newClient = new PlexApi({
hostname: connection.address,
port: connection.port,
https: connection.protocol === 'https',
timeout: 5000,
token: settings.plex_auth_token,
authenticator: {
authenticate: (
_plexApi,
cb: (err?: string, token?: string) => void,
) => {
if (!settings.plex_auth_token) {
return cb('Plex Token not found!');
}
cb(undefined, settings.plex_auth_token);
},
},
options: this.maintainerrClientOptions,
});

// test connection
return (await newClient.getStatus()) ? connection : null;
},
);

device.connection = (
await Promise.all(filteredConnectionPromises)
).filter(Boolean);
}),
);
}
return devices;
} catch (e) {
this.logger.warn(
'Plex api communication failure.. Is the application running?',
);
this.logger.debug(e);
return [];
}
}

public async getAllIdsForContextAction(
collectionType: EPlexDataType,
context: { type: EPlexDataType; id: number },
Expand Down
9 changes: 9 additions & 0 deletions server/src/modules/settings/settings.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export class SettingsController {
updateSettings(@Body() payload: SettingDto) {
return this.settingsService.updateSettings(payload);
}
@Post('/plex/token')
updateAuthToken(@Body() payload: { plex_auth_token: string }) {
return this.settingsService.savePlexApiAuthToken(payload.plex_auth_token);
}
@Get('/test/setup')
testSetup() {
return this.settingsService.testSetup();
Expand All @@ -50,6 +54,11 @@ export class SettingsController {
return this.settingsService.testPlex();
}

@Get('/plex/devices/servers')
async getPlexServers() {
return await this.settingsService.getPlexServers();
}

@Post('/cron/validate')
validateSingleCron(@Body() payload: CronScheduleDto) {
return this.settingsService.cronIsValid(payload.schedule)
Expand Down
27 changes: 24 additions & 3 deletions server/src/modules/settings/settings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,22 @@ export class SettingsService implements SettingDto {
}
}

public async savePlexApiAuthToken(plex_auth_token: string) {
try {
const settingsDb = await this.settingsRepo.findOne({ where: {} });

await this.settingsRepo.save({
...settingsDb,
plex_auth_token: plex_auth_token,
});

return { status: 'OK', code: 1, message: 'Success' };
} catch (e) {
this.logger.error('Error while updating Plex auth token: ', e);
return { status: 'NOK', code: 0, message: 'Failed' };
}
}

public async updateSettings(settings: Settings): Promise<BasicResponseDto> {
try {
settings.plex_hostname = settings.plex_hostname?.toLowerCase();
Expand All @@ -136,12 +152,13 @@ export class SettingsService implements SettingDto {
// Plex SSL specifics

settings.plex_ssl =
settings.plex_hostname.includes('https://') || settings.plex_port == 443
settings.plex_hostname?.includes('https://') ||
settings.plex_port == 443
? 1
: 0;
settings.plex_hostname = settings.plex_hostname
.replace('https://', '')
.replace('http://', '');
?.replace('https://', '')
?.replace('http://', '');

if (
this.cronIsValid(settings.collection_handler_job_cron) &&
Expand Down Expand Up @@ -331,4 +348,8 @@ export class SettingsService implements SettingDto {
}
return false;
}

public async getPlexServers() {
return await this.plexApi.getAvailableServers();
}
}
Loading

0 comments on commit 8be95d1

Please sign in to comment.