From dbdca6a1bef4795d3cae2ffc7111dc8f94f59606 Mon Sep 17 00:00:00 2001 From: Christian Benincasa Date: Mon, 11 Mar 2024 14:52:47 -0400 Subject: [PATCH] Implement fixer to backfill plex server client identifiers --- pnpm-lock.yaml | 20 +++-- server/package.json | 3 +- server/src/api.ts | 2 +- server/src/dao/entities/PlexServerSettings.ts | 1 + server/src/plex.ts | 80 +++++++++++++------ server/src/plexTranscoder.ts | 6 +- server/src/tasks/fixers/addPlexServerIds.ts | 30 +++++++ server/src/tasks/fixers/index.ts | 6 +- .../tasks/fixers/missingSeasonNumbersFixer.ts | 4 +- server/src/tasks/updateXmlTvTask.ts | 8 +- types/src/PlexSettings.ts | 6 -- types/src/api/index.ts | 1 + types/src/plex/index.ts | 61 ++++++++++++++ types/src/schemas/settingsSchemas.ts | 13 +-- web/src/components/settings/AddPlexServer.tsx | 1 + web/src/helpers/plexLogin.ts | 54 +------------ 16 files changed, 183 insertions(+), 113 deletions(-) create mode 100644 server/src/tasks/fixers/addPlexServerIds.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c779fba7..eaf719043 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/server/package.json b/server/package.json index fb2f278cc..f2f517fa2 100644 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/src/api.ts b/server/src/api.ts index c61fa7ee1..6eab7fd7d 100644 --- a/server/src/api.ts +++ b/server/src/api.ts @@ -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)); }, ); diff --git a/server/src/dao/entities/PlexServerSettings.ts b/server/src/dao/entities/PlexServerSettings.ts index e9bc4b344..4de4b7baf 100644 --- a/server/src/dao/entities/PlexServerSettings.ts +++ b/server/src/dao/entities/PlexServerSettings.ts @@ -36,6 +36,7 @@ export class PlexServerSettings extends BaseEntity { sendChannelUpdates: this.sendChannelUpdates, sendGuideUpdates: this.sendGuideUpdates, index: this.index, + clientIdentifier: this.clientIdentifier, }; } } diff --git a/server/src/plex.ts b/server/src/plex.ts index 808be5fe2..a27a6661a 100644 --- a/server/src/plex.ts +++ b/server/src/plex.ts @@ -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( + async doGet( path: string, optionalHeaders: RawAxiosRequestHeaders = {}, ): Promise>> { @@ -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('/livetv/dvrs'); + const result = await this.doGet('/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> { + const response = await this.doRequest({ + 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[] }; +}; diff --git a/server/src/plexTranscoder.ts b/server/src/plexTranscoder.ts index 49e157021..a45968048 100644 --- a/server/src/plexTranscoder.ts +++ b/server/src/plexTranscoder.ts @@ -553,7 +553,7 @@ lang=en`; } async getDecisionUnmanaged(directPlay: boolean) { - this.decisionJson = await this.plex.Get( + this.decisionJson = await this.plex.doGet( `/video/:/transcode/universal/decision?${this.transcodingArgs}`, ); @@ -626,7 +626,7 @@ lang=en`; return this.cachedItemMetadata; } - this.cachedItemMetadata = await this.plex.Get(this.key); + this.cachedItemMetadata = await this.plex.doGet(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}: `, diff --git a/server/src/tasks/fixers/addPlexServerIds.ts b/server/src/tasks/fixers/addPlexServerIds.ts new file mode 100644 index 000000000..808a1e7e9 --- /dev/null +++ b/server/src/tasks/fixers/addPlexServerIds.ts @@ -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 { + 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(); + } +} diff --git a/server/src/tasks/fixers/index.ts b/server/src/tasks/fixers/index.ts index c31587d3c..c8191074b 100644 --- a/server/src/tasks/fixers/index.ts +++ b/server/src/tasks/fixers/index.ts @@ -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 { diff --git a/server/src/tasks/fixers/missingSeasonNumbersFixer.ts b/server/src/tasks/fixers/missingSeasonNumbersFixer.ts index bff981c5e..932f3a665 100644 --- a/server/src/tasks/fixers/missingSeasonNumbersFixer.ts +++ b/server/src/tasks/fixers/missingSeasonNumbersFixer.ts @@ -122,7 +122,7 @@ export class MissingSeasonNumbersFixer extends Fixer { private async findSeasonNumberUsingEpisode(episodeId: string, plex: Plex) { try { - const episode = await plex.Get( + const episode = await plex.doGet( `/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( + const season = await plex.doGet( `/library/metadata/${seasonId}`, ); return first(season?.Metadata ?? [])?.index; diff --git a/server/src/tasks/updateXmlTvTask.ts b/server/src/tasks/updateXmlTvTask.ts index c031fffa8..6a7d04c7c 100644 --- a/server/src/tasks/updateXmlTvTask.ts +++ b/server/src/tasks/updateXmlTvTask.ts @@ -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 { } 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 { } 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 { } 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.`, diff --git a/types/src/PlexSettings.ts b/types/src/PlexSettings.ts index 753de7010..fb596aae3 100644 --- a/types/src/PlexSettings.ts +++ b/types/src/PlexSettings.ts @@ -1,17 +1,11 @@ import z from 'zod'; import { - PlexServerSettingsInsert, - PlexServerSettingsRemove, PlexServerSettingsSchema, PlexStreamSettingsSchema, } from './schemas/settingsSchemas.js'; export type PlexServerSettings = z.infer; -export type PlexServerInsert = z.infer; - -export type PlexServerRemove = z.infer; - export type PlexStreamSettings = z.infer; export const defaultPlexStreamSettings = PlexStreamSettingsSchema.parse({}); diff --git a/types/src/api/index.ts b/types/src/api/index.ts index 9d0d9ff27..2d8e28549 100644 --- a/types/src/api/index.ts +++ b/types/src/api/index.ts @@ -117,6 +117,7 @@ export const InsertPlexServerRequestSchema = PlexServerSettingsSchema.partial({ sendChannelUpdates: true, sendGuideUpdates: true, index: true, + clientIdentifier: true, }).omit({ id: true, }); diff --git a/types/src/plex/index.ts b/types/src/plex/index.ts index 265a73d00..ca517f971 100644 --- a/types/src/plex/index.ts +++ b/types/src/plex/index.ts @@ -488,3 +488,64 @@ export type PlexChildMediaApiType = 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>; + +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>; + +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>; + +export const PlexResourcesResponseSchema = z.array(PlexResourceSchema); + +export type PlexResourcesResponse = Alias< + z.infer +>; diff --git a/types/src/schemas/settingsSchemas.ts b/types/src/schemas/settingsSchemas.ts index 27bf49fa8..66028b006 100644 --- a/types/src/schemas/settingsSchemas.ts +++ b/types/src/schemas/settingsSchemas.ts @@ -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({ diff --git a/web/src/components/settings/AddPlexServer.tsx b/web/src/components/settings/AddPlexServer.tsx index 7fad57c07..8fc4c8f9d 100644 --- a/web/src/components/settings/AddPlexServer.tsx +++ b/web/src/components/settings/AddPlexServer.tsx @@ -33,6 +33,7 @@ export default function AddPlexServer(props: AddPlexServer) { name: server.name, uri: connection.uri, accessToken: server.accessToken, + clientIdentifier: server.clientIdentifier, }), ); }) diff --git a/web/src/helpers/plexLogin.ts b/web/src/helpers/plexLogin.ts index 7b812bbb2..c9a7dbd5d 100644 --- a/web/src/helpers/plexLogin.ts +++ b/web/src/helpers/plexLogin.ts @@ -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); + ).then((res) => res.json() as Promise); 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,