mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03:35 -04:00
Persist client_identifier for plex server to DB (#167)
* Add new Plex server column for client identifier * Implement fixer to backfill plex server client identifiers * Fix build error
This commit is contained in:
committed by
GitHub
parent
3cdcc09296
commit
16ab7cdf6f
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@@ -89,6 +89,9 @@ importers:
|
||||
fast-json-stringify:
|
||||
specifier: ^5.9.1
|
||||
version: 5.9.1
|
||||
fast-xml-parser:
|
||||
specifier: ^4.3.5
|
||||
version: 4.3.5
|
||||
fastify:
|
||||
specifier: ^4.24.3
|
||||
version: 4.24.3
|
||||
@@ -104,9 +107,6 @@ importers:
|
||||
fluent-ffmpeg:
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2
|
||||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
@@ -183,9 +183,6 @@ importers:
|
||||
'@types/fluent-ffmpeg':
|
||||
specifier: ^2.1.23
|
||||
version: 2.1.23
|
||||
'@types/lodash':
|
||||
specifier: ^4.14.202
|
||||
version: 4.14.202
|
||||
'@types/lodash-es':
|
||||
specifier: ^4.17.10
|
||||
version: 4.17.10
|
||||
@@ -4925,6 +4922,13 @@ packages:
|
||||
resolution: {integrity: sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==}
|
||||
dev: false
|
||||
|
||||
/fast-xml-parser@4.3.5:
|
||||
resolution: {integrity: sha512-sWvP1Pl8H03B8oFJpFR3HE31HUfwtX7Rlf9BNsvdpujD4n7WMhfmu8h9wOV2u+c1k0ZilTADhPqypzx2J690ZQ==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
strnum: 1.0.5
|
||||
dev: false
|
||||
|
||||
/fastify-plugin@4.5.1:
|
||||
resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==}
|
||||
dev: false
|
||||
@@ -8373,6 +8377,10 @@ packages:
|
||||
escape-string-regexp: 1.0.5
|
||||
dev: true
|
||||
|
||||
/strnum@1.0.5:
|
||||
resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
|
||||
dev: false
|
||||
|
||||
/stylis@4.2.0:
|
||||
resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
|
||||
dev: false
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Entity, Property, Unique } from '@mikro-orm/core';
|
||||
import { BaseEntity } from './BaseEntity.js';
|
||||
import { PlexServerSettings as PlexServerSettingsDTO } from '@tunarr/types';
|
||||
import { BaseEntity } from './BaseEntity.js';
|
||||
|
||||
@Entity()
|
||||
@Unique({ properties: ['name', 'uri'] })
|
||||
@@ -23,6 +23,10 @@ export class PlexServerSettings extends BaseEntity {
|
||||
@Property()
|
||||
index: number;
|
||||
|
||||
// Nullable for now!
|
||||
@Property({ nullable: true })
|
||||
clientIdentifier?: string;
|
||||
|
||||
toDTO(): PlexServerSettingsDTO {
|
||||
return {
|
||||
id: this.uuid,
|
||||
@@ -32,6 +36,7 @@ export class PlexServerSettings extends BaseEntity {
|
||||
sendChannelUpdates: this.sendChannelUpdates,
|
||||
sendGuideUpdates: this.sendGuideUpdates,
|
||||
index: this.index,
|
||||
clientIdentifier: this.clientIdentifier,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,6 +617,15 @@
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "integer"
|
||||
},
|
||||
"client_identifier": {
|
||||
"name": "client_identifier",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "text"
|
||||
}
|
||||
},
|
||||
"name": "plex_server_settings",
|
||||
|
||||
9
server/src/migrations/Migration20240308184352.ts
Normal file
9
server/src/migrations/Migration20240308184352.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20240308184352 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('alter table `plex_server_settings` add column `client_identifier` text null;');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { EntityDTO } from '@mikro-orm/core';
|
||||
import { PlexDvr, PlexDvrsResponse, PlexResource } from '@tunarr/types/plex';
|
||||
import axios, {
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
@@ -5,18 +7,19 @@ import axios, {
|
||||
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: {
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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)}`
|
||||
@@ -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;
|
||||
},
|
||||
@@ -139,7 +145,7 @@ export class Plex {
|
||||
}
|
||||
}
|
||||
|
||||
async Get<T>(
|
||||
async doGet<T>(
|
||||
path: string,
|
||||
optionalHeaders: RawAxiosRequestHeaders = {},
|
||||
): Promise<Maybe<PlexMediaContainer<T>>> {
|
||||
@@ -162,7 +168,7 @@ export class Plex {
|
||||
return res?.MediaContainer;
|
||||
}
|
||||
|
||||
Put(
|
||||
doPut(
|
||||
path: string,
|
||||
query: ParsedUrlQueryInput | URLSearchParams = {},
|
||||
optionalHeaders: RawAxiosRequestHeaders = {},
|
||||
@@ -183,7 +189,7 @@ export class Plex {
|
||||
return this.doRequest(req);
|
||||
}
|
||||
|
||||
Post(
|
||||
doPost(
|
||||
path: string,
|
||||
query: ParsedUrlQueryInput | URLSearchParams = {},
|
||||
optionalHeaders: RawAxiosRequestHeaders = {},
|
||||
@@ -206,7 +212,7 @@ export class Plex {
|
||||
|
||||
async checkServerStatus() {
|
||||
try {
|
||||
await this.Get('/');
|
||||
await this.doGet('/');
|
||||
return 1;
|
||||
} catch (err) {
|
||||
console.error('Error getting Plex server status', err);
|
||||
@@ -214,10 +220,10 @@ export class Plex {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -225,18 +231,20 @@ export class Plex {
|
||||
}
|
||||
}
|
||||
|
||||
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[] = [];
|
||||
@@ -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[] };
|
||||
};
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}: `,
|
||||
|
||||
30
server/src/tasks/fixers/addPlexServerIds.ts
Normal file
30
server/src/tasks/fixers/addPlexServerIds.ts
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import createLogger from '../../logger.js';
|
||||
import { AddPlexServerIdsFixer } from './addPlexServerIds.js';
|
||||
import Fixer from './fixer.js';
|
||||
import { MissingSeasonNumbersFixer } from './missingSeasonNumbersFixer.js';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Loaded, wrap } from '@mikro-orm/core';
|
||||
import { ChannelCache } from '../channelCache.js';
|
||||
import { withDb } from '../dao/dataSource.js';
|
||||
import { Settings } from '../dao/settings.js';
|
||||
import { Channel } from '../dao/entities/Channel.js';
|
||||
import { PlexServerSettings } from '../dao/entities/PlexServerSettings.js';
|
||||
import { Settings } from '../dao/settings.js';
|
||||
import createLogger from '../logger.js';
|
||||
import { Plex } from '../plex.js';
|
||||
import { ServerContext } from '../serverContext.js';
|
||||
@@ -86,7 +86,7 @@ export class UpdateXmlTvTask extends Task<void> {
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
dvrs = await plex.GetDVRS(); // Refresh guide and channel mappings
|
||||
dvrs = await plex.getDvrs(); // Refresh guide and channel mappings
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Couldn't get DVRS list from ${plexServer.name}. This error will prevent 'refresh guide' or 'refresh channels' from working for this Plex server. But it is NOT related to playback issues.`,
|
||||
@@ -96,7 +96,7 @@ export class UpdateXmlTvTask extends Task<void> {
|
||||
}
|
||||
if (plexServer.sendGuideUpdates) {
|
||||
try {
|
||||
await plex.RefreshGuide(dvrs);
|
||||
await plex.refreshGuide(dvrs);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Couldn't tell Plex ${plexServer.name} to refresh guide for some reason. This error will prevent 'refresh guide' from working for this Plex server. But it is NOT related to playback issues.`,
|
||||
@@ -106,7 +106,7 @@ export class UpdateXmlTvTask extends Task<void> {
|
||||
}
|
||||
if (plexServer.sendChannelUpdates && channels.length !== 0) {
|
||||
try {
|
||||
await plex.RefreshChannels(channels, dvrs);
|
||||
await plex.refreshChannels(channels, dvrs);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Couldn't tell Plex ${plexServer.name} to refresh channels for some reason. This error will prevent 'refresh channels' from working for this Plex server. But it is NOT related to playback issues.`,
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import z from 'zod';
|
||||
import {
|
||||
PlexServerSettingsInsert,
|
||||
PlexServerSettingsRemove,
|
||||
PlexServerSettingsSchema,
|
||||
PlexStreamSettingsSchema,
|
||||
} from './schemas/settingsSchemas.js';
|
||||
|
||||
export type PlexServerSettings = z.infer<typeof PlexServerSettingsSchema>;
|
||||
|
||||
export type PlexServerInsert = z.infer<typeof PlexServerSettingsInsert>;
|
||||
|
||||
export type PlexServerRemove = z.infer<typeof PlexServerSettingsRemove>;
|
||||
|
||||
export type PlexStreamSettings = z.infer<typeof PlexStreamSettingsSchema>;
|
||||
|
||||
export const defaultPlexStreamSettings = PlexStreamSettingsSchema.parse({});
|
||||
|
||||
@@ -106,6 +106,7 @@ export type UpdateChannelProgrammingRequest = Alias<
|
||||
export const UpdatePlexServerRequestSchema = PlexServerSettingsSchema.partial({
|
||||
sendChannelUpdates: true,
|
||||
sendGuideUpdates: true,
|
||||
clientIdentifier: true,
|
||||
id: true,
|
||||
});
|
||||
|
||||
@@ -117,6 +118,7 @@ export const InsertPlexServerRequestSchema = PlexServerSettingsSchema.partial({
|
||||
sendChannelUpdates: true,
|
||||
sendGuideUpdates: true,
|
||||
index: true,
|
||||
clientIdentifier: true,
|
||||
}).omit({
|
||||
id: true,
|
||||
});
|
||||
|
||||
@@ -488,3 +488,64 @@ export type PlexChildMediaApiType<Target extends PlexMedia> = FindChild0<
|
||||
Target,
|
||||
PlexMediaApiChildType
|
||||
>;
|
||||
|
||||
export const PlexPinsResponseSchema = z.object({
|
||||
authToken: z.string().nullable(),
|
||||
clientIdentifier: z.string(),
|
||||
code: z.string(),
|
||||
createdAt: z.string(),
|
||||
expiresAt: z.string(),
|
||||
expiresIn: z.number(),
|
||||
id: z.number(),
|
||||
product: z.string(),
|
||||
qr: z.string(),
|
||||
trusted: z.boolean(),
|
||||
});
|
||||
|
||||
export type PlexPinsResponse = Alias<z.infer<typeof PlexPinsResponseSchema>>;
|
||||
|
||||
export const PlexConnectionSchema = z.object({
|
||||
IPv6: z.boolean(),
|
||||
address: z.string(),
|
||||
local: z.boolean(),
|
||||
port: z.number(),
|
||||
protocol: z.string(),
|
||||
relay: z.boolean(),
|
||||
uri: z.string(),
|
||||
});
|
||||
|
||||
export type PlexConnection = Alias<z.infer<typeof PlexConnectionSchema>>;
|
||||
|
||||
export const PlexResourceSchema = z.object({
|
||||
accessToken: z.string(),
|
||||
clientIdentifier: z.string(),
|
||||
connections: z.array(PlexConnectionSchema),
|
||||
createdAt: z.string(),
|
||||
device: z.string(),
|
||||
dnsRebindingProtection: z.boolean(),
|
||||
home: z.boolean(),
|
||||
httpsRequired: z.boolean(),
|
||||
lastSeenAt: z.string(),
|
||||
name: z.string(),
|
||||
owned: z.boolean(),
|
||||
ownerId: z.string().nullable(),
|
||||
platform: z.string(),
|
||||
platformVersion: z.string(),
|
||||
presence: z.boolean(),
|
||||
product: z.string(),
|
||||
productVersion: z.string(),
|
||||
provides: z.string(),
|
||||
publicAddress: z.string(),
|
||||
publicAddressMatches: z.boolean(),
|
||||
relay: z.boolean(),
|
||||
sourceTitle: z.string().nullable(),
|
||||
synced: z.boolean(),
|
||||
});
|
||||
|
||||
export type PlexResource = Alias<z.infer<typeof PlexResourceSchema>>;
|
||||
|
||||
export const PlexResourcesResponseSchema = z.array(PlexResourceSchema);
|
||||
|
||||
export type PlexResourcesResponse = Alias<
|
||||
z.infer<typeof PlexResourcesResponseSchema>
|
||||
>;
|
||||
|
||||
@@ -62,18 +62,7 @@ export const PlexServerSettingsSchema = z.object({
|
||||
sendGuideUpdates: z.boolean(),
|
||||
sendChannelUpdates: z.boolean(),
|
||||
index: z.number(),
|
||||
});
|
||||
|
||||
export const PlexServerSettingsInsert = z.object({
|
||||
name: z.string(),
|
||||
uri: z.string(),
|
||||
accessToken: z.string(),
|
||||
sendGuideUpdates: z.boolean().optional(),
|
||||
sendChannelUpdates: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const PlexServerSettingsRemove = z.object({
|
||||
id: z.string(),
|
||||
clientIdentifier: z.string().optional(),
|
||||
});
|
||||
|
||||
export const PlexStreamSettingsSchema = z.object({
|
||||
|
||||
@@ -33,6 +33,7 @@ export default function AddPlexServer(props: AddPlexServer) {
|
||||
name: server.name,
|
||||
uri: connection.uri,
|
||||
accessToken: server.accessToken,
|
||||
clientIdentifier: server.clientIdentifier,
|
||||
}),
|
||||
);
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PlexPinsResponse, PlexResourcesResponse } from '@tunarr/types/plex';
|
||||
import { compact, partition } from 'lodash-es';
|
||||
import { apiClient } from '../external/api.ts';
|
||||
import { AsyncInterval } from './AsyncInterval.ts';
|
||||
@@ -14,55 +15,6 @@ const PlexLoginHeaders = {
|
||||
'X-Plex-Model': 'Plex OAuth',
|
||||
};
|
||||
|
||||
type PlexPinsResponse = {
|
||||
authToken: string | null;
|
||||
clientIdentifier: string;
|
||||
code: string;
|
||||
createdAt: string;
|
||||
expiresAt: string;
|
||||
expiresIn: number;
|
||||
id: number;
|
||||
product: string;
|
||||
qr: string;
|
||||
trusted: boolean;
|
||||
};
|
||||
|
||||
type PlexConnection = {
|
||||
IPv6: boolean;
|
||||
address: string;
|
||||
local: boolean;
|
||||
port: number;
|
||||
protocol: string;
|
||||
relay: boolean;
|
||||
uri: string;
|
||||
};
|
||||
|
||||
type PlexResourcesResponse = {
|
||||
accessToken: string;
|
||||
clientIdentifier: string;
|
||||
connections: PlexConnection[];
|
||||
createdAt: string;
|
||||
device: string;
|
||||
dnsRebindingProtection: boolean;
|
||||
home: boolean;
|
||||
httpsRequired: boolean;
|
||||
lastSeenAt: string;
|
||||
name: string;
|
||||
owned: boolean;
|
||||
ownerId: string | null;
|
||||
platform: string;
|
||||
platformVersion: string;
|
||||
presence: boolean;
|
||||
product: string;
|
||||
productVersion: string;
|
||||
provides: string;
|
||||
publicAddress: string;
|
||||
publicAddressMatches: boolean;
|
||||
relay: boolean;
|
||||
sourceTitle: string | null;
|
||||
synced: boolean;
|
||||
};
|
||||
|
||||
export const plexLoginFlow = async () => {
|
||||
const request = new Request('https://plex.tv/api/v2/pins?strong=true', {
|
||||
method: 'POST',
|
||||
@@ -144,12 +96,12 @@ export const plexLoginFlow = async () => {
|
||||
'X-Plex-Token': authToken,
|
||||
},
|
||||
},
|
||||
).then((res) => res.json() as Promise<PlexResourcesResponse[]>);
|
||||
).then((res) => res.json() as Promise<PlexResourcesResponse>);
|
||||
|
||||
return serversResponse.filter((server) => server.provides.includes('server'));
|
||||
};
|
||||
|
||||
export const checkNewPlexServers = async (servers: PlexResourcesResponse[]) => {
|
||||
export const checkNewPlexServers = async (servers: PlexResourcesResponse) => {
|
||||
return sequentialPromises(servers, async (server) => {
|
||||
const [localConnections, remoteConnections] = partition(
|
||||
server.connections,
|
||||
|
||||
Reference in New Issue
Block a user