Skip to content

Commit

Permalink
Implement fixer to backfill plex server client identifiers
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbenincasa committed Mar 11, 2024
1 parent 759b379 commit dbdca6a
Show file tree
Hide file tree
Showing 16 changed files with 183 additions and 113 deletions.
20 changes: 14 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@
"dayjs": "^1.11.10",
"express-fileupload": "^1.2.1",
"fast-json-stringify": "^5.9.1",
"fast-xml-parser": "^4.3.5",
"fastify": "^4.24.3",
"fastify-plugin": "^4.5.1",
"fastify-print-routes": "^2.2.0",
"fastify-type-provider-zod": "^1.1.9",
"fluent-ffmpeg": "^2.1.2",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lowdb": "^7.0.0",
"morgan": "^1.10.0",
Expand Down Expand Up @@ -75,7 +75,6 @@
"@types/express": "^4.17.20",
"@types/express-fileupload": "^1.4.3",
"@types/fluent-ffmpeg": "^2.1.23",
"@types/lodash": "^4.14.202",
"@types/lodash-es": "^4.17.10",
"@types/morgan": "^1.9.7",
"@types/node": "^20.8.9",
Expand Down
2 changes: 1 addition & 1 deletion server/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export const miscRouter: RouterPluginCallback = (fastify, _opts, done) => {
}

const plex = new Plex(server);
return res.send(await plex.Get(req.query.path));
return res.send(await plex.doGet(req.query.path));
},
);

Expand Down
1 change: 1 addition & 0 deletions server/src/dao/entities/PlexServerSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export class PlexServerSettings extends BaseEntity {
sendChannelUpdates: this.sendChannelUpdates,
sendGuideUpdates: this.sendGuideUpdates,
index: this.index,
clientIdentifier: this.clientIdentifier,
};
}
}
80 changes: 55 additions & 25 deletions server/src/plex.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { EntityDTO } from '@mikro-orm/core';
import { PlexDvr, PlexDvrsResponse, PlexResource } from '@tunarr/types/plex';
import axios, {
AxiosInstance,
AxiosRequestConfig,
InternalAxiosRequestConfig,
RawAxiosRequestHeaders,
isAxiosError,
} from 'axios';
import { isUndefined } from 'lodash-es';
import { XMLParser } from 'fast-xml-parser';
import { isNil, isUndefined } from 'lodash-es';
import NodeCache from 'node-cache';
import querystring, { ParsedUrlQueryInput } from 'querystring';
import { PlexServerSettings } from './dao/entities/PlexServerSettings.js';
import createLogger from './logger.js';
import { Maybe } from './types.js';
import {
PlexMediaContainer,
PlexMediaContainerResponse,
} from './types/plexApiTypes.js';
import { PlexServerSettings } from './dao/entities/PlexServerSettings.js';
import { EntityDTO } from '@mikro-orm/core';
import { PlexDvr, PlexDvrsResponse } from '@tunarr/types/plex';
import NodeCache from 'node-cache';

const ClientIdentifier = 'p86cy1w47clco3ro8t92nfy1';

type AxiosConfigWithMetadata = InternalAxiosRequestConfig & {
metadata: {
Expand All @@ -28,11 +31,11 @@ const logger = createLogger(import.meta);

const DEFAULT_HEADERS = {
Accept: 'application/json',
'X-Plex-Device': 'dizqueTV',
'X-Plex-Device-Name': 'dizqueTV',
'X-Plex-Product': 'dizqueTV',
'X-Plex-Device': 'Tunarr',
'X-Plex-Device-Name': 'Tunarr',
'X-Plex-Product': 'Tunarr',
'X-Plex-Version': '0.1',
'X-Plex-Client-Identifier': 'rg14zekk3pa5zp4safjwaa8z',
'X-Plex-Client-Identifier': ClientIdentifier,
'X-Plex-Platform': 'Chrome',
'X-Plex-Platform-Version': '80.0',
};
Expand Down Expand Up @@ -85,7 +88,7 @@ export class Plex {
return req;
});

const logAxiosRequest = (req: AxiosConfigWithMetadata) => {
const logAxiosRequest = (req: AxiosConfigWithMetadata, status: number) => {
const query = req.params
? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
`?${querystring.stringify(req.params)}`
Expand All @@ -94,18 +97,21 @@ export class Plex {
logger.debug(
`[Axios Request]: ${req.method?.toUpperCase()} ${req.baseURL}${
req.url
}${query} - ${elapsedTime}ms`,
}${query} - (${status}) ${elapsedTime}ms`,
);
};

this.axiosInstance.interceptors.response.use(
(resp) => {
logAxiosRequest(resp.config as AxiosConfigWithMetadata);
logAxiosRequest(resp.config as AxiosConfigWithMetadata, resp.status);
return resp;
},
(err) => {
if (isAxiosError(err) && err.config) {
logAxiosRequest(err.config as AxiosConfigWithMetadata);
logAxiosRequest(
err.config as AxiosConfigWithMetadata,
err.status ?? -1,
);
}
throw err;
},
Expand Down Expand Up @@ -139,7 +145,7 @@ export class Plex {
}
}

async Get<T>(
async doGet<T>(
path: string,
optionalHeaders: RawAxiosRequestHeaders = {},
): Promise<Maybe<PlexMediaContainer<T>>> {
Expand All @@ -162,7 +168,7 @@ export class Plex {
return res?.MediaContainer;
}

Put(
doPut(
path: string,
query: ParsedUrlQueryInput | URLSearchParams = {},
optionalHeaders: RawAxiosRequestHeaders = {},
Expand All @@ -183,7 +189,7 @@ export class Plex {
return this.doRequest(req);
}

Post(
doPost(
path: string,
query: ParsedUrlQueryInput | URLSearchParams = {},
optionalHeaders: RawAxiosRequestHeaders = {},
Expand All @@ -206,37 +212,39 @@ export class Plex {

async checkServerStatus() {
try {
await this.Get('/');
await this.doGet('/');
return 1;
} catch (err) {
console.error('Error getting Plex server status', err);
return -1;
}
}

async GetDVRS() {
async getDvrs() {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await this.Get<PlexDvrsResponse>('/livetv/dvrs');
const result = await this.doGet<PlexDvrsResponse>('/livetv/dvrs');
return isUndefined(result?.Dvr) ? [] : result?.Dvr;
} catch (err) {
logger.error('GET /livetv/drs failed: ', err);
throw err;
}
}

async RefreshGuide(_dvrs?: PlexDvr[]) {
const dvrs = !isUndefined(_dvrs) ? _dvrs : await this.GetDVRS();
async getResources() {}

async refreshGuide(_dvrs?: PlexDvr[]) {
const dvrs = !isUndefined(_dvrs) ? _dvrs : await this.getDvrs();
if (!dvrs) {
throw new Error('Could not retrieve Plex DVRs');
}
for (let i = 0; i < dvrs.length; i++) {
await this.Post(`/livetv/dvrs/${dvrs[i].key}/reloadGuide`);
await this.doPost(`/livetv/dvrs/${dvrs[i].key}/reloadGuide`);
}
}

async RefreshChannels(channels: { number: number }[], _dvrs?: PlexDvr[]) {
const dvrs = !isUndefined(_dvrs) ? _dvrs : await this.GetDVRS();
async refreshChannels(channels: { number: number }[], _dvrs?: PlexDvr[]) {
const dvrs = !isUndefined(_dvrs) ? _dvrs : await this.getDvrs();
if (!dvrs) throw new Error('Could not retrieve Plex DVRs');

const _channels: number[] = [];
Expand All @@ -251,11 +259,33 @@ export class Plex {
}
for (let i = 0; i < dvrs.length; i++) {
for (let y = 0; y < dvrs[i].Device.length; y++) {
await this.Put(
await this.doPut(
`/media/grabbers/devices/${dvrs[i].Device[y].key}/channelmap`,
qs,
);
}
}
}

async getDevices(): Promise<Maybe<PlexTvDevicesResponse>> {
const response = await this.doRequest<string>({
method: 'get',
baseURL: 'https://plex.tv',
url: '/devices.xml',
});

if (isNil(response)) {
return;
}

const parsed = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
}).parse(response) as PlexTvDevicesResponse;
return parsed;
}
}

type PlexTvDevicesResponse = {
MediaContainer: { Device: PlexResource[] };
};
6 changes: 3 additions & 3 deletions server/src/plexTranscoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ lang=en`;
}

async getDecisionUnmanaged(directPlay: boolean) {
this.decisionJson = await this.plex.Get<TranscodeDecision>(
this.decisionJson = await this.plex.doGet<TranscodeDecision>(
`/video/:/transcode/universal/decision?${this.transcodingArgs}`,
);

Expand Down Expand Up @@ -626,7 +626,7 @@ lang=en`;
return this.cachedItemMetadata;
}

this.cachedItemMetadata = await this.plex.Get<PlexItemMetadata>(this.key);
this.cachedItemMetadata = await this.plex.doGet<PlexItemMetadata>(this.key);
return this.cachedItemMetadata;
}

Expand Down Expand Up @@ -654,7 +654,7 @@ lang=en`;
this.log('Updating plex status');
const { path: statusUrl, params } = this.getStatusUrl();
try {
await this.plex.Post(statusUrl, params);
await this.plex.doPost(statusUrl, params);
} catch (error) {
this.log(
`Problem updating Plex status using status URL ${statusUrl}: `,
Expand Down
30 changes: 30 additions & 0 deletions server/src/tasks/fixers/addPlexServerIds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { find, isNil } from 'lodash-es';
import { EntityManager } from '../../dao/dataSource.js';
import { PlexServerSettings } from '../../dao/entities/PlexServerSettings.js';
import { Plex } from '../../plex.js';
import Fixer from './fixer.js';

export class AddPlexServerIdsFixer extends Fixer {
async runInternal(em: EntityManager): Promise<void> {
const plexServers = await em
.repo(PlexServerSettings)
.find({ clientIdentifier: null });

for (const server of plexServers) {
const api = new Plex(server);
const devices = await api.getDevices();
if (!isNil(devices) && devices.MediaContainer.Device) {
const matchingServer = find(
devices.MediaContainer.Device,
(d) => d.provides.includes('server') && d.name === server.name,
);
if (matchingServer) {
server.clientIdentifier = matchingServer.clientIdentifier;
em.persist(server);
}
}
}

await em.flush();
}
}
6 changes: 5 additions & 1 deletion server/src/tasks/fixers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import createLogger from '../../logger.js';
import { AddPlexServerIdsFixer } from './addPlexServerIds.js';
import Fixer from './fixer.js';
import { MissingSeasonNumbersFixer } from './missingSeasonNumbersFixer.js';

Expand All @@ -11,7 +12,10 @@ const logger = createLogger(import.meta);
// Maybe one day we'll import these all dynamically and run
// them, but not today.
export const runFixers = async () => {
const allFixers: Fixer[] = [new MissingSeasonNumbersFixer()];
const allFixers: Fixer[] = [
new MissingSeasonNumbersFixer(),
new AddPlexServerIdsFixer(),
];

for (const fixer of allFixers) {
try {
Expand Down
4 changes: 2 additions & 2 deletions server/src/tasks/fixers/missingSeasonNumbersFixer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export class MissingSeasonNumbersFixer extends Fixer {

private async findSeasonNumberUsingEpisode(episodeId: string, plex: Plex) {
try {
const episode = await plex.Get<PlexEpisodeView>(
const episode = await plex.doGet<PlexEpisodeView>(
`/library/metadata/${episodeId}`,
);
return episode?.parentIndex;
Expand All @@ -136,7 +136,7 @@ export class MissingSeasonNumbersFixer extends Fixer {
// We get the parent because we're dealing with an episode and we want the
// season index.
try {
const season = await plex.Get<PlexSeasonView>(
const season = await plex.doGet<PlexSeasonView>(
`/library/metadata/${seasonId}`,
);
return first(season?.Metadata ?? [])?.index;
Expand Down
Loading

0 comments on commit dbdca6a

Please sign in to comment.