Skip to content

Commit

Permalink
fix: use stable ID when reporting Plex playback session status
Browse files Browse the repository at this point in the history
Fixes #960
  • Loading branch information
chrisbenincasa committed Nov 24, 2024
1 parent 3597546 commit 86e7ab8
Show file tree
Hide file tree
Showing 12 changed files with 55 additions and 38 deletions.
19 changes: 10 additions & 9 deletions server/src/api/mediaSourceApi.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -148,7 +149,7 @@ export const mediaSourceRouter: RouterPluginAsyncCallback = async (
let healthyPromise: Promise<boolean>;
switch (req.body.type) {
case 'plex': {
const plex = new PlexApiClient({
const plex = MediaSourceApiFactory().get({
...req.body,
name: req.body.name ?? 'unknown',
clientIdentifier: null,
Expand Down
11 changes: 5 additions & 6 deletions server/src/external/MediaSourceApiFactory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
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';
Expand Down Expand Up @@ -165,11 +164,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;
9 changes: 9 additions & 0 deletions server/src/external/plex/PlexApiClient.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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,
},
});
Expand Down
11 changes: 6 additions & 5 deletions server/src/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down Expand Up @@ -81,11 +82,11 @@ export const dbOptions = () => {
};
};

type Initializer = () => unknown;
type Initializer<T> = (ctx: ServerContext) => T;
let initalized = false;
const initializers: Initializer[] = [];
const initializers: Initializer<unknown>[] = [];

export const registerSingletonInitializer = <T>(f: () => T) => {
export const registerSingletonInitializer = <T>(f: Initializer<T>) => {
if (initalized) {
throw new Error(
'Attempted to register singleton after intialization. This singleton will never be initialized!!',
Expand All @@ -95,7 +96,7 @@ export const registerSingletonInitializer = <T>(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;
});
3 changes: 1 addition & 2 deletions server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,7 +45,7 @@ export class PlexContentSourceUpdater extends ContentSourceUpdater<DynamicConten
throw new Error('media source not found');
}

this.#plex = new PlexApiClient(server);
this.#plex = MediaSourceApiFactory().get(server);
}

protected async run() {
Expand Down
4 changes: 2 additions & 2 deletions server/src/tasks/UpdateXmlTvTask.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChannelDB } from '@/db/ChannelDB.ts';
import { SettingsDB, defaultXmlTvSettings } from '@/db/SettingsDB.ts';
import { MediaSourceDB } from '@/db/mediaSourceDB.ts';
import { PlexApiClient } from '@/external/plex/PlexApiClient.js';
import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.ts';
import { globalOptions } from '@/globals.js';
import { ServerContext } from '@/serverContext.js';
import { TVGuideService } from '@/services/TvGuideService.ts';
Expand Down Expand Up @@ -92,7 +92,7 @@ export class UpdateXmlTvTask extends Task<void> {
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) {
Expand Down
4 changes: 2 additions & 2 deletions server/src/tasks/fixers/addPlexServerIds.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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(
Expand Down
6 changes: 2 additions & 4 deletions server/src/tasks/fixers/missingSeasonNumbersFixer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down
14 changes: 10 additions & 4 deletions server/src/tasks/plex/UpdatePlexPlayStatusTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -13,6 +15,7 @@ type UpdatePlexPlayStatusScheduleRequest = {
startTime: number;
duration: number;
channelNumber: number;
updateIntervalSeconds?: number;
};

type UpdatePlexPlayStatusInvocation = UpdatePlexPlayStatusScheduleRequest & {
Expand Down Expand Up @@ -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(),
Expand All @@ -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(),
);
}
Expand All @@ -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,
),
};
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions shared/src/util/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const constants = {
DEFAULT_DATA_DIR: '.tunarr',
};

const PlexClientIdentifier = 'p86cy1w47clco3ro8t92nfy1';
export const PlexClientIdentifier = 'p86cy1w47clco3ro8t92nfy1';

export const DefaultPlexHeaders = {
Accept: 'application/json',
Expand All @@ -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;
5 changes: 4 additions & 1 deletion web/src/helpers/plexLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down

0 comments on commit 86e7ab8

Please sign in to comment.