diff --git a/server/src/api/mediaSourceApi.ts b/server/src/api/mediaSourceApi.ts index 29ff27d37..859e64458 100644 --- a/server/src/api/mediaSourceApi.ts +++ b/server/src/api/mediaSourceApi.ts @@ -1,7 +1,6 @@ import { MediaSourceType } from '@/db/schema/MediaSource.ts'; import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.js'; import { JellyfinApiClient } from '@/external/jellyfin/JellyfinApiClient.js'; -import { PlexApiClient } from '@/external/plex/PlexApiClient.js'; import { GlobalScheduler } from '@/services/Scheduler.ts'; import { UpdateXmlTvTask } from '@/tasks/UpdateXmlTvTask.js'; import { RouterPluginAsyncCallback } from '@/types/serverType.js'; @@ -91,14 +90,16 @@ export const mediaSourceRouter: RouterPluginAsyncCallback = async ( const healthyPromise = match(server) .with({ type: 'plex' }, (server) => { - return new PlexApiClient(server).checkServerStatus(); + return MediaSourceApiFactory().get(server).checkServerStatus(); }) - .with({ type: 'jellyfin' }, (server) => { - return new JellyfinApiClient({ - url: server.uri, - apiKey: server.accessToken, - name: server.name, - }) + .with({ type: 'jellyfin' }, async (server) => { + return ( + await MediaSourceApiFactory().getJellyfinClient({ + url: server.uri, + apiKey: server.accessToken, + name: server.name, + }) + ) .getSystemInfo() .then(() => true) .catch(() => false); @@ -148,7 +149,7 @@ export const mediaSourceRouter: RouterPluginAsyncCallback = async ( let healthyPromise: Promise; switch (req.body.type) { case 'plex': { - const plex = new PlexApiClient({ + const plex = MediaSourceApiFactory().get({ ...req.body, name: req.body.name ?? 'unknown', clientIdentifier: null, diff --git a/server/src/external/MediaSourceApiFactory.ts b/server/src/external/MediaSourceApiFactory.ts index f5223de19..233432a80 100644 --- a/server/src/external/MediaSourceApiFactory.ts +++ b/server/src/external/MediaSourceApiFactory.ts @@ -1,7 +1,7 @@ -import { ChannelDB } from '@/db/ChannelDB.ts'; import { SettingsDB, getSettings } from '@/db/SettingsDB.ts'; import { MediaSourceDB } from '@/db/mediaSourceDB.ts'; import { MediaSourceType } from '@/db/schema/MediaSource.ts'; +import { registerSingletonInitializer } from '@/globals.ts'; import { Maybe } from '@/types/util.js'; import { isDefined } from '@/util/index.js'; import { LoggerFactory } from '@/util/logging/LoggerFactory.js'; @@ -165,11 +165,11 @@ export class MediaSourceApiFactoryImpl { } } -export const MediaSourceApiFactory = ( - mediaSourceDB: MediaSourceDB = new MediaSourceDB(new ChannelDB()), -) => { +registerSingletonInitializer((ctx) => { if (!instance) { - instance = new MediaSourceApiFactoryImpl(mediaSourceDB); + instance = new MediaSourceApiFactoryImpl(ctx.mediaSourceDB); } return instance; -}; +}); + +export const MediaSourceApiFactory = () => instance; diff --git a/server/src/external/plex/PlexApiClient.ts b/server/src/external/plex/PlexApiClient.ts index e86b6c69e..c48527c3b 100644 --- a/server/src/external/plex/PlexApiClient.ts +++ b/server/src/external/plex/PlexApiClient.ts @@ -1,6 +1,8 @@ import { Maybe, Nilable } from '@/types/util.js'; import { getChannelId } from '@/util/channels.js'; import { isSuccess } from '@/util/index.js'; +import { getTunarrVersion } from '@/util/version.ts'; +import { PlexClientIdentifier } from '@tunarr/shared/constants'; import { PlexDvr, PlexDvrsResponse, @@ -50,6 +52,11 @@ export type PlexApiOptions = { const PlexCache = new PlexQueryCache(); +const PlexHeaders = { + 'X-Plex-Product': 'Tunarr', + 'X-Plex-Client-Identifier': PlexClientIdentifier, +}; + export class PlexApiClient extends BaseApiClient { private opts: PlexApiOptions; private accessToken: string; @@ -59,6 +66,8 @@ export class PlexApiClient extends BaseApiClient { url: opts.uri, name: opts.name, extraHeaders: { + ...PlexHeaders, + 'X-Plex-Version': getTunarrVersion(), 'X-Plex-Token': opts.accessToken, }, }); diff --git a/server/src/globals.ts b/server/src/globals.ts index 4323edab7..c7facc36d 100644 --- a/server/src/globals.ts +++ b/server/src/globals.ts @@ -4,6 +4,7 @@ import once from 'lodash-es/once.js'; import path, { resolve } from 'node:path'; import { ServerArgsType } from './cli/RunServerCommand.ts'; import { GlobalArgsType } from './cli/types.ts'; +import { ServerContext } from './serverContext.ts'; import { LogLevels } from './util/logging/LoggerFactory.ts'; export type GlobalOptions = GlobalArgsType & { @@ -81,11 +82,11 @@ export const dbOptions = () => { }; }; -type Initializer = () => unknown; +type Initializer = (ctx: ServerContext) => T; let initalized = false; -const initializers: Initializer[] = []; +const initializers: Initializer[] = []; -export const registerSingletonInitializer = (f: () => T) => { +export const registerSingletonInitializer = (f: Initializer) => { if (initalized) { throw new Error( 'Attempted to register singleton after intialization. This singleton will never be initialized!!', @@ -95,7 +96,7 @@ export const registerSingletonInitializer = (f: () => T) => { initializers.push(f); }; -export const initializeSingletons = once(() => { - forEach(initializers, (f) => f()); +export const initializeSingletons = once((ctx: ServerContext) => { + forEach(initializers, (f) => f(ctx)); initalized = true; }); diff --git a/server/src/server.ts b/server/src/server.ts index adf2b6396..f495a92a0 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -97,9 +97,8 @@ export async function initServer(opts: ServerOptions) { logger.info('Using Tunarr database directory: %s', opts.databaseDirectory); - initializeSingletons(); - const ctx = serverContext(); + initializeSingletons(ctx); registerHealthChecks(ctx); await ctx.m3uService.clearCache(); await new ChannelLineupMigrator(ctx.channelDB).run(); diff --git a/server/src/services/dynamic_channels/PlexContentSourceUpdater.ts b/server/src/services/dynamic_channels/PlexContentSourceUpdater.ts index e08ec43eb..6bfd95ffc 100644 --- a/server/src/services/dynamic_channels/PlexContentSourceUpdater.ts +++ b/server/src/services/dynamic_channels/PlexContentSourceUpdater.ts @@ -3,6 +3,7 @@ import { ProgramDB } from '@/db/ProgramDB.ts'; import { PendingProgram } from '@/db/derived_types/Lineup.ts'; import { MediaSourceDB } from '@/db/mediaSourceDB.ts'; import { Channel } from '@/db/schema/Channel.ts'; +import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.ts'; import { PlexApiClient } from '@/external/plex/PlexApiClient.js'; import { Logger, LoggerFactory } from '@/util/logging/LoggerFactory.js'; import { Timer } from '@/util/perf.js'; @@ -44,7 +45,7 @@ export class PlexContentSourceUpdater extends ContentSourceUpdater { const allMediaSources = await this.mediaSourceDB.findByType('plex'); await mapAsyncSeq(allMediaSources, async (plexServer) => { - const plex = new PlexApiClient(plexServer); + const plex = MediaSourceApiFactory().get(plexServer); let dvrs: PlexDvr[] = []; if (!plexServer.sendGuideUpdates && !plexServer.sendChannelUpdates) { diff --git a/server/src/tasks/fixers/addPlexServerIds.ts b/server/src/tasks/fixers/addPlexServerIds.ts index 5f0bdfbfd..f85b493b2 100644 --- a/server/src/tasks/fixers/addPlexServerIds.ts +++ b/server/src/tasks/fixers/addPlexServerIds.ts @@ -1,6 +1,6 @@ import { getDatabase } from '@/db/DBAccess.ts'; import { MediaSourceType } from '@/db/schema/MediaSource.ts'; -import { PlexApiClient } from '@/external/plex/PlexApiClient.js'; +import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.ts'; import { find, isNil } from 'lodash-es'; import Fixer from './fixer.js'; @@ -13,7 +13,7 @@ export class AddPlexServerIdsFixer extends Fixer { .where('type', '=', MediaSourceType.Plex) .execute(); for (const server of plexServers) { - const api = new PlexApiClient(server); + const api = MediaSourceApiFactory().get(server); const devices = await api.getDevices(); if (!isNil(devices) && devices.MediaContainer.Device) { const matchingServer = find( diff --git a/server/src/tasks/fixers/missingSeasonNumbersFixer.ts b/server/src/tasks/fixers/missingSeasonNumbersFixer.ts index 2b97528b9..3cfd49d64 100644 --- a/server/src/tasks/fixers/missingSeasonNumbersFixer.ts +++ b/server/src/tasks/fixers/missingSeasonNumbersFixer.ts @@ -52,10 +52,8 @@ export class MissingSeasonNumbersFixer extends Fixer { return; } - const plexByName = groupByUniqPropAndMap( - allPlexServers, - 'name', - (server) => new PlexApiClient(server), + const plexByName = groupByUniqPropAndMap(allPlexServers, 'name', (server) => + MediaSourceApiFactory().get(server), ); const updatedPrograms: RawProgram[] = []; diff --git a/server/src/tasks/plex/UpdatePlexPlayStatusTask.ts b/server/src/tasks/plex/UpdatePlexPlayStatusTask.ts index cb2b6b3f4..6fbc9c2ad 100644 --- a/server/src/tasks/plex/UpdatePlexPlayStatusTask.ts +++ b/server/src/tasks/plex/UpdatePlexPlayStatusTask.ts @@ -4,6 +4,8 @@ import { GlobalScheduler } from '@/services/Scheduler.ts'; import { ScheduledTask } from '@/tasks/ScheduledTask.ts'; import { Task } from '@/tasks/Task.ts'; import { run } from '@/util/index.ts'; +import { getTunarrVersion } from '@/util/version.ts'; +import { PlexClientIdentifier } from '@tunarr/shared/constants'; import dayjs from 'dayjs'; import { RecurrenceRule } from 'node-schedule'; import { v4 } from 'uuid'; @@ -13,6 +15,7 @@ type UpdatePlexPlayStatusScheduleRequest = { startTime: number; duration: number; channelNumber: number; + updateIntervalSeconds?: number; }; type UpdatePlexPlayStatusInvocation = UpdatePlexPlayStatusScheduleRequest & { @@ -41,7 +44,7 @@ export class UpdatePlexPlayStatusScheduledTask extends ScheduledTask { UpdatePlexPlayStatusScheduledTask.name, run(() => { const rule = new RecurrenceRule(); - rule.second = 30; + rule.second = request.updateIntervalSeconds ?? 10; return rule; }), () => this.getNextTask(), @@ -64,7 +67,7 @@ export class UpdatePlexPlayStatusScheduledTask extends ScheduledTask { this.playState = 'stopped'; GlobalScheduler.scheduleOneOffTask( UpdatePlexPlayStatusTask.name, - dayjs().add(30, 'seconds').toDate(), + dayjs().add(5, 'seconds').toDate(), this.getNextTask(), ); } @@ -79,7 +82,8 @@ export class UpdatePlexPlayStatusScheduledTask extends ScheduledTask { this.request = { ...this.request, startTime: Math.min( - this.request.startTime + 30000, + this.request.startTime + + (this.request.updateIntervalSeconds ?? 10) * 1000, this.request.duration, ), }; @@ -113,9 +117,11 @@ class UpdatePlexPlayStatusTask extends Task { key: `/library/metadata/${this.request.ratingKey}`, time: this.request.startTime, duration: this.request.duration, + 'X-Plex-Product': 'Tunarr', + 'X-Plex-Version': getTunarrVersion(), 'X-Plex-Device-Name': deviceName, 'X-Plex-Device': deviceName, - 'X-Plex-Client-Identifier': this.request.sessionId, + 'X-Plex-Client-Identifier': PlexClientIdentifier, }; try { diff --git a/shared/src/util/constants.ts b/shared/src/util/constants.ts index cbe5a33b8..1a55ad80e 100644 --- a/shared/src/util/constants.ts +++ b/shared/src/util/constants.ts @@ -7,7 +7,7 @@ const constants = { DEFAULT_DATA_DIR: '.tunarr', }; -const PlexClientIdentifier = 'p86cy1w47clco3ro8t92nfy1'; +export const PlexClientIdentifier = 'p86cy1w47clco3ro8t92nfy1'; export const DefaultPlexHeaders = { Accept: 'application/json', @@ -17,7 +17,7 @@ export const DefaultPlexHeaders = { 'X-Plex-Version': '0.1', 'X-Plex-Client-Identifier': PlexClientIdentifier, 'X-Plex-Platform': 'Chrome', - 'X-Plex-Platform-Version': '80.0', + 'X-Plex-Platform-Version': '130.0', }; export default constants; diff --git a/web/src/helpers/plexLogin.ts b/web/src/helpers/plexLogin.ts index 4b6448aa3..9e19d1302 100644 --- a/web/src/helpers/plexLogin.ts +++ b/web/src/helpers/plexLogin.ts @@ -4,7 +4,10 @@ import { ApiClient } from '../external/api.ts'; import { AsyncInterval } from './AsyncInterval.ts'; import { sequentialPromises } from './util.ts'; -// From Plex: The Client Identifier identifies the specific instance of your app. A random string or UUID is sufficient here. There are no hard requirements for Client Identifier length or format, but once one is generated the client should store and re-use this identifier for subsequent requests. +// From Plex: The Client Identifier identifies the specific instance of your app. +// A random string or UUID is sufficient here. There are no hard requirements for +// Client Identifier length or format, but once one is generated the client should store +// and re-use this identifier for subsequent requests. const ClientIdentifier = 'p86cy1w47clco3ro8t92nfy1'; const PlexLoginHeaders = {